├── client ├── jetson_webrtc.py └── start.sh ├── docs └── Accelerated_GStreamer_User_Guide.pdf ├── patch ├── extract_to_usr_lib.tar.gz └── readme ├── readme ├── server ├── Dockerfile ├── Protocol.md ├── README.md ├── cert.pem ├── generate_cert.sh ├── key.pem ├── room-client.py ├── session-client.py └── simple-server.py └── www ├── Dockerfile ├── index.html └── webrtc.js /client/jetson_webrtc.py: -------------------------------------------------------------------------------- 1 | import random 2 | import ssl 3 | import websockets 4 | import asyncio 5 | import os 6 | import sys 7 | import json 8 | import argparse 9 | import time 10 | 11 | import gi 12 | gi.require_version('Gst', '1.0') 13 | from gi.repository import Gst 14 | gi.require_version('GstWebRTC', '1.0') 15 | from gi.repository import GstWebRTC 16 | gi.require_version('GstSdp', '1.0') 17 | from gi.repository import GstSdp 18 | 19 | PIPELINE_DESC = ''' 20 | webrtcbin name=sendrecv bundle-policy=max-bundle \ 21 | nvarguscamerasrc ! video/x-raw(memory:NVMM),width=1280, height=720, framerate=30/1, format=NV12 \ 22 | ! nvv4l2h264enc control-rate=1 bitrate=1000000 peak-bitrate=2000000 cabac-entropy-coding=true insert-sps-pps=true profile=4 ! video/x-h264, stream-format=(string)byte-stream \ 23 | ! rtph264pay config-interval=-1 \ 24 | ! queue ! application/x-rtp,media=video,encoding-name=H264,payload=97 ! sendrecv. \ 25 | alsasrc device=plughw:CARD=Device,DEV=0 ! audioconvert ! audioresample ! queue ! opusenc ! rtpopuspay ! \ 26 | queue ! application/x-rtp,media=audio,encoding-name=OPUS,payload=96 ! sendrecv. 27 | ''' 28 | 29 | class WebRTCClient: 30 | def __init__(self, id_, peer_id, server): 31 | self.id_ = id_ 32 | self.conn = None 33 | self.pipe = None 34 | self.webrtc = None 35 | self.peer_id = peer_id 36 | self.start_connection = False 37 | 38 | self.server = server or 'wss://webrtc.nirbheek.in:8443' 39 | 40 | async def connect(self): 41 | sslctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) 42 | self.conn = await websockets.connect(self.server, ssl=sslctx) 43 | await self.conn.send('HELLO %d' % our_id) 44 | 45 | async def setup_call(self): 46 | self.start_connection = True 47 | await self.conn.send('SESSION {}'.format(self.peer_id)) 48 | 49 | def send_sdp_offer(self, offer): 50 | text = offer.sdp.as_text() 51 | print ('Sending offer:\n%s' % text) 52 | msg = json.dumps({'sdp': {'type': 'offer', 'sdp': text}}) 53 | loop = asyncio.new_event_loop() 54 | loop.run_until_complete(self.conn.send(msg)) 55 | 56 | def on_offer_created(self, promise, _, __): 57 | promise.wait() 58 | reply = promise.get_reply() 59 | offer = reply.get_value('offer') 60 | promise = Gst.Promise.new() 61 | self.webrtc.emit('set-local-description', offer, promise) 62 | promise.interrupt() 63 | self.send_sdp_offer(offer) 64 | 65 | def on_negotiation_needed(self, element): 66 | promise = Gst.Promise.new_with_change_func(self.on_offer_created, element, None) 67 | element.emit('create-offer', None, promise) 68 | 69 | def send_ice_candidate_message(self, _, mlineindex, candidate): 70 | icemsg = json.dumps({'ice': {'candidate': candidate, 'sdpMLineIndex': mlineindex}}) 71 | loop = asyncio.new_event_loop() 72 | loop.run_until_complete(self.conn.send(icemsg)) 73 | 74 | def on_incoming_decodebin_stream(self, _, pad): 75 | if not pad.has_current_caps(): 76 | print (pad, 'has no caps, ignoring') 77 | return 78 | 79 | caps = pad.get_current_caps() 80 | assert (len(caps)) 81 | s = caps[0] 82 | name = s.get_name() 83 | if name.startswith('video'): 84 | q = Gst.ElementFactory.make('queue') 85 | conv = Gst.ElementFactory.make('videoconvert') 86 | sink = Gst.ElementFactory.make('autovideosink') 87 | self.pipe.add(q, conv, sink) 88 | self.pipe.sync_children_states() 89 | pad.link(q.get_static_pad('sink')) 90 | q.link(conv) 91 | conv.link(sink) 92 | elif name.startswith('audio'): 93 | q = Gst.ElementFactory.make('queue') 94 | conv = Gst.ElementFactory.make('audioconvert') 95 | resample = Gst.ElementFactory.make('audioresample') 96 | sink = Gst.ElementFactory.make('autoaudiosink') 97 | self.pipe.add(q, conv, resample, sink) 98 | self.pipe.sync_children_states() 99 | pad.link(q.get_static_pad('sink')) 100 | q.link(conv) 101 | conv.link(resample) 102 | resample.link(sink) 103 | 104 | def on_incoming_stream(self, _, pad): 105 | return 106 | if pad.direction != Gst.PadDirection.SRC: 107 | return 108 | 109 | decodebin = Gst.ElementFactory.make('decodebin') 110 | decodebin.connect('pad-added', self.on_incoming_decodebin_stream) 111 | self.pipe.add(decodebin) 112 | decodebin.sync_state_with_parent() 113 | self.webrtc.link(decodebin) 114 | 115 | def start_pipeline(self): 116 | self.pipe = Gst.parse_launch(PIPELINE_DESC) 117 | self.webrtc = self.pipe.get_by_name('sendrecv') 118 | self.webrtc.connect('on-negotiation-needed', self.on_negotiation_needed) 119 | self.webrtc.connect('on-ice-candidate', self.send_ice_candidate_message) 120 | self.webrtc.connect('pad-added', self.on_incoming_stream) 121 | self.pipe.set_state(Gst.State.PLAYING) 122 | 123 | async def handle_sdp(self, message): 124 | assert (self.webrtc) 125 | msg = json.loads(message) 126 | if 'sdp' in msg: 127 | sdp = msg['sdp'] 128 | assert(sdp['type'] == 'answer') 129 | sdp = sdp['sdp'] 130 | print ('Received answer:\n%s' % sdp) 131 | res, sdpmsg = GstSdp.SDPMessage.new() 132 | GstSdp.sdp_message_parse_buffer(bytes(sdp.encode()), sdpmsg) 133 | answer = GstWebRTC.WebRTCSessionDescription.new(GstWebRTC.WebRTCSDPType.ANSWER, sdpmsg) 134 | promise = Gst.Promise.new() 135 | self.webrtc.emit('set-remote-description', answer, promise) 136 | promise.interrupt() 137 | elif 'ice' in msg: 138 | ice = msg['ice'] 139 | candidate = ice['candidate'] 140 | sdpmlineindex = ice['sdpMLineIndex'] 141 | self.webrtc.emit('add-ice-candidate', sdpmlineindex, candidate) 142 | 143 | async def loop(self): 144 | assert self.conn 145 | async for message in self.conn: 146 | if message.startswith('ONLINE:'): 147 | if not self.start_connection: 148 | self.peer_id = int(message.split(':')[1]) 149 | print ('new peer online %s' % self.peer_id) 150 | time.sleep(2) 151 | await self.setup_call() 152 | elif message == 'HELLO': 153 | pass 154 | #await self.setup_call() 155 | elif message == 'SESSION_OK': 156 | self.start_pipeline() 157 | elif message.startswith('ERROR'): 158 | print (message) 159 | return 1 160 | else: 161 | await self.handle_sdp(message) 162 | return 0 163 | 164 | 165 | def check_plugins(): 166 | needed = ["opus", "vpx", "nice", "webrtc", "dtls", "srtp", "rtp", 167 | "rtpmanager", "videotestsrc", "audiotestsrc"] 168 | missing = list(filter(lambda p: Gst.Registry.get().find_plugin(p) is None, needed)) 169 | if len(missing): 170 | print('Missing gstreamer plugins:', missing) 171 | return False 172 | return True 173 | 174 | 175 | if __name__=='__main__': 176 | Gst.init(None) 177 | if not check_plugins(): 178 | sys.exit(1) 179 | parser = argparse.ArgumentParser() 180 | parser.add_argument('peerid', help='String ID of the peer to connect to') 181 | parser.add_argument('--server', help='Signalling server to connect to, eg "wss://127.0.0.1:8443"') 182 | args = parser.parse_args() 183 | our_id = random.randrange(10, 10000) 184 | c = WebRTCClient(our_id, args.peerid, args.server) 185 | asyncio.get_event_loop().run_until_complete(c.connect()) 186 | res = asyncio.get_event_loop().run_until_complete(c.loop()) 187 | sys.exit(res) 188 | -------------------------------------------------------------------------------- /client/start.sh: -------------------------------------------------------------------------------- 1 | python3 jetson_webrtc.py --server=wss://127.0.0.1:8443 -1 2 | -------------------------------------------------------------------------------- /docs/Accelerated_GStreamer_User_Guide.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walletiger/jetson_nano_webrtc/5146122d9cec3a58560b0917fa7d4899b6907ebf/docs/Accelerated_GStreamer_User_Guide.pdf -------------------------------------------------------------------------------- /patch/extract_to_usr_lib.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walletiger/jetson_nano_webrtc/5146122d9cec3a58560b0917fa7d4899b6907ebf/patch/extract_to_usr_lib.tar.gz -------------------------------------------------------------------------------- /patch/readme: -------------------------------------------------------------------------------- 1 | orginal webrtcbin plugin in jetson-nano does not work for dependence missing nicesrc/nicesink, 2 | i just re-compiling the plugin :) 3 | 4 | usage: 5 | tar xzf extract_to_usr_lib.tar.gz -C /usr/lib 6 | 7 | 8 | -------------------------------------------------------------------------------- /readme: -------------------------------------------------------------------------------- 1 | 1 webrtcbin plugin in jetson nano does not work for missing dependence nicesrc/nicesink and GstWebRTC-1.0.typelib, 2 | the patch direction just fixed this problems. 3 | 4 | 2 modified https://github.com/centricular/gstwebrtc-demos.git , the demo needs to start browsers first and then start jeston-client, 5 | just add client-list-push to auto connect 6 | 7 | 3 modified the webrtc client, to use jetson_nano hw-acceled pipline and hardware 8 | 9 | start_the_server: 10 | cd server && python3 simple-server.py 11 | 12 | 13 | start_the_client: 14 | cd client && python3 jetson_webrtc.py --server=wss://127.0.0.1:8443 -1 15 | 16 | 17 | start the browser: 18 | https://xxx.xxx.xx.xxx 19 | 20 | 21 | 22 | faq: 23 | 1 pipeline start failed 24 | I use the csi camera and usb-microphone, to replace it with your environment . reference "docs/Accelerated_GStreamer_User_Guide.pdf " 25 | 26 | 27 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | 3 | RUN pip3 install --user websockets 4 | 5 | WORKDIR /opt/ 6 | COPY . /opt/ 7 | 8 | CMD python -u ./simple-server.py --disable-ssl 9 | -------------------------------------------------------------------------------- /server/Protocol.md: -------------------------------------------------------------------------------- 1 | ## Terminology 2 | 3 | ### Client 4 | 5 | A GStreamer-based application 6 | 7 | ### Browser 8 | 9 | A JS application that runs in the browser and uses built-in browser webrtc APIs 10 | 11 | ### Peer 12 | 13 | Any webrtc-using application that can participate in a call 14 | 15 | ### Signalling server 16 | 17 | Basic websockets server implemented in Python that manages the peers list and shovels data between peers 18 | 19 | ## Overview 20 | 21 | This is a basic protocol for doing 1-1 audio+video calls between a gstreamer app and a JS app in a browser. 22 | 23 | ## Peer registration 24 | 25 | Peers must register with the signalling server before a call can be initiated. The server connection should stay open as long as the peer is available or in a call. 26 | 27 | This protocol builds upon https://github.com/shanet/WebRTC-Example/ 28 | 29 | * Connect to the websocket server 30 | * Send `HELLO ` where `` is a string which will uniquely identify this peer 31 | * Receive `HELLO` 32 | * Any other message starting with `ERROR` is an error. 33 | 34 | ### 1-1 calls with a 'session' 35 | 36 | * To connect to a single peer, send `SESSION ` where `` identifies the peer to connect to, and receive `SESSION_OK` 37 | * All further messages will be forwarded to the peer 38 | * The call negotiation with the peer can be started by sending JSON encoded SDP and ICE 39 | 40 | * Closure of the server connection means the call has ended; either because the other peer ended it or went away 41 | * To end the call, disconnect from the server. You may reconnect again whenever you wish. 42 | 43 | ### Multi-party calls with a 'room' 44 | 45 | * To create a multi-party call, you must first register (or join) a room. Send `ROOM ` where `` is a unique room name 46 | * Receive `ROOM_OK ` from the server if this is a new room, or `ROOM_OK ...` where `` are unique identifiers for the peers already in the room 47 | * To send messages to a specific peer within the room for call negotiation (or any other purpose, use `ROOM_PEER_MSG ` 48 | * When a new peer joins the room, you will receive a `ROOM_PEER_JOINED ` message 49 | - For the purposes of convention and to avoid overwhelming newly-joined peers, offers must only be sent by the newly-joined peer 50 | * When a peer leaves the room, you will receive a `ROOM_PEER_LEFT ` message 51 | - You should stop sending/receiving media from/to this peer 52 | * To get a list of all peers currently in the room, send `ROOM_PEER_LIST` and receive `ROOM_PEER_LIST ...` 53 | - This list will never contain your own `` 54 | - In theory you should never need to use this since you are guaranteed to receive JOINED and LEFT messages for all peers in a room 55 | * You may stay connected to a room for as long as you like 56 | 57 | ## Negotiation 58 | 59 | Once a call has been setup with the signalling server, the peers must negotiate SDP and ICE candidates with each other. 60 | 61 | The calling side must create an SDP offer and send it to the peer as a JSON object: 62 | 63 | ```json 64 | { 65 | "sdp": { 66 | "sdp": "o=- [....]", 67 | "type": "offer" 68 | } 69 | } 70 | ``` 71 | 72 | The callee must then reply with an answer: 73 | 74 | ```json 75 | { 76 | "sdp": { 77 | "sdp": "o=- [....]", 78 | "type": "answer" 79 | } 80 | } 81 | ``` 82 | 83 | ICE candidates must be exchanged similarly by exchanging JSON objects: 84 | 85 | 86 | ```json 87 | { 88 | "ice": { 89 | "candidate": ..., 90 | "sdpMLineIndex": ..., 91 | ... 92 | } 93 | } 94 | ``` 95 | 96 | Note that the structure of these is the same as that specified by the WebRTC spec. 97 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | Read Protocol.md 4 | 5 | ## Dependencies 6 | 7 | * Python 3 8 | * pip3 install --user websockets 9 | 10 | ## Example usage 11 | 12 | In three separate tabs, run consecutively: 13 | 14 | ```console 15 | $ ./generate_cert.sh 16 | $ ./simple-server.py 17 | ``` 18 | 19 | ### Session Based 20 | 21 | ```console 22 | $ ./session-client.py 23 | Our uid is 'ws-test-client-8f63b9' 24 | ``` 25 | 26 | ```console 27 | $ ./session-client.py --call ws-test-client-8f63b9 28 | ``` 29 | ### Room Based 30 | 31 | ```console 32 | $ ./room-client.py --room 123 33 | Our uid is 'ws-test-client-bdb5b9' 34 | Got ROOM_OK for room '123' 35 | ``` 36 | 37 | Another window 38 | 39 | ```console 40 | $ ./room-client.py --room 123 41 | Our uid is 'ws-test-client-78b59a' 42 | Got ROOM_OK for room '123' 43 | Sending offer to 'ws-test-client-bdb5b9' 44 | Sent: ROOM_PEER_MSG ws-test-client-bdb5b9 {"sdp": "initial sdp"} 45 | Got answer from 'ws-test-client-bdb5b9': {"sdp": "reply sdp"} 46 | ``` 47 | 48 | .. and similar output with more clients in the same room. 49 | -------------------------------------------------------------------------------- /server/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFjjCCA3agAwIBAgIJAKVEM7DZ73qoMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV 3 | BAYTAkNOMQswCQYDVQQIDAJzZDELMAkGA1UEBwwCeXQxDjAMBgNVBAoMBXdhbGxl 4 | MQ4wDAYDVQQLDAV0aWdlcjETMBEGA1UEAwwKd2FsbGV0aWdlcjAeFw0xOTA2MDcw 5 | NDQ2MjFaFw0yMDA2MDYwNDQ2MjFaMFwxCzAJBgNVBAYTAkNOMQswCQYDVQQIDAJz 6 | ZDELMAkGA1UEBwwCeXQxDjAMBgNVBAoMBXdhbGxlMQ4wDAYDVQQLDAV0aWdlcjET 7 | MBEGA1UEAwwKd2FsbGV0aWdlcjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC 8 | ggIBALi4BRbp+gsmIxBoZs8hJ64CYTj0Jc5QTvJuQUJlniqWCopckyNBal6wE5pI 9 | Vuz97pbvrOml/umHYM19S7Na5QeQhavYCXWx59D+K2n0w66U+KL7HWaLfEGCJgJi 10 | 5id3rR5EtRjhpc0zVudKFN8esg5DJDkg97O9tg3j8ZbzMfTlhByK6XxtGkoMQxmF 11 | Np6Q1dyF0rirTjjFN74PAxfQEzr3nMVX8hpldKwydTsO95rC6qowYKxl+EMJz08e 12 | uQYamXRFpN8+xu1WpqmElrwh2lx6UneOFx19R3hCJ/D/tIoLwBT7k0Hbd9hg++yh 13 | /JXJLJvpRx4Fa59/XK6yQIf8EiJwsYfKWqLiiQgm09GXjCyvaBz7JZxTFNLGHN9p 14 | ej1t9S/QeJM02zHeSl/BsxYIjMIdjYlL3+HcAejdP/aQ+Ffu4BR8WDhgsfJiW0Gb 15 | 1Egeu+qGPuryHgCeR2UVMZ2jZS/zkK1N0ABFtsgs6RP6j5KXxKO7IdUyWaprXg5E 16 | 6DrqMEpzZvQwWQ8LOEdmgJV+tTVS8/JXw6vTKqxMWmPz4dHzgqnx5OVHmur0szqw 17 | dBmw/7CM1c7uwcPomr1zMBR+6gMHInfPulMo1M41LDJvgMrDCYwPfaeEJag3cpjJ 18 | KGLV91PHJvZBwOmNIwams2BHLUzurotg6fvCcBqPXYGiWYCVAgMBAAGjUzBRMB0G 19 | A1UdDgQWBBShMgGYsx98TmeGGhsT6EfToAPNGjAfBgNVHSMEGDAWgBShMgGYsx98 20 | TmeGGhsT6EfToAPNGjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IC 21 | AQB/JD5zAyMh1x2Zkq8sBJNXgOVGslQyo24a90dYCg72FCKg0pRgVJcd4KCW+LI1 22 | 2xRNNB0eiq2AHeKl2STP3r/nhVL+/TWBLNHoM1F4irG60Siotna6UF6qQpitoE6M 23 | nT+i514ra5q2EmL70JfFIgy/nJlFUAas0gHsBN3vp7RvRluNCPRkiGDZPgDKTG2B 24 | yytEYB474BKn4eZp8fF9gZ1QNPjMuR//QRRXZN3ikwPH0wLHIFatHd7FH4sYicWz 25 | jow3qmcKZiPjSe7DLP1oaeYFVqJ0VJIWG/fkNCvFA1xygfap1n6CWnkc0Ql69l// 26 | yZL3VzaW32HxrY2DGmATXYpYiLfzT7FH5yIKnfrkhbl15g63Y5hEMpTH9CkCoXNf 27 | iH3g9PrhSmZBT3HMnWaEZGAe1SKtlFszkYWcwPp3yXjgFqA7derd9rxzmkTovcNd 28 | c4W7SXBPCCVqgNNFFfkc0+Y4QSUUK31lKE3b5Cvyd4OHJG61Mnhwk1RU+c3aW9TQ 29 | efQRFjj+CcDggJbSlMbfKwJ6ZrNsbXYd9hJKELMWkvJe59MHaIkEyUfwjIQY4xB6 30 | 3ynZvEdCt+7MLaff6Ccko1CdDSGv0ObWWfEVumF/GqafE8RxDgG3cuBJpB86CN0A 31 | JtRV01OXlN7Knvmd0zHyg8nKmIGAbIVnJco+loqGFucZsw== 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /server/generate_cert.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes 4 | -------------------------------------------------------------------------------- /server/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | 9OvUZPGSx91gdqq28qMk7nDCgDVAiUc= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /server/room-client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Test client for simple room-based multi-peer p2p calling 4 | # 5 | # Copyright (C) 2017 Centricular Ltd. 6 | # 7 | # Author: Nirbheek Chauhan 8 | # 9 | 10 | import sys 11 | import ssl 12 | import json 13 | import uuid 14 | import asyncio 15 | import websockets 16 | import argparse 17 | 18 | parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) 19 | parser.add_argument('--url', default='wss://localhost:8443', help='URL to connect to') 20 | parser.add_argument('--room', default=None, help='the room to join') 21 | 22 | options = parser.parse_args(sys.argv[1:]) 23 | 24 | SERVER_ADDR = options.url 25 | PEER_ID = 'ws-test-client-' + str(uuid.uuid4())[:6] 26 | ROOM_ID = options.room 27 | if ROOM_ID is None: 28 | print('--room argument is required') 29 | sys.exit(1) 30 | 31 | sslctx = False 32 | if SERVER_ADDR.startswith(('wss://', 'https://')): 33 | sslctx = ssl.create_default_context() 34 | # FIXME 35 | sslctx.check_hostname = False 36 | sslctx.verify_mode = ssl.CERT_NONE 37 | 38 | def get_answer_sdp(offer, peer_id): 39 | # Here we'd parse the incoming JSON message for ICE and SDP candidates 40 | print("Got: " + offer) 41 | sdp = json.dumps({'sdp': 'reply sdp'}) 42 | answer = 'ROOM_PEER_MSG {} {}'.format(peer_id, sdp) 43 | print("Sent: " + answer) 44 | return answer 45 | 46 | def get_offer_sdp(peer_id): 47 | sdp = json.dumps({'sdp': 'initial sdp'}) 48 | offer = 'ROOM_PEER_MSG {} {}'.format(peer_id, sdp) 49 | print("Sent: " + offer) 50 | return offer 51 | 52 | async def hello(): 53 | async with websockets.connect(SERVER_ADDR, ssl=sslctx) as ws: 54 | await ws.send('HELLO ' + PEER_ID) 55 | assert(await ws.recv() == 'HELLO') 56 | 57 | await ws.send('ROOM {}'.format(ROOM_ID)) 58 | 59 | sent_offers = set() 60 | # Receive messages 61 | while True: 62 | msg = await ws.recv() 63 | if msg.startswith('ERROR'): 64 | # On error, we bring down the webrtc pipeline, etc 65 | print('{!r}, exiting'.format(msg)) 66 | return 67 | if msg.startswith('ROOM_OK'): 68 | print('Got ROOM_OK for room {!r}'.format(ROOM_ID)) 69 | _, *room_peers = msg.split() 70 | for peer_id in room_peers: 71 | print('Sending offer to {!r}'.format(peer_id)) 72 | # Create a peer connection for each peer and start 73 | # exchanging SDP and ICE candidates 74 | await ws.send(get_offer_sdp(peer_id)) 75 | sent_offers.add(peer_id) 76 | continue 77 | elif msg.startswith('ROOM_PEER'): 78 | if msg.startswith('ROOM_PEER_JOINED'): 79 | _, peer_id = msg.split(maxsplit=1) 80 | print('Peer {!r} joined the room'.format(peer_id)) 81 | # Peer will send us an offer 82 | continue 83 | if msg.startswith('ROOM_PEER_LEFT'): 84 | _, peer_id = msg.split(maxsplit=1) 85 | print('Peer {!r} left the room'.format(peer_id)) 86 | continue 87 | elif msg.startswith('ROOM_PEER_MSG'): 88 | _, peer_id, msg = msg.split(maxsplit=2) 89 | if peer_id in sent_offers: 90 | print('Got answer from {!r}: {}'.format(peer_id, msg)) 91 | continue 92 | print('Got offer from {!r}, replying'.format(peer_id)) 93 | await ws.send(get_answer_sdp(msg, peer_id)) 94 | continue 95 | print('Unknown msg: {!r}, exiting'.format(msg)) 96 | return 97 | 98 | print('Our uid is {!r}'.format(PEER_ID)) 99 | 100 | try: 101 | asyncio.get_event_loop().run_until_complete(hello()) 102 | except websockets.exceptions.InvalidHandshake: 103 | print('Invalid handshake: are you sure this is a websockets server?\n') 104 | raise 105 | except ssl.SSLError: 106 | print('SSL Error: are you sure the server is using TLS?\n') 107 | raise 108 | -------------------------------------------------------------------------------- /server/session-client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Test client for simple 1-1 call signalling server 4 | # 5 | # Copyright (C) 2017 Centricular Ltd. 6 | # 7 | # Author: Nirbheek Chauhan 8 | # 9 | 10 | import sys 11 | import ssl 12 | import json 13 | import uuid 14 | import asyncio 15 | import websockets 16 | import argparse 17 | 18 | parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) 19 | parser.add_argument('--url', default='wss://localhost:8443', help='URL to connect to') 20 | parser.add_argument('--call', default=None, help='uid of peer to call') 21 | 22 | options = parser.parse_args(sys.argv[1:]) 23 | 24 | SERVER_ADDR = options.url 25 | CALLEE_ID = options.call 26 | PEER_ID = 'ws-test-client-' + str(uuid.uuid4())[:6] 27 | 28 | sslctx = False 29 | if SERVER_ADDR.startswith(('wss://', 'https://')): 30 | sslctx = ssl.create_default_context() 31 | # FIXME 32 | sslctx.check_hostname = False 33 | sslctx.verify_mode = ssl.CERT_NONE 34 | 35 | def reply_sdp_ice(msg): 36 | # Here we'd parse the incoming JSON message for ICE and SDP candidates 37 | print("Got: " + msg) 38 | reply = json.dumps({'sdp': 'reply sdp'}) 39 | print("Sent: " + reply) 40 | return reply 41 | 42 | def send_sdp_ice(): 43 | reply = json.dumps({'sdp': 'initial sdp'}) 44 | print("Sent: " + reply) 45 | return reply 46 | 47 | async def hello(): 48 | async with websockets.connect(SERVER_ADDR, ssl=sslctx) as ws: 49 | await ws.send('HELLO ' + PEER_ID) 50 | assert(await ws.recv() == 'HELLO') 51 | 52 | # Initiate call if requested 53 | if CALLEE_ID: 54 | await ws.send('SESSION {}'.format(CALLEE_ID)) 55 | 56 | # Receive messages 57 | sent_sdp = False 58 | while True: 59 | msg = await ws.recv() 60 | if msg.startswith('ERROR'): 61 | # On error, we bring down the webrtc pipeline, etc 62 | print('{!r}, exiting'.format(msg)) 63 | return 64 | if sent_sdp: 65 | print('Got reply sdp: ' + msg) 66 | return # Done 67 | if CALLEE_ID: 68 | if msg == 'SESSION_OK': 69 | await ws.send(send_sdp_ice()) 70 | sent_sdp = True 71 | else: 72 | print('Unknown reply: {!r}, exiting'.format(msg)) 73 | return 74 | else: 75 | await ws.send(reply_sdp_ice(msg)) 76 | return # Done 77 | 78 | print('Our uid is {!r}'.format(PEER_ID)) 79 | 80 | try: 81 | asyncio.get_event_loop().run_until_complete(hello()) 82 | except websockets.exceptions.InvalidHandshake: 83 | print('Invalid handshake: are you sure this is a websockets server?\n') 84 | raise 85 | except ssl.SSLError: 86 | print('SSL Error: are you sure the server is using TLS?\n') 87 | raise 88 | -------------------------------------------------------------------------------- /server/simple-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Example 1-1 call signalling server 4 | # 5 | # Copyright (C) 2017 Centricular Ltd. 6 | # 7 | # Author: Nirbheek Chauhan 8 | # 9 | 10 | import os 11 | import sys 12 | import ssl 13 | import logging 14 | import asyncio 15 | import websockets 16 | import argparse 17 | 18 | from concurrent.futures._base import TimeoutError 19 | 20 | parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) 21 | parser.add_argument('--addr', default='0.0.0.0', help='Address to listen on') 22 | parser.add_argument('--port', default=8443, type=int, help='Port to listen on') 23 | parser.add_argument('--keepalive-timeout', dest='keepalive_timeout', default=30, type=int, help='Timeout for keepalive (in seconds)') 24 | parser.add_argument('--cert-path', default=os.path.dirname(__file__)) 25 | parser.add_argument('--disable-ssl', default=False, help='Disable ssl', action='store_true') 26 | 27 | options = parser.parse_args(sys.argv[1:]) 28 | 29 | ADDR_PORT = (options.addr, options.port) 30 | KEEPALIVE_TIMEOUT = options.keepalive_timeout 31 | 32 | ############### Global data ############### 33 | 34 | # Format: {uid: (Peer WebSocketServerProtocol, 35 | # remote_address, 36 | # <'session'|room_id|None>)} 37 | peers = dict() 38 | # Format: {caller_uid: callee_uid, 39 | # callee_uid: caller_uid} 40 | # Bidirectional mapping between the two peers 41 | sessions = dict() 42 | # Format: {room_id: {peer1_id, peer2_id, peer3_id, ...}} 43 | # Room dict with a set of peers in each room 44 | rooms = dict() 45 | 46 | ############### Helper functions ############### 47 | 48 | async def recv_msg_ping(ws, raddr): 49 | ''' 50 | Wait for a message forever, and send a regular ping to prevent bad routers 51 | from closing the connection. 52 | ''' 53 | msg = None 54 | while msg is None: 55 | try: 56 | msg = await asyncio.wait_for(ws.recv(), KEEPALIVE_TIMEOUT) 57 | except TimeoutError: 58 | print('Sending keepalive ping to {!r} in recv'.format(raddr)) 59 | await ws.ping() 60 | return msg 61 | 62 | async def disconnect(ws, peer_id): 63 | ''' 64 | Remove @peer_id from the list of sessions and close our connection to it. 65 | This informs the peer that the session and all calls have ended, and it 66 | must reconnect. 67 | ''' 68 | global sessions 69 | if peer_id in sessions: 70 | del sessions[peer_id] 71 | # Close connection 72 | if ws and ws.open: 73 | # Don't care about errors 74 | asyncio.ensure_future(ws.close(reason='hangup')) 75 | 76 | async def cleanup_session(uid): 77 | if uid in sessions: 78 | other_id = sessions[uid] 79 | del sessions[uid] 80 | print("Cleaned up {} session".format(uid)) 81 | if other_id in sessions: 82 | del sessions[other_id] 83 | print("Also cleaned up {} session".format(other_id)) 84 | # If there was a session with this peer, also 85 | # close the connection to reset its state. 86 | if other_id in peers: 87 | print("Closing connection to {}".format(other_id)) 88 | wso, oaddr, _ = peers[other_id] 89 | del peers[other_id] 90 | await wso.close() 91 | 92 | async def cleanup_room(uid, room_id): 93 | room_peers = rooms[room_id] 94 | if uid not in room_peers: 95 | return 96 | room_peers.remove(uid) 97 | for pid in room_peers: 98 | wsp, paddr, _ = peers[pid] 99 | msg = 'ROOM_PEER_LEFT {}'.format(uid) 100 | print('room {}: {} -> {}: {}'.format(room_id, uid, pid, msg)) 101 | await wsp.send(msg) 102 | 103 | async def remove_peer(uid): 104 | await cleanup_session(uid) 105 | if uid in peers: 106 | ws, raddr, status = peers[uid] 107 | if status and status != 'session': 108 | await cleanup_room(uid, status) 109 | del peers[uid] 110 | await ws.close() 111 | print("Disconnected from peer {!r} at {!r}".format(uid, raddr)) 112 | 113 | async def notify_online(uid): 114 | global peers 115 | for peer_uid, v in peers.items(): 116 | if peer_uid == uid: 117 | continue 118 | print('-------------[%s] notify peer online ' % peer_uid) 119 | ws, _, _ = v 120 | await ws.send("ONLINE:%s" % uid) 121 | 122 | ############### Handler functions ############### 123 | 124 | async def connection_handler(ws, uid): 125 | global peers, sessions, rooms 126 | raddr = ws.remote_address 127 | peer_status = None 128 | peers[uid] = [ws, raddr, peer_status] 129 | print("Registered peer {!r} at {!r}".format(uid, raddr)) 130 | 131 | await notify_online(uid) 132 | 133 | while True: 134 | # Receive command, wait forever if necessary 135 | msg = await recv_msg_ping(ws, raddr) 136 | # Update current status 137 | peer_status = peers[uid][2] 138 | # We are in a session or a room, messages must be relayed 139 | if peer_status is not None: 140 | # We're in a session, route message to connected peer 141 | if peer_status == 'session': 142 | other_id = sessions[uid] 143 | wso, oaddr, status = peers[other_id] 144 | assert(status == 'session') 145 | print("{} -> {}: {}".format(uid, other_id, msg)) 146 | await wso.send(msg) 147 | # We're in a room, accept room-specific commands 148 | elif peer_status: 149 | # ROOM_PEER_MSG peer_id MSG 150 | if msg.startswith('ROOM_PEER_MSG'): 151 | _, other_id, msg = msg.split(maxsplit=2) 152 | if other_id not in peers: 153 | await ws.send('ERROR peer {!r} not found' 154 | ''.format(other_id)) 155 | continue 156 | wso, oaddr, status = peers[other_id] 157 | if status != room_id: 158 | await ws.send('ERROR peer {!r} is not in the room' 159 | ''.format(other_id)) 160 | continue 161 | msg = 'ROOM_PEER_MSG {} {}'.format(uid, msg) 162 | print('room {}: {} -> {}: {}'.format(room_id, uid, other_id, msg)) 163 | await wso.send(msg) 164 | elif msg == 'ROOM_PEER_LIST': 165 | room_id = peers[peer_id][2] 166 | room_peers = ' '.join([pid for pid in rooms[room_id] if pid != peer_id]) 167 | msg = 'ROOM_PEER_LIST {}'.format(room_peers) 168 | print('room {}: -> {}: {}'.format(room_id, uid, msg)) 169 | await ws.send(msg) 170 | else: 171 | await ws.send('ERROR invalid msg, already in room') 172 | continue 173 | else: 174 | raise AssertionError('Unknown peer status {!r}'.format(peer_status)) 175 | # Requested a session with a specific peer 176 | elif msg.startswith('SESSION'): 177 | print("{!r} command {!r}".format(uid, msg)) 178 | _, callee_id = msg.split(maxsplit=1) 179 | if callee_id not in peers: 180 | await ws.send('ERROR peer {!r} not found'.format(callee_id)) 181 | continue 182 | if peer_status is not None: 183 | await ws.send('ERROR peer {!r} busy'.format(callee_id)) 184 | continue 185 | await ws.send('SESSION_OK') 186 | wsc = peers[callee_id][0] 187 | print('Session from {!r} ({!r}) to {!r} ({!r})' 188 | ''.format(uid, raddr, callee_id, wsc.remote_address)) 189 | # Register session 190 | peers[uid][2] = peer_status = 'session' 191 | sessions[uid] = callee_id 192 | peers[callee_id][2] = 'session' 193 | sessions[callee_id] = uid 194 | # Requested joining or creation of a room 195 | elif msg.startswith('ROOM'): 196 | print('{!r} command {!r}'.format(uid, msg)) 197 | _, room_id = msg.split(maxsplit=1) 198 | # Room name cannot be 'session', empty, or contain whitespace 199 | if room_id == 'session' or room_id.split() != [room_id]: 200 | await ws.send('ERROR invalid room id {!r}'.format(room_id)) 201 | continue 202 | if room_id in rooms: 203 | if uid in rooms[room_id]: 204 | raise AssertionError('How did we accept a ROOM command ' 205 | 'despite already being in a room?') 206 | else: 207 | # Create room if required 208 | rooms[room_id] = set() 209 | room_peers = ' '.join([pid for pid in rooms[room_id]]) 210 | await ws.send('ROOM_OK {}'.format(room_peers)) 211 | # Enter room 212 | peers[uid][2] = peer_status = room_id 213 | rooms[room_id].add(uid) 214 | for pid in rooms[room_id]: 215 | if pid == uid: 216 | continue 217 | wsp, paddr, _ = peers[pid] 218 | msg = 'ROOM_PEER_JOINED {}'.format(uid) 219 | print('room {}: {} -> {}: {}'.format(room_id, uid, pid, msg)) 220 | await wsp.send(msg) 221 | else: 222 | print('Ignoring unknown message {!r} from {!r}'.format(msg, uid)) 223 | 224 | async def hello_peer(ws): 225 | ''' 226 | Exchange hello, register peer 227 | ''' 228 | raddr = ws.remote_address 229 | hello = await ws.recv() 230 | hello, uid = hello.split(maxsplit=1) 231 | if hello != 'HELLO': 232 | await ws.close(code=1002, reason='invalid protocol') 233 | raise Exception("Invalid hello from {!r}".format(raddr)) 234 | if not uid or uid in peers or uid.split() != [uid]: # no whitespace 235 | await ws.close(code=1002, reason='invalid peer uid') 236 | raise Exception("Invalid uid {!r} from {!r}".format(uid, raddr)) 237 | # Send back a HELLO 238 | await ws.send('HELLO') 239 | return uid 240 | 241 | async def handler(ws, path): 242 | ''' 243 | All incoming messages are handled here. @path is unused. 244 | ''' 245 | raddr = ws.remote_address 246 | print("Connected to {!r}".format(raddr)) 247 | peer_id = await hello_peer(ws) 248 | try: 249 | await connection_handler(ws, peer_id) 250 | except websockets.ConnectionClosed: 251 | print("Connection to peer {!r} closed, exiting handler".format(raddr)) 252 | finally: 253 | await remove_peer(peer_id) 254 | 255 | sslctx = None 256 | if not options.disable_ssl: 257 | # Create an SSL context to be used by the websocket server 258 | certpath = options.cert_path 259 | print('Using TLS with keys in {!r}'.format(certpath)) 260 | if 'letsencrypt' in certpath: 261 | chain_pem = os.path.join(certpath, 'fullchain.pem') 262 | key_pem = os.path.join(certpath, 'privkey.pem') 263 | else: 264 | chain_pem = os.path.join(certpath, 'cert.pem') 265 | key_pem = os.path.join(certpath, 'key.pem') 266 | 267 | sslctx = ssl.create_default_context() 268 | try: 269 | sslctx.load_cert_chain(chain_pem, keyfile=key_pem) 270 | except FileNotFoundError: 271 | print("Certificates not found, did you run generate_cert.sh?") 272 | sys.exit(1) 273 | # FIXME 274 | sslctx.check_hostname = False 275 | sslctx.verify_mode = ssl.CERT_NONE 276 | 277 | print("Listening on https://{}:{}".format(*ADDR_PORT)) 278 | # Websocket server 279 | wsd = websockets.serve(handler, *ADDR_PORT, ssl=sslctx, 280 | # Maximum number of messages that websockets will pop 281 | # off the asyncio and OS buffers per connection. See: 282 | # https://websockets.readthedocs.io/en/stable/api.html#websockets.protocol.WebSocketCommonProtocol 283 | max_queue=16) 284 | 285 | logger = logging.getLogger('websockets.server') 286 | 287 | logger.setLevel(logging.ERROR) 288 | logger.addHandler(logging.StreamHandler()) 289 | 290 | asyncio.get_event_loop().run_until_complete(wsd) 291 | asyncio.get_event_loop().run_forever() 292 | -------------------------------------------------------------------------------- /www/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | COPY . /usr/share/nginx/html 4 | 5 | RUN sed -i 's/var default_peer_id;/var default_peer_id = 1;/g' \ 6 | /usr/share/nginx/html/webrtc.js 7 | RUN sed -i 's/wss/ws/g' \ 8 | /usr/share/nginx/html/webrtc.js 9 | 10 | 11 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 17 | 18 | 19 | 22 | 23 | 24 | 25 |
26 |
Status: unknown
27 |
28 |
Our id is unknown
29 |
30 |
31 |
getUserMedia constraints being used:
32 |
33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /www/webrtc.js: -------------------------------------------------------------------------------- 1 | /* vim: set sts=4 sw=4 et : 2 | * 3 | * Demo Javascript app for negotiating and streaming a sendrecv webrtc stream 4 | * with a GStreamer app. Runs only in passive mode, i.e., responds to offers 5 | * with answers, exchanges ICE candidates, and streams. 6 | * 7 | * Author: Nirbheek Chauhan 8 | */ 9 | 10 | // Set this to override the automatic detection in websocketServerConnect() 11 | var ws_server; 12 | var ws_port; 13 | // Set this to use a specific peer id instead of a random one 14 | var default_peer_id; 15 | // Override with your own STUN servers if you want 16 | var rtc_configuration = {iceServers: [{urls: "stun:stun.services.mozilla.com"}, 17 | {urls: "stun:stun.l.google.com:19302"}]}; 18 | // The default constraints that will be attempted. Can be overriden by the user. 19 | var default_constraints = {video: true, audio: true}; 20 | 21 | var connect_attempts = 0; 22 | var peer_connection; 23 | var send_channel; 24 | var ws_conn; 25 | // Promise for local stream after constraints are approved by the user 26 | var local_stream_promise; 27 | 28 | function getOurId() { 29 | return Math.floor(Math.random() * (9000 - 10) + 10).toString(); 30 | } 31 | 32 | function resetState() { 33 | // This will call onServerClose() 34 | ws_conn.close(); 35 | } 36 | 37 | function handleIncomingError(error) { 38 | setError("ERROR: " + error); 39 | resetState(); 40 | } 41 | 42 | function getVideoElement() { 43 | return document.getElementById("stream"); 44 | } 45 | 46 | function setStatus(text) { 47 | console.log(text); 48 | var span = document.getElementById("status") 49 | // Don't set the status if it already contains an error 50 | if (!span.classList.contains('error')) 51 | span.textContent = text; 52 | } 53 | 54 | function setError(text) { 55 | console.error(text); 56 | var span = document.getElementById("status") 57 | span.textContent = text; 58 | span.classList.add('error'); 59 | } 60 | 61 | function resetVideo() { 62 | // Release the webcam and mic 63 | if (local_stream_promise) 64 | local_stream_promise.then(stream => { 65 | if (stream) { 66 | stream.getTracks().forEach(function (track) { track.stop(); }); 67 | } 68 | }); 69 | 70 | // Reset the video element and stop showing the last received frame 71 | var videoElement = getVideoElement(); 72 | videoElement.pause(); 73 | videoElement.src = ""; 74 | videoElement.load(); 75 | } 76 | 77 | // SDP offer received from peer, set remote description and create an answer 78 | function onIncomingSDP(sdp) { 79 | peer_connection.setRemoteDescription(sdp).then(() => { 80 | setStatus("Remote SDP set"); 81 | if (sdp.type != "offer") 82 | return; 83 | setStatus("Got SDP offer"); 84 | local_stream_promise.then((stream) => { 85 | setStatus("Got local stream, creating answer"); 86 | peer_connection.createAnswer() 87 | .then(onLocalDescription).catch(setError); 88 | }).catch(setError); 89 | }).catch(setError); 90 | } 91 | 92 | // Local description was set, send it to peer 93 | function onLocalDescription(desc) { 94 | console.log("Got local description: " + JSON.stringify(desc)); 95 | peer_connection.setLocalDescription(desc).then(function() { 96 | setStatus("Sending SDP answer"); 97 | sdp = {'sdp': peer_connection.localDescription} 98 | ws_conn.send(JSON.stringify(sdp)); 99 | }); 100 | } 101 | 102 | // ICE candidate received from peer, add it to the peer connection 103 | function onIncomingICE(ice) { 104 | var candidate = new RTCIceCandidate(ice); 105 | peer_connection.addIceCandidate(candidate).catch(setError); 106 | } 107 | 108 | function onServerMessage(event) { 109 | console.log("Received " + event.data); 110 | switch (event.data) { 111 | case "HELLO": 112 | setStatus("Registered with server, waiting for call"); 113 | return; 114 | default: 115 | if (event.data.startsWith("ERROR")) { 116 | handleIncomingError(event.data); 117 | return; 118 | } 119 | // Handle incoming JSON SDP and ICE messages 120 | try { 121 | msg = JSON.parse(event.data); 122 | } catch (e) { 123 | if (e instanceof SyntaxError) { 124 | handleIncomingError("Error parsing incoming JSON: " + event.data); 125 | } else { 126 | handleIncomingError("Unknown error parsing response: " + event.data); 127 | } 128 | return; 129 | } 130 | 131 | // Incoming JSON signals the beginning of a call 132 | if (!peer_connection) 133 | createCall(msg); 134 | 135 | if (msg.sdp != null) { 136 | onIncomingSDP(msg.sdp); 137 | } else if (msg.ice != null) { 138 | onIncomingICE(msg.ice); 139 | } else { 140 | handleIncomingError("Unknown incoming JSON: " + msg); 141 | } 142 | } 143 | } 144 | 145 | function onServerClose(event) { 146 | setStatus('Disconnected from server'); 147 | resetVideo(); 148 | 149 | if (peer_connection) { 150 | peer_connection.close(); 151 | peer_connection = null; 152 | } 153 | 154 | // Reset after a second 155 | window.setTimeout(websocketServerConnect, 1000); 156 | } 157 | 158 | function onServerError(event) { 159 | setError("Unable to connect to server, did you add an exception for the certificate?") 160 | // Retry after 3 seconds 161 | window.setTimeout(websocketServerConnect, 3000); 162 | } 163 | 164 | function getLocalStream() { 165 | var constraints; 166 | var textarea = document.getElementById('constraints'); 167 | try { 168 | constraints = JSON.parse(textarea.value); 169 | } catch (e) { 170 | console.error(e); 171 | setError('ERROR parsing constraints: ' + e.message + ', using default constraints'); 172 | constraints = default_constraints; 173 | } 174 | console.log(JSON.stringify(constraints)); 175 | 176 | // Add local stream 177 | if (navigator.mediaDevices.getUserMedia) { 178 | return navigator.mediaDevices.getUserMedia(constraints); 179 | } else { 180 | errorUserMediaHandler(); 181 | } 182 | } 183 | 184 | function websocketServerConnect() { 185 | connect_attempts++; 186 | if (connect_attempts > 3) { 187 | setError("Too many connection attempts, aborting. Refresh page to try again"); 188 | return; 189 | } 190 | // Clear errors in the status span 191 | var span = document.getElementById("status"); 192 | span.classList.remove('error'); 193 | span.textContent = ''; 194 | // Populate constraints 195 | var textarea = document.getElementById('constraints'); 196 | if (textarea.value == '') 197 | textarea.value = JSON.stringify(default_constraints); 198 | // Fetch the peer id to use 199 | peer_id = default_peer_id || getOurId(); 200 | ws_port = ws_port || '8443'; 201 | if (window.location.protocol.startsWith ("file")) { 202 | ws_server = ws_server || "127.0.0.1"; 203 | } else if (window.location.protocol.startsWith ("http")) { 204 | ws_server = ws_server || window.location.hostname; 205 | } else { 206 | throw new Error ("Don't know how to connect to the signalling server with uri" + window.location); 207 | } 208 | var ws_url = 'wss://' + ws_server + ':' + ws_port 209 | setStatus("Connecting to server " + ws_url); 210 | ws_conn = new WebSocket(ws_url); 211 | /* When connected, immediately register with the server */ 212 | ws_conn.addEventListener('open', (event) => { 213 | document.getElementById("peer-id").textContent = peer_id; 214 | ws_conn.send('HELLO ' + peer_id); 215 | setStatus("Registering with server"); 216 | }); 217 | ws_conn.addEventListener('error', onServerError); 218 | ws_conn.addEventListener('message', onServerMessage); 219 | ws_conn.addEventListener('close', onServerClose); 220 | } 221 | 222 | function onRemoteTrack(event) { 223 | if (getVideoElement().srcObject !== event.streams[0]) { 224 | console.log('Incoming stream'); 225 | getVideoElement().srcObject = event.streams[0]; 226 | } 227 | } 228 | 229 | function errorUserMediaHandler() { 230 | setError("Browser doesn't support getUserMedia!"); 231 | } 232 | 233 | const handleDataChannelOpen = (event) =>{ 234 | console.log("dataChannel.OnOpen", event); 235 | }; 236 | 237 | const handleDataChannelMessageReceived = (event) =>{ 238 | console.log("dataChannel.OnMessage:", event, event.data.type); 239 | 240 | setStatus("Received data channel message"); 241 | if (typeof event.data === 'string' || event.data instanceof String) { 242 | console.log('Incoming string message: ' + event.data); 243 | textarea = document.getElementById("text") 244 | textarea.value = textarea.value + '\n' + event.data 245 | } else { 246 | console.log('Incoming data message'); 247 | } 248 | send_channel.send("Hi! (from browser)"); 249 | }; 250 | 251 | const handleDataChannelError = (error) =>{ 252 | console.log("dataChannel.OnError:", error); 253 | }; 254 | 255 | const handleDataChannelClose = (event) =>{ 256 | console.log("dataChannel.OnClose", event); 257 | }; 258 | 259 | function onDataChannel(event) { 260 | setStatus("Data channel created"); 261 | let receiveChannel = event.channel; 262 | receiveChannel.onopen = handleDataChannelOpen; 263 | receiveChannel.onmessage = handleDataChannelMessageReceived; 264 | receiveChannel.onerror = handleDataChannelError; 265 | receiveChannel.onclose = handleDataChannelClose; 266 | } 267 | 268 | function createCall(msg) { 269 | // Reset connection attempts because we connected successfully 270 | connect_attempts = 0; 271 | 272 | console.log('Creating RTCPeerConnection'); 273 | 274 | peer_connection = new RTCPeerConnection(rtc_configuration); 275 | send_channel = peer_connection.createDataChannel('label', null); 276 | send_channel.onopen = handleDataChannelOpen; 277 | send_channel.onmessage = handleDataChannelMessageReceived; 278 | send_channel.onerror = handleDataChannelError; 279 | send_channel.onclose = handleDataChannelClose; 280 | peer_connection.ondatachannel = onDataChannel; 281 | peer_connection.ontrack = onRemoteTrack; 282 | /* Send our video/audio to the other peer */ 283 | local_stream_promise = getLocalStream().then((stream) => { 284 | console.log('Adding local stream'); 285 | peer_connection.addStream(stream); 286 | return stream; 287 | }).catch(setError); 288 | 289 | if (!msg.sdp) { 290 | console.log("WARNING: First message wasn't an SDP message!?"); 291 | } 292 | 293 | peer_connection.onicecandidate = (event) => { 294 | // We have a candidate, send it to the remote party with the 295 | // same uuid 296 | if (event.candidate == null) { 297 | console.log("ICE Candidate was null, done"); 298 | return; 299 | } 300 | ws_conn.send(JSON.stringify({'ice': event.candidate})); 301 | }; 302 | 303 | setStatus("Created peer connection for call, waiting for SDP"); 304 | } 305 | --------------------------------------------------------------------------------