"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