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()
Exemple #2
0
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
Exemple #3
0
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))
Exemple #4
0
    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)
Exemple #5
0
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"]))
Exemple #6
0
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')
Exemple #9
0
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)
Exemple #10
0
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)
Exemple #11
0
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()
Exemple #12
0
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'])
Exemple #14
0
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)
Exemple #15
0
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"
Exemple #16
0
    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'])
Exemple #17
0
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())
Exemple #18
0
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()
Exemple #19
0
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}")
                    )