"use strict"; // Get our hostname var myHostname = window.location.hostname; if (!myHostname) { myHostname = "localhost"; } log("Hostname: " + myHostname); // WebSocket chat/signaling channel variables. var connection = null; var clientID = 0; // The media constraints object describes what sort of stream we want // to request from the local A/V hardware (typically a webcam and // microphone). Here, we specify only that we want both audio and // video; however, you can be more specific. It's possible to state // that you would prefer (or require) specific resolutions of video, // whether to prefer the user-facing or rear-facing camera (if available), // and so on. // // See also: // https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints // https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia // var mediaConstraints = { audio: true, // We want an audio track video: { aspectRatio: { ideal: 1.333333 // 3:2 aspect is preferred } } }; var myUsername = null; var targetUsername = null; // To store username of other peer var myPeerConnection = null; // RTCPeerConnection var transceiver = null; // RTCRtpTransceiver var webcamStream = null; // MediaStream from webcam // Output logging information to console. function log(text) { var time = new Date(); console.log("[" + time.toLocaleTimeString() + "] " + text); } // Output an error message to console. function log_error(text) { var time = new Date(); console.trace("[" + time.toLocaleTimeString() + "] " + text); } // Send a JavaScript object by converting it to JSON and sending // it as a message on the WebSocket connection. function sendToServer(msg) { var msgJSON = JSON.stringify(msg); log("Sending '" + msg.type + "' message: " + msgJSON); connection.send(msgJSON); } // Called when the "id" message is received; this message is sent by the // server to assign this login session a unique ID number; in response, // this function sends a "username" message to set our username for this // session. function setUsername() { myUsername = document.getElementById("name").value; sendToServer({ name: myUsername, date: Date.now(), id: clientID, type: "username" }); } // Open and configure the connection to the WebSocket server. function connect() { var serverUrl; var scheme = "ws"; // If this is an HTTPS connection, we have to use a secure WebSocket // connection too, so add another "s" to the scheme. if (document.location.protocol === "https:") { scheme += "s"; } serverUrl = scheme + "://" + myHostname + ":6503"; log(`Connecting to server: ${serverUrl}`); connection = new WebSocket(serverUrl, "json"); connection.onopen = function(evt) { document.getElementById("text").disabled = false; document.getElementById("send").disabled = false; }; connection.onerror = function(evt) { console.dir(evt); } connection.onmessage = function(evt) { var chatBox = document.querySelector(".chatbox"); var text = ""; var msg = JSON.parse(evt.data); log("Message received: "); console.dir(msg); var time = new Date(msg.date); var timeStr = time.toLocaleTimeString(); switch(msg.type) { case "id": clientID = msg.id; setUsername(); break; case "username": text = "User " + msg.name + " signed in at " + timeStr + "
"; break; case "message": text = "(" + timeStr + ") " + msg.name + ": " + msg.text + "
"; break; case "rejectusername": myUsername = msg.name; text = "Your username has been set to " + myUsername + " because the name you chose is in use.
"; break; case "userlist": // Received an updated user list handleUserlistMsg(msg); break; // Signaling messages: these messages are used to trade WebRTC // signaling information during negotiations leading up to a video // call. case "video-offer": // Invitation and offer to chat handleVideoOfferMsg(msg); break; case "video-answer": // Callee has answered our offer handleVideoAnswerMsg(msg); break; case "new-ice-candidate": // A new ICE candidate has been received handleNewICECandidateMsg(msg); break; case "hang-up": // The other peer has hung up the call handleHangUpMsg(msg); break; // Unknown message; output to console for debugging. default: log_error("Unknown message received:"); log_error(msg); } // If there's text to insert into the chat buffer, do so now, then // scroll the chat panel so that the new text is visible. if (text.length) { chatBox.innerHTML += text; chatBox.scrollTop = chatBox.scrollHeight - chatBox.clientHeight; } }; } // Handles a click on the Send button (or pressing return/enter) by // building a "message" object and sending it to the server. function handleSendButton() { var msg = { text: document.getElementById("text").value, type: "message", id: clientID, date: Date.now() }; sendToServer(msg); document.getElementById("text").value = ""; } // Handler for keyboard events. This is used to intercept the return and // enter keys so that we can call send() to transmit the entered text // to the server. function handleKey(evt) { if (evt.keyCode === 13 || evt.keyCode === 14) { if (!document.getElementById("send").disabled) { handleSendButton(); } } } // Create the RTCPeerConnection which knows how to talk to our // selected STUN/TURN server and then uses getUserMedia() to find // our camera and microphone and add that stream to the connection for // use in our video call. Then we configure event handlers to get // needed notifications on the call. async function createPeerConnection() { log("Setting up a connection..."); // Create an RTCPeerConnection which knows to use our chosen // STUN server. myPeerConnection = new RTCPeerConnection({ iceServers: [ // Information about ICE servers - Use your own! { urls: "turn:" + myHostname, // A TURN server username: "webrtc", credential: "turnserver" } ] }); // Set up event handlers for the ICE negotiation process. myPeerConnection.onicecandidate = handleICECandidateEvent; myPeerConnection.oniceconnectionstatechange = handleICEConnectionStateChangeEvent; myPeerConnection.onicegatheringstatechange = handleICEGatheringStateChangeEvent; myPeerConnection.onsignalingstatechange = handleSignalingStateChangeEvent; myPeerConnection.onnegotiationneeded = handleNegotiationNeededEvent; myPeerConnection.ontrack = handleTrackEvent; } // Called by the WebRTC layer to let us know when it's time to // begin, resume, or restart ICE negotiation. async function handleNegotiationNeededEvent() { log("*** Negotiation needed"); try { log("---> Creating offer"); const offer = await myPeerConnection.createOffer(); // If the connection hasn't yet achieved the "stable" state, // return to the caller. Another negotiationneeded event // will be fired when the state stabilizes. if (myPeerConnection.signalingState != "stable") { log(" -- The connection isn't stable yet; postponing...") return; } // Establish the offer as the local peer's current // description. log("---> Setting local description to the offer"); await myPeerConnection.setLocalDescription(offer); // Send the offer to the remote peer. log("---> Sending the offer to the remote peer"); sendToServer({ name: myUsername, target: targetUsername, type: "video-offer", sdp: myPeerConnection.localDescription }); } catch(err) { log("*** The following error occurred while handling the negotiationneeded event:"); reportError(err); }; } // Called by the WebRTC layer when events occur on the media tracks // on our WebRTC call. This includes when streams are added to and // removed from the call. // // track events include the following fields: // // RTCRtpReceiver receiver // MediaStreamTrack track // MediaStream[] streams // RTCRtpTransceiver transceiver // // In our case, we're just taking the first stream found and attaching // it to the