2020-02-12 10:56:34 +00:00
|
|
|
# Copyright (c) 2020, Matthew Waters <matthew@centricular.com>
|
2018-12-17 11:34:10 +00:00
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or
|
|
|
|
# modify it under the terms of the GNU Lesser General Public
|
|
|
|
# License as published by the Free Software Foundation; either
|
|
|
|
# version 2.1 of the License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
# Lesser General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Lesser General Public
|
|
|
|
# License along with this program; if not, write to the
|
|
|
|
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
|
|
# Boston, MA 02110-1301, USA.
|
|
|
|
|
|
|
|
import websockets
|
|
|
|
import asyncio
|
|
|
|
import ssl
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import threading
|
|
|
|
import json
|
2020-02-12 10:56:34 +00:00
|
|
|
import logging
|
|
|
|
|
|
|
|
from observer import Signal, StateObserver, WebRTCObserver, DataChannelObserver
|
|
|
|
from enums import SignallingState, NegotiationState, DataChannelState
|
2018-12-17 11:34:10 +00:00
|
|
|
|
2020-02-12 10:56:34 +00:00
|
|
|
l = logging.getLogger(__name__)
|
2018-12-17 11:34:10 +00:00
|
|
|
|
|
|
|
class AsyncIOThread(threading.Thread):
|
2020-02-12 10:56:34 +00:00
|
|
|
"""
|
|
|
|
Run an asyncio loop in another thread.
|
|
|
|
"""
|
2018-12-17 11:34:10 +00:00
|
|
|
def __init__ (self, loop):
|
|
|
|
threading.Thread.__init__(self)
|
|
|
|
self.loop = loop
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
asyncio.set_event_loop(self.loop)
|
|
|
|
self.loop.run_forever()
|
|
|
|
|
|
|
|
def stop_thread(self):
|
|
|
|
self.loop.call_soon_threadsafe(self.loop.stop)
|
|
|
|
|
2020-02-12 10:56:34 +00:00
|
|
|
|
2018-12-17 11:34:10 +00:00
|
|
|
class SignallingClientThread(object):
|
2020-02-12 10:56:34 +00:00
|
|
|
"""
|
|
|
|
Connect to a signalling server
|
|
|
|
"""
|
2018-12-17 11:34:10 +00:00
|
|
|
def __init__(self, server):
|
2020-02-12 10:56:34 +00:00
|
|
|
# server string to connect to. Passed directly to websockets.connect()
|
2018-12-17 11:34:10 +00:00
|
|
|
self.server = server
|
|
|
|
|
2020-02-12 10:56:34 +00:00
|
|
|
# fired after we have connected to the signalling server
|
2018-12-17 11:34:10 +00:00
|
|
|
self.wss_connected = Signal()
|
2020-02-12 10:56:34 +00:00
|
|
|
# fired every time we receive a message from the signalling server
|
2018-12-17 11:34:10 +00:00
|
|
|
self.message = Signal()
|
|
|
|
|
|
|
|
self._init_async()
|
|
|
|
|
|
|
|
def _init_async(self):
|
2020-02-12 10:56:34 +00:00
|
|
|
self._running = False
|
2018-12-17 11:34:10 +00:00
|
|
|
self.conn = None
|
|
|
|
self._loop = asyncio.new_event_loop()
|
|
|
|
|
|
|
|
self._thread = AsyncIOThread(self._loop)
|
|
|
|
self._thread.start()
|
|
|
|
|
|
|
|
self._loop.call_soon_threadsafe(lambda: asyncio.ensure_future(self._a_loop()))
|
|
|
|
|
|
|
|
async def _a_connect(self):
|
2020-02-12 10:56:34 +00:00
|
|
|
# connect to the signalling server
|
2018-12-17 11:34:10 +00:00
|
|
|
assert not self.conn
|
|
|
|
sslctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
|
|
|
self.conn = await websockets.connect(self.server, ssl=sslctx)
|
|
|
|
|
|
|
|
async def _a_loop(self):
|
2020-02-12 10:56:34 +00:00
|
|
|
self._running = True
|
|
|
|
l.info('loop started')
|
2018-12-17 11:34:10 +00:00
|
|
|
await self._a_connect()
|
|
|
|
self.wss_connected.fire()
|
|
|
|
assert self.conn
|
|
|
|
async for message in self.conn:
|
|
|
|
self.message.fire(message)
|
2020-02-12 10:56:34 +00:00
|
|
|
l.info('loop exited')
|
2018-12-17 11:34:10 +00:00
|
|
|
|
|
|
|
def send(self, data):
|
2020-02-12 10:56:34 +00:00
|
|
|
# send some information to the peer
|
2018-12-17 11:34:10 +00:00
|
|
|
async def _a_send():
|
|
|
|
await self.conn.send(data)
|
|
|
|
self._loop.call_soon_threadsafe(lambda: asyncio.ensure_future(_a_send()))
|
|
|
|
|
|
|
|
def stop(self):
|
2020-02-12 10:56:34 +00:00
|
|
|
if self._running == False:
|
|
|
|
return
|
|
|
|
|
2018-12-17 11:34:10 +00:00
|
|
|
cond = threading.Condition()
|
|
|
|
|
|
|
|
# asyncio, why you so complicated to stop ?
|
|
|
|
tasks = asyncio.all_tasks(self._loop)
|
|
|
|
async def _a_stop():
|
|
|
|
if self.conn:
|
|
|
|
await self.conn.close()
|
|
|
|
self.conn = None
|
|
|
|
|
|
|
|
to_wait = [t for t in tasks if not t.done()]
|
|
|
|
if to_wait:
|
2020-02-12 10:56:34 +00:00
|
|
|
l.info('waiting for ' + str(to_wait))
|
2018-12-17 11:34:10 +00:00
|
|
|
done, pending = await asyncio.wait(to_wait)
|
|
|
|
with cond:
|
2020-02-12 10:56:34 +00:00
|
|
|
l.error('notifying cond')
|
2018-12-17 11:34:10 +00:00
|
|
|
cond.notify()
|
2020-02-12 10:56:34 +00:00
|
|
|
self._running = False
|
2018-12-17 11:34:10 +00:00
|
|
|
|
|
|
|
with cond:
|
2020-02-12 10:56:34 +00:00
|
|
|
self._loop.call_soon_threadsafe(lambda: asyncio.ensure_future(_a_stop()))
|
|
|
|
l.error('cond waiting')
|
|
|
|
cond.wait()
|
|
|
|
l.error('cond waited')
|
2018-12-17 11:34:10 +00:00
|
|
|
self._thread.stop_thread()
|
|
|
|
self._thread.join()
|
2020-02-12 10:56:34 +00:00
|
|
|
l.error('thread joined')
|
|
|
|
|
2018-12-17 11:34:10 +00:00
|
|
|
|
|
|
|
class WebRTCSignallingClient(SignallingClientThread):
|
2020-02-12 10:56:34 +00:00
|
|
|
"""
|
|
|
|
Signalling client implementation. Deals wit session management over the
|
|
|
|
signalling protocol. Sends and receives from a peer.
|
|
|
|
"""
|
2018-12-17 11:34:10 +00:00
|
|
|
def __init__(self, server, id_):
|
|
|
|
super().__init__(server)
|
|
|
|
|
|
|
|
self.wss_connected.connect(self._on_connection)
|
|
|
|
self.message.connect(self._on_message)
|
|
|
|
self.state = SignallingState.NEW
|
2020-02-12 10:56:34 +00:00
|
|
|
self._state_observer = StateObserver(self, "state", threading.Condition())
|
2018-12-17 11:34:10 +00:00
|
|
|
|
|
|
|
self.id = id_
|
|
|
|
self._peerid = None
|
|
|
|
|
2020-02-12 10:56:34 +00:00
|
|
|
# fired when the hello has been received
|
2018-12-17 11:34:10 +00:00
|
|
|
self.connected = Signal()
|
2020-02-12 10:56:34 +00:00
|
|
|
# fired when the signalling server responds that the session creation is ok
|
2018-12-17 11:34:10 +00:00
|
|
|
self.session_created = Signal()
|
2020-02-12 10:56:34 +00:00
|
|
|
# fired on an error
|
2018-12-17 11:34:10 +00:00
|
|
|
self.error = Signal()
|
2020-02-12 10:56:34 +00:00
|
|
|
# fired when the peer receives some json data
|
2018-12-17 11:34:10 +00:00
|
|
|
self.have_json = Signal()
|
|
|
|
|
2020-02-12 10:56:34 +00:00
|
|
|
def _update_state(self, new_state):
|
|
|
|
self._state_observer.update (new_state)
|
|
|
|
|
2018-12-17 11:34:10 +00:00
|
|
|
def wait_for_states(self, states):
|
2020-02-12 10:56:34 +00:00
|
|
|
return self._state_observer.wait_for (states)
|
2018-12-17 11:34:10 +00:00
|
|
|
|
|
|
|
def hello(self):
|
|
|
|
self.send('HELLO ' + str(self.id))
|
2020-02-12 10:56:34 +00:00
|
|
|
l.info("sent HELLO")
|
2018-12-17 11:34:10 +00:00
|
|
|
self.wait_for_states([SignallingState.HELLO])
|
|
|
|
|
|
|
|
def create_session(self, peerid):
|
|
|
|
self._peerid = peerid
|
|
|
|
self.send('SESSION {}'.format(self._peerid))
|
2020-02-12 10:56:34 +00:00
|
|
|
l.info("sent SESSION")
|
|
|
|
self.wait_for_states([SignallingState.SESSION])
|
2018-12-17 11:34:10 +00:00
|
|
|
|
|
|
|
def _on_connection(self):
|
|
|
|
self._update_state (SignallingState.OPEN)
|
|
|
|
|
|
|
|
def _on_message(self, message):
|
2020-02-12 10:56:34 +00:00
|
|
|
l.debug("received: " + message)
|
2018-12-17 11:34:10 +00:00
|
|
|
if message == 'HELLO':
|
|
|
|
self._update_state (SignallingState.HELLO)
|
|
|
|
self.connected.fire()
|
|
|
|
elif message == 'SESSION_OK':
|
|
|
|
self._update_state (SignallingState.SESSION)
|
|
|
|
self.session_created.fire()
|
|
|
|
elif message.startswith('ERROR'):
|
|
|
|
self._update_state (SignallingState.ERROR)
|
|
|
|
self.error.fire(message)
|
|
|
|
else:
|
|
|
|
msg = json.loads(message)
|
|
|
|
self.have_json.fire(msg)
|
|
|
|
return False
|
|
|
|
|
2020-02-12 10:56:34 +00:00
|
|
|
|
|
|
|
class RemoteWebRTCObserver(WebRTCObserver):
|
|
|
|
"""
|
|
|
|
Use information sent over the signalling channel to construct the current
|
|
|
|
state of a remote peer. Allow performing actions by sending requests over
|
|
|
|
the signalling channel.
|
|
|
|
"""
|
|
|
|
def __init__(self, signalling):
|
|
|
|
super().__init__()
|
|
|
|
self.signalling = signalling
|
|
|
|
|
|
|
|
def on_json(msg):
|
|
|
|
if 'STATE' in msg:
|
|
|
|
state = NegotiationState (msg['STATE'])
|
|
|
|
self._update_negotiation_state(state)
|
|
|
|
if state == NegotiationState.OFFER_CREATED:
|
|
|
|
self.on_offer_created.fire(msg['description'])
|
|
|
|
elif state == NegotiationState.ANSWER_CREATED:
|
|
|
|
self.on_answer_created.fire(msg['description'])
|
|
|
|
elif state == NegotiationState.OFFER_SET:
|
|
|
|
self.on_offer_set.fire (msg['description'])
|
|
|
|
elif state == NegotiationState.ANSWER_SET:
|
|
|
|
self.on_answer_set.fire (msg['description'])
|
|
|
|
elif 'DATA-NEW' in msg:
|
|
|
|
new = msg['DATA-NEW']
|
|
|
|
observer = RemoteDataChannelObserver(new['id'], new['location'], self)
|
|
|
|
self.add_channel (observer)
|
|
|
|
elif 'DATA-STATE' in msg:
|
|
|
|
ident = msg['id']
|
|
|
|
channel = self.find_channel(ident)
|
|
|
|
channel._update_state (DataChannelState(msg['DATA-STATE']))
|
|
|
|
elif 'DATA-MSG' in msg:
|
|
|
|
ident = msg['id']
|
|
|
|
channel = self.find_channel(ident)
|
|
|
|
channel.got_message(msg['DATA-MSG'])
|
|
|
|
self.signalling.have_json.connect (on_json)
|
|
|
|
|
|
|
|
def add_data_channel (self, ident):
|
|
|
|
msg = json.dumps({'DATA_CREATE': {'id': ident}})
|
|
|
|
self.signalling.send (msg)
|
|
|
|
|
|
|
|
def create_offer (self):
|
|
|
|
msg = json.dumps({'CREATE_OFFER': ""})
|
|
|
|
self.signalling.send (msg)
|
|
|
|
|
|
|
|
def create_answer (self):
|
|
|
|
msg = json.dumps({'CREATE_ANSWER': ""})
|
|
|
|
self.signalling.send (msg)
|
|
|
|
|
|
|
|
def set_title (self, title):
|
|
|
|
# entirely for debugging purposes
|
|
|
|
msg = json.dumps({'SET_TITLE': title})
|
|
|
|
self.signalling.send (msg)
|
|
|
|
|
|
|
|
def set_options (self, opts):
|
|
|
|
options = {}
|
|
|
|
if opts.has_field("remote-bundle-policy"):
|
|
|
|
options["bundlePolicy"] = opts["remote-bundle-policy"]
|
|
|
|
msg = json.dumps({'OPTIONS' : options})
|
|
|
|
self.signalling.send (msg)
|
|
|
|
|
|
|
|
|
|
|
|
class RemoteDataChannelObserver(DataChannelObserver):
|
|
|
|
"""
|
|
|
|
Use information sent over the signalling channel to construct the current
|
|
|
|
state of a remote peer's data channel. Allow performing actions by sending
|
|
|
|
requests over the signalling channel.
|
|
|
|
"""
|
|
|
|
def __init__(self, ident, location, webrtc):
|
|
|
|
super().__init__(ident, location)
|
|
|
|
self.webrtc = webrtc
|
|
|
|
|
|
|
|
def send_string(self, msg):
|
|
|
|
msg = json.dumps({'DATA_SEND_MSG': {'msg' : msg, 'id': self.ident}})
|
|
|
|
self.webrtc.signalling.send (msg)
|
|
|
|
|
|
|
|
def close (self):
|
|
|
|
msg = json.dumps({'DATA_CLOSE': {'id': self.ident}})
|
|
|
|
self.webrtc.signalling.send (msg)
|