/* vim: set sts=4 sw=4 et : * * Demo Javascript app for negotiating and streaming a sendrecv webrtc stream * with a GStreamer app. Runs only in passive mode, i.e., responds to offers * with answers, exchanges ICE candidates, and streams. * * Author: Nirbheek Chauhan */ // Set this to override the automatic detection in websocketServerConnect() var ws_server; var ws_port; // Override with your own STUN servers if you want var rtc_configuration = {iceServers: [{urls: "stun:stun.l.google.com:19302"}, /* TODO: do not keep these static and in clear text in production, * and instead use one of the mechanisms discussed in * https://groups.google.com/forum/#!topic/discuss-webrtc/nn8b6UboqRA */ {'urls': 'turn:turn.homeneural.net:3478?transport=udp', 'credential': '1qaz2wsx', 'username': 'test' }], /* Uncomment the following line to ensure the turn server is used * while testing. This should be kept commented out in production, * as non-relay ice candidates should be preferred */ // iceTransportPolicy: "relay", }; var sessions = {} /* https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript */ function getOurId() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } function Uint8ToString(u8a){ var CHUNK_SZ = 0x8000; var c = []; for (var i=0; i < u8a.length; i+=CHUNK_SZ) { c.push(String.fromCharCode.apply(null, u8a.subarray(i, i+CHUNK_SZ))); } return c.join(""); } function Session(our_id, peer_id, closed_callback) { this.id = null; this.peer_connection = null; this.ws_conn = null; this.peer_id = peer_id; this.our_id = our_id; this.closed_callback = closed_callback; this.data_channel = null; this.input = null; this.getVideoElement = function() { return document.getElementById("stream-" + this.our_id); }; this.resetState = function() { if (this.peer_connection) { this.peer_connection.close(); this.peer_connection = null; } var videoElement = this.getVideoElement(); if (videoElement) { videoElement.pause(); videoElement.src = ""; } var session_div = document.getElementById("session-" + this.our_id); if (session_div) { session_div.parentNode.removeChild(session_div); } if (this.ws_conn) { this.ws_conn.close(); this.ws_conn = null; } this.input && this.input.detach(); this.data_channel = null; }; this.handleIncomingError = function(error) { this.resetState(); this.closed_callback(this.our_id); }; this.setStatus = function(text) { console.log(text); var span = document.getElementById("status-" + this.our_id); // Don't set the status if it already contains an error if (!span.classList.contains('error')) span.textContent = text; }; this.setError = function(text) { console.error(text); var span = document.getElementById("status-" + this.our_id); span.textContent = text; span.classList.add('error'); this.resetState(); this.closed_callback(this.our_id); }; // Local description was set, send it to peer this.onLocalDescription = function(desc) { console.log("Got local description: " + JSON.stringify(desc), this); var thiz = this; this.peer_connection.setLocalDescription(desc).then(() => { this.setStatus("Sending SDP answer"); var sdp = { 'type': 'peer', 'sessionId': this.id, 'sdp': this.peer_connection.localDescription.toJSON() }; this.ws_conn.send(JSON.stringify(sdp)); }).catch(function(e) { thiz.setError(e); }); }; this.onRemoteDescriptionSet = function() { this.setStatus("Remote SDP set"); this.setStatus("Got SDP offer"); this.peer_connection.createAnswer() .then(this.onLocalDescription.bind(this)).catch(this.setError); } // SDP offer received from peer, set remote description and create an answer this.onIncomingSDP = function(sdp) { var thiz = this; this.peer_connection.setRemoteDescription(sdp) .then(this.onRemoteDescriptionSet.bind(this)) .catch(function(e) { thiz.setError(e) }); }; // ICE candidate received from peer, add it to the peer connection this.onIncomingICE = function(ice) { var candidate = new RTCIceCandidate(ice); var thiz = this; this.peer_connection.addIceCandidate(candidate).catch(function(e) { thiz.setError(e) }); }; this.onServerMessage = function(event) { console.log("Received " + event.data); try { msg = JSON.parse(event.data); } catch (e) { if (e instanceof SyntaxError) { this.handleIncomingError("Error parsing incoming JSON: " + event.data); } else { this.handleIncomingError("Unknown error parsing response: " + event.data); } return; } if (msg.type == "registered") { this.setStatus("Registered with server"); this.connectPeer(); } else if (msg.type == "sessionStarted") { this.setStatus("Registered with server"); this.id = msg.sessionId; } else if (msg.type == "error") { this.handleIncomingError(msg.details); } else if (msg.type == "endSession") { this.resetState(); this.closed_callback(this.our_id); } else if (msg.type == "peer") { // Incoming peer message signals the beginning of a call if (!this.peer_connection) this.createCall(msg); if (msg.sdp != null) { this.onIncomingSDP(msg.sdp); } else if (msg.ice != null) { this.onIncomingICE(msg.ice); } else { this.handleIncomingError("Unknown incoming JSON: " + msg); } } }; this.streamIsPlaying = function(e) { this.setStatus("Streaming"); }; this.onServerClose = function(event) { this.resetState(); this.closed_callback(this.our_id); }; this.onServerError = function(event) { this.handleIncomingError('Server error'); }; this.websocketServerConnect = function() { // Clear errors in the status span var span = document.getElementById("status-" + this.our_id); span.classList.remove('error'); span.textContent = ''; console.log("Our ID:", this.our_id); var ws_port = ws_port || '8443'; if (window.location.protocol.startsWith ("file")) { var ws_server = ws_server || "127.0.0.1"; } else if (window.location.protocol.startsWith ("http")) { var ws_server = ws_server || window.location.hostname; } else { throw new Error ("Don't know how to connect to the signalling server with uri" + window.location); } var ws_url = 'ws://' + ws_server + ':' + ws_port this.setStatus("Connecting to server " + ws_url); this.ws_conn = new WebSocket(ws_url); /* When connected, immediately register with the server */ this.ws_conn.addEventListener('open', (event) => { this.setStatus("Connecting to the peer"); this.connectPeer(); }); this.ws_conn.addEventListener('error', this.onServerError.bind(this)); this.ws_conn.addEventListener('message', this.onServerMessage.bind(this)); this.ws_conn.addEventListener('close', this.onServerClose.bind(this)); }; this.connectPeer = function() { this.setStatus("Connecting " + this.peer_id); this.ws_conn.send(JSON.stringify({ "type": "startSession", "peerId": this.peer_id })); }; this.onRemoteStreamAdded = function(event) { var videoTracks = event.stream.getVideoTracks(); var audioTracks = event.stream.getAudioTracks(); console.log(videoTracks); if (videoTracks.length > 0) { console.log('Incoming stream: ' + videoTracks.length + ' video tracks and ' + audioTracks.length + ' audio tracks'); this.getVideoElement().srcObject = event.stream; this.getVideoElement().play(); } else { this.handleIncomingError('Stream with unknown tracks added, resetting'); } }; this.createCall = function(msg) { console.log('Creating RTCPeerConnection'); this.peer_connection = new RTCPeerConnection(rtc_configuration); this.peer_connection.onaddstream = this.onRemoteStreamAdded.bind(this); this.peer_connection.ondatachannel = (event) => { console.log(`Data channel created: ${event.channel.label}`); this.data_channel = event.channel; video_element = this.getVideoElement(); if (video_element) { this.input = new Input(video_element, (data) => { if (this.data_channel) { console.log(`Navigation data: ${data}`); this.data_channel.send(JSON.stringify(data)); } }); } this.data_channel.onopen = (event) => { console.log("Receive channel opened, attaching input"); this.input.attach(); } this.data_channel.onclose = (event) => { console.info("Receive channel closed"); this.input && this.input.detach(); this.data_channel = null; } this.data_channel.onerror = (event) => { this.input && this.input.detach(); console.warn("Error on receive channel", event.data); this.data_channel = null; } let buffer = []; this.data_channel.onmessage = (event) => { if (typeof event.data === 'string' || event.data instanceof String) { if (event.data == 'BEGIN_IMAGE') buffer = []; else if (event.data == 'END_IMAGE') { var decoder = new TextDecoder("ascii"); var array_buffer = new Uint8Array(buffer); var str = decoder.decode(array_buffer); let img = document.getElementById("image"); img.src = 'data:image/png;base64, ' + str; } } else { var i, len = buffer.length var view = new DataView(event.data); for (i = 0; i < view.byteLength; i++) { buffer[len + i] = view.getUint8(i); } } } } this.peer_connection.onicecandidate = (event) => { if (event.candidate == null) { console.log("ICE Candidate was null, done"); return; } this.ws_conn.send(JSON.stringify({ "type": "peer", "sessionId": this.id, "ice": event.candidate.toJSON() })); }; this.setStatus("Created peer connection for call, waiting for SDP"); }; document.getElementById("stream-" + this.our_id).addEventListener("playing", this.streamIsPlaying.bind(this), false); this.websocketServerConnect(); } function startSession() { var peer_id = document.getElementById("camera-id").value; if (peer_id === "") { return; } sessions[peer_id] = new Session(peer_id); } function session_closed(peer_id) { sessions[peer_id] = null; } function addPeer(peer_id, meta) { console.log("Meta: ", JSON.stringify(meta)); var nav_ul = document.getElementById("camera-list"); meta = meta ? meta : {"display-name": peer_id}; let display_html = `${meta["display-name"] ? meta["display-name"] : peer_id}" var li_str = '
  • '; nav_ul.insertAdjacentHTML('beforeend', li_str); var li = document.getElementById("peer-" + peer_id); li.onclick = function(e) { var sessions_div = document.getElementById('sessions'); var our_id = getOurId(); var session_div_str = '

    Status: unknown

    ' sessions_div.insertAdjacentHTML('beforeend', session_div_str); sessions[peer_id] = new Session(our_id, peer_id, session_closed); } } function clearPeers() { var nav_ul = document.getElementById("camera-list"); while (nav_ul.firstChild) { nav_ul.removeChild(nav_ul.firstChild); } } function onServerMessage(event) { console.log("Received " + event.data); try { msg = JSON.parse(event.data); } catch (e) { if (e instanceof SyntaxError) { console.error("Error parsing incoming JSON: " + event.data); } else { console.error("Unknown error parsing response: " + event.data); } return; } if (msg.type == "welcome") { console.info(`Got welcomed with ID ${msg.peer_id}`); ws_conn.send(JSON.stringify({ "type": "list" })); } else if (msg.type == "list") { clearPeers(); for (i = 0; i < msg.producers.length; i++) { addPeer(msg.producers[i].id, msg.producers[i].meta); } } else if (msg.type == "peerStatusChanged") { var li = document.getElementById("peer-" + msg.peerId); if (msg.roles.includes("producer")) { if (li == null) { console.error('Adding peer'); addPeer(msg.peerId, msg.meta); } } else if (li != null) { li.parentNode.removeChild(li); } } else { console.error("Unsupported message: ", msg); } }; function clearConnection() { ws_conn.removeEventListener('error', onServerError); ws_conn.removeEventListener('message', onServerMessage); ws_conn.removeEventListener('close', onServerClose); ws_conn = null; } function onServerClose(event) { clearConnection(); clearPeers(); console.log("Close"); window.setTimeout(connect, 1000); }; function onServerError(event) { clearConnection(); clearPeers(); console.log("Error", event); window.setTimeout(connect, 1000); }; function connect() { var ws_port = ws_port || '8443'; if (window.location.protocol.startsWith ("file")) { var ws_server = ws_server || "127.0.0.1"; } else if (window.location.protocol.startsWith ("http")) { var ws_server = ws_server || window.location.hostname; } else { throw new Error ("Don't know how to connect to the signalling server with uri" + window.location); } var ws_url = 'ws://' + ws_server + ':' + ws_port console.log("Connecting listener"); ws_conn = new WebSocket(ws_url); ws_conn.addEventListener('open', (event) => { ws_conn.send(JSON.stringify({ "type": "setPeerStatus", "roles": ["listener"] })); }); ws_conn.addEventListener('error', onServerError); ws_conn.addEventListener('message', onServerMessage); ws_conn.addEventListener('close', onServerClose); } function setup() { connect(); }