Browse Source

On the way to WebRTC

master
Roderic Day 5 years ago
parent
commit
30e31ea478
3 changed files with 154 additions and 117 deletions
  1. +46
    -1
      pico.js
  2. +43
    -39
      pico.py
  3. +65
    -77
      test.py

+ 46
- 1
pico.js View File

users: [], users: [],
} }


/*
*
* WEBRTC
*
*/
const rpcCall = async (username) => {
const constraints = {audio: true, video: true}
const media = await navigator.mediaDevices.getUserMedia(constraints)
const rpc = new RTCPeerConnection({iceServers: []})
// rpc.oniceconnectionstatechange = (e) => {
// console.log(e)
// }
// rpc.onicecandidate = (e) => {
// e.candidate
// const candidate = new RTCIceCandidate();
// rpc.addIceCandidate(candidate);
// }
// rpc.ontrack = (e) => {
// console.log(e)
// // appendVideo(media)
// }

const makeOffer = async () => {
rpc.addStream(media)
const offer = await rpc.createOffer()
await rpc.setLocalDescription(offer)
return {msgType: 'offer', rsd: rpc.localDescription}
}

const makeAnswer = async (msg) => {
const offer = new RTCSessionDescription(msg.rsd)
await rpc.setRemoteDescription(offer)
const answer = await rpc.createAnswer()
await rpc.setLocalDescription(answer)
return {msgType: 'answer', rsd: rpc.localDescription}
}

const finishShake = async (msg) => {
const answer = new RTCSessionDescription(msg.rsd)
await rpc.setRemoteDescription(answer)

}
finishShake(await makeAnswer(await makeOffer()))
}

/* /*
* *
* GUI * GUI
), ),
m('.users', m('.users',
m('button', {onclick: Login.sendLogout}, 'Logout'), m('button', {onclick: Login.sendLogout}, 'Logout'),
m('ul.user-list', State.users.map(username => m('li', username)))
m('ul.user-list', State.users.map(username => m('li', username))),
), ),
) )
}, },

+ 43
- 39
pico.py View File

import datetime import datetime
import http import http
import json import json
import functools
from functools import partial
from pathlib import Path from pathlib import Path


import websockets import websockets




rooms = collections.defaultdict(set)
rooms = collections.defaultdict(dict)




class PicoProtocol(websockets.WebSocketServerProtocol): class PicoProtocol(websockets.WebSocketServerProtocol):
return await super().process_request(path, request_headers) return await super().process_request(path, request_headers)




def get_usernames(sockets):
return sorted(ws.username for ws in sockets)


async def send_json_many(targets, **data): async def send_json_many(targets, **data):
for websocket in list(targets): for websocket in list(targets):
await send_json(websocket, **data) await send_json(websocket, **data)
try: try:
return json.loads(await websocket.recv()) return json.loads(await websocket.recv())
except websockets.exceptions.ConnectionClosed: except websockets.exceptions.ConnectionClosed:
return {'action': 'logout'}
return {'kind': 'logout'}
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
return {} return {}




async def core(ws, path, server_name): async def core(ws, path, server_name):
sockets = rooms[path]
room = rooms[path]
usernames = room.keys()
sockets = room.values()
username = None


while True: while True:
data = await recv_json(ws) data = await recv_json(ws)
ts = datetime.datetime.now().isoformat() ts = datetime.datetime.now().isoformat()
reply = functools.partial(send_json, websocket=ws, ts=ts)
error = functools.partial(reply, kind='error')
broadcast = functools.partial(send_json_many, targets=set(sockets), ts=ts)

if 'action' not in data:
await error(info='Message without action is invalid')

elif data['action'] == 'login':
ws.username = data['username']
if not ws.username:
await error(info='Username not allowed')
emit = partial(send_json_many, kind=data['kind'], value=data.get('value'), ts=ts)
broadcast = partial(emit, targets=sockets)
reply = partial(emit, targets=[ws])
error = partial(reply, kind='error')

if 'kind' not in data:
await error(value='Message without kind is invalid')

elif data['kind'] == 'login':
username = data['value']
if not username:
await error(value='Username not allowed')
break break
if ws.username in get_usernames(sockets):
await error(info='Username taken')
if username in usernames:
await error(value='Username taken')
break break


sockets.add(ws)
online = get_usernames(sockets)
await reply(kind='update', users=online, info=f'Welcome to {path}', username=ws.username)
await broadcast(kind='update', users=online, info=f'{ws.username} joined')

elif data['action'] == 'post':
text = data['text']
if not text:
continue
elif 'target' in data:
targets = {ss for ss in sockets if ss.username in {ws.username, data['target']}}
await send_json_many(ts=ts, targets=targets, kind='post', source=ws.username, text=text)
else:
await broadcast(kind='post', source=ws.username, text=text)
others = list(sockets)
room[username] = ws
online = list(usernames)
await reply(online=online)
await broadcast(kind='post', value=f'{username} joined', online=online, targets=others)
await reply(kind='post', value=f'Welcome to {path}')


elif data['action'] == 'logout':
sockets.discard(ws)
await broadcast(kind='update', users=get_usernames(sockets), info=f'{ws.username} left')
elif username not in room:
await error(value='Login required')
break break


elif data['kind'] == 'logout':
del room[username]
online = list(usernames)
await broadcast(kind='post', value=f'{username} left', online=online)
break

else:
if 'target' in data:
targets = {v for k, v in room.items() if k in {username, data['target']}}
await broadcast(source=username, targets=targets)
else:
await broadcast(source=username)



async def start_server(host, port, server_name): async def start_server(host, port, server_name):
bound_core = functools.partial(core, server_name=server_name)
bound_core = partial(core, server_name=server_name)
return await websockets.serve(bound_core, host, port, create_protocol=PicoProtocol) return await websockets.serve(bound_core, host, port, create_protocol=PicoProtocol)





+ 65
- 77
test.py View File

if kind == 'recv': if kind == 'recv':
A, B = message, await recv_json(websocket) A, B = message, await recv_json(websocket)
B.pop('ts') B.pop('ts')
if B['kind'] == 'login':
username = B['value']
state.update(message) state.update(message)
if A != B: if A != B:
error = True error = True
elif kind == 'send': elif kind == 'send':
await send_json(websocket, **message) await send_json(websocket, **message)


while 'users' in state:
if min(state['users']) == state['username']:
while 'online' in state:
if state['online'][0] == username:
break break
message = await recv_json(websocket) message = await recv_json(websocket)
message.pop('ts') message.pop('ts')


def test_happy_path(): def test_happy_path():
client = _make_client('ws://localhost:8642/A', 0.1, Script() client = _make_client('ws://localhost:8642/A', 0.1, Script()
+ {'action': 'login', 'username': 'TestUser'}
- {'kind': 'update', 'users': ['TestUser'], 'info': 'Welcome to /A', 'username': 'TestUser'}
+ {'action': 'post', 'text': 'Hello World!'}
- {'kind': 'post', 'source': 'TestUser', 'text': 'Hello World!'}
+ {'kind': 'login', 'value': 'TestUser'}
- {'kind': 'login', 'value': 'TestUser', 'online': ['TestUser']}
- {'kind': 'post', 'value': 'Welcome to /A'}
+ {'kind': 'post', 'value': 'Hello World!'}
- {'kind': 'post', 'value': 'Hello World!', 'source': 'TestUser'}
) )
return _test(client) return _test(client)




def test_name_taken(): def test_name_taken():
client1 = _make_client('ws://localhost:8642/', 0.10, Script() client1 = _make_client('ws://localhost:8642/', 0.10, Script()
+ {'action': 'login', 'username': 'A'}
- {'kind': 'update', 'users': ['A'], 'info': 'Welcome to /', 'username': 'A'}
- {'kind': 'update', 'users': ['A', 'B'], 'info': 'B joined'}
+ {'kind': 'login', 'value': 'A'}
- {'kind': 'login', 'value': 'A', 'online': ['A']}
- {'kind': 'post', 'value': 'Welcome to /'}
- {'kind': 'post', 'value': 'B joined', 'online': ['A', 'B']}
) )
client2 = _make_client('ws://localhost:8642/', 0.11, Script() client2 = _make_client('ws://localhost:8642/', 0.11, Script()
+ {'action': 'login', 'username': 'A'}
- {'kind': 'error', 'info': 'Username taken'}
+ {'kind': 'login', 'value': 'A'}
- {'kind': 'error', 'value': 'Username taken'}
) )
client3 = _make_client('ws://localhost:8642/', 0.12, Script() client3 = _make_client('ws://localhost:8642/', 0.12, Script()
+ {'action': 'login', 'username': 'B'}
- {'kind': 'update', 'users': ['A', 'B'], 'info': 'Welcome to /', 'username': 'B'}
- {'kind': 'update', 'users': ['B'], 'info': 'A left'}
+ {'kind': 'login', 'value': 'B'}
- {'kind': 'login', 'value': 'B', 'online': ['A', 'B']}
- {'kind': 'post', 'value': 'Welcome to /'}
- {'kind': 'post', 'value': 'A left', 'online': ['B']}
) )
return _test(client1, client2, client3) return _test(client1, client2, client3)




def test_interact():
def test_interaction():
client1 = _make_client('ws://localhost:8642/', 0.10, Script() client1 = _make_client('ws://localhost:8642/', 0.10, Script()
+ {'action': 'login', 'username': 'Alice'}
- {'kind': 'update', 'users': ['Alice'], 'info': 'Welcome to /', 'username': 'Alice'}
- {'kind': 'update', 'users': ['Alice', 'Bob'], 'info': 'Bob joined'}
+ {'action': 'post', 'text': 'Hey Bob!'}
- {'kind': 'post', 'source': 'Alice', 'text': 'Hey Bob!'}
- {'kind': 'post', 'source': 'Bob', 'text': 'Howdy!'}
+ {'kind': 'login', 'value': 'Alice'}
- {'kind': 'login', 'value': 'Alice', 'online': ['Alice']}
- {'kind': 'post', 'value': 'Welcome to /'}
- {'kind': 'post', 'value': 'Bob joined', 'online': ['Alice', 'Bob']}
+ {'kind': 'post', 'value': 'Hey Bob!'}
- {'kind': 'post', 'value': 'Hey Bob!', 'source': 'Alice'}
- {'kind': 'post', 'value': 'Howdy!', 'source': 'Bob'}
) )
client2 = _make_client('ws://localhost:8642/', 0.11, Script() client2 = _make_client('ws://localhost:8642/', 0.11, Script()
+ {'action': 'login', 'username': 'Bob'}
- {'kind': 'update', 'users': ['Alice', 'Bob'], 'info': 'Welcome to /', 'username': 'Bob'}
- {'kind': 'post', 'source': 'Alice', 'text': 'Hey Bob!'}
+ {'action': 'post', 'text': 'Howdy!'}
- {'kind': 'post', 'source': 'Bob', 'text': 'Howdy!'}
+ {'action': 'post', 'text': ''}
- {'kind': 'update', 'users': ['Bob'], 'info': 'Alice left'}
+ {'kind': 'login', 'value': 'Bob'}
- {'kind': 'login', 'value': 'Bob', 'online': ['Alice', 'Bob']}
- {'kind': 'post', 'value': 'Welcome to /'}
- {'kind': 'post', 'value': 'Hey Bob!', 'source': 'Alice'}
+ {'kind': 'post', 'value': 'Howdy!'}
- {'kind': 'post', 'value': 'Howdy!', 'source': 'Bob'}
- {'kind': 'post', 'value': 'Alice left', 'online': ['Bob']}
) )
return _test(client1, client2) return _test(client1, client2)




def test_party():
client1 = _make_client('ws://localhost:8642/', 0.10, Script()
+ {'action': 'login', 'username': 'Norman'}
- {'kind': 'update', 'users': ['Norman'], 'info': 'Welcome to /', 'username': 'Norman'}
- {'kind': 'update', 'users': ['Norman', 'Ray'], 'info': 'Ray joined'}
- {'kind': 'update', 'users': ['Emma', 'Norman', 'Ray'], 'info': 'Emma joined'}
+ {'action': 'post', 'text': 'なに?'}
- {'kind': 'post', 'source': 'Norman', 'text': 'なに?'}
- {'kind': 'update', 'users': ['Norman', 'Ray'], 'info': 'Emma left'}
)
client2 = _make_client('ws://localhost:8642/', 0.11, Script()
+ {'action': 'login', 'username': 'Ray'}
- {'kind': 'update', 'users': ['Norman', 'Ray'], 'info': 'Welcome to /', 'username': 'Ray'}
- {'kind': 'update', 'users': ['Emma', 'Norman', 'Ray'], 'info': 'Emma joined'}
- {'kind': 'post', 'source': 'Norman', 'text': 'なに?'}
- {'kind': 'update', 'users': ['Norman', 'Ray'], 'info': 'Emma left'}
- {'kind': 'update', 'users': ['Ray'], 'info': 'Norman left'}
)
client3 = _make_client('ws://localhost:8642/', 0.12, Script()
+ {'action': 'login', 'username': 'Emma'}
- {'kind': 'update', 'users': ['Emma', 'Norman', 'Ray'], 'info': 'Welcome to /', 'username': 'Emma'}
- {'kind': 'post', 'source': 'Norman', 'text': 'なに?'}
)
return _test(client1, client2, client3)


def test_rooms(): def test_rooms():
client1 = _make_client('ws://localhost:8642/A', 0.10, Script() client1 = _make_client('ws://localhost:8642/A', 0.10, Script()
+ {'action': 'login', 'username': 'Dandy'}
- {'kind': 'update', 'users': ['Dandy'], 'info': 'Welcome to /A', 'username': 'Dandy'}
+ {'action': 'post', 'text': 'Hi'}
- {'kind': 'post', 'source': 'Dandy', 'text': 'Hi'}
+ {'kind': 'login', 'value': 'Dandy'}
- {'kind': 'login', 'value': 'Dandy', 'online': ['Dandy']}
- {'kind': 'post', 'value': 'Welcome to /A'}
+ {'kind': 'post', 'value': 'Hi', 'source': 'Dandy'}
- {'kind': 'post', 'value': 'Hi', 'source': 'Dandy'}
) )
client2 = _make_client('ws://localhost:8642/B', 0.10, Script() client2 = _make_client('ws://localhost:8642/B', 0.10, Script()
+ {'action': 'login', 'username': 'Dandy'}
- {'kind': 'update', 'users': ['Dandy'], 'info': 'Welcome to /B', 'username': 'Dandy'}
+ {'action': 'post', 'text': 'Howdy'}
- {'kind': 'post', 'source': 'Dandy', 'text': 'Howdy'}
+ {'kind': 'login', 'value': 'Dandy'}
- {'kind': 'login', 'value': 'Dandy', 'online': ['Dandy']}
- {'kind': 'post', 'value': 'Welcome to /B'}
+ {'kind': 'post', 'value': 'Hi', 'source': 'Dandy'}
- {'kind': 'post', 'value': 'Hi', 'source': 'Dandy'}
) )
return _test(client1, client2) return _test(client1, client2)




def test_private_message(): def test_private_message():
client1 = _make_client('ws://localhost:8642/', 0.10, Script() client1 = _make_client('ws://localhost:8642/', 0.10, Script()
+ {'action': 'login', 'username': 'Norman'}
- {'kind': 'update', 'users': ['Norman'], 'info': 'Welcome to /', 'username': 'Norman'}
- {'kind': 'update', 'users': ['Norman', 'Ray'], 'info': 'Ray joined'}
- {'kind': 'update', 'users': ['Emma', 'Norman', 'Ray'], 'info': 'Emma joined'}
+ {'action': 'post', 'target': 'Emma', 'text': 'なに?'}
- {'kind': 'post', 'source': 'Norman', 'text': 'なに?'}
- {'kind': 'update', 'users': ['Norman', 'Ray'], 'info': 'Emma left'}
+ {'kind': 'login', 'value': 'Norman'}
- {'kind': 'login', 'value': 'Norman', 'online': ['Norman']}
- {'kind': 'post', 'value': 'Welcome to /'}
- {'kind': 'post', 'value': 'Ray joined', 'online': ['Norman', 'Ray']}
- {'kind': 'post', 'value': 'Emma joined', 'online': ['Norman', 'Ray', 'Emma']}
+ {'kind': 'post', 'value': 'なに?', 'target': 'Emma'}
- {'kind': 'post', 'value': 'なに?', 'source': 'Norman'}
) )
client2 = _make_client('ws://localhost:8642/', 0.11, Script() client2 = _make_client('ws://localhost:8642/', 0.11, Script()
+ {'action': 'login', 'username': 'Ray'}
- {'kind': 'update', 'users': ['Norman', 'Ray'], 'info': 'Welcome to /', 'username': 'Ray'}
- {'kind': 'update', 'users': ['Emma', 'Norman', 'Ray'], 'info': 'Emma joined'}
- {'kind': 'update', 'users': ['Norman', 'Ray'], 'info': 'Emma left'}
- {'kind': 'update', 'users': ['Ray'], 'info': 'Norman left'}
+ {'kind': 'login', 'value': 'Ray'}
- {'kind': 'login', 'value': 'Ray', 'online': ['Norman', 'Ray']}
- {'kind': 'post', 'value': 'Welcome to /'}
- {'kind': 'post', 'value': 'Emma joined', 'online': ['Norman', 'Ray', 'Emma']}
- {'kind': 'post', 'value': '秘密!', 'source': 'Emma'}
- {'kind': 'post', 'value': 'Norman left', 'online': ['Ray', 'Emma']}
) )
client3 = _make_client('ws://localhost:8642/', 0.12, Script() client3 = _make_client('ws://localhost:8642/', 0.12, Script()
+ {'action': 'login', 'username': 'Emma'}
- {'kind': 'update', 'users': ['Emma', 'Norman', 'Ray'], 'info': 'Welcome to /', 'username': 'Emma'}
- {'kind': 'post', 'source': 'Norman', 'text': 'なに?'}
+ {'kind': 'login', 'value': 'Emma'}
- {'kind': 'login', 'value': 'Emma', 'online': ['Norman', 'Ray', 'Emma']}
- {'kind': 'post', 'value': 'Welcome to /'}
- {'kind': 'post', 'value': 'なに?', 'source': 'Norman'}
+ {'kind': 'post', 'value': '秘密!', 'target': 'Ray'}
- {'kind': 'post', 'value': '秘密!', 'source': 'Emma'}
- {'kind': 'post', 'value': 'Norman left', 'online': ['Ray', 'Emma']}
- {'kind': 'post', 'value': 'Ray left', 'online': ['Emma']}
) )
return _test(client1, client2, client3) return _test(client1, client2, client3)



Loading…
Cancel
Save