async def _test(): client = RTCPeerConnection() client.createDataChannel("test") offer = await client.createOffer() webrtc_worker = WebRtcWorker(mode=WebRtcMode.SENDRECV) localDescription = webrtc_worker.process_offer(offer.sdp, offer.type) print("localDescription:") print(localDescription) webrtc_worker.stop()
def host_room(room, username): queue = Queue() threading.Thread(target=get_messages_thread, args=(queue, username)).start() rtc = RTCPeerConnection(rtcConfiguration) channel = Channel(rtc.createDataChannel("data", negotiated=True, id=0)) offer = run(rtc.createOffer()) run(rtc.setLocalDescription(offer)) res = post("/api/host/%s" % room, { "room": room, "offer": object_to_string(offer) }, username) print("Got res %s" % res) run(rtc.setRemoteDescription(object_from_string(res["answer"]))) for candidate in rtc.sctp.transport.transport.iceGatherer.getLocalCandidates( ): send_message(username, res["username"], "ice", fix_candidate(object_to_string(candidate))) time.sleep(3) while not queue.empty(): for message in queue.get(): if message["type"] == "ice": print("Got candidate: %s" % message["data"]) rtc.sctp.transport.transport.addRemoteCandidate( object_from_string(fix_candidate2(message["data"]))) rtc.sctp.transport.transport.addRemoteCandidate(None) return channel, rtc
def _test(): # Mock functions that depend on Streamlit global server object global get_global_relay, get_server_event_loop loop = asyncio.get_event_loop() def get_server_event_loop_mock(): return loop get_server_event_loop = get_server_event_loop_mock fake_global_relay = MediaRelay() def get_global_relay_mock(): return fake_global_relay get_global_relay = get_global_relay_mock # Start the test client = RTCPeerConnection() client.createDataChannel("test") offer = loop.run_until_complete(client.createOffer()) logger.debug("Offer for mock testing: %s", offer) def test_thread_fn(): webrtc_worker = WebRtcWorker(mode=WebRtcMode.SENDRECV) localDescription = webrtc_worker.process_offer(offer.sdp, offer.type) logger.debug("localDescription:") logger.debug(localDescription) webrtc_worker.stop() test_thread = threading.Thread(target=test_thread_fn) test_thread.start() # HACK for _ in range(100): loop.run_until_complete(asyncio.sleep(0.01))
async def get_offer(self, request): pc = RTCPeerConnection() pcid = self._connection_counter self._connection_counter += 1 self.pcs[pcid] = pc @pc.on("iceconnectionstatechange") async def on_iceconnectionstatechange(): print("ICE connection state is %s" % pc.iceConnectionState) if pc.iceConnectionState == "failed": print(' .... CLOSING PEER CONNECTION:') await pc.close() del self.pcs[pcid] @pc.on("close") async def on_pc_close(): print('this is pc.on close') dc = pc.createDataChannel('myDataChannel') self.dcs[dc] = pc # reverse-mapping to find my connection @dc.on('message') async def dc_on_message(message): print('dc, rx message:', message) sys.stdout.flush() if isinstance(message, str) and message.startswith("ping"): dc.send("pong" + message[4:]) dc.on('close', dc_on_close) offer = await pc.createOffer() await pc.setLocalDescription(offer) offerDict = { "sdp": pc.localDescription.sdp, "type": pc.localDescription.type, 'pcid': pcid } #print(' TX offer:') #pprint.pprint(offerDict) print('\n TX SDP offer:') print('-----------------------------------------------------------') print(pc.localDescription.sdp) print('-----------------------------------------------------------') sys.stdout.flush() return web.json_response(offerDict)
async def publish(plugin, player): """ Send video to the room. """ pc = RTCPeerConnection() pcs.add(pc) global channels channels["tx"] = pc.createDataChannel("JanusDataChannel") # configure media media = {"audio": False, "video": True, "data": True} if player and player.audio: pc.addTrack(player.audio) media["audio"] = True if player and player.video: pc.addTrack(player.video) else: pc.addTrack(OpenCVVideoStreamTrack()) # send offer await pc.setLocalDescription(await pc.createOffer()) request = {"request": "configure"} request.update(media) response = await plugin.send({ "body": request, "jsep": { "sdp": pc.localDescription.sdp, "trickle": False, "type": pc.localDescription.type, }, }) # apply answer await pc.setRemoteDescription( RTCSessionDescription(sdp=response["jsep"]["sdp"], type=response["jsep"]["type"]))
class Handler: def __init__(self, handlerId: str, channel: Channel, loop: asyncio.AbstractEventLoop, getTrack, addRemoteTrack, getRemoteTrack, configuration: Optional[RTCConfiguration] = None) -> None: self._handlerId = handlerId self._channel = channel self._pc = RTCPeerConnection(configuration or None) # dictionary of sending transceivers mapped by given localId self._sendTransceivers = dict() # type: Dict[str, RTCRtpTransceiver] # dictionary of dataChannelds mapped by internal id self._dataChannels = dict() # type: Dict[str, RTCDataChannel] # function returning a sending track given a player id and a kind self._getTrack = getTrack # function to store a receiving track self._addRemoteTrack = addRemoteTrack # function returning a receiving track self._getRemoteTrack = getRemoteTrack @self._pc.on("track") # type: ignore def on_track(track) -> None: Logger.debug( f"handler: ontrack [kind:{track.kind}, id:{track.id}]") # store it self._addRemoteTrack(track) @self._pc.on("signalingstatechange") # type: ignore async def on_signalingstatechange() -> None: Logger.debug( f"handler: signalingstatechange [state:{self._pc.signalingState}]" ) await self._channel.notify(self._handlerId, "signalingstatechange", self._pc.signalingState) @self._pc.on("icegatheringstatechange") # type: ignore async def on_icegatheringstatechange() -> None: Logger.debug( f"handler: icegatheringstatechange [state:{self._pc.iceGatheringState}]" ) await self._channel.notify(self._handlerId, "icegatheringstatechange", self._pc.iceGatheringState) @self._pc.on("iceconnectionstatechange") # type: ignore async def on_iceconnectionstatechange() -> None: Logger.debug( f"handler: iceconnectionstatechange [state:{self._pc.iceConnectionState}]" ) await self._channel.notify(self._handlerId, "iceconnectionstatechange", self._pc.iceConnectionState) async def checkDataChannelsBufferedAmount() -> None: while True: await asyncio.sleep(1) for dataChannelId, dataChannel in self._dataChannels.items(): await self._channel.notify(dataChannelId, "bufferedamount", dataChannel.bufferedAmount) self._dataChannelsBufferedAmountTask = loop.create_task( checkDataChannelsBufferedAmount()) async def close(self) -> None: # stop the periodic task self._dataChannelsBufferedAmountTask.cancel() # close peerconnection await self._pc.close() def dump(self) -> Any: result = { "id": self._handlerId, "signalingState": self._pc.signalingState, "iceConnectionState": self._pc.iceConnectionState, "iceGatheringState": self._pc.iceGatheringState, "transceivers": [], "sendTransceivers": [] } for transceiver in self._pc.getTransceivers(): transceiverInfo = { "mid": transceiver.mid, "stopped": transceiver.stopped, "kind": transceiver.kind, "currentDirection": transceiver.currentDirection, "direction": transceiver.direction, "sender": { "trackId": transceiver.sender.track.id if transceiver.sender.track else None }, "receiver": { "trackId": transceiver.receiver.track.id if transceiver.receiver.track else None } } result["transceivers"].append(transceiverInfo) for localId, transceiver in self._sendTransceivers.items(): sendTransceiverInfo = {"localId": localId, "mid": transceiver.mid} result["sendTransceivers"].append(sendTransceiverInfo) return result async def processRequest(self, request: Request) -> Any: if request.method == "handler.getLocalDescription": localDescription = self._pc.localDescription if (localDescription is not None): return { "type": localDescription.type, "sdp": localDescription.sdp } else: return None elif request.method == "handler.createOffer": offer = await self._pc.createOffer() return {"type": offer.type, "sdp": offer.sdp} elif request.method == "handler.createAnswer": answer = await self._pc.createAnswer() return {"type": answer.type, "sdp": answer.sdp} elif request.method == "handler.setLocalDescription": data = request.data if isinstance(data, RTCSessionDescription): raise TypeError("request data not a RTCSessionDescription") description = RTCSessionDescription(**data) await self._pc.setLocalDescription(description) elif request.method == "handler.setRemoteDescription": data = request.data if isinstance(data, RTCSessionDescription): raise TypeError("request data not a RTCSessionDescription") description = RTCSessionDescription(**data) await self._pc.setRemoteDescription(description) elif request.method == "handler.getMid": data = request.data localId = data.get("localId") if localId is None: raise TypeError("missing data.localId") # raise on purpose if the key is not found transceiver = self._sendTransceivers[localId] return transceiver.mid elif request.method == "handler.addTrack": data = request.data localId = data.get("localId") if localId is None: raise TypeError("missing data.localId") kind = data["kind"] playerId = data.get("playerId") recvTrackId = data.get("recvTrackId") # sending a track got from a MediaPlayer if playerId: track = self._getTrack(playerId, kind) transceiver = self._pc.addTransceiver(track) # sending a track which is a remote/receiving track elif recvTrackId: track = self._getRemoteTrack(recvTrackId, kind) transceiver = self._pc.addTransceiver(track) else: raise TypeError("missing data.playerId or data.recvTrackId") # store transceiver in the dictionary self._sendTransceivers[localId] = transceiver elif request.method == "handler.removeTrack": data = request.data localId = data.get("localId") if localId is None: raise TypeError("missing data.localId") transceiver = self._sendTransceivers[localId] transceiver.direction = "inactive" transceiver.sender.replaceTrack(None) # NOTE: do not remove transceiver from the dictionary elif request.method == "handler.replaceTrack": data = request.data localId = data.get("localId") if localId is None: raise TypeError("missing data.localId") kind = data["kind"] playerId = data.get("playerId") recvTrackId = data.get("recvTrackId") transceiver = self._sendTransceivers[localId] # sending a track got from a MediaPlayer if playerId: track = self._getTrack(playerId, kind) # sending a track which is a remote/receiving track elif recvTrackId: track = self._getRemoteTrack(recvTrackId, kind) else: raise TypeError("missing data.playerId or data.recvTrackId") transceiver.sender.replaceTrack(track) elif request.method == "handler.getTransportStats": result = {} stats = await self._pc.getStats() for key in stats: type = stats[key].type if type == "inbound-rtp": result[key] = self._serializeInboundStats(stats[key]) elif type == "outbound-rtp": result[key] = self._serializeOutboundStats(stats[key]) elif type == "remote-inbound-rtp": result[key] = self._serializeRemoteInboundStats(stats[key]) elif type == "remote-outbound-rtp": result[key] = self._serializeRemoteOutboundStats( stats[key]) elif type == "transport": result[key] = self._serializeTransportStats(stats[key]) return result elif request.method == "handler.getSenderStats": data = request.data mid = data.get("mid") if mid is None: raise TypeError("missing data.mid") transceiver = self._getTransceiverByMid(mid) sender = transceiver.sender result = {} stats = await sender.getStats() for key in stats: type = stats[key].type if type == "outbound-rtp": result[key] = self._serializeOutboundStats(stats[key]) elif type == "remote-inbound-rtp": result[key] = self._serializeRemoteInboundStats(stats[key]) elif type == "transport": result[key] = self._serializeTransportStats(stats[key]) return result elif request.method == "handler.getReceiverStats": data = request.data mid = data.get("mid") if mid is None: raise TypeError("missing data.mid") transceiver = self._getTransceiverByMid(mid) receiver = transceiver.receiver result = {} stats = await receiver.getStats() for key in stats: type = stats[key].type if type == "inbound-rtp": result[key] = self._serializeInboundStats(stats[key]) elif type == "remote-outbound-rtp": result[key] = self._serializeRemoteOutboundStats( stats[key]) elif type == "transport": result[key] = self._serializeTransportStats(stats[key]) return result elif request.method == "handler.createDataChannel": internal = request.internal dataChannelId = internal.get("dataChannelId") data = request.data id = data.get("id") ordered = data.get("ordered") maxPacketLifeTime = data.get("maxPacketLifeTime") maxRetransmits = data.get("maxRetransmits") label = data.get("label") protocol = data.get("protocol") dataChannel = self._pc.createDataChannel( negotiated=True, id=id, ordered=ordered, maxPacketLifeTime=maxPacketLifeTime, maxRetransmits=maxRetransmits, label=label, protocol=protocol) # store datachannel in the dictionary self._dataChannels[dataChannelId] = dataChannel @dataChannel.on("open") # type: ignore async def on_open() -> None: await self._channel.notify(dataChannelId, "open") @dataChannel.on("closing") # type: ignore async def on_closing() -> None: await self._channel.notify(dataChannelId, "closing") @dataChannel.on("close") # type: ignore async def on_close() -> None: # NOTE: After calling dataChannel.close() aiortc emits "close" event # on the dataChannel. Probably it shouldn't do it. So caution. try: del self._dataChannels[dataChannelId] await self._channel.notify(dataChannelId, "close") except KeyError: pass @dataChannel.on("message") # type: ignore async def on_message(message) -> None: if isinstance(message, str): await self._channel.notify(dataChannelId, "message", message) if isinstance(message, bytes): message_bytes = base64.b64encode(message) await self._channel.notify(dataChannelId, "binary", str(message_bytes)) @dataChannel.on("bufferedamountlow") # type: ignore async def on_bufferedamountlow() -> None: await self._channel.notify(dataChannelId, "bufferedamountlow") return { "streamId": dataChannel.id, "ordered": dataChannel.ordered, "maxPacketLifeTime": dataChannel.maxPacketLifeTime, "maxRetransmits": dataChannel.maxRetransmits, "label": dataChannel.label, "protocol": dataChannel.protocol, # status fields "readyState": dataChannel.readyState, "bufferedAmount": dataChannel.bufferedAmount, "bufferedAmountLowThreshold": dataChannel.bufferedAmountLowThreshold } else: raise TypeError("unknown request method") async def processNotification(self, notification: Notification) -> None: if notification.event == "enableTrack": Logger.warning("handler: enabling track not implemented") elif notification.event == "disableTrack": Logger.warning("handler: disabling track not implemented") elif notification.event == "datachannel.send": internal = notification.internal dataChannelId = internal.get("dataChannelId") if dataChannelId is None: raise TypeError("missing internal.dataChannelId") data = notification.data dataChannel = self._dataChannels[dataChannelId] dataChannel.send(data) # Good moment to update bufferedAmount in Node.js side await self._channel.notify(dataChannelId, "bufferedamount", dataChannel.bufferedAmount) elif notification.event == "datachannel.sendBinary": internal = notification.internal dataChannelId = internal.get("dataChannelId") if dataChannelId is None: raise TypeError("missing internal.dataChannelId") data = notification.data dataChannel = self._dataChannels[dataChannelId] dataChannel.send(base64.b64decode(data)) # Good moment to update bufferedAmount in Node.js side await self._channel.notify(dataChannelId, "bufferedamount", dataChannel.bufferedAmount) elif notification.event == "datachannel.close": internal = notification.internal dataChannelId = internal.get("dataChannelId") if dataChannelId is None: raise TypeError("missing internal.dataChannelId") dataChannel = self._dataChannels.get(dataChannelId) if dataChannel is None: return # NOTE: After calling dataChannel.close() aiortc emits "close" event # on the dataChannel. Probably it shouldn't do it. So caution. try: del self._dataChannels[dataChannelId] except KeyError: pass dataChannel.close() elif notification.event == "datachannel.setBufferedAmountLowThreshold": internal = notification.internal dataChannelId = internal.get("dataChannelId") if dataChannelId is None: raise TypeError("missing internal.dataChannelId") value = notification.data dataChannel = self._dataChannels[dataChannelId] dataChannel.bufferedAmountLowThreshold = value else: raise TypeError("unknown notification event") """ Helper functions """ def _getTransceiverByMid(self, mid: str) -> Optional[RTCRtpTransceiver]: return next(filter(lambda x: x.mid == mid, self._pc.getTransceivers()), None) def _serializeInboundStats(self, stats: RTCStatsReport) -> Dict[str, Any]: return { # RTCStats "timestamp": stats.timestamp.timestamp(), "type": stats.type, "id": stats.id, # RTCStreamStats "ssrc": stats.ssrc, "kind": stats.kind, "transportId": stats.transportId, # RTCReceivedRtpStreamStats "packetsReceived": stats.packetsReceived, "packetsLost": stats.packetsLost, "jitter": stats.jitter } def _serializeOutboundStats(self, stats: RTCStatsReport) -> Dict[str, Any]: return { # RTCStats "timestamp": stats.timestamp.timestamp(), "type": stats.type, "id": stats.id, # RTCStreamStats "ssrc": stats.ssrc, "kind": stats.kind, "transportId": stats.transportId, # RTCSentRtpStreamStats "packetsSent": stats.packetsSent, "bytesSent": stats.bytesSent, # RTCOutboundRtpStreamStats "trackId": stats.trackId } def _serializeRemoteInboundStats(self, stats: RTCStatsReport) -> Dict[str, Any]: return { # RTCStats "timestamp": stats.timestamp.timestamp(), "type": stats.type, "id": stats.id, # RTCStreamStats "ssrc": stats.ssrc, "kind": stats.kind, "transportId": stats.transportId, # RTCReceivedRtpStreamStats "packetsReceived": stats.packetsReceived, "packetsLost": stats.packetsLost, "jitter": stats.jitter, # RTCRemoteInboundRtpStreamStats "roundTripTime": stats.roundTripTime, "fractionLost": stats.fractionLost } def _serializeRemoteOutboundStats(self, stats: RTCStatsReport) -> Dict[str, Any]: return { # RTCStats "timestamp": stats.timestamp.timestamp(), "type": stats.type, "id": stats.id, # RTCStreamStats "ssrc": stats.ssrc, "kind": stats.kind, "transportId": stats.transportId, # RTCSentRtpStreamStats "packetsSent": stats.packetsSent, "bytesSent": stats.bytesSent, # RTCRemoteOutboundRtpStreamStats "remoteTimestamp": stats.remoteTimestamp.timestamp() } def _serializeTransportStats(self, stats: RTCStatsReport) -> Dict[str, Any]: return { # RTCStats "timestamp": stats.timestamp.timestamp(), "type": stats.type, "id": stats.id, # RTCTransportStats "packetsSent": stats.packetsSent, "packetsReceived": stats.packetsReceived, "bytesSent": stats.bytesSent, "bytesReceived": stats.bytesReceived, "iceRole": stats.iceRole, "dtlsState": stats.dtlsState }
async def ask_stream(ask_stream, timeout): while True: await asyncio.sleep(3) if ask_stream == "False": return # Onko meillä streami if broadcast: return print("Haetaan streami") ## TODO: testaa onko meillä streami olemassa pc = RTCPeerConnection() pc_id = "PeerConnection(%s)" % uuid.uuid4() pcs.add(pc) def log_info(msg, *args): logger.info(pc_id + " " + msg, *args) # Videolle on kanava "track" pc.createDataChannel("track") pc.addTransceiver("video", direction="recvonly") # Tämä luo itse offerin oikeassa muodossa await pc.setLocalDescription(await pc.createOffer()) # Asetetaan pyynnön parametreiksi params = { "sdp": pc.localDescription.sdp, "type": pc.localDescription.type, "listen_video": True } @pc.on("track") def on_track(track): log_info("Track %s received from other server", track.kind) if track.kind == "audio": pc.addTrack(player.audio) elif track.kind == "video": create_broadcast(track) pc.addTrack(relay.subscribe(broadcast)) @track.on("ended") async def on_ended(): log_info("Track %s ended", track.kind) broadcast_ended() coros = [pc.close() for pc in pcs] await asyncio.gather(*coros) pcs.clear() @track.on("oninactive") async def on_inactive(): log_info("Track inactive") @pc.on("connectionstatechange") async def on_connectionstatechange(): log_info("Connection state is %s", pc.connectionState) if pc.connectionState == "failed": broadcast_ended() coros = [pc.close() for pc in pcs] await asyncio.gather(*coros) pcs.clear() print("onko jumiss") # POST-pyyntö dispatcherille session = ClientSession() res = await session.post('https://localhost:8080/offer', json=params, ssl=False, timeout=3) if res.status == "500": continue try: result = await res.json() except: continue await session.close() answer = RTCSessionDescription(sdp=result["sdp"], type=result["type"]) #print(answer.sdp) await pc.setRemoteDescription(answer)
class TunnelClient: def __init__(self, host: str, port: int, destination_port: int, signal_server, destination: str): self._host = host self._port = port self._destination_port = destination_port self._signal_server = signal_server self._destination = destination self._running = asyncio.Event() self._tasks = Tasks() self._server = None self._peer_connection = None async def run_async(self): logging.info('[INIT] Creating RTC Connection') self._peer_connection = RTCPeerConnection() self._create_healthcheck_channel() await self._peer_connection.setLocalDescription( await self._peer_connection.createOffer()) logging.info('[INIT] Connecting with signaling server') await self._signal_server.connect_async() logging.info('[INIT] Sending local descriptor to signaling server') self._signal_server.send(self._peer_connection.localDescription, self._destination) logging.info('[INIT] Awaiting answer from signaling server') obj, src = await self._signal_server.receive_async() if not isinstance(obj, RTCSessionDescription) or obj.type != 'answer': logging.info('[ERROR] Unexpected answer from signaling server') return await self._peer_connection.setRemoteDescription(obj) logging.info('[INIT] Established RTC connection') await self._signal_server.close_async() logging.info('[INIT] Closed signaling server') logging.info('[INIT] Starting socket server on [%s:%s]', self._host, self._port) self._server = await asyncio.start_server(self._handle_new_client, host=self._host, port=self._port) logging.info('[INIT] Socket server started') logging.info('[STARTED] Tunneling client started') await self._running.wait() logging.info('[EXIT] Tunneling client main loop closing') def _create_healthcheck_channel(self): channel = self._peer_connection.createDataChannel('healthcheck') @channel.on('open') def on_open(): outer = {'last_healthcheck': now()} @channel.on('close') def on_close(): logging.info('[HEALTH CHECK] Datachannel closed') self._running.set() @channel.on('message') def on_message(message): outer['last_healthcheck'] = now() async def healthcheck_loop_async(): while now() - outer['last_healthcheck'] < 20000: try: channel.send('ping') await asyncio.sleep(3) except Exception: break logging.info('[HEALTH CHECK] Datachannel timeout') self._running.set() self._tasks.start_cancellable_task(healthcheck_loop_async()) def _handle_new_client(self, reader: StreamReader, writer: StreamWriter): client_id = ''.join( random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(8)) logging.info('[CLIENT %s] New client connected', client_id) connection = SocketConnection(reader, writer) channel = self._peer_connection.createDataChannel( 'tunnel-%s-%s' % (client_id, self._destination_port)) logging.info('[CLIENT %s] Datachannel %s created', client_id, channel.label) @channel.on('open') def on_open(): self._configure_channel(channel, connection, client_id) def _configure_channel(self, channel: RTCDataChannel, connection: SocketConnection, client_id: str): @channel.on('message') def on_message(message): connection.send(message) @channel.on('close') def on_close(): logging.info('[CLIENT %s] Datachannel %s closed', client_id, channel.label) connection.close() async def receive_loop_async(): while True: try: data = await connection.receive_async() except Exception: traceback.print_exc() break if not data: break channel.send(data) logging.info('[CLIENT %s] Socket connection closed', client_id) connection.close() channel.close() self._tasks.start_task(receive_loop_async()) logging.info('[CLIENT %s] Datachannel %s configured', client_id, channel.label) async def close_async(self): self._running.set() logging.info('[EXIT] Closing signalling server') if self._signal_server is not None: await self._signal_server.close_async() logging.info('[EXIT] Closing socket server') if self._server is not None: self._server.close() await self._server.wait_closed() logging.info('[EXIT] Closing RTC connection') if self._peer_connection is not None: await self._peer_connection.close() logging.info('[EXIT] Waiting for all tasks to finish') await self._tasks.close_async() logging.info('[EXIT] Closed tunneling client')
class WebRTCConnection(BidirectionalConnection): loop: Any def __init__(self, node: AbstractNode) -> None: # WebRTC Connection representation # As we have a full-duplex connection, # it's necessary to use a node instance # inside of this connection. In order to # be able to process requests sent by # the other peer. # All the requests messages will be forwarded # to this node. self.node = node # EventLoop that manages async tasks (producer/consumer) # This structure is global and needs to be # defined beforehand. try: self.loop = get_running_loop() log = "♫♫♫ > ...using a running event loop..." logger.debug(log) print(log) except RuntimeError as e: self.loop = None log = f"♫♫♫ > ...error getting a running event Loop... {e}" logger.error(log) print(log) if self.loop is None: log = "♫♫♫ > ...creating a new event loop..." print(log) logger.debug(log) self.loop = asyncio.new_event_loop() # Message pool (High Priority) # These queues will be used to manage # async messages. try: self.producer_pool: asyncio.Queue = asyncio.Queue( loop=self.loop, ) # Request Messages / Request Responses self.consumer_pool: asyncio.Queue = asyncio.Queue( loop=self.loop, ) # Request Responses # Initialize a PeerConnection structure self.peer_connection = RTCPeerConnection() # Set channel descriptor as None # This attribute will be used for external classes # in order to verify if the connection channel # was established. self.channel: Optional[RTCDataChannel] = None self._client_address: Optional[Address] = None # asyncio.ensure_future(self.heartbeat()) except Exception as e: log = f"Got an exception in WebRTCConnection __init__. {e}" logger.error(log) raise e @syft_decorator(typechecking=True) async def _set_offer(self) -> str: """Initialize a Real Time Communication Data Channel, set datachannel callbacks/tasks, and send offer payload message. :return: returns a signaling offer payload containing local description. :rtype: str """ try: # Use the Peer Connection structure to # set the channel as a RTCDataChannel. self.channel = self.peer_connection.createDataChannel( "datachannel") # This method will be called by as a callback # function by the aioRTC lib when the when # the connection opens. @self.channel.on("open") async def on_open() -> None: # type : ignore self.__producer_task = asyncio.ensure_future(self.producer()) # This method is the aioRTC "consumer" task # and will be running as long as connection remains. # At this point we're just setting the method behavior # It'll start running after the connection opens. @self.channel.on("message") async def on_message( message: Union[bin, str]) -> None: # type: ignore # Forward all received messages to our own consumer method. await self.consumer(msg=message) # Set peer_connection to generate an offer message type. await self.peer_connection.setLocalDescription( await self.peer_connection.createOffer()) # Generates the local description structure # and serialize it to string afterwards. local_description = object_to_string( self.peer_connection.localDescription) # Return the Offer local_description payload. return local_description except Exception as e: log = f"Got an exception in WebRTCConnection _set_offer. {e}" logger.error(log) raise e @syft_decorator(typechecking=True) async def _set_answer(self, payload: str) -> str: """Receives a signaling offer payload, initialize/set datachannel callbacks/tasks, updates remote local description using offer's payload message and returns a signaling answer payload. :return: returns a signaling answer payload containing local description. :rtype: str """ try: @self.peer_connection.on("datachannel") def on_datachannel(channel: RTCDataChannel) -> None: self.channel = channel self.__producer_task = asyncio.ensure_future(self.producer()) @self.channel.on("message") async def on_message( message: Union[bin, str]) -> None: # type: ignore await self.consumer(msg=message) return await self._process_answer(payload=payload) except Exception as e: log = f"Got an exception in WebRTCConnection _set_answer. {e}" logger.error(log) raise e @syft_decorator(typechecking=True) async def _process_answer(self, payload: str) -> Union[str, None]: # Converts payload received by # the other peer in aioRTC Object # instance. try: msg = object_from_string(payload) # Check if Object instance is a # description of RTC Session. if isinstance(msg, RTCSessionDescription): # Use the target's network address/metadata # to set the remote description of this peer. # This will basically say to this peer how to find/connect # with to other peer. await self.peer_connection.setRemoteDescription(msg) # If it's an offer message type, # generates your own local description # and send it back in order to tell # to the other peer how to find you. if msg.type == "offer": # Set peer_connection to generate an offer message type. await self.peer_connection.setLocalDescription( await self.peer_connection.createAnswer()) # Generates the local description structure # and serialize it to string afterwards. local_description = object_to_string( self.peer_connection.localDescription) # Returns the answer peer's local description return local_description except Exception as e: log = f"Got an exception in WebRTCConnection _process_answer. {e}" logger.error(log) raise e return None @syft_decorator(typechecking=True) async def producer(self) -> None: # Async task to send messages to the other side. # These messages will be enqueued by PySyft Node Clients # by using PySyft routes and ClientConnection's inheritance. try: while True: # If self.producer_pool is empty # give up task queue priority, giving # computing time to the next task. msg = await self.producer_pool.get() await asyncio.sleep(message_cooldown) # If self.producer_pool.get() returned a message # send it as a binary using the RTCDataChannel. # logger.critical(f"> Sending MSG {msg.message} ID: {msg.id}") self.channel.send(msg.to_bytes()) # type: ignore except Exception as e: log = f"Got an exception in WebRTCConnection producer. {e}" logger.error(log) raise e def close(self) -> None: try: # Build Close Message to warn the other peer bye_msg = CloseConnectionMessage(address=Address()) self.channel.send(bye_msg.to_bytes()) # type: ignore # Finish async tasks related with this connection self._finish_coroutines() except Exception as e: log = f"Got an exception in WebRTCConnection close. {e}" logger.error(log) raise e def _finish_coroutines(self) -> None: try: asyncio.run(self.peer_connection.close()) self.__producer_task.cancel() except Exception as e: log = f"Got an exception in WebRTCConnection _finish_coroutines. {e}" logger.error(log) raise e @syft_decorator(typechecking=True) async def consumer(self, msg: bin) -> None: # type: ignore try: # Async task to receive/process messages sent by the other side. # These messages will be sent by the other peer # as a service requests or responses for requests made by # this connection previously (ImmediateSyftMessageWithReply). # Deserialize the received message _msg = _deserialize(blob=msg, from_bytes=True) # Check if it's NOT a response generated by a previous request # made by the client instance that uses this connection as a route. # PS: The "_client_address" attribute will be defined during # Node Client initialization. if _msg.address != self._client_address: # If it's a new service request, route it properly # using the node instance owned by this connection. # Immediate message with reply if isinstance(_msg, SignedImmediateSyftMessageWithReply): reply = self.recv_immediate_msg_with_reply(msg=_msg) await self.producer_pool.put(reply) # Immediate message without reply elif isinstance(_msg, SignedImmediateSyftMessageWithoutReply): self.recv_immediate_msg_without_reply(msg=_msg) elif isinstance(_msg, CloseConnectionMessage): # Just finish async tasks related with this connection self._finish_coroutines() # Eventual message without reply else: self.recv_eventual_msg_without_reply(msg=_msg) # If it's true, the message will have the client's address as destination. else: await self.consumer_pool.put(_msg) except Exception as e: log = f"Got an exception in WebRTCConnection consumer. {e}" logger.error(log) raise e @syft_decorator(typechecking=True) def recv_immediate_msg_with_reply( self, msg: SignedImmediateSyftMessageWithReply ) -> SignedImmediateSyftMessageWithoutReply: """Executes/Replies requests instantly. :return: returns an instance of SignedImmediateSyftMessageWithReply :rtype: SignedImmediateSyftMessageWithoutReply """ # Execute node services now try: r = random.randint(0, 100000) logger.debug( f"> Before recv_immediate_msg_with_reply {r} {msg.message} {type(msg.message)}" ) reply = self.node.recv_immediate_msg_with_reply(msg=msg) logger.debug( f"> After recv_immediate_msg_with_reply {r} {msg.message} {type(msg.message)}" ) return reply except Exception as e: log = f"Got an exception in WebRTCConnection recv_immediate_msg_with_reply. {e}" logger.error(log) raise e @syft_decorator(typechecking=True) def recv_immediate_msg_without_reply( self, msg: SignedImmediateSyftMessageWithoutReply) -> None: """ Executes requests instantly. """ try: r = random.randint(0, 100000) logger.debug( f"> Before recv_immediate_msg_without_reply {r} {msg.message} {type(msg.message)}" ) self.node.recv_immediate_msg_without_reply(msg=msg) logger.debug( f"> After recv_immediate_msg_without_reply {r} {msg.message} {type(msg.message)}" ) except Exception as e: log = f"Got an exception in WebRTCConnection recv_immediate_msg_without_reply. {e}" logger.error(log) raise e @syft_decorator(typechecking=True) def recv_eventual_msg_without_reply( self, msg: SignedEventualSyftMessageWithoutReply) -> None: """ Executes requests eventually. """ try: self.node.recv_eventual_msg_without_reply(msg=msg) except Exception as e: log = f"Got an exception in WebRTCConnection recv_eventual_msg_without_reply. {e}" logger.error(log) raise e @syft_decorator(typechecking=False) def send_immediate_msg_with_reply( self, msg: SignedImmediateSyftMessageWithReply ) -> SignedImmediateSyftMessageWithReply: """Sends high priority messages and wait for their responses. :return: returns an instance of SignedImmediateSyftMessageWithReply. :rtype: SignedImmediateSyftMessageWithReply """ try: return asyncio.run(self.send_sync_message(msg=msg)) except Exception as e: log = f"Got an exception in WebRTCConnection send_immediate_msg_with_reply. {e}" logger.error(log) raise e @syft_decorator(typechecking=True) def send_immediate_msg_without_reply( self, msg: SignedImmediateSyftMessageWithoutReply) -> None: """" Sends high priority messages without waiting for their reply. """ try: # asyncio.run(self.producer_pool.put_nowait(msg)) self.producer_pool.put_nowait(msg) time.sleep(message_cooldown) except Exception as e: log = f"Got an exception in WebRTCConnection send_immediate_msg_without_reply. {e}" logger.error(log) raise e @syft_decorator(typechecking=True) def send_eventual_msg_without_reply( self, msg: SignedEventualSyftMessageWithoutReply) -> None: """" Sends low priority messages without waiting for their reply. """ try: asyncio.run(self.producer_pool.put(msg)) time.sleep(message_cooldown) except Exception as e: log = f"Got an exception in WebRTCConnection send_eventual_msg_without_reply. {e}" logger.error(log) raise e @syft_decorator(typechecking=True) async def send_sync_message( self, msg: SignedImmediateSyftMessageWithReply ) -> SignedImmediateSyftMessageWithoutReply: """Send sync messages generically. :return: returns an instance of SignedImmediateSyftMessageWithoutReply. :rtype: SignedImmediateSyftMessageWithoutReply """ try: # To ensure the sequence of sending / receiving messages # it's necessary to keep only a unique reference for reading # inputs (producer) and outputs (consumer). r = random.randint(0, 100000) # To be able to perform this method synchronously (waiting for the reply) # without blocking async methods, we need to use queues. # Enqueue the message to be sent to the target. logger.debug( f"> Before send_sync_message producer_pool.put blocking {r}") # self.producer_pool.put_nowait(msg) await self.producer_pool.put(msg) logger.debug( f"> After send_sync_message producer_pool.put blocking {r}") # Wait for the response checking the consumer queue. logger.debug( f"> Before send_sync_message consumer_pool.get blocking {r} {msg}" ) logger.debug( f"> Before send_sync_message consumer_pool.get blocking {r} {msg.message}" ) # before = time.time() # timeout_secs = 15 response = await self.consumer_pool.get() # asyncio.run() # self.async_check(before=before, timeout_secs=timeout_secs, r=r) logger.debug( f"> After send_sync_message consumer_pool.get blocking {r}") return response except Exception as e: log = f"Got an exception in WebRTCConnection send_eventual_msg_without_reply. {e}" logger.error(log) raise e async def async_check(self, before: float, timeout_secs: int, r: float) -> SignedImmediateSyftMessageWithoutReply: while True: await asyncio.sleep(message_cooldown) try: response = self.consumer_pool.get_nowait() return response except Exception as e: now = time.time() logger.debug( f"> During send_sync_message consumer_pool.get blocking {r}. {e}" ) if now - before > timeout_secs: log = f"send_sync_message timeout {timeout_secs} {r}" logger.critical(log) raise Exception(log)
class RTCConnection(SubscriptionProducerConsumer): _log = logging.getLogger("rtcbot.RTCConnection") def __init__(self, defaultChannelOrdered=True, loop=None): super().__init__( directPutSubscriptionType=asyncio.Queue, defaultSubscriptionType=asyncio.Queue, logger=self._log, ) self._loop = loop if self._loop is None: self._loop = asyncio.get_event_loop() self._dataChannels = {} # These allow us to easily signal when the given events happen self._dataChannelSubscriber = SubscriptionProducer( logger=self._log.getChild("dataChannelSubscriber") ) self._rtc = RTCPeerConnection() self._rtc.on("datachannel", self._onDatachannel) # self._rtc.on("iceconnectionstatechange", self._onIceConnectionStateChange) self._rtc.on("track", self._onTrack) self._hasRemoteDescription = False self._defaultChannelOrdered = defaultChannelOrdered self._videoHandler = ConnectionVideoHandler(self._rtc) self._audioHandler = ConnectionAudioHandler(self._rtc) async def getLocalDescription(self, description=None): """ Gets the description to send on. Creates an initial description if no remote description was passed, and creates a response if a remote was given, """ if self._hasRemoteDescription or description is not None: # This means that we received an offer - either the remote description # was already set, or we passed in a description. In either case, # instead of initializing a new connection, we prepare a response if not self._hasRemoteDescription: await self.setRemoteDescription(description) self._log.debug("Creating response to connection offer") try: answer = await self._rtc.createAnswer() except AttributeError: self._log.exception( "\n>>> Looks like the offer didn't include the necessary info to set up audio/video. See RTCConnection.video.offerToReceive(). <<<\n\n" ) raise await self._rtc.setLocalDescription(answer) return { "sdp": self._rtc.localDescription.sdp, "type": self._rtc.localDescription.type, } # There was no remote description, which means that we are initializing the # connection. # Before starting init, we create a default data channel for the connection self._log.debug("Setting up default data channel") channel = DataChannel( self._rtc.createDataChannel("default", ordered=self._defaultChannelOrdered) ) # Subscribe the default channel directly to our own inputs and outputs. # We have it listen to our own self._get, and write to our self._put_nowait channel.putSubscription(NoClosedSubscription(self._get)) channel.subscribe(self._put_nowait) self._dataChannels[channel.name] = channel # Make sure we offer to receive video and audio if if isn't set up yet if len(self.video._senders) == 0 and self.video._offerToReceive: self._log.debug("Offering to receive video") self._rtc.addTransceiver("video", "recvonly") if len(self.audio._senders) == 0 and self.audio._offerToReceive: self._log.debug("Offering to receive audio") self._rtc.addTransceiver("audio", "recvonly") self._log.debug("Creating new connection offer") offer = await self._rtc.createOffer() await self._rtc.setLocalDescription(offer) return { "sdp": self._rtc.localDescription.sdp, "type": self._rtc.localDescription.type, } async def setRemoteDescription(self, description): self._log.debug("Setting remote connection description") await self._rtc.setRemoteDescription(RTCSessionDescription(**description)) self._hasRemoteDescription = True def _onDatachannel(self, channel): """ When a data channel comes in, adds it to the data channels, and sets up its messaging and stuff. """ channel = DataChannel(channel) self._log.debug("Got channel: %s", channel.name) if channel.name == "default": # Subscribe the default channel directly to our own inputs and outputs. # We have it listen to our own self._get, and write to our self._put_nowait channel.putSubscription(NoClosedSubscription(self._get)) channel.subscribe(self._put_nowait) # Set the default channel self._defaultChannel = channel else: self._dataChannelSubscriber.put_nowait(channel) self._dataChannels[channel.name] = channel def _onTrack(self, track): self._log.debug("Received %s track from connection", track.kind) if track.kind == "audio": self._audioHandler._onTrack(track) elif track.kind == "video": self._videoHandler._onTrack(track) def onDataChannel(self, callback=None): """ Acts as a subscriber... """ return self._dataChannelSubscriber.subscribe(callback) def addDataChannel(self, name, ordered=True): """ Adds a data channel to the connection. Note that the RTCConnection adds a "default" channel automatically, which you can subscribe to directly. """ self._log.debug("Adding data channel to connection") if name in self._dataChannels or name == "default": raise KeyError("Data channel %s already exists", name) dc = DataChannel(self._rtc.createDataChannel(name, ordered=ordered)) self._dataChannels[name] = dc return dc def getDataChannel(self, name): """ Returns the data channel with the given name. Please note that the "default" channel is considered special, and is not returned. """ if name == "default": raise KeyError( "Default channel not available for 'get'. Use the RTCConnection's subscribe and put_nowait methods for access to it." ) return self._dataChannels[name] @property def video(self): """ Convenience function - you can subscribe to it to get video frames once they show up """ return self._videoHandler @property def audio(self): """ Convenience function - you can subscribe to it to get video frames once they show up """ return self._audioHandler def close(self): """ If the loop is running, returns a future that will close the connection. Otherwise, runs the loop temporarily to complete closing. """ super().close() # And closes all tracks self.video.close() self.audio.close() for dc in self._dataChannels: self._dataChannels[dc].close() self._dataChannelSubscriber.close() if self._loop.is_running(): self._log.debug("Loop is running - close will return a future!") return asyncio.ensure_future(self._rtc.close()) else: self._loop.run_until_complete(self._rtc.close()) return None def send(self, msg): """ Send is an alias for put_nowait - makes it easier for people new to rtcbot to understand what is going on """ self.put_nowait(msg)
async def subscribe(session, room, feed, recorder, create_dc=False): pc = RTCPeerConnection() pcs.add(pc) if create_dc: channel = pc.createDataChannel("chat") channels["rx"] = channel @channel.on("open") async def on_open(): print("Data Channel Opened!") @pc.on("track") async def on_track(track): print("Track %s received" % track.kind) if track.kind == "video" and recorder is not None: recorder.addTrack(track) if track.kind == "audio" and recorder is not None: recorder.addTrack(track) @pc.on('datachannel') async def on_datachannel(channel): @channel.on("message") async def on_message(message): # this is where we get data (ie. control messages) back from the server # these messages are basically our "remote control" events channel_log(channel, current_stamp(), message) #echo back just for fun global channels #channel_send(channels["tx"], message) # subscribe plugin = await session.attach("janus.plugin.videoroom") response = await plugin.send({ "body": { "request": "join", "ptype": "subscriber", "room": room, "feed": feed } }) # apply offer await pc.setRemoteDescription( RTCSessionDescription(sdp=response["jsep"]["sdp"], type=response["jsep"]["type"])) # send answer await pc.setLocalDescription(await pc.createAnswer()) response = await plugin.send({ "body": { "request": "start" }, "jsep": { "sdp": pc.localDescription.sdp, "trickle": False, "type": pc.localDescription.type, }, }) if recorder is not None: await recorder.start()
class RTCConnection(SubscriptionProducerConsumer): _log = logging.getLogger("rtcbot.RTCConnection") def __init__( self, defaultChannelOrdered=True, loop=None, rtcConfiguration=RTCConfiguration( [RTCIceServer(urls="stun:stun.l.google.com:19302")]), ): super().__init__( directPutSubscriptionType=asyncio.Queue, defaultSubscriptionType=asyncio.Queue, logger=self._log, ) self._loop = loop if self._loop is None: self._loop = asyncio.get_event_loop() self._dataChannels = {} # These allow us to easily signal when the given events happen self._dataChannelSubscriber = SubscriptionProducer( logger=self._log.getChild("dataChannelSubscriber")) self._rtc = RTCPeerConnection(configuration=rtcConfiguration) self._rtc.on("datachannel", self._onDatachannel) # self._rtc.on("iceconnectionstatechange", self._onIceConnectionStateChange) self._rtc.on("track", self._onTrack) self._hasRemoteDescription = False self._defaultChannelOrdered = defaultChannelOrdered self._videoHandler = ConnectionVideoHandler(self._rtc) self._audioHandler = ConnectionAudioHandler(self._rtc) async def getLocalDescription(self, description=None): """ Gets the description to send on. Creates an initial description if no remote description was passed, and creates a response if a remote was given, """ if self._hasRemoteDescription or description is not None: # This means that we received an offer - either the remote description # was already set, or we passed in a description. In either case, # instead of initializing a new connection, we prepare a response if not self._hasRemoteDescription: await self.setRemoteDescription(description) self._log.debug("Creating response to connection offer") try: answer = await self._rtc.createAnswer() except AttributeError: self._log.exception( "\n>>> Looks like the offer didn't include the necessary info to set up audio/video. See RTCConnection.video.offerToReceive(). <<<\n\n" ) raise await self._rtc.setLocalDescription(answer) return { "sdp": self._rtc.localDescription.sdp, "type": self._rtc.localDescription.type, } # There was no remote description, which means that we are initializing the # connection. # Before starting init, we create a default data channel for the connection self._log.debug("Setting up default data channel") channel = DataChannel( self._rtc.createDataChannel("default", ordered=self._defaultChannelOrdered)) # Subscribe the default channel directly to our own inputs and outputs. # We have it listen to our own self._get, and write to our self._put_nowait channel.putSubscription(NoClosedSubscription(self._get)) channel.subscribe(self._put_nowait) channel.onReady(lambda: self._setReady(channel.ready)) self._dataChannels[channel.name] = channel # Make sure we offer to receive video and audio if if isn't set up yet with # all the receiving transceivers if len(self.video._senders) < self.video._offerToReceive: self._log.debug("Offering to receive video") for i in range(self.video._offerToReceive - len(self.video._senders)): self._rtc.addTransceiver("video", "recvonly") if len(self.audio._senders) < self.audio._offerToReceive: self._log.debug("Offering to receive audio") for i in range(self.audio._offerToReceive - len(self.audio._senders)): self._rtc.addTransceiver("audio", "recvonly") self._log.debug("Creating new connection offer") offer = await self._rtc.createOffer() await self._rtc.setLocalDescription(offer) return { "sdp": self._rtc.localDescription.sdp, "type": self._rtc.localDescription.type, } async def setRemoteDescription(self, description): self._log.debug("Setting remote connection description") await self._rtc.setRemoteDescription( RTCSessionDescription(**description)) self._hasRemoteDescription = True def _onDatachannel(self, channel): """ When a data channel comes in, adds it to the data channels, and sets up its messaging and stuff. """ channel = DataChannel(channel) self._log.debug("Got channel: %s", channel.name) if channel.name == "default": # Subscribe the default channel directly to our own inputs and outputs. # We have it listen to our own self._get, and write to our self._put_nowait channel.putSubscription(NoClosedSubscription(self._get)) channel.subscribe(self._put_nowait) channel.onReady(lambda: self._setReady(channel.ready)) # Set the default channel self._defaultChannel = channel else: self._dataChannelSubscriber.put_nowait(channel) self._dataChannels[channel.name] = channel def _onTrack(self, track): self._log.debug("Received %s track from connection", track.kind) if track.kind == "audio": self._audioHandler._onTrack(track) elif track.kind == "video": self._videoHandler._onTrack(track) def onDataChannel(self, callback=None): """ Acts as a subscriber... """ return self._dataChannelSubscriber.subscribe(callback) def addDataChannel(self, name, ordered=True): """ Adds a data channel to the connection. Note that the RTCConnection adds a "default" channel automatically, which you can subscribe to directly. """ self._log.debug("Adding data channel to connection") if name in self._dataChannels or name == "default": raise KeyError("Data channel %s already exists", name) dc = DataChannel(self._rtc.createDataChannel(name, ordered=ordered)) self._dataChannels[name] = dc return dc def getDataChannel(self, name): """ Returns the data channel with the given name. Please note that the "default" channel is considered special, and is not returned. """ if name == "default": raise KeyError( "Default channel not available for 'get'. Use the RTCConnection's subscribe and put_nowait methods for access to it." ) return self._dataChannels[name] @property def video(self): """ Convenience function - you can subscribe to it to get video frames once they show up """ return self._videoHandler @property def audio(self): """ Convenience function - you can subscribe to it to get audio once a stream is received """ return self._audioHandler def close(self): """ If the loop is running, returns a future that will close the connection. Otherwise, runs the loop temporarily to complete closing. """ super().close() # And closes all tracks self.video.close() self.audio.close() for dc in self._dataChannels: self._dataChannels[dc].close() self._dataChannelSubscriber.close() if self._loop.is_running(): self._log.debug("Loop is running - close will return a future!") return asyncio.ensure_future(self._rtc.close()) else: self._loop.run_until_complete(self._rtc.close()) return None def send(self, msg): """ Send is an alias for put_nowait - makes it easier for people new to rtcbot to understand what is going on """ self.put_nowait(msg)
def test_connect_datachannel(self): pc1 = RTCPeerConnection() pc1_data_messages = [] pc1_states = track_states(pc1) pc2 = RTCPeerConnection() pc2_data_channels = [] pc2_data_messages = [] pc2_states = track_states(pc2) @pc2.on('datachannel') def on_datachannel(channel): self.assertEqual(channel.readyState, 'open') pc2_data_channels.append(channel) @channel.on('message') def on_message(message): pc2_data_messages.append(message) if isinstance(message, str): channel.send('string-echo: ' + message) else: channel.send(b'binary-echo: ' + message) # create data channel dc = pc1.createDataChannel('chat', protocol='bob') self.assertEqual(dc.label, 'chat') self.assertEqual(dc.protocol, 'bob') self.assertEqual(dc.readyState, 'connecting') # send messages dc.send('hello') dc.send('') dc.send(b'\x00\x01\x02\x03') dc.send(b'') dc.send(LONG_DATA) with self.assertRaises(ValueError) as cm: dc.send(1234) self.assertEqual(str(cm.exception), "Cannot send unsupported data type: <class 'int'>") @dc.on('message') def on_message(message): pc1_data_messages.append(message) # create offer offer = run(pc1.createOffer()) self.assertEqual(offer.type, 'offer') self.assertTrue('m=application ' in offer.sdp) self.assertFalse('a=candidate:' in offer.sdp) run(pc1.setLocalDescription(offer)) self.assertEqual(pc1.iceConnectionState, 'new') self.assertEqual(pc1.iceGatheringState, 'complete') self.assertTrue('m=application ' in pc1.localDescription.sdp) self.assertTrue('a=candidate:' in pc1.localDescription.sdp) self.assertTrue('a=sctpmap:5000 webrtc-datachannel 65535' in pc1.localDescription.sdp) self.assertTrue('a=fingerprint:sha-256' in pc1.localDescription.sdp) self.assertTrue('a=setup:actpass' in pc1.localDescription.sdp) # handle offer run(pc2.setRemoteDescription(pc1.localDescription)) self.assertEqual(pc2.remoteDescription, pc1.localDescription) self.assertEqual(len(pc2.getReceivers()), 0) self.assertEqual(len(pc2.getSenders()), 0) self.assertEqual(len(pc2.getSenders()), 0) # create answer answer = run(pc2.createAnswer()) self.assertEqual(answer.type, 'answer') self.assertTrue('m=application ' in answer.sdp) self.assertFalse('a=candidate:' in answer.sdp) run(pc2.setLocalDescription(answer)) self.assertEqual(pc2.iceConnectionState, 'checking') self.assertEqual(pc2.iceGatheringState, 'complete') self.assertTrue('m=application ' in pc2.localDescription.sdp) self.assertTrue('a=candidate:' in pc2.localDescription.sdp) self.assertTrue('a=sctpmap:5000 webrtc-datachannel 65535' in pc2.localDescription.sdp) self.assertTrue('a=fingerprint:sha-256' in pc2.localDescription.sdp) self.assertTrue('a=setup:active' in pc2.localDescription.sdp) # handle answer run(pc1.setRemoteDescription(pc2.localDescription)) self.assertEqual(pc1.remoteDescription, pc2.localDescription) self.assertEqual(pc1.iceConnectionState, 'checking') # check outcome run(asyncio.sleep(1)) self.assertEqual(pc1.iceConnectionState, 'completed') self.assertEqual(pc2.iceConnectionState, 'completed') self.assertEqual(dc.readyState, 'open') # check pc2 got a datachannel self.assertEqual(len(pc2_data_channels), 1) self.assertEqual(pc2_data_channels[0].label, 'chat') self.assertEqual(pc2_data_channels[0].protocol, 'bob') # check pc2 got messages run(asyncio.sleep(1)) self.assertEqual(pc2_data_messages, [ 'hello', '', b'\x00\x01\x02\x03', b'', LONG_DATA, ]) # check pc1 got replies self.assertEqual(pc1_data_messages, [ 'string-echo: hello', 'string-echo: ', b'binary-echo: \x00\x01\x02\x03', b'binary-echo: ', b'binary-echo: ' + LONG_DATA, ]) # close data channel dc.close() self.assertEqual(dc.readyState, 'closed') # close run(pc1.close()) run(pc2.close()) self.assertEqual(pc1.iceConnectionState, 'closed') self.assertEqual(pc2.iceConnectionState, 'closed') # check state changes self.assertEqual(pc1_states['iceConnectionState'], ['new', 'checking', 'completed', 'closed']) self.assertEqual(pc1_states['iceGatheringState'], ['new', 'gathering', 'complete']) self.assertEqual(pc1_states['signalingState'], ['stable', 'have-local-offer', 'stable', 'closed']) self.assertEqual(pc2_states['iceConnectionState'], ['new', 'checking', 'completed', 'closed']) self.assertEqual(pc2_states['iceGatheringState'], ['new', 'gathering', 'complete']) self.assertEqual(pc2_states['signalingState'], ['stable', 'have-remote-offer', 'stable', 'closed'])
async def ask_stream(interval): global blackhole await asyncio.sleep(interval) print("Haetaan streami") pc = RTCPeerConnection() pc_id = "PeerConnection(%s)" % uuid.uuid4() pcs.add(pc) def log_info(msg, *args): logger.info(pc_id + " " + msg, *args) # Videolle on kanava "track" pc.createDataChannel("track") pc.addTransceiver("video", direction="recvonly") # Tämä luo itse offerin oikeassa muodossa await pc.setLocalDescription(await pc.createOffer()) # Asetetaan pyynnön parametreiksi params = { "sdp": pc.localDescription.sdp, "type": pc.localDescription.type, "listen_video": True } @pc.on("track") def on_track(track): log_info("Track %s received from other server", track.kind) blackhole.addTrack(track) ''' if track.kind == "audio": pc.addTrack(player.audio) elif track.kind == "video": create_broadcast(track) pc.addTrack(relay.subscribe(broadcast)) ''' @track.on("ended") async def on_ended(): log_info("Track %s ended", track.kind) @pc.on("connectionstatechange") async def on_connectionstatechange(): log_info("Connection state is %s", pc.connectionState) if pc.connectionState == "failed": await pc.close() pcs.discard(pc) status = False session = ClientSession() # POST-pyyntö dispatcherille while not status: status = True await asyncio.sleep(1) try: res = await session.post('https://localhost:8080/offer', json=params, ssl=False, timeout=3) except: status = False continue print("onko jumissa") if res.status == "500": status = False continue try: result = await res.json() except: status = False continue answer = RTCSessionDescription(sdp=result["sdp"], type=result["type"]) await session.close() #print(answer.sdp) await pc.setRemoteDescription(answer)
class WebRTCVPN: def __init__(self): self.pc = RTCPeerConnection() self.channel = None async def create_offer(self): channel = self.pc.createDataChannel("chat") self.channel = channel await self.pc.setLocalDescription(await self.pc.createOffer()) return object_to_string(self.pc.localDescription) async def create_answer(self, offer): offer = object_from_string(offer) await self.pc.setRemoteDescription(offer) await self.pc.setLocalDescription(await self.pc.createAnswer()) @self.pc.on("datachannel") def on_datachannel(channel): self.channel = channel return object_to_string(self.pc.localDescription) async def set_answer(self, answer): answer = object_from_string(answer) await self.pc.setRemoteDescription(answer) def get_channel(self): return self.channel def get_pc(self): return self.pc def create_tuntap(self, name, address, mtu, channel): self.tap = tuntap.Tun(name=name) self.tap.open() #channel.on("message")(self.tap.fd.write) @channel.on("message") def on_message(message): self.tap.fd.write(message) def tun_reader(): data = self.tap.fd.read(self.tap.mtu) channel_state = self.channel.transport.transport.state if data and channel_state == "connected": channel.send(data) loop = asyncio.get_event_loop() loop.add_reader(self.tap.fd, tun_reader) self.tap.up() ip = IPRoute() index = ip.link_lookup(ifname=name)[0] ip.addr('add', index=index, address=address, mask=24) ip.link("set", index=index, mtu=mtu) def set_route(self, dst, gateway): ip = IPRoute() ip.route('add', dst=dst, gateway=gateway) async def input(self): loop = asyncio.get_event_loop() reader = asyncio.StreamReader(loop=loop) read_pipe = sys.stdin read_transport, _ = await loop.connect_read_pipe( lambda: asyncio.StreamReaderProtocol(reader), read_pipe ) data = await reader.readline() return data.decode(read_pipe.encoding) async def hold(self): loop = asyncio.get_event_loop() reader = asyncio.StreamReader(loop=loop) data = await reader.readline() return data async def monitor(self): loop = asyncio.get_event_loop() while True: if not self.channel == None: if self.channel.transport.transport.state == "closed": loop.run_until_complete(self.pc.close()) self.tap.close() break await asyncio.sleep(1) return "closed"
def test_connect_audio_and_video_and_data_channel(self): pc1 = RTCPeerConnection() pc1_states = track_states(pc1) pc2 = RTCPeerConnection() pc2_states = track_states(pc2) self.assertEqual(pc1.iceConnectionState, 'new') self.assertEqual(pc1.iceGatheringState, 'new') self.assertIsNone(pc1.localDescription) self.assertIsNone(pc1.remoteDescription) self.assertEqual(pc2.iceConnectionState, 'new') self.assertEqual(pc2.iceGatheringState, 'new') self.assertIsNone(pc2.localDescription) self.assertIsNone(pc2.remoteDescription) # create offer pc1.addTrack(AudioStreamTrack()) pc1.addTrack(VideoStreamTrack()) pc1.createDataChannel('chat', protocol='bob') offer = run(pc1.createOffer()) self.assertEqual(offer.type, 'offer') self.assertTrue('m=audio ' in offer.sdp) self.assertTrue('m=video ' in offer.sdp) self.assertTrue('m=application ' in offer.sdp) run(pc1.setLocalDescription(offer)) self.assertEqual(pc1.iceConnectionState, 'new') self.assertEqual(pc1.iceGatheringState, 'complete') # handle offer run(pc2.setRemoteDescription(pc1.localDescription)) self.assertEqual(pc2.remoteDescription, pc1.localDescription) self.assertEqual(len(pc2.getSenders()), 2) self.assertEqual(len(pc2.getReceivers()), 2) # create answer pc2.addTrack(AudioStreamTrack()) pc2.addTrack(VideoStreamTrack()) answer = run(pc2.createAnswer()) self.assertEqual(answer.type, 'answer') self.assertTrue('m=audio ' in answer.sdp) self.assertTrue('m=video ' in answer.sdp) self.assertTrue('m=application ' in answer.sdp) run(pc2.setLocalDescription(answer)) self.assertEqual(pc2.iceConnectionState, 'checking') self.assertEqual(pc2.iceGatheringState, 'complete') self.assertTrue('m=audio ' in pc2.localDescription.sdp) self.assertTrue('m=video ' in pc2.localDescription.sdp) self.assertTrue('m=application ' in pc2.localDescription.sdp) # handle answer run(pc1.setRemoteDescription(pc2.localDescription)) self.assertEqual(pc1.remoteDescription, pc2.localDescription) self.assertEqual(pc1.iceConnectionState, 'checking') # check outcome run(asyncio.sleep(1)) self.assertEqual(pc1.iceConnectionState, 'completed') self.assertEqual(pc2.iceConnectionState, 'completed') # check a single transport is used self.assertBundled(pc1) self.assertBundled(pc2) # close run(pc1.close()) run(pc2.close()) self.assertEqual(pc1.iceConnectionState, 'closed') self.assertEqual(pc2.iceConnectionState, 'closed') # check state changes self.assertEqual(pc1_states['iceConnectionState'], ['new', 'checking', 'completed', 'closed']) self.assertEqual(pc1_states['iceGatheringState'], [ 'new', 'gathering', 'new', 'gathering', 'new', 'gathering', 'complete' ]) self.assertEqual(pc1_states['signalingState'], ['stable', 'have-local-offer', 'stable', 'closed']) self.assertEqual(pc2_states['iceConnectionState'], ['new', 'checking', 'completed', 'closed']) self.assertEqual(pc2_states['iceGatheringState'], ['new', 'gathering', 'complete']) self.assertEqual(pc2_states['signalingState'], ['stable', 'have-remote-offer', 'stable', 'closed'])
class Peer: """ The Peer class represents the local peer in a WebRTC application based on Hyperpeer. It manages both the Websocket connection with the signaling server and the peer-to-peer communication via WebRTC with remote peers. # Attributes id (string): id of the instance. readyState (PeerState): State of the peer instance. It may have one of the values specified in the class [PeerState](#peerstate). disconnection_event (asyncio.Event): Notify that the current peer connection is closing # Arguments server_address (str): URL of the Hyperpeer signaling server, it should include the protocol prefix *ws://* or *wss://* that specify the websocket protocol to use. peer_type (str): Peer type. It can be used by other peers to know the role of the peer in the current application. id (str): Peer unique identification string. Must be unique among all connected peers. If it's undefined or null, the server will assign a random string. key (str): Peer validation string. It may be used by the server to verify the peer. media_source (str): Path or URL of the media source or file. media_source_format (str): Specific format of the media source. Defaults to autodect. media_sink (str): Path or filename to write with incoming video. frame_generator (generator function): Generator function that produces video frames as [NumPy arrays](https://docs.scipy.org/doc/numpy/reference/arrays.html) with [sRGB format](https://en.wikipedia.org/wiki/SRGB) with 24 bits per pixel (8 bits for each color). It should use the `yield` statement to generate arrays with elements of type `uint8` and with shape (vertical-resolution, horizontal-resolution, 3). frame_consumer (function): Function used to consume incoming video frames as [NumPy arrays](https://docs.scipy.org/doc/numpy/reference/arrays.html) with [sRGB format](https://en.wikipedia.org/wiki/SRGB) with 24 bits per pixel (8 bits for each color). It should receive an argument called `frame` which will be a NumPy array with elements of type `uint8` and with shape (vertical-resolution, horizontal-resolution, 3). frame_rate (int): Streaming frame rate ssl_context (ssl.SSLContext): Oject used to manage SSL settings and certificates in the connection with the signaling server when using wss. See [ssl documentation](https://docs.python.org/3/library/ssl.html?highlight=ssl.sslcontext#ssl.SSLContext) for more details. datachannel_options (dict): Dictionary with the following keys: *label*, *maxPacketLifeTime*, *maxRetransmits*, *ordered*, and *protocol*. See the [documentation of *RTCPeerConnection.createDataChannel()*](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createDataChannel#RTCDataChannelInit_dictionary) method of the WebRTC API for more details. # Example ```python from hyperpeer import Peer, PeerState import asyncio import numpy # Function used to generate video frames. It simply produce random images. def video_frame_generator(): while True: frame = numpy.random.rand(720, 1280, 3) frame = numpy.uint8(frame * 100) yield frame # Frame counter received_frames = 0 # Function used for consuming incoming video frames. It simply counts frames. def video_frame_consumer(frame): global received_frames received_frames += 1 # Function used to consume incoming data. It simply print messages. def on_data(data): print('Remote message:') print(data) # Data channel settings. It sets the values for maximun throughout using UDP. datachannel_options = { 'label': 'data_channel', 'maxPacketLifeTime': None, 'maxRetransmits': 0, 'ordered': False, 'protocol': '' } # Instanciate peer peer = Peer('wss://*****:*****@self._datachannel.on('message') async def on_message(message): try: data = json.loads(message) except: raise TypeError('Received an invalid json message data') self._data = data try: for handler in self._data_handlers: if inspect.iscoroutinefunction(handler): await handler(data) else: handler(data) except Exception as e: logging.exception(e, traceback.format_exc()) raise e @self._datachannel.on('close') async def on_close(): if self.readyState == PeerState.CONNECTED: logging.info('Datachannel lost, disconnecting...') self.disconnection_event.set() @self._datachannel.on('error') async def on_error(error): logging.error('Datachannel error: ' + str(error)) self.disconnection_event.set() @self._pc.on('track') def on_track(track): """ Set the consumer or destination of the incomming video and audio tracks """ logging.info('Track %s received' % track.kind) if track.kind == 'audio': #webrtc_connection.addTrack(player.audio) #recorder.addTrack(track) pass elif track.kind == 'video': #local_video = VideoTransformTrack(track, transform=signal['video_transform']) #webrtc_connection.addTrack(local_video) if self._frame_consumer_feeder: self._track_consumer_task = asyncio.create_task( self._frame_consumer_feeder.feed_with(track)) @track.on('ended') async def on_ended(): logging.info('Remote track %s ended' % track.kind) if self.readyState == PeerState.CONNECTED: logging.info('Remote media track ended, disconnecting...') self.disconnection_event.set() #await recorder.stop() @self._pc.on('iceconnectionstatechange') async def on_iceconnectionstatechange(): """ Monitor the ICE connection state """ logging.info('ICE connection state of peer (%s) is %s', self.id, self._pc.iceConnectionState) if self._pc.iceConnectionState == 'failed': self.disconnection_event.set() elif self._pc.iceConnectionState == 'completed': # self._set_readyState(PeerState.CONNECTED) pass # Add media tracks if self._media_source: if self._media_source_format: self._player = MediaPlayer(self._media_source, format=self._media_source_format) else: self._player = MediaPlayer(self._media_source) if self._player.audio: self._pc.addTrack(self._player.audio) if self._player.video: self._pc.addTrack(self._player.video) @self._player.video.on('ended') async def on_ended(): logging.info('Local track %s ended' % self._player.video.kind) if self.readyState == PeerState.CONNECTED: logging.info('disconnecting...') self.disconnection_event.set() logging.info('Video player track added') elif self._frame_generator: if inspect.isgeneratorfunction(self._frame_generator): self._pc.addTrack( FrameGeneratorTrack(self._frame_generator, frame_rate=self._frame_rate)) logging.info('Video frame generator track added') else: logging.info('No video track to add') if initiator: logging.info('Initiating peer connection...') do = self._datachannel_options if do: self._datachannel = self._pc.createDataChannel( do['label'], do['maxPacketLifeTime'], do['maxRetransmits'], do['ordered'], do['protocol']) else: self._datachannel = self._pc.createDataChannel('data_channel') await self._pc.setLocalDescription(await self._pc.createOffer()) signal = { 'sdp': self._pc.localDescription.sdp, 'type': self._pc.localDescription.type } await self._send(signal) signal = await self._get_signal() if signal['type'] != 'answer': raise Exception('Expected answer from remote peer', signal) answer = RTCSessionDescription(sdp=signal['sdp'], type=signal['type']) await self._pc.setRemoteDescription(answer) @self._datachannel.on('open') async def on_open(): self._set_readyState(PeerState.CONNECTED) await add_datachannel_listeners() pass #asyncio.ensure_future(send_pings()) else: logging.info('Waiting for peer connection...') @self._pc.on('datachannel') async def on_datachannel(channel): self._datachannel = channel self._set_readyState(PeerState.CONNECTED) await add_datachannel_listeners() signal = await self._get_signal() if signal['type'] != 'offer': raise Exception('Expected offer from remote peer', signal) offer = RTCSessionDescription(sdp=signal['sdp'], type=signal['type']) await self._pc.setRemoteDescription(offer) answer = await self._pc.createAnswer() await self._pc.setLocalDescription(answer) answer = { 'sdp': self._pc.localDescription.sdp, 'type': self._pc.localDescription.type } await self._send(answer) logging.info('starting _handle_candidates_task...') self._handle_candidates_task = asyncio.create_task( self._handle_ice_candidates()) logging.info('sending local ice candidates...') # ice_servers = RTCIceGatherer.getDefaultIceServers() logging.debug(f'ice_servers: {ice_servers}') ice_gatherer = RTCIceGatherer(ice_servers) local_candidates = ice_gatherer.getLocalCandidates() logging.debug(f'local_candidates: {local_candidates}') for candidate in local_candidates: sdp = ( f"{candidate.foundation} {candidate.component} {candidate.protocol} " f"{candidate.priority} {candidate.ip} {candidate.port} typ {candidate.type}" ) if candidate.relatedAddress is not None: sdp += f" raddr {candidate.relatedAddress}" if candidate.relatedPort is not None: sdp += f" rport {candidate.relatedPort}" if candidate.tcpType is not None: sdp += f" tcptype {candidate.tcpType}" message = { "candidate": "candidate:" + sdp, "id": candidate.sdpMid, "label": candidate.sdpMLineIndex, "type": "candidate", } logging.info(message) await self._send(message) while self.readyState == PeerState.CONNECTING: await asyncio.sleep(0.2) if self._track_consumer_task: logging.info('starting _remote_track_monitor_task...') self._remote_track_monitor_task = asyncio.create_task( self._remote_track_monitor()) self._connection_monitor_task = asyncio.create_task( self._connection_monitor())
class WebRTCClient: def __init__(self, config: Config, track: ScreenCaptureTrack, input_handler: InputHandler): self._config = config peer_connection_config = config.get_peer_connection_config() ice_servers: List[RTCIceServer] = [ RTCIceServer(ice.url, ice.username, ice.credential) for ice in peer_connection_config.ice_servers ] self._pc = RTCPeerConnection(RTCConfiguration(iceServers=ice_servers)) self._track = track self._input_handler = input_handler self._pc.addTrack(self._track) self._control_channel: RTCDataChannel = self._pc.createDataChannel( "control") self._control_channel.on("message", self._on_message) self._lock = asyncio.Lock() self._connection_task: Optional[asyncio.Task[None]] = None async def _run(self): self._track.active = True try: # TODO(igarashi): Handle connection closed while True: if self._pc.sctp.transport.state == "closed": _logger.warn("stream cancelled by remote") break await asyncio.sleep(0.1) except asyncio.CancelledError: _logger.warn("operation cancelled") finally: self._connection_task = None self._track.active = False await self._pc.close() def _on_message(self, message_str: str) -> None: try: data = json.loads(message_str) message = models.from_dict(data) if isinstance(message, models.InputReport): self._input_handler.send(message) except Exception: _logger.exception("got an unexpected exception") async def _establish_connection(self): loop = asyncio.get_event_loop() signaling = get_signaling_method(self._config.signaling_plugin) ret = await signaling(self._pc) if not ret: raise RuntimeError( "signaling failed: failed to establish connection") self._connection_task = loop.create_task(self._run()) async def connect(self) -> None: await self._establish_connection() async def wait_until_complete(self) -> None: t = self._connection_task if t is None: return await t async def disconnect(self) -> None: _logger.info("disconnecting WebRTC peer connection") await self._pc.close()
class WebRTCConnection(BidirectionalConnection): loop: Any def __init__(self, node: AbstractNode) -> None: # WebRTC Connection representation # As we have a full-duplex connection, # it's necessary to use a node instance # inside of this connection. In order to # be able to process requests sent by # the other peer. # All the requests messages will be forwarded # to this node. self.node = node # EventLoop that manages async tasks (producer/consumer) # This structure is global and needs to be # defined beforehand. self.loop = loop # Message pool (High Priority) # These queues will be used to manage # async messages. try: self.producer_pool: asyncio.Queue = asyncio.Queue( loop=self.loop, ) # Request Messages / Request Responses self.consumer_pool: asyncio.Queue = asyncio.Queue( loop=self.loop, ) # Request Responses # Initialize a PeerConnection structure self.peer_connection = RTCPeerConnection() # Set channel descriptor as None # This attribute will be used for external classes # in order to verify if the connection channel # was established. self.channel: RTCDataChannel self._client_address: Optional[Address] = None except Exception as e: traceback_and_raise(e) async def _set_offer(self) -> str: """ Initialize a Real-Time Communication Data Channel, set data channel callbacks/tasks, and send offer payload message. :return: returns a signaling offer payload containing local description. :rtype: str """ try: # Use the Peer Connection structure to # set the channel as a RTCDataChannel. self.channel = self.peer_connection.createDataChannel( "datachannel", ) # Keep send buffer busy with chunks self.channel.bufferedAmountLowThreshold = 4 * DC_MAX_CHUNK_SIZE # This method will be called by aioRTC lib as a callback # function when the connection opens. @self.channel.on("open") async def on_open() -> None: # type : ignore self.__producer_task = asyncio.ensure_future(self.producer()) chunked_msg = [] chunks_pending = 0 # This method is the aioRTC "consumer" task # and will be running as long as connection remains. # At this point we're just setting the method behavior # It'll start running after the connection opens. @self.channel.on("message") async def on_message(raw: bytes) -> None: nonlocal chunked_msg, chunks_pending chunk = OrderedChunk.load(raw) message = chunk.data if message == DC_CHUNK_START_SIGN: chunks_pending = chunk.idx chunked_msg = [b""] * chunks_pending elif chunks_pending: if chunked_msg[chunk.idx] == b"": chunks_pending -= 1 chunked_msg[chunk.idx] = message if chunks_pending == 0: await self.consumer(msg=b"".join(chunked_msg)) else: # Forward all received messages to our own consumer method. await self.consumer(msg=message) # Set peer_connection to generate an offer message type. await self.peer_connection.setLocalDescription( await self.peer_connection.createOffer() ) # Generates the local description structure # and serialize it to string afterwards. local_description = object_to_string(self.peer_connection.localDescription) # Return the Offer local_description payload. return local_description except Exception as e: traceback_and_raise(e) async def _set_answer(self, payload: str) -> str: """ Receives a signaling offer payload, initialize/set Data channel callbacks/tasks, updates remote local description using offer's payload message and returns a signaling answer payload. :return: returns a signaling answer payload containing local description. :rtype: str """ try: @self.peer_connection.on("datachannel") def on_datachannel(channel: RTCDataChannel) -> None: self.channel = channel self.__producer_task = asyncio.ensure_future(self.producer()) chunked_msg = [] chunks_pending = 0 @self.channel.on("message") async def on_message(raw: bytes) -> None: nonlocal chunked_msg, chunks_pending chunk = OrderedChunk.load(raw) message = chunk.data if message == DC_CHUNK_START_SIGN: chunks_pending = chunk.idx chunked_msg = [b""] * chunks_pending elif chunks_pending: if chunked_msg[chunk.idx] == b"": chunks_pending -= 1 chunked_msg[chunk.idx] = message if chunks_pending == 0: await self.consumer(msg=b"".join(chunked_msg)) else: await self.consumer(msg=message) result = await self._process_answer(payload=payload) return validate_type(result, str) except Exception as e: traceback_and_raise(e) raise Exception("mypy workaound: should not get here") async def _process_answer(self, payload: str) -> Union[str, None]: # Converts payload received by # the other peer in aioRTC Object # instance. try: msg = object_from_string(payload) # Check if Object instance is a # description of RTC Session. if isinstance(msg, RTCSessionDescription): # Use the target's network address/metadata # to set the remote description of this peer. # This will basically say to this peer how to find/connect # with to other peer. await self.peer_connection.setRemoteDescription(msg) # If it's an offer message type, # generates your own local description # and send it back in order to tell # to the other peer how to find you. if msg.type == "offer": # Set peer_connection to generate an offer message type. await self.peer_connection.setLocalDescription( await self.peer_connection.createAnswer() ) # Generates the local description structure # and serialize it to string afterwards. local_description = object_to_string( self.peer_connection.localDescription ) # Returns the answer peer's local description return local_description except Exception as e: traceback_and_raise(e) return None async def producer(self) -> None: """ Async task to send messages to the other side. These messages will be enqueued by PySyft Node Clients by using PySyft routes and ClientConnection's inheritance. """ try: while True: # If self.producer_pool is empty, give up task queue priority # and give computing time to the next task. msg = await self.producer_pool.get() # If self.producer_pool.get() returns a message # send it as a binary using the RTCDataChannel. data = serialize(msg, to_bytes=True) data_len = len(data) if DC_CHUNKING_ENABLED and data_len > DC_MAX_CHUNK_SIZE: chunk_num = 0 done = False sent: asyncio.Future = asyncio.Future(loop=self.loop) def send_data_chunks() -> None: nonlocal chunk_num, data_len, done, sent # Send chunks until buffered amount is big or we're done while ( self.channel.bufferedAmount <= DC_MAX_BUFSIZE and not done ): start_offset = chunk_num * DC_MAX_CHUNK_SIZE end_offset = min( (chunk_num + 1) * DC_MAX_CHUNK_SIZE, data_len ) chunk = data[start_offset:end_offset] self.channel.send(OrderedChunk(chunk_num, chunk).save()) chunk_num += 1 if chunk_num * DC_MAX_CHUNK_SIZE >= data_len: done = True sent.set_result(True) if not done: # Set listener for next round of sending when buffer is empty self.channel.once("bufferedamountlow", send_data_chunks) chunk_count = math.ceil(data_len / DC_MAX_CHUNK_SIZE) self.channel.send( OrderedChunk(chunk_count, DC_CHUNK_START_SIGN).save() ) send_data_chunks() # Wait until all chunks are dispatched await sent else: self.channel.send(OrderedChunk(0, data).save()) except Exception as e: traceback_and_raise(e) def close(self) -> None: try: # Build Close Message to warn the other peer bye_msg = CloseConnectionMessage(address=Address()) self.channel.send(OrderedChunk(0, serialize(bye_msg, to_bytes=True)).save()) # Finish async tasks related with this connection self._finish_coroutines() except Exception as e: traceback_and_raise(e) def _finish_coroutines(self) -> None: try: asyncio.run(self.peer_connection.close()) self.__producer_task.cancel() except Exception as e: traceback_and_raise(e) async def consumer(self, msg: bytes) -> None: """ Async task to receive/process messages sent by the other side. These messages will be sent by the other peer as a service requests or responses for requests made by this connection previously (ImmediateSyftMessageWithReply). """ try: # Deserialize the received message _msg = _deserialize(blob=msg, from_bytes=True) # Check if it's NOT a response generated by a previous request # made by the client instance that uses this connection as a route. # PS: The "_client_address" attribute will be defined during # Node Client initialization. if _msg.address != self._client_address: # If it's a new service request, route it properly # using the node instance owned by this connection. # Immediate message with reply if isinstance(_msg, SignedImmediateSyftMessageWithReply): reply = self.recv_immediate_msg_with_reply(msg=_msg) await self.producer_pool.put(reply) # Immediate message without reply elif isinstance(_msg, SignedImmediateSyftMessageWithoutReply): self.recv_immediate_msg_without_reply(msg=_msg) elif isinstance(_msg, CloseConnectionMessage): # Just finish async tasks related with this connection self._finish_coroutines() # Eventual message without reply else: self.recv_eventual_msg_without_reply(msg=_msg) # If it's true, the message will have the client's address as destination. else: await self.consumer_pool.put(_msg) except Exception as e: traceback_and_raise(e) def recv_immediate_msg_with_reply( self, msg: SignedImmediateSyftMessageWithReply ) -> SignedImmediateSyftMessageWithoutReply: """ Executes/Replies requests instantly. :return: returns an instance of SignedImmediateSyftMessageWithReply :rtype: SignedImmediateSyftMessageWithoutReply """ # Execute node services now try: r = secrets.randbelow(100000) debug( f"> Before recv_immediate_msg_with_reply {r} {msg.message} {type(msg.message)}" ) reply = self.node.recv_immediate_msg_with_reply(msg=msg) debug( f"> After recv_immediate_msg_with_reply {r} {msg.message} {type(msg.message)}" ) return reply except Exception as e: traceback_and_raise(e) def recv_immediate_msg_without_reply( self, msg: SignedImmediateSyftMessageWithoutReply ) -> None: """ Executes requests instantly. """ try: r = secrets.randbelow(100000) debug( f"> Before recv_immediate_msg_without_reply {r} {msg.message} {type(msg.message)}" ) self.node.recv_immediate_msg_without_reply(msg=msg) debug( f"> After recv_immediate_msg_without_reply {r} {msg.message} {type(msg.message)}" ) except Exception as e: traceback_and_raise(e) def recv_eventual_msg_without_reply( self, msg: SignedEventualSyftMessageWithoutReply ) -> None: """ Executes requests eventually. """ try: self.node.recv_eventual_msg_without_reply(msg=msg) except Exception as e: traceback_and_raise(e) raise Exception("mypy workaound: should not get here") # TODO: fix this mypy madness def send_immediate_msg_with_reply( # type: ignore self, msg: SignedImmediateSyftMessageWithReply ) -> SignedImmediateSyftMessageWithReply: """ Sends high priority messages and wait for their responses. :return: returns an instance of SignedImmediateSyftMessageWithReply. :rtype: SignedImmediateSyftMessageWithReply """ try: # properly fix this! return validate_type( asyncio.run(self.send_sync_message(msg=msg)), object, ) except Exception as e: traceback_and_raise(e) raise Exception("mypy workaound: should not get here") def send_immediate_msg_without_reply( self, msg: SignedImmediateSyftMessageWithoutReply ) -> None: """ Sends high priority messages without waiting for their reply. """ try: # asyncio.run(self.producer_pool.put_nowait(msg)) self.producer_pool.put_nowait(msg) except Exception as e: traceback_and_raise(e) def send_eventual_msg_without_reply( self, msg: SignedEventualSyftMessageWithoutReply ) -> None: """ Sends low priority messages without waiting for their reply. """ try: asyncio.run(self.producer_pool.put(msg)) except Exception as e: traceback_and_raise(e) async def send_sync_message( self, msg: SignedImmediateSyftMessageWithReply ) -> SignedImmediateSyftMessageWithoutReply: """ Send sync messages generically. :return: returns an instance of SignedImmediateSyftMessageWithoutReply. :rtype: SignedImmediateSyftMessageWithoutReply """ try: # To ensure the sequence of sending / receiving messages # it's necessary to keep only a unique reference for reading # inputs (producer) and outputs (consumer). r = secrets.randbelow(100000) # To be able to perform this method synchronously (waiting for the reply) # without blocking async methods, we need to use queues. # Enqueue the message to be sent to the target. debug(f"> Before send_sync_message producer_pool.put blocking {r}") # self.producer_pool.put_nowait(msg) await self.producer_pool.put(msg) debug(f"> After send_sync_message producer_pool.put blocking {r}") # Wait for the response checking the consumer queue. debug(f"> Before send_sync_message consumer_pool.get blocking {r} {msg}") debug( f"> Before send_sync_message consumer_pool.get blocking {r} {msg.message}" ) response = await self.consumer_pool.get() debug(f"> After send_sync_message consumer_pool.get blocking {r}") return response except Exception as e: traceback_and_raise(e) async def async_check( self, before: float, timeout_secs: int, r: float ) -> SignedImmediateSyftMessageWithoutReply: while True: try: response = self.consumer_pool.get_nowait() return response except Exception as e: now = time.time() debug(f"> During send_sync_message consumer_pool.get blocking {r}. {e}") if now - before > timeout_secs: traceback_and_raise( Exception(f"send_sync_message timeout {timeout_secs} {r}") )