/* 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; // Set this to use a specific peer id instead of a random one var default_peer_id; // Override with your own STUN servers if you want var rtc_configuration = {iceServers: [{urls: "stun:stun.l.google.com:19302"}]}; // The default constraints that will be attempted. Can be overriden by the user. var default_constraints = {video: true, audio: true}; var connect_attempts = 0; var peer_connection = new RTCPeerConnection(rtc_configuration); var send_channel; var ws_conn; // Local stream after constraints are approved by the user var local_stream = null; // keep track of some negotiation state to prevent races and errors var callCreateTriggered = false; var makingOffer = false; var isSettingRemoteAnswerPending = false; function setConnectButtonState(value) { document.getElementById("peer-connect-button").value = value; } function wantRemoteOfferer() { return document.getElementById("remote-offerer").checked; } function onConnectClicked() { if (document.getElementById("peer-connect-button").value == "Disconnect") { resetState(); return; } var id = document.getElementById("peer-connect").value; if (id == "") { alert("Peer id must be filled out"); return; } ws_conn.send("SESSION " + id); setConnectButtonState("Disconnect"); } function onTextKeyPress(e) { e = e ? e : window.event; if (e.code == "Enter") { onConnectClicked(); return false; } return true; } function getOurId() { return Math.floor(Math.random() * (9000 - 10) + 10).toString(); } function resetState() { // This will call onServerClose() ws_conn.close(); } function handleIncomingError(error) { setError("ERROR: " + error); resetState(); } function getVideoElement() { var div = document.getElementById("video"); var video_tag = document.createElement("video"); video_tag.textContent = "Your browser doesn't support video"; video_tag.autoplay = true; video_tag.playsinline = true; div.appendChild(video_tag); return video_tag } function setStatus(text) { console.log(text); var span = document.getElementById("status") // Don't set the status if it already contains an error if (!span.classList.contains('error')) span.textContent = text; } function setError(text) { console.error(text); var span = document.getElementById("status") span.textContent = text; span.classList.add('error'); } function resetVideo() { // Release the webcam and mic if (local_stream) { local_stream.then(stream => { if (stream) { stream.getTracks().forEach(function (track) { track.stop(); }); } }); local_stream = null; } // Remove all video players document.getElementById("video").innerHTML = ""; } function onIncomingSDP(sdp) { try { // An offer may come in while we are busy processing SRD(answer). // In this case, we will be in "stable" by the time the offer is processed // so it is safe to chain it on our Operations Chain now. const readyForOffer = !makingOffer && (peer_connection.signalingState == "stable" || isSettingRemoteAnswerPending); const offerCollision = sdp.type == "offer" && !readyForOffer; if (offerCollision) { return; } isSettingRemoteAnswerPending = sdp.type == "answer"; peer_connection.setRemoteDescription(sdp).then(() => { setStatus("Remote SDP set"); isSettingRemoteAnswerPending = false; if (sdp.type == "offer") { setStatus("Got SDP offer, waiting for getUserMedia to complete"); local_stream.then((stream) => { setStatus("getUserMedia to completed, setting local description"); peer_connection.setLocalDescription().then(() => { let desc = peer_connection.localDescription; console.log("Got local description: " + JSON.stringify(desc)); setStatus("Sending SDP " + desc.type); ws_conn.send(JSON.stringify({'sdp': desc})); if (peer_connection.iceConnectionState == "connected") { setStatus("SDP " + desc.type + " sent, ICE connected, all looks OK"); } }); }); } }); } catch (err) { handleIncomingError(err); } } // Local description was set by incoming SDP offer, send answer to peer function onLocalDescription(desc) { if (desc.type != "answer") { console.warn("Expected SDP answer, received: " + desc.type); } console.log("Got local description: " + JSON.stringify(desc)); peer_connection.setLocalDescription(desc).then(() => { var dsc = peer_connection.localDescription; setStatus("Sending SDP " + desc.type); ws_conn.send(JSON.stringify({'sdp': desc})); }); } // ICE candidate received from peer, add it to the peer connection function onIncomingICE(ice) { var candidate = new RTCIceCandidate(ice); peer_connection.addIceCandidate(candidate).catch(setError); } function onServerMessage(event) { console.log("Received " + event.data); switch (event.data) { case "HELLO": setStatus("Registered with server, waiting for call"); return; case "SESSION_OK": setStatus("Starting negotiation"); if (wantRemoteOfferer()) { ws_conn.send("OFFER_REQUEST"); setStatus("Sent OFFER_REQUEST, waiting for offer"); return; } if (!callCreateTriggered) { createCall(); setStatus("Created peer connection for call, waiting for SDP"); } return; case "OFFER_REQUEST": // The peer wants us to set up and then send an offer if (!callCreateTriggered) createCall(); return; default: if (event.data.startsWith("ERROR")) { handleIncomingError(event.data); return; } // Handle incoming JSON SDP and ICE messages try { msg = JSON.parse(event.data); } catch (e) { if (e instanceof SyntaxError) { handleIncomingError("Error parsing incoming JSON: " + event.data); } else { handleIncomingError("Unknown error parsing response: " + event.data); } return; } // Incoming JSON signals the beginning of a call if (!callCreateTriggered) createCall(msg); if (msg.sdp != null) { onIncomingSDP(msg.sdp); } else if (msg.ice != null) { onIncomingICE(msg.ice); } else { handleIncomingError("Unknown incoming JSON: " + msg); } } } function onServerClose(event) { setStatus('Disconnected from server'); resetVideo(); if (peer_connection) { peer_connection.close(); peer_connection = new RTCPeerConnection(rtc_configuration); } callCreateTriggered = false; // Reset after a second window.setTimeout(websocketServerConnect, 1000); } function onServerError(event) { setError("Unable to connect to server, did you add an exception for the certificate?") // Retry after 3 seconds window.setTimeout(websocketServerConnect, 3000); } function getLocalStream() { var constraints; var textarea = document.getElementById('constraints'); try { constraints = JSON.parse(textarea.value); } catch (e) { console.error(e); setError('ERROR parsing constraints: ' + e.message + ', using default constraints'); constraints = default_constraints; } console.log(JSON.stringify(constraints)); // Add local stream if (navigator.mediaDevices.getUserMedia) { return navigator.mediaDevices.getUserMedia(constraints); } else { errorUserMediaHandler(); } } function websocketServerConnect() { connect_attempts++; if (connect_attempts > 3) { setError("Too many connection attempts, aborting. Refresh page to try again"); return; } // Clear errors in the status span var span = document.getElementById("status"); span.classList.remove('error'); span.textContent = ''; // Populate constraints var textarea = document.getElementById('constraints'); if (textarea.value == '') textarea.value = JSON.stringify(default_constraints); // Fetch the peer id to use peer_id = default_peer_id || getOurId(); ws_port = ws_port || '8443'; if (window.location.protocol.startsWith ("file")) { ws_server = ws_server || "127.0.0.1"; } else if (window.location.protocol.startsWith ("http")) { 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 = 'wss://' + ws_server + ':' + ws_port setStatus("Connecting to server " + ws_url); ws_conn = new WebSocket(ws_url); /* When connected, immediately register with the server */ ws_conn.addEventListener('open', (event) => { document.getElementById("peer-id").textContent = peer_id; ws_conn.send('HELLO ' + peer_id); setStatus("Registering with server"); setConnectButtonState("Connect"); // Reset connection attempts because we connected successfully connect_attempts = 0; }); ws_conn.addEventListener('error', onServerError); ws_conn.addEventListener('message', onServerMessage); ws_conn.addEventListener('close', onServerClose); } function errorUserMediaHandler() { setError("Browser doesn't support getUserMedia!"); } const handleDataChannelOpen = (event) =>{ console.log("dataChannel.OnOpen", event); }; const handleDataChannelMessageReceived = (event) =>{ console.log("dataChannel.OnMessage:", event, event.data.type); setStatus("Received data channel message"); if (typeof event.data === 'string' || event.data instanceof String) { console.log('Incoming string message: ' + event.data); textarea = document.getElementById("text") textarea.value = textarea.value + '\n' + event.data } else { console.log('Incoming data message'); } send_channel.send("Hi! (from browser)"); }; const handleDataChannelError = (error) =>{ console.log("dataChannel.OnError:", error); }; const handleDataChannelClose = (event) =>{ console.log("dataChannel.OnClose", event); }; function onDataChannel(event) { setStatus("Data channel created"); let receiveChannel = event.channel; receiveChannel.onopen = handleDataChannelOpen; receiveChannel.onmessage = handleDataChannelMessageReceived; receiveChannel.onerror = handleDataChannelError; receiveChannel.onclose = handleDataChannelClose; } function createCall() { callCreateTriggered = true; console.log('Configuring RTCPeerConnection'); send_channel = peer_connection.createDataChannel('label', null); send_channel.onopen = handleDataChannelOpen; send_channel.onmessage = handleDataChannelMessageReceived; send_channel.onerror = handleDataChannelError; send_channel.onclose = handleDataChannelClose; peer_connection.ondatachannel = onDataChannel; peer_connection.ontrack = ({track, streams}) => { console.log("ontrack triggered"); var videoElem = getVideoElement(); if (track.kind === 'audio') videoElem.style.display = 'none'; videoElem.srcObject = streams[0]; videoElem.srcObject.addEventListener('mute', (e) => { console.log("track muted, hiding video element"); videoElem.style.display = 'none'; }); videoElem.srcObject.addEventListener('unmute', (e) => { console.log("track unmuted, showing video element"); videoElem.style.display = 'block'; }); videoElem.srcObject.addEventListener('removetrack', (e) => { console.log("track removed, removing video element"); videoElem.remove(); }); }; peer_connection.onicecandidate = (event) => { // We have a candidate, send it to the remote party with the // same uuid if (event.candidate == null) { console.log("ICE Candidate was null, done"); return; } ws_conn.send(JSON.stringify({'ice': event.candidate})); }; peer_connection.oniceconnectionstatechange = (event) => { if (peer_connection.iceConnectionState == "connected") { setStatus("ICE gathering complete"); } }; // let the "negotiationneeded" event trigger offer generation peer_connection.onnegotiationneeded = async () => { setStatus("Negotiation needed"); if (wantRemoteOfferer()) return; try { makingOffer = true; await peer_connection.setLocalDescription(); let desc = peer_connection.localDescription; setStatus("Sending SDP " + desc.type); ws_conn.send(JSON.stringify({'sdp': desc})); } catch (err) { handleIncomingError(err); } finally { makingOffer = false; } }; /* Send our video/audio to the other peer */ local_stream = getLocalStream().then((stream) => { console.log('Adding local stream'); for (const track of stream.getTracks()) { peer_connection.addTrack(track, stream); } return stream; }).catch(setError); setConnectButtonState("Disconnect"); }