Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeffAwesome/bb1d5b03e14fccac8d1bae33a75c2e5e to your computer and use it in GitHub Desktop.
Save jeffAwesome/bb1d5b03e14fccac8d1bae33a75c2e5e to your computer and use it in GitHub Desktop.
AppRTC main JS code
var RTCPeerConnection = null;
var getUserMedia = null;
var attachMediaStream = null;
var reattachMediaStream = null;
var webrtcDetectedBrowser = null;
var webrtcDetectedVersion = null;
function trace(text) {
if (text[text.length - 1] === "\n") {
text = text.substring(0, text.length - 1);
}
if (window.performance) {
var now = (window.performance.now() / 1E3).toFixed(3);
console.log(now + ": " + text);
} else {
console.log(text);
}
}
if (navigator.mozGetUserMedia) {
console.log("This appears to be Firefox");
webrtcDetectedBrowser = "firefox";
webrtcDetectedVersion = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
RTCPeerConnection = function(pcConfig, pcConstraints) {
if (pcConfig && pcConfig.iceServers) {
for (var i = 0; i < pcConfig.iceServers.length; i++) {
if (pcConfig.iceServers[i].hasOwnProperty("urls")) {
pcConfig.iceServers[i].url = pcConfig.iceServers[i].urls;
delete pcConfig.iceServers[i].urls;
}
}
}
return new mozRTCPeerConnection(pcConfig, pcConstraints);
};
window.RTCSessionDescription = mozRTCSessionDescription;
window.RTCIceCandidate = mozRTCIceCandidate;
getUserMedia = navigator.mozGetUserMedia.bind(navigator);
navigator.getUserMedia = getUserMedia;
MediaStreamTrack.getSources = function(successCb) {
setTimeout(function() {
var infos = [{
kind: "audio",
id: "default",
label: "",
facing: ""
}, {
kind: "video",
id: "default",
label: "",
facing: ""
}];
successCb(infos);
}, 0);
};
window.createIceServer = function(url, username, password) {
var iceServer = null;
var urlParts = url.split(":");
if (urlParts[0].indexOf("stun") === 0) {
iceServer = {
"url": url
};
} else {
if (urlParts[0].indexOf("turn") === 0) {
if (webrtcDetectedVersion < 27) {
var turnUrlParts = url.split("?");
if (turnUrlParts.length === 1 || turnUrlParts[1].indexOf("transport=udp") === 0) {
iceServer = {
"url": turnUrlParts[0],
"credential": password,
"username": username
};
}
} else {
iceServer = {
"url": url,
"credential": password,
"username": username
};
}
}
}
return iceServer;
};
window.createIceServers = function(urls, username, password) {
var iceServers = [];
for (var i = 0; i < urls.length; i++) {
var iceServer = window.createIceServer(urls[i], username, password);
if (iceServer !== null) {
iceServers.push(iceServer);
}
}
return iceServers;
};
attachMediaStream = function(element, stream) {
console.log("Attaching media stream");
element.mozSrcObject = stream;
};
reattachMediaStream = function(to, from) {
console.log("Reattaching media stream");
to.mozSrcObject = from.mozSrcObject;
};
} else {
if (navigator.webkitGetUserMedia) {
console.log("This appears to be Chrome");
webrtcDetectedBrowser = "chrome";
var result = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
if (result !== null) {
webrtcDetectedVersion = parseInt(result[2], 10);
} else {
webrtcDetectedVersion = 999;
}
window.createIceServer = function(url, username, password) {
var iceServer = null;
var urlParts = url.split(":");
if (urlParts[0].indexOf("stun") === 0) {
iceServer = {
"url": url
};
} else {
if (urlParts[0].indexOf("turn") === 0) {
iceServer = {
"url": url,
"credential": password,
"username": username
};
}
}
return iceServer;
};
window.createIceServers = function(urls, username, password) {
return {
"urls": urls,
"credential": password,
"username": username
};
};
RTCPeerConnection = function(pcConfig, pcConstraints) {
return new webkitRTCPeerConnection(pcConfig, pcConstraints);
};
getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
navigator.getUserMedia = getUserMedia;
attachMediaStream = function(element, stream) {
if (typeof element.srcObject !== "undefined") {
element.srcObject = stream;
} else {
if (typeof element.mozSrcObject !== "undefined") {
element.mozSrcObject = stream;
} else {
if (typeof element.src !== "undefined") {
element.src = URL.createObjectURL(stream);
} else {
console.log("Error attaching stream to element.");
}
}
}
};
reattachMediaStream = function(to, from) {
to.src = from.src;
};
} else {
console.log("Browser does not appear to be WebRTC-capable");
}
}
function requestUserMedia(constraints) {
return new Promise(function(resolve, reject) {
var onSuccess = function(stream) {
resolve(stream);
};
var onError = function(error) {
reject(error);
};
try {
getUserMedia(constraints, onSuccess, onError);
} catch (e) {
reject(e);
}
});
};
var remoteVideo = $("#remote-video");
var UI_CONSTANTS = {
confirmJoinButton: "#confirm-join-button",
confirmJoinDiv: "#confirm-join-div",
confirmJoinRoomSpan: "#confirm-join-room-span",
fullscreenSvg: "#fullscreen",
hangupSvg: "#hangup",
icons: "#icons",
infoDiv: "#info-div",
localVideo: "#local-video",
miniVideo: "#mini-video",
muteAudioSvg: "#mute-audio",
muteVideoSvg: "#mute-video",
newRoomButton: "#new-room-button",
newRoomLink: "#new-room-link",
remoteVideo: "#remote-video",
rejoinButton: "#rejoin-button",
rejoinDiv: "#rejoin-div",
rejoinLink: "#rejoin-link",
roomLinkHref: "#room-link-href",
roomSelectionDiv: "#room-selection",
roomSelectionInput: "#room-id-input",
roomSelectionInputLabel: "#room-id-input-label",
roomSelectionJoinButton: "#join-button",
roomSelectionRandomButton: "#random-button",
roomSelectionRecentList: "#recent-rooms-list",
sharingDiv: "#sharing-div",
statusDiv: "#status-div",
videosDiv: "#videos"
};
var AppController = function(loadingParams) {
trace("Initializing; server= " + loadingParams.roomServer + ".");
trace("Initializing; room=" + loadingParams.roomId + ".");
this.hangupSvg_ = $(UI_CONSTANTS.hangupSvg);
this.icons_ = $(UI_CONSTANTS.icons);
this.localVideo_ = $(UI_CONSTANTS.localVideo);
this.miniVideo_ = $(UI_CONSTANTS.miniVideo);
this.sharingDiv_ = $(UI_CONSTANTS.sharingDiv);
this.statusDiv_ = $(UI_CONSTANTS.statusDiv);
this.remoteVideo_ = $(UI_CONSTANTS.remoteVideo);
this.videosDiv_ = $(UI_CONSTANTS.videosDiv);
this.roomLinkHref_ = $(UI_CONSTANTS.roomLinkHref);
this.rejoinDiv_ = $(UI_CONSTANTS.rejoinDiv);
this.rejoinLink_ = $(UI_CONSTANTS.rejoinLink);
this.newRoomLink_ = $(UI_CONSTANTS.newRoomLink);
this.rejoinButton_ = $(UI_CONSTANTS.rejoinButton);
this.newRoomButton_ = $(UI_CONSTANTS.newRoomButton);
this.newRoomButton_.addEventListener("click", this.onNewRoomClick_.bind(this), false);
this.rejoinButton_.addEventListener("click", this.onRejoinClick_.bind(this), false);
this.muteAudioIconSet_ = new AppController.IconSet_(UI_CONSTANTS.muteAudioSvg);
this.muteVideoIconSet_ = new AppController.IconSet_(UI_CONSTANTS.muteVideoSvg);
this.fullscreenIconSet_ = new AppController.IconSet_(UI_CONSTANTS.fullscreenSvg);
this.loadingParams_ = loadingParams;
this.loadUrlParams_();
var paramsPromise = Promise.resolve({});
if (this.loadingParams_.paramsFunction) {
paramsPromise = this.loadingParams_.paramsFunction();
}
Promise.resolve(paramsPromise).then(function(newParams) {
if (newParams) {
Object.keys(newParams).forEach(function(key) {
this.loadingParams_[key] = newParams[key];
}.bind(this));
}
this.roomLink_ = "";
this.roomSelection_ = null;
this.localStream_ = null;
this.remoteVideoResetTimer_ = null;
if (this.loadingParams_.roomId) {
this.createCall_();
if (!RoomSelection.matchRandomRoomPattern(this.loadingParams_.roomId)) {
$(UI_CONSTANTS.confirmJoinRoomSpan).textContent = ' "' + this.loadingParams_.roomId + '"';
}
var confirmJoinDiv = $(UI_CONSTANTS.confirmJoinDiv);
this.show_(confirmJoinDiv);
$(UI_CONSTANTS.confirmJoinButton).onclick = function() {
this.hide_(confirmJoinDiv);
var recentlyUsedList = new RoomSelection.RecentlyUsedList;
recentlyUsedList.pushRecentRoom(this.loadingParams_.roomId);
this.finishCallSetup_(this.loadingParams_.roomId);
}.bind(this);
if (this.loadingParams_.bypassJoinConfirmation) {
$(UI_CONSTANTS.confirmJoinButton).onclick();
}
} else {
this.showRoomSelection_();
}
}.bind(this)).catch(function(error) {
trace("Error initializing: " + error.message);
}.bind(this));
};
AppController.prototype.createCall_ = function() {
this.call_ = new Call(this.loadingParams_);
this.infoBox_ = new InfoBox($(UI_CONSTANTS.infoDiv), this.remoteVideo_, this.call_, this.loadingParams_.versionInfo);
var roomErrors = this.loadingParams_.errorMessages;
if (roomErrors && roomErrors.length > 0) {
for (var i = 0; i < roomErrors.length; ++i) {
this.infoBox_.pushErrorMessage(roomErrors[i]);
}
return;
}
this.call_.onremotehangup = this.onRemoteHangup_.bind(this);
this.call_.onremotesdpset = this.onRemoteSdpSet_.bind(this);
this.call_.onremotestreamadded = this.onRemoteStreamAdded_.bind(this);
this.call_.onlocalstreamadded = this.onLocalStreamAdded_.bind(this);
this.call_.onsignalingstatechange = this.infoBox_.updateInfoDiv.bind(this.infoBox_);
this.call_.oniceconnectionstatechange = this.infoBox_.updateInfoDiv.bind(this.infoBox_);
this.call_.onnewicecandidate = this.infoBox_.recordIceCandidateTypes.bind(this.infoBox_);
this.call_.onerror = this.displayError_.bind(this);
this.call_.onstatusmessage = this.displayStatus_.bind(this);
this.call_.oncallerstarted = this.displaySharingInfo_.bind(this);
};
AppController.prototype.showRoomSelection_ = function() {
var roomSelectionDiv = $(UI_CONSTANTS.roomSelectionDiv);
this.roomSelection_ = new RoomSelection(roomSelectionDiv, UI_CONSTANTS);
this.show_(roomSelectionDiv);
this.roomSelection_.onRoomSelected = function(roomName) {
this.hide_(roomSelectionDiv);
this.createCall_();
this.finishCallSetup_(roomName);
this.roomSelection_ = null;
if (this.localStream_) {
this.attachLocalStream_();
}
}.bind(this);
};
AppController.prototype.finishCallSetup_ = function(roomId) {
this.call_.start(roomId);
window.onbeforeunload = this.call_.hangup.bind(this.call_);
document.onkeypress = this.onKeyPress_.bind(this);
window.onmousemove = this.showIcons_.bind(this);
$(UI_CONSTANTS.muteAudioSvg).onclick = this.toggleAudioMute_.bind(this);
$(UI_CONSTANTS.muteVideoSvg).onclick = this.toggleVideoMute_.bind(this);
$(UI_CONSTANTS.fullscreenSvg).onclick = this.toggleFullScreen_.bind(this);
$(UI_CONSTANTS.hangupSvg).onclick = this.hangup_.bind(this);
setUpFullScreen();
if (!isChromeApp()) {
window.onpopstate = function(event) {
if (!event.state) {
trace("Reloading main page.");
location.href = location.origin;
} else {
if (event.state.roomLink) {
location.href = event.state.roomLink;
}
}
};
}
};
AppController.prototype.hangup_ = function() {
trace("Hanging up.");
this.hide_(this.icons_);
this.displayStatus_("Hanging up");
this.transitionToDone_();
this.call_.hangup();
};
AppController.prototype.onRemoteHangup_ = function() {
this.displayStatus_("The remote side hung up.");
this.transitionToWaiting_();
this.call_.onRemoteHangup();
};
AppController.prototype.onRemoteSdpSet_ = function(hasRemoteVideo) {
if (hasRemoteVideo) {
trace("Waiting for remote video.");
this.waitForRemoteVideo_();
} else {
trace("No remote video stream; not waiting for media to arrive.");
this.transitionToActive_();
}
};
AppController.prototype.waitForRemoteVideo_ = function() {
if (this.remoteVideo_.readyState >= 2) {
trace("Remote video started; currentTime: " + this.remoteVideo_.currentTime);
this.transitionToActive_();
} else {
this.remoteVideo_.oncanplay = this.waitForRemoteVideo_.bind(this);
}
};
AppController.prototype.onRemoteStreamAdded_ = function(stream) {
this.deactivate_(this.sharingDiv_);
trace("Remote stream added.");
attachMediaStream(this.remoteVideo_, stream);
if (this.remoteVideoResetTimer_) {
clearTimeout(this.remoteVideoResetTimer_);
this.remoteVideoResetTimer_ = null;
}
};
AppController.prototype.onLocalStreamAdded_ = function(stream) {
trace("User has granted access to local media.");
this.localStream_ = stream;
if (!this.roomSelection_) {
this.attachLocalStream_();
}
};
AppController.prototype.attachLocalStream_ = function() {
attachMediaStream(this.localVideo_, this.localStream_);
this.displayStatus_("");
this.activate_(this.localVideo_);
this.show_(this.icons_);
};
AppController.prototype.transitionToActive_ = function() {
this.remoteVideo_.oncanplay = undefined;
var connectTime = window.performance.now();
this.infoBox_.setSetupTimes(this.call_.startTime, connectTime);
this.infoBox_.updateInfoDiv();
trace("Call setup time: " + (connectTime - this.call_.startTime).toFixed(0) + "ms.");
trace("reattachMediaStream: " + this.localVideo_.src);
reattachMediaStream(this.miniVideo_, this.localVideo_);
this.activate_(this.remoteVideo_);
this.activate_(this.miniVideo_);
this.deactivate_(this.localVideo_);
this.localVideo_.src = "";
this.activate_(this.videosDiv_);
this.show_(this.hangupSvg_);
this.displayStatus_("");
};
AppController.prototype.transitionToWaiting_ = function() {
this.remoteVideo_.oncanplay = undefined;
this.hide_(this.hangupSvg_);
this.deactivate_(this.videosDiv_);
if (!this.remoteVideoResetTimer_) {
this.remoteVideoResetTimer_ = setTimeout(function() {
this.remoteVideoResetTimer_ = null;
trace("Resetting remoteVideo src after transitioning to waiting.");
this.remoteVideo_.src = "";
}.bind(this), 800);
}
this.localVideo_.src = this.miniVideo_.src;
this.activate_(this.localVideo_);
this.deactivate_(this.remoteVideo_);
this.deactivate_(this.miniVideo_);
};
AppController.prototype.transitionToDone_ = function() {
this.remoteVideo_.oncanplay = undefined;
this.deactivate_(this.localVideo_);
this.deactivate_(this.remoteVideo_);
this.deactivate_(this.miniVideo_);
this.hide_(this.hangupSvg_);
this.activate_(this.rejoinDiv_);
this.show_(this.rejoinDiv_);
this.displayStatus_("");
};
AppController.prototype.onRejoinClick_ = function() {
this.deactivate_(this.rejoinDiv_);
this.hide_(this.rejoinDiv_);
this.call_.restart();
};
AppController.prototype.onNewRoomClick_ = function() {
this.deactivate_(this.rejoinDiv_);
this.hide_(this.rejoinDiv_);
this.showRoomSelection_();
};
AppController.prototype.onKeyPress_ = function(event) {
switch (String.fromCharCode(event.charCode)) {
case " ":
;
case "m":
if (this.call_) {
this.call_.toggleAudioMute();
}
return false;
case "c":
if (this.call_) {
this.call_.toggleVideoMute();
}
return false;
case "f":
this.toggleFullScreen_();
return false;
case "i":
this.infoBox_.toggleInfoDiv();
return false;
case "q":
this.hangup_();
return false;
default:
return;
}
};
AppController.prototype.pushCallNavigation_ = function(roomId, roomLink) {
if (!isChromeApp()) {
window.history.pushState({
"roomId": roomId,
"roomLink": roomLink
}, roomId, roomLink);
}
};
AppController.prototype.displaySharingInfo_ = function(roomId, roomLink) {
this.roomLinkHref_.href = roomLink;
this.roomLinkHref_.text = roomLink;
this.roomLink_ = roomLink;
this.pushCallNavigation_(roomId, roomLink);
this.activate_(this.sharingDiv_);
};
AppController.prototype.displayStatus_ = function(status) {
if (status === "") {
this.deactivate_(this.statusDiv_);
} else {
this.activate_(this.statusDiv_);
}
this.statusDiv_.innerHTML = status;
};
AppController.prototype.displayError_ = function(error) {
trace(error);
this.infoBox_.pushErrorMessage(error);
};
AppController.prototype.toggleAudioMute_ = function() {
this.call_.toggleAudioMute();
this.muteAudioIconSet_.toggle();
};
AppController.prototype.toggleVideoMute_ = function() {
this.call_.toggleVideoMute();
this.muteVideoIconSet_.toggle();
};
AppController.prototype.toggleFullScreen_ = function() {
if (isFullScreen()) {
trace("Exiting fullscreen.");
document.cancelFullScreen();
} else {
trace("Entering fullscreen.");
document.body.requestFullScreen();
}
this.fullscreenIconSet_.toggle();
};
AppController.prototype.hide_ = function(element) {
element.classList.add("hidden");
};
AppController.prototype.show_ = function(element) {
element.classList.remove("hidden");
};
AppController.prototype.activate_ = function(element) {
element.classList.add("active");
};
AppController.prototype.deactivate_ = function(element) {
element.classList.remove("active");
};
AppController.prototype.showIcons_ = function() {
if (!this.icons_.classList.contains("active")) {
this.activate_(this.icons_);
setTimeout(function() {
this.deactivate_(this.icons_);
}.bind(this), 5E3);
}
};
AppController.prototype.loadUrlParams_ = function() {
var urlParams = queryStringToDictionary(window.location.search);
this.loadingParams_.audioSendBitrate = urlParams["asbr"];
this.loadingParams_.audioSendCodec = urlParams["asc"];
this.loadingParams_.audioRecvBitrate = urlParams["arbr"];
this.loadingParams_.audioRecvCodec = urlParams["arc"];
this.loadingParams_.opusMaxPbr = urlParams["opusmaxpbr"];
this.loadingParams_.opusFec = urlParams["opusfec"];
this.loadingParams_.opusStereo = urlParams["stereo"];
this.loadingParams_.videoSendBitrate = urlParams["vsbr"];
this.loadingParams_.videoSendInitialBitrate = urlParams["vsibr"];
this.loadingParams_.videoSendCodec = urlParams["vsc"];
this.loadingParams_.videoRecvBitrate = urlParams["vrbr"];
this.loadingParams_.videoRecvCodec = urlParams["vrc"];
};
AppController.IconSet_ = function(iconSelector) {
this.iconElement = document.querySelector(iconSelector);
};
AppController.IconSet_.prototype.toggle = function() {
if (this.iconElement.classList.contains("on")) {
this.iconElement.classList.remove("on");
} else {
this.iconElement.classList.add("on");
}
};
var Call = function(params) {
this.params_ = params;
this.roomServer_ = params.roomServer || "";
this.channel_ = new SignalingChannel(params.wssUrl, params.wssPostUrl);
this.channel_.onmessage = this.onRecvSignalingChannelMessage_.bind(this);
this.pcClient_ = null;
this.localStream_ = null;
this.startTime = null;
this.oncallerstarted = null;
this.onerror = null;
this.oniceconnectionstatechange = null;
this.onlocalstreamadded = null;
this.onnewicecandidate = null;
this.onremotehangup = null;
this.onremotesdpset = null;
this.onremotestreamadded = null;
this.onsignalingstatechange = null;
this.onstatusmessage = null;
this.getMediaPromise_ = null;
this.getTurnServersPromise_ = null;
this.requestMediaAndTurnServers_();
};
Call.prototype.requestMediaAndTurnServers_ = function() {
this.getMediaPromise_ = this.maybeGetMedia_();
this.getTurnServersPromise_ = this.maybeGetTurnServers_();
};
Call.prototype.isInitiator = function() {
return this.params_.isInitiator;
};
Call.prototype.start = function(roomId) {
this.connectToRoom_(roomId);
if (this.params_.isLoopback) {
setupLoopback(this.params_.wssUrl, roomId);
}
};
Call.prototype.restart = function() {
this.requestMediaAndTurnServers_();
this.start(this.params_.previousRoomId);
};
Call.prototype.hangup = function() {
this.startTime = null;
if (this.localStream_) {
this.localStream_.stop();
this.localStream_ = null;
}
if (!this.params_.roomId) {
return;
}
if (this.pcClient_) {
this.pcClient_.close();
this.pcClient_ = null;
}
var path = this.roomServer_ + "/leave/" + this.params_.roomId + "/" + this.params_.clientId;
var xhr = new XMLHttpRequest;
xhr.open("POST", path, false);
xhr.send();
this.channel_.send(JSON.stringify({
type: "bye"
}));
this.channel_.close();
this.params_.previousRoomId = this.params_.roomId;
this.params_.roomId = null;
this.params_.clientId = null;
};
Call.prototype.onRemoteHangup = function() {
this.startTime = null;
this.params_.isInitiator = true;
if (this.pcClient_) {
this.pcClient_.close();
this.pcClient_ = null;
}
this.startSignaling_();
};
Call.prototype.getPeerConnectionStates = function() {
if (!this.pcClient_) {
return null;
}
return this.pcClient_.getPeerConnectionStates();
};
Call.prototype.getPeerConnectionStats = function(callback) {
if (!this.pcClient_) {
return;
}
this.pcClient_.getPeerConnectionStats(callback);
};
Call.prototype.toggleVideoMute = function() {
var videoTracks = this.localStream_.getVideoTracks();
if (videoTracks.length === 0) {
trace("No local video available.");
return;
}
trace("Toggling video mute state.");
for (var i = 0; i < videoTracks.length; ++i) {
videoTracks[i].enabled = !videoTracks[i].enabled;
}
trace("Video " + (videoTracks[0].enabled ? "unmuted." : "muted."));
};
Call.prototype.toggleAudioMute = function() {
var audioTracks = this.localStream_.getAudioTracks();
if (audioTracks.length === 0) {
trace("No local audio available.");
return;
}
trace("Toggling audio mute state.");
for (var i = 0; i < audioTracks.length; ++i) {
audioTracks[i].enabled = !audioTracks[i].enabled;
}
trace("Audio " + (audioTracks[0].enabled ? "unmuted." : "muted."));
};
Call.prototype.connectToRoom_ = function(roomId) {
this.params_.roomId = roomId;
var channelPromise = this.channel_.open().catch(function(error) {
this.onError_("WebSocket open error: " + error.message);
return Promise.reject(error);
}.bind(this));
var joinPromise = this.joinRoom_().then(function(roomParams) {
this.params_.clientId = roomParams.client_id;
this.params_.roomId = roomParams.room_id;
this.params_.roomLink = roomParams.room_link;
this.params_.isInitiator = roomParams.is_initiator === "true";
this.params_.messages = roomParams.messages;
}.bind(this)).catch(function(error) {
this.onError_("Room server join error: " + error.message);
return Promise.reject(error);
}.bind(this));
Promise.all([channelPromise, joinPromise]).then(function() {
this.channel_.register(this.params_.roomId, this.params_.clientId);
Promise.all([this.getTurnServersPromise_, this.getMediaPromise_]).then(function() {
this.startSignaling_();
}.bind(this)).catch(function(error) {
this.onError_("Failed to start signaling: " + error.message);
}.bind(this));
}.bind(this)).catch(function(error) {
this.onError_("WebSocket register error: " + error.message);
}.bind(this));
};
Call.prototype.maybeGetMedia_ = function() {
var needStream = this.params_.mediaConstraints.audio !== false || this.params_.mediaConstraints.video !== false;
var mediaPromise = null;
if (needStream) {
var mediaConstraints = this.params_.mediaConstraints;
mediaPromise = requestUserMedia(mediaConstraints).then(function(stream) {
trace("Got access to local media with mediaConstraints:\n" + " '" + JSON.stringify(mediaConstraints) + "'");
this.onUserMediaSuccess_(stream);
}.bind(this)).catch(function(error) {
this.onError_("Error getting user media: " + error.message);
this.onUserMediaError_(error);
}.bind(this));
} else {
mediaPromise = Promise.resolve();
}
return mediaPromise;
};
Call.prototype.maybeGetTurnServers_ = function() {
var shouldRequestTurnServers = this.params_.turnRequestUrl && this.params_.turnRequestUrl.length > 0;
var turnPromise = null;
if (shouldRequestTurnServers) {
var requestUrl = this.params_.turnRequestUrl;
turnPromise = requestTurnServers(requestUrl, this.params_.turnTransports).then(function(turnServers) {
var iceServers = this.params_.peerConnectionConfig.iceServers;
this.params_.peerConnectionConfig.iceServers = iceServers.concat(turnServers);
}.bind(this)).catch(function(error) {
if (this.onstatusmessage) {
var subject = encodeURIComponent("AppRTC demo TURN server not working");
this.onstatusmessage("No TURN server; unlikely that media will traverse networks. " + "If this persists please " + '<a href="mailto:discuss-webrtc@googlegroups.com?' + "subject=" + subject + '">' + "report it to discuss-webrtc@googlegroups.com</a>.");
}
trace(error.message);
}.bind(this));
} else {
turnPromise = Promise.resolve();
}
return turnPromise;
};
Call.prototype.onUserMediaSuccess_ = function(stream) {
this.localStream_ = stream;
if (this.onlocalstreamadded) {
this.onlocalstreamadded(stream);
}
};
Call.prototype.onUserMediaError_ = function(error) {
var errorMessage = "Failed to get access to local media. Error name was " + error.name + ". Continuing without sending a stream.";
this.onError_("getUserMedia error: " + errorMessage);
alert(errorMessage);
};
Call.prototype.maybeCreatePcClient_ = function() {
if (this.pcClient_) {
return;
}
try {
this.pcClient_ = new PeerConnectionClient(this.params_, this.startTime);
this.pcClient_.onsignalingmessage = this.sendSignalingMessage_.bind(this);
this.pcClient_.onremotehangup = this.onremotehangup;
this.pcClient_.onremotesdpset = this.onremotesdpset;
this.pcClient_.onremotestreamadded = this.onremotestreamadded;
this.pcClient_.onsignalingstatechange = this.onsignalingstatechange;
this.pcClient_.oniceconnectionstatechange = this.oniceconnectionstatechange;
this.pcClient_.onnewicecandidate = this.onnewicecandidate;
this.pcClient_.onerror = this.onerror;
trace("Created PeerConnectionClient");
} catch (e) {
this.onError_("Create PeerConnection exception: " + e.message);
alert("Cannot create RTCPeerConnection; " + "WebRTC is not supported by this browser.");
return;
}
};
Call.prototype.startSignaling_ = function() {
trace("Starting signaling.");
if (this.isInitiator() && this.oncallerstarted) {
this.oncallerstarted(this.params_.roomId, this.params_.roomLink);
}
this.startTime = window.performance.now();
this.maybeCreatePcClient_();
if (this.localStream_) {
trace("Adding local stream.");
this.pcClient_.addStream(this.localStream_);
}
if (this.params_.isInitiator) {
this.pcClient_.startAsCaller(this.params_.offerConstraints);
} else {
this.pcClient_.startAsCallee(this.params_.messages);
}
};
Call.prototype.joinRoom_ = function() {
return new Promise(function(resolve, reject) {
if (!this.params_.roomId) {
reject(Error("Missing room id."));
}
var path = this.roomServer_ + "/join/" + this.params_.roomId + window.location.search;
sendAsyncUrlRequest("POST", path).then(function(response) {
var responseObj = parseJSON(response);
if (!responseObj) {
reject(Error("Error parsing response JSON."));
return;
}
if (responseObj.result !== "SUCCESS") {
reject(Error("Registration error: " + responseObj.result));
return;
}
trace("Joined the room.");
resolve(responseObj.params);
}.bind(this)).catch(function(error) {
reject(Error("Failed to join the room: " + error.message));
return;
}.bind(this));
}.bind(this));
};
Call.prototype.onRecvSignalingChannelMessage_ = function(msg) {
this.maybeCreatePcClient_();
this.pcClient_.receiveSignalingMessage(msg);
};
Call.prototype.sendSignalingMessage_ = function(message) {
var msgString = JSON.stringify(message);
if (this.params_.isInitiator) {
var path = this.roomServer_ + "/message/" + this.params_.roomId + "/" + this.params_.clientId + window.location.search;
var xhr = new XMLHttpRequest;
xhr.open("POST", path, true);
xhr.send(msgString);
trace("C->GAE: " + msgString);
} else {
this.channel_.send(msgString);
}
};
Call.prototype.onError_ = function(message) {
if (this.onerror) {
this.onerror(message);
}
};
var InfoBox = function(infoDiv, remoteVideo, call, versionInfo) {
this.infoDiv_ = infoDiv;
this.remoteVideo_ = remoteVideo;
this.call_ = call;
this.versionInfo_ = versionInfo;
this.errorMessages_ = [];
this.startTime_ = null;
this.connectTime_ = null;
this.stats_ = null;
this.prevStats_ = null;
this.getStatsTimer_ = null;
this.iceCandidateTypes_ = {
Local: {},
Remote: {}
};
};
InfoBox.prototype.recordIceCandidateTypes = function(location, candidate) {
var type = iceCandidateType(candidate);
var types = this.iceCandidateTypes_[location];
if (!types[type]) {
types[type] = 1;
} else {
++types[type];
}
this.updateInfoDiv();
};
InfoBox.prototype.pushErrorMessage = function(msg) {
this.errorMessages_.push(msg);
this.updateInfoDiv();
this.showInfoDiv();
};
InfoBox.prototype.setSetupTimes = function(startTime, connectTime) {
this.startTime_ = startTime;
this.connectTime_ = connectTime;
};
InfoBox.prototype.showInfoDiv = function() {
this.getStatsTimer_ = setInterval(this.refreshStats_.bind(this), 1E3);
this.refreshStats_();
this.infoDiv_.classList.add("active");
};
InfoBox.prototype.toggleInfoDiv = function() {
if (this.infoDiv_.classList.contains("active")) {
clearInterval(this.getStatsTimer_);
this.infoDiv_.classList.remove("active");
} else {
this.showInfoDiv();
}
};
InfoBox.prototype.refreshStats_ = function() {
this.call_.getPeerConnectionStats(function(response) {
this.prevStats_ = this.stats_;
this.stats_ = response.result();
this.updateInfoDiv();
}.bind(this));
};
InfoBox.prototype.updateInfoDiv = function() {
var contents = '<pre id="info-box-stats" style="line-height: initial">';
if (this.stats_) {
var states = this.call_.getPeerConnectionStates();
if (!states) {
return;
}
contents += this.buildLine_("States");
contents += this.buildLine_("Signaling", states.signalingState);
contents += this.buildLine_("Gathering", states.iceGatheringState);
contents += this.buildLine_("Connection", states.iceConnectionState);
for (var endpoint in this.iceCandidateTypes_) {
var types = [];
for (var type in this.iceCandidateTypes_[endpoint]) {
types.push(type + ":" + this.iceCandidateTypes_[endpoint][type]);
}
contents += this.buildLine_(endpoint, types.join(" "));
}
var activeCandPair = getStatsReport(this.stats_, "googCandidatePair", "googActiveConnection", "true");
var localAddr;
var remoteAddr;
var localAddrType;
var remoteAddrType;
if (activeCandPair) {
localAddr = activeCandPair.stat("googLocalAddress");
remoteAddr = activeCandPair.stat("googRemoteAddress");
localAddrType = activeCandPair.stat("googLocalCandidateType");
remoteAddrType = activeCandPair.stat("googRemoteCandidateType");
}
if (localAddr && remoteAddr) {
contents += this.buildLine_("LocalAddr", localAddr + " (" + localAddrType + ")");
contents += this.buildLine_("RemoteAddr", remoteAddr + " (" + remoteAddrType + ")");
}
contents += this.buildLine_();
contents += this.buildStatsSection_();
}
if (this.errorMessages_.length) {
this.infoDiv_.classList.add("warning");
for (var i = 0; i !== this.errorMessages_.length; ++i) {
contents += this.errorMessages_[i] + "\n";
}
} else {
this.infoDiv_.classList.remove("warning");
}
if (this.versionInfo_) {
contents += this.buildLine_();
contents += this.buildLine_("Version");
for (var key in this.versionInfo_) {
contents += this.buildLine_(key, this.versionInfo_[key]);
}
}
contents += "</pre>";
if (this.infoDiv_.innerHTML !== contents) {
this.infoDiv_.innerHTML = contents;
}
};
InfoBox.prototype.buildStatsSection_ = function() {
var contents = this.buildLine_("Stats");
var rtt = extractStatAsInt(this.stats_, "ssrc", "googRtt");
var captureStart = extractStatAsInt(this.stats_, "ssrc", "googCaptureStartNtpTimeMs");
var e2eDelay = computeE2EDelay(captureStart, this.remoteVideo_.currentTime);
if (this.endTime_ !== null) {
contents += this.buildLine_("Call time", InfoBox.formatInterval_(window.performance.now() - this.connectTime_));
contents += this.buildLine_("Setup time", InfoBox.formatMsec_(this.connectTime_ - this.startTime_));
}
if (rtt !== null) {
contents += this.buildLine_("RTT", InfoBox.formatMsec_(rtt));
}
if (e2eDelay !== null) {
contents += this.buildLine_("End to end", InfoBox.formatMsec_(e2eDelay));
}
var txAudio = getStatsReport(this.stats_, "ssrc", "audioInputLevel");
var rxAudio = getStatsReport(this.stats_, "ssrc", "audioOutputLevel");
var txVideo = getStatsReport(this.stats_, "ssrc", "googFirsReceived");
var rxVideo = getStatsReport(this.stats_, "ssrc", "googFirsSent");
var txPrevAudio = getStatsReport(this.prevStats_, "ssrc", "audioInputLevel");
var rxPrevAudio = getStatsReport(this.prevStats_, "ssrc", "audioOutputLevel");
var txPrevVideo = getStatsReport(this.prevStats_, "ssrc", "googFirsReceived");
var rxPrevVideo = getStatsReport(this.prevStats_, "ssrc", "googFirsSent");
var txAudioCodec;
var txAudioBitrate;
var txAudioPacketRate;
var rxAudioCodec;
var rxAudioBitrate;
var rxAudioPacketRate;
var txVideoHeight;
var txVideoFps;
var txVideoCodec;
var txVideoBitrate;
var txVideoPacketRate;
var rxVideoHeight;
var rxVideoFps;
var rxVideoCodec;
var rxVideoBitrate;
var rxVideoPacketRate;
if (txAudio) {
txAudioCodec = txAudio.stat("googCodecName");
txAudioBitrate = computeBitrate(txAudio, txPrevAudio, "bytesSent");
txAudioPacketRate = computeRate(txAudio, txPrevAudio, "packetsSent");
contents += this.buildLine_("Audio Tx", txAudioCodec + ", " + InfoBox.formatBitrate_(txAudioBitrate) + ", " + InfoBox.formatPacketRate_(txAudioPacketRate));
}
if (rxAudio) {
rxAudioCodec = rxAudio.stat("googCodecName");
rxAudioBitrate = computeBitrate(rxAudio, rxPrevAudio, "bytesReceived");
rxAudioPacketRate = computeRate(rxAudio, rxPrevAudio, "packetsReceived");
contents += this.buildLine_("Audio Rx", rxAudioCodec + ", " + InfoBox.formatBitrate_(rxAudioBitrate) + ", " + InfoBox.formatPacketRate_(rxAudioPacketRate));
}
if (txVideo) {
txVideoCodec = txVideo.stat("googCodecName");
txVideoHeight = txVideo.stat("googFrameHeightSent");
txVideoFps = txVideo.stat("googFrameRateSent");
txVideoBitrate = computeBitrate(txVideo, txPrevVideo, "bytesSent");
txVideoPacketRate = computeRate(txVideo, txPrevVideo, "packetsSent");
contents += this.buildLine_("Video Tx", txVideoCodec + ", " + txVideoHeight.toString() + "p" + txVideoFps.toString() + ", " + InfoBox.formatBitrate_(txVideoBitrate) + ", " + InfoBox.formatPacketRate_(txVideoPacketRate));
}
if (rxVideo) {
rxVideoCodec = "TODO";
rxVideoHeight = this.remoteVideo_.videoHeight;
rxVideoFps = rxVideo.stat("googFrameRateDecoded");
rxVideoBitrate = computeBitrate(rxVideo, rxPrevVideo, "bytesReceived");
rxVideoPacketRate = computeRate(rxVideo, rxPrevVideo, "packetsReceived");
contents += this.buildLine_("Video Rx", rxVideoCodec + ", " + rxVideoHeight.toString() + "p" + rxVideoFps.toString() + ", " + InfoBox.formatBitrate_(rxVideoBitrate) + ", " + InfoBox.formatPacketRate_(rxVideoPacketRate));
}
return contents;
};
InfoBox.prototype.buildLine_ = function(label, value) {
var columnWidth = 12;
var line = "";
if (label) {
line += label + ":";
while (line.length < columnWidth) {
line += " ";
}
if (value) {
line += value;
}
}
line += "\n";
return line;
};
InfoBox.formatInterval_ = function(value) {
var result = "";
var seconds = Math.floor(value / 1E3);
var minutes = Math.floor(seconds / 60);
var hours = Math.floor(minutes / 60);
var formatTwoDigit = function(twodigit) {
return (twodigit < 10 ? "0" : "") + twodigit.toString();
};
if (hours > 0) {
result += formatTwoDigit(hours) + ":";
}
result += formatTwoDigit(minutes - hours * 60) + ":";
result += formatTwoDigit(seconds - minutes * 60);
return result;
};
InfoBox.formatMsec_ = function(value) {
return value.toFixed(0).toString() + " ms";
};
InfoBox.formatBitrate_ = function(value) {
if (!value) {
return "- bps";
}
var suffix;
if (value < 1E3) {
suffix = "bps";
} else {
if (value < 1E6) {
suffix = "kbps";
value /= 1E3;
} else {
suffix = "Mbps";
value /= 1E6;
}
}
var str = value.toPrecision(3) + " " + suffix;
return str;
};
InfoBox.formatPacketRate_ = function(value) {
if (!value) {
return "- pps";
}
return value.toPrecision(3) + " " + "pps";
};
var PeerConnectionClient = function(params, startTime) {
this.params_ = params;
this.startTime_ = startTime;
trace("Creating RTCPeerConnnection with:\n" + " config: '" + JSON.stringify(params.peerConnectionConfig) + "';\n" + " constraints: '" + JSON.stringify(params.peerConnectionConstraints) + "'.");
this.pc_ = new RTCPeerConnection(params.peerConnectionConfig, params.peerConnectionConstraints);
this.pc_.onicecandidate = this.onIceCandidate_.bind(this);
this.pc_.onaddstream = this.onRemoteStreamAdded_.bind(this);
this.pc_.onremovestream = trace.bind(null, "Remote stream removed.");
this.pc_.onsignalingstatechange = this.onSignalingStateChanged_.bind(this);
this.pc_.oniceconnectionstatechange = this.onIceConnectionStateChanged_.bind(this);
this.hasRemoteSdp_ = false;
this.messageQueue_ = [];
this.isInitiator_ = false;
this.started_ = false;
this.onerror = null;
this.oniceconnectionstatechange = null;
this.onnewicecandidate = null;
this.onremotehangup = null;
this.onremotesdpset = null;
this.onremotestreamadded = null;
this.onsignalingmessage = null;
this.onsignalingstatechange = null;
};
PeerConnectionClient.DEFAULT_SDP_CONSTRAINTS_ = {
"mandatory": {
"OfferToReceiveAudio": true,
"OfferToReceiveVideo": true
},
"optional": [{
"VoiceActivityDetection": false
}]
};
PeerConnectionClient.prototype.addStream = function(stream) {
if (!this.pc_) {
return;
}
this.pc_.addStream(stream);
};
PeerConnectionClient.prototype.startAsCaller = function(offerConstraints) {
if (!this.pc_) {
return false;
}
if (this.started_) {
return false;
}
this.isInitiator_ = true;
this.started_ = true;
var constraints = mergeConstraints(offerConstraints, PeerConnectionClient.DEFAULT_SDP_CONSTRAINTS_);
trace("Sending offer to peer, with constraints: \n'" + JSON.stringify(constraints) + "'.");
this.pc_.createOffer(this.setLocalSdpAndNotify_.bind(this), this.onError_.bind(this, "createOffer"), constraints);
return true;
};
PeerConnectionClient.prototype.startAsCallee = function(initialMessages) {
if (!this.pc_) {
return false;
}
if (this.started_) {
return false;
}
this.isInitiator_ = false;
this.started_ = true;
if (initialMessages && initialMessages.length > 0) {
for (var i = 0, len = initialMessages.length; i < len; i++) {
this.receiveSignalingMessage(initialMessages[i]);
}
return true;
}
if (this.messageQueue_.length > 0) {
this.drainMessageQueue_();
}
return true;
};
PeerConnectionClient.prototype.receiveSignalingMessage = function(message) {
var messageObj = parseJSON(message);
if (!messageObj) {
return;
}
if (this.isInitiator_ && messageObj.type === "answer" || !this.isInitiator_ && messageObj.type === "offer") {
this.hasRemoteSdp_ = true;
this.messageQueue_.unshift(messageObj);
} else {
if (messageObj.type === "candidate") {
this.messageQueue_.push(messageObj);
} else {
if (messageObj.type === "bye") {
if (this.onremotehangup) {
this.onremotehangup();
}
}
}
}
this.drainMessageQueue_();
};
PeerConnectionClient.prototype.close = function() {
if (!this.pc_) {
return;
}
this.pc_.close();
this.pc_ = null;
};
PeerConnectionClient.prototype.getPeerConnectionStates = function() {
if (!this.pc_) {
return null;
}
return {
"signalingState": this.pc_.signalingState,
"iceGatheringState": this.pc_.iceGatheringState,
"iceConnectionState": this.pc_.iceConnectionState
};
};
PeerConnectionClient.prototype.getPeerConnectionStats = function(callback) {
if (!this.pc_) {
return;
}
this.pc_.getStats(callback);
};
PeerConnectionClient.prototype.doAnswer_ = function() {
trace("Sending answer to peer.");
this.pc_.createAnswer(this.setLocalSdpAndNotify_.bind(this), this.onError_.bind(this, "createAnswer"), PeerConnectionClient.DEFAULT_SDP_CONSTRAINTS_);
};
PeerConnectionClient.prototype.setLocalSdpAndNotify_ = function(sessionDescription) {
sessionDescription.sdp = maybePreferAudioReceiveCodec(sessionDescription.sdp, this.params_);
sessionDescription.sdp = maybePreferVideoReceiveCodec(sessionDescription.sdp, this.params_);
sessionDescription.sdp = maybeSetAudioReceiveBitRate(sessionDescription.sdp, this.params_);
sessionDescription.sdp = maybeSetVideoReceiveBitRate(sessionDescription.sdp, this.params_);
this.pc_.setLocalDescription(sessionDescription, trace.bind(null, "Set session description success."), this.onError_.bind(this, "setLocalDescription"));
if (this.onsignalingmessage) {
this.onsignalingmessage(sessionDescription);
}
};
PeerConnectionClient.prototype.setRemoteSdp_ = function(message) {
message.sdp = maybeSetOpusOptions(message.sdp, this.params_);
message.sdp = maybePreferAudioSendCodec(message.sdp, this.params_);
message.sdp = maybePreferVideoSendCodec(message.sdp, this.params_);
message.sdp = maybeSetAudioSendBitRate(message.sdp, this.params_);
message.sdp = maybeSetVideoSendBitRate(message.sdp, this.params_);
message.sdp = maybeSetVideoSendInitialBitRate(message.sdp, this.params_);
this.pc_.setRemoteDescription(new RTCSessionDescription(message), this.onSetRemoteDescriptionSuccess_.bind(this), this.onError_.bind(this, "setRemoteDescription"));
};
PeerConnectionClient.prototype.onSetRemoteDescriptionSuccess_ = function() {
trace("Set remote session description success.");
var remoteStreams = this.pc_.getRemoteStreams();
if (this.onremotesdpset) {
this.onremotesdpset(remoteStreams.length > 0 && remoteStreams[0].getVideoTracks().length > 0);
}
};
PeerConnectionClient.prototype.processSignalingMessage_ = function(message) {
if (message.type === "offer" && !this.isInitiator_) {
if (this.pc_.signalingState !== "stable") {
trace("ERROR: remote offer received in unexpected state: " + this.pc_.signalingState);
return;
}
this.setRemoteSdp_(message);
this.doAnswer_();
} else {
if (message.type === "answer" && this.isInitiator_) {
if (this.pc_.signalingState !== "have-local-offer") {
trace("ERROR: remote answer received in unexpected state: " + this.pc_.signalingState);
return;
}
this.setRemoteSdp_(message);
} else {
if (message.type === "candidate") {
var candidate = new RTCIceCandidate({
sdpMLineIndex: message.label,
candidate: message.candidate
});
this.recordIceCandidate_("Remote", candidate);
this.pc_.addIceCandidate(candidate, trace.bind(null, "Remote candidate added successfully."), this.onError_.bind(this, "addIceCandidate"));
} else {
trace("WARNING: unexpected message: " + JSON.stringify(message));
}
}
}
};
PeerConnectionClient.prototype.drainMessageQueue_ = function() {
if (!this.pc_ || !this.started_ || !this.hasRemoteSdp_) {
return;
}
for (var i = 0, len = this.messageQueue_.length; i < len; i++) {
this.processSignalingMessage_(this.messageQueue_[i]);
}
this.messageQueue_ = [];
};
PeerConnectionClient.prototype.onIceCandidate_ = function(event) {
if (event.candidate) {
if (this.filterIceCandidate_(event.candidate)) {
var message = {
type: "candidate",
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate
};
if (this.onsignalingmessage) {
this.onsignalingmessage(message);
}
this.recordIceCandidate_("Local", event.candidate);
}
} else {
trace("End of candidates.");
}
};
PeerConnectionClient.prototype.onSignalingStateChanged_ = function() {
if (!this.pc_) {
return;
}
trace("Signaling state changed to: " + this.pc_.signalingState);
if (this.onsignalingstatechange) {
this.onsignalingstatechange();
}
};
PeerConnectionClient.prototype.onIceConnectionStateChanged_ = function() {
if (!this.pc_) {
return;
}
trace("ICE connection state changed to: " + this.pc_.iceConnectionState);
if (this.pc_.iceConnectionState === "completed") {
trace("ICE complete time: " + (window.performance.now() - this.startTime_).toFixed(0) + "ms.");
}
if (this.oniceconnectionstatechange) {
this.oniceconnectionstatechange();
}
};
PeerConnectionClient.prototype.filterIceCandidate_ = function(candidateObj) {
var candidateStr = candidateObj.candidate;
if (candidateStr.indexOf("tcp") !== -1) {
return false;
}
if (this.params_.peerConnectionConfig.iceTransports === "relay" && iceCandidateType(candidateStr) !== "relay") {
return false;
}
return true;
};
PeerConnectionClient.prototype.recordIceCandidate_ = function(location, candidateObj) {
if (this.onnewicecandidate) {
this.onnewicecandidate(location, candidateObj.candidate);
}
};
PeerConnectionClient.prototype.onRemoteStreamAdded_ = function(event) {
if (this.onremotestreamadded) {
this.onremotestreamadded(event.stream);
}
};
PeerConnectionClient.prototype.onError_ = function(tag, error) {
if (this.onerror) {
this.onerror(tag + ": " + error.toString());
}
};
var RoomSelection = function(roomSelectionDiv, uiConstants, recentRoomsKey, setupCompletedCallback) {
this.roomSelectionDiv_ = roomSelectionDiv;
this.setupCompletedCallback_ = setupCompletedCallback;
this.roomIdInput_ = this.roomSelectionDiv_.querySelector(uiConstants.roomSelectionInput);
this.roomIdInputLabel_ = this.roomSelectionDiv_.querySelector(uiConstants.roomSelectionInputLabel);
this.roomJoinButton_ = this.roomSelectionDiv_.querySelector(uiConstants.roomSelectionJoinButton);
this.roomRandomButton_ = this.roomSelectionDiv_.querySelector(uiConstants.roomSelectionRandomButton);
this.roomRecentList_ = this.roomSelectionDiv_.querySelector(uiConstants.roomSelectionRecentList);
this.roomIdInput_.value = randomString(9);
this.onRoomIdInput_();
this.roomIdInput_.addEventListener("input", this.onRoomIdInput_.bind(this), false);
this.roomIdInput_.addEventListener("keyup", this.onRoomIdKeyPress_.bind(this), false);
this.roomRandomButton_.addEventListener("click", this.onRandomButton_.bind(this), false);
this.roomJoinButton_.addEventListener("click", this.onJoinButton_.bind(this), false);
this.onRoomSelected = null;
this.recentlyUsedList_ = new RoomSelection.RecentlyUsedList(recentRoomsKey);
this.startBuildingRecentRoomList_();
};
RoomSelection.matchRandomRoomPattern = function(input) {
return input.match(/^\d{9}$/) !== null;
};
RoomSelection.prototype.startBuildingRecentRoomList_ = function() {
this.recentlyUsedList_.getRecentRooms().then(function(recentRooms) {
this.buildRecentRoomList_(recentRooms);
if (this.setupCompletedCallback_) {
this.setupCompletedCallback_();
}
}.bind(this)).catch(function(error) {
trace("Error building recent rooms list: " + error.message);
}.bind(this));
};
RoomSelection.prototype.buildRecentRoomList_ = function(recentRooms) {
var lastChild = this.roomRecentList_.lastChild;
while (lastChild) {
this.roomRecentList_.removeChild(lastChild);
lastChild = this.roomRecentList_.lastChild;
}
for (var i = 0; i < recentRooms.length; ++i) {
var li = document.createElement("li");
var href = document.createElement("a");
var linkText = document.createTextNode(recentRooms[i]);
href.appendChild(linkText);
href.href = location.origin + "/r/" + encodeURIComponent(recentRooms[i]);
li.appendChild(href);
this.roomRecentList_.appendChild(li);
href.addEventListener("click", this.makeRecentlyUsedClickHandler_(recentRooms[i]).bind(this), false);
}
};
RoomSelection.prototype.onRoomIdInput_ = function() {
var room = this.roomIdInput_.value;
var valid = room.length >= 5;
var re = /^\w+$/;
valid = valid && re.exec(room);
if (valid) {
this.roomJoinButton_.disabled = false;
this.roomIdInput_.classList.remove("invalid");
this.roomIdInputLabel_.classList.add("hidden");
} else {
this.roomJoinButton_.disabled = true;
this.roomIdInput_.classList.add("invalid");
this.roomIdInputLabel_.classList.remove("hidden");
}
};
RoomSelection.prototype.onRoomIdKeyPress_ = function(event) {
if (event.which !== 13 || this.roomJoinButton_.disabled) {
return;
}
this.onJoinButton_();
};
RoomSelection.prototype.onRandomButton_ = function() {
this.roomIdInput_.value = randomString(9);
this.onRoomIdInput_();
};
RoomSelection.prototype.onJoinButton_ = function() {
this.loadRoom_(this.roomIdInput_.value);
};
RoomSelection.prototype.makeRecentlyUsedClickHandler_ = function(roomName) {
return function(e) {
e.preventDefault();
this.loadRoom_(roomName);
};
};
RoomSelection.prototype.loadRoom_ = function(roomName) {
this.recentlyUsedList_.pushRecentRoom(roomName);
if (this.onRoomSelected) {
this.onRoomSelected(roomName);
}
};
RoomSelection.RecentlyUsedList = function(key) {
this.LISTLENGTH_ = 10;
this.RECENTROOMSKEY_ = key || "recentRooms";
this.storage_ = new Storage;
};
RoomSelection.RecentlyUsedList.prototype.pushRecentRoom = function(roomId) {
return new Promise(function(resolve, reject) {
if (!roomId) {
resolve();
return;
}
this.getRecentRooms().then(function(recentRooms) {
recentRooms = [roomId].concat(recentRooms);
recentRooms = recentRooms.filter(function(value, index, self) {
return self.indexOf(value) === index;
});
recentRooms = recentRooms.slice(0, this.LISTLENGTH_);
this.storage_.setStorage(this.RECENTROOMSKEY_, JSON.stringify(recentRooms), function() {
resolve();
});
}.bind(this)).catch(function(err) {
reject(err);
}.bind(this));
}.bind(this));
};
RoomSelection.RecentlyUsedList.prototype.getRecentRooms = function() {
return new Promise(function(resolve) {
this.storage_.getStorage(this.RECENTROOMSKEY_, function(value) {
var recentRooms = parseJSON(value);
if (!recentRooms) {
recentRooms = [];
}
resolve(recentRooms);
});
}.bind(this));
};
function mergeConstraints(cons1, cons2) {
if (!cons1 || !cons2) {
return cons1 || cons2;
}
var merged = cons1;
for (var name in cons2.mandatory) {
merged.mandatory[name] = cons2.mandatory[name];
}
merged.optional = merged.optional.concat(cons2.optional);
return merged;
}
function iceCandidateType(candidateStr) {
return candidateStr.split(" ")[7];
}
function maybeSetOpusOptions(sdp, params) {
if (params.opusStereo === "true") {
sdp = setCodecParam(sdp, "opus/48000", "stereo", "1");
} else {
if (params.opusStereo === "false") {
sdp = removeCodecParam(sdp, "opus/48000", "stereo");
}
}
if (params.opusFec === "true") {
sdp = setCodecParam(sdp, "opus/48000", "useinbandfec", "1");
} else {
if (params.opusFec === "false") {
sdp = removeCodecParam(sdp, "opus/48000", "useinbandfec");
}
}
if (params.opusMaxPbr) {
sdp = setCodecParam(sdp, "opus/48000", "maxplaybackrate", params.opusMaxPbr);
}
return sdp;
}
function maybeSetAudioSendBitRate(sdp, params) {
if (!params.audioSendBitrate) {
return sdp;
}
trace("Prefer audio send bitrate: " + params.audioSendBitrate);
return preferBitRate(sdp, params.audioSendBitrate, "audio");
}
function maybeSetAudioReceiveBitRate(sdp, params) {
if (!params.audioRecvBitrate) {
return sdp;
}
trace("Prefer audio receive bitrate: " + params.audioRecvBitrate);
return preferBitRate(sdp, params.audioRecvBitrate, "audio");
}
function maybeSetVideoSendBitRate(sdp, params) {
if (!params.videoSendBitrate) {
return sdp;
}
trace("Prefer video send bitrate: " + params.videoSendBitrate);
return preferBitRate(sdp, params.videoSendBitrate, "video");
}
function maybeSetVideoReceiveBitRate(sdp, params) {
if (!params.videoRecvBitrate) {
return sdp;
}
trace("Prefer video receive bitrate: " + params.videoRecvBitrate);
return preferBitRate(sdp, params.videoRecvBitrate, "video");
}
function preferBitRate(sdp, bitrate, mediaType) {
var sdpLines = sdp.split("\r\n");
var mLineIndex = findLine(sdpLines, "m=", mediaType);
if (mLineIndex === null) {
trace("Failed to add bandwidth line to sdp, as no m-line found");
return sdp;
}
var nextMLineIndex = findLineInRange(sdpLines, mLineIndex + 1, -1, "m=");
if (nextMLineIndex === null) {
nextMLineIndex = sdpLines.length;
}
var cLineIndex = findLineInRange(sdpLines, mLineIndex + 1, nextMLineIndex, "c=");
if (cLineIndex === null) {
trace("Failed to add bandwidth line to sdp, as no c-line found");
return sdp;
}
var bLineIndex = findLineInRange(sdpLines, cLineIndex + 1, nextMLineIndex, "b=AS");
if (bLineIndex) {
sdpLines.splice(bLineIndex, 1);
}
var bwLine = "b=AS:" + bitrate;
sdpLines.splice(cLineIndex + 1, 0, bwLine);
sdp = sdpLines.join("\r\n");
return sdp;
}
function maybeSetVideoSendInitialBitRate(sdp, params) {
var initialBitrate = params.videoSendInitialBitrate;
if (!initialBitrate) {
return sdp;
}
var maxBitrate = initialBitrate;
var bitrate = params.videoSendBitrate;
if (bitrate) {
if (initialBitrate > bitrate) {
trace("Clamping initial bitrate to max bitrate of " + bitrate + " kbps.");
initialBitrate = bitrate;
params.videoSendInitialBitrate = initialBitrate;
}
maxBitrate = bitrate;
}
var sdpLines = sdp.split("\r\n");
var mLineIndex = findLine(sdpLines, "m=", "video");
if (mLineIndex === null) {
trace("Failed to find video m-line");
return sdp;
}
sdp = setCodecParam(sdp, "VP8/90000", "x-google-min-bitrate", params.videoSendInitialBitrate.toString());
sdp = setCodecParam(sdp, "VP8/90000", "x-google-max-bitrate", maxBitrate.toString());
return sdp;
}
function maybePreferAudioSendCodec(sdp, params) {
return maybePreferCodec(sdp, "audio", "send", params.audioSendCodec);
}
function maybePreferAudioReceiveCodec(sdp, params) {
return maybePreferCodec(sdp, "audio", "receive", params.audioRecvCodec);
}
function maybePreferVideoSendCodec(sdp, params) {
return maybePreferCodec(sdp, "video", "send", params.videoSendCodec);
}
function maybePreferVideoReceiveCodec(sdp, params) {
return maybePreferCodec(sdp, "video", "receive", params.videoRecvCodec);
}
function maybePreferCodec(sdp, type, dir, codec) {
var str = type + " " + dir + " codec";
if (codec === "") {
trace("No preference on " + str + ".");
return sdp;
}
trace("Prefer " + str + ": " + codec);
var sdpLines = sdp.split("\r\n");
var mLineIndex = findLine(sdpLines, "m=", type);
if (mLineIndex === null) {
return sdp;
}
var payload = getCodecPayloadType(sdpLines, codec);
if (payload) {
sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], payload);
}
sdp = sdpLines.join("\r\n");
return sdp;
}
function setCodecParam(sdp, codec, param, value) {
var sdpLines = sdp.split("\r\n");
var fmtpLineIndex = findFmtpLine(sdpLines, codec);
var fmtpObj = {};
if (fmtpLineIndex === null) {
var index = findLine(sdpLines, "a=rtpmap", codec);
if (index === null) {
return sdp;
}
var payload = getCodecPayloadTypeFromLine(sdpLines[index]);
fmtpObj.pt = payload.toString();
fmtpObj.params = {};
fmtpObj.params[param] = value;
sdpLines.splice(index + 1, 0, writeFmtpLine(fmtpObj));
} else {
fmtpObj = parseFmtpLine(sdpLines[fmtpLineIndex]);
fmtpObj.params[param] = value;
sdpLines[fmtpLineIndex] = writeFmtpLine(fmtpObj);
}
sdp = sdpLines.join("\r\n");
return sdp;
}
function removeCodecParam(sdp, codec, param) {
var sdpLines = sdp.split("\r\n");
var fmtpLineIndex = findFmtpLine(sdpLines, codec);
if (fmtpLineIndex === null) {
return sdp;
}
var map = parseFmtpLine(sdpLines[fmtpLineIndex]);
delete map.params[param];
var newLine = writeFmtpLine(map);
if (newLine === null) {
sdpLines.splice(fmtpLineIndex, 1);
} else {
sdpLines[fmtpLineIndex] = newLine;
}
sdp = sdpLines.join("\r\n");
return sdp;
}
function parseFmtpLine(fmtpLine) {
var fmtpObj = {};
var spacePos = fmtpLine.indexOf(" ");
var keyValues = fmtpLine.substring(spacePos + 1).split("; ");
var pattern = new RegExp("a=fmtp:(\\d+)");
var result = fmtpLine.match(pattern);
if (result && result.length === 2) {
fmtpObj.pt = result[1];
} else {
return null;
}
var params = {};
for (var i = 0; i < keyValues.length; ++i) {
var pair = keyValues[i].split("=");
if (pair.length === 2) {
params[pair[0]] = pair[1];
}
}
fmtpObj.params = params;
return fmtpObj;
}
function writeFmtpLine(fmtpObj) {
if (!fmtpObj.hasOwnProperty("pt") || !fmtpObj.hasOwnProperty("params")) {
return null;
}
var pt = fmtpObj.pt;
var params = fmtpObj.params;
var keyValues = [];
var i = 0;
for (var key in params) {
keyValues[i] = key + "=" + params[key];
++i;
}
if (i === 0) {
return null;
}
return "a=fmtp:" + pt.toString() + " " + keyValues.join("; ");
}
function findFmtpLine(sdpLines, codec) {
var payload = getCodecPayloadType(sdpLines, codec);
return payload ? findLine(sdpLines, "a=fmtp:" + payload.toString()) : null;
}
function findLine(sdpLines, prefix, substr) {
return findLineInRange(sdpLines, 0, -1, prefix, substr);
}
function findLineInRange(sdpLines, startLine, endLine, prefix, substr) {
var realEndLine = endLine !== -1 ? endLine : sdpLines.length;
for (var i = startLine; i < realEndLine; ++i) {
if (sdpLines[i].indexOf(prefix) === 0) {
if (!substr || sdpLines[i].toLowerCase().indexOf(substr.toLowerCase()) !== -1) {
return i;
}
}
}
return null;
}
function getCodecPayloadType(sdpLines, codec) {
var index = findLine(sdpLines, "a=rtpmap", codec);
return index ? getCodecPayloadTypeFromLine(sdpLines[index]) : null;
}
function getCodecPayloadTypeFromLine(sdpLine) {
var pattern = new RegExp("a=rtpmap:(\\d+) \\w+\\/\\d+");
var result = sdpLine.match(pattern);
return result && result.length === 2 ? result[1] : null;
}
function setDefaultCodec(mLine, payload) {
var elements = mLine.split(" ");
var newLine = elements.slice(0, 3);
newLine.push(payload);
for (var i = 3; i < elements.length; i++) {
if (elements[i] !== payload) {
newLine.push(elements[i]);
}
}
return newLine.join(" ");
};
var SignalingChannel = function(wssUrl, wssPostUrl) {
this.wssUrl_ = wssUrl;
this.wssPostUrl_ = wssPostUrl;
this.roomId_ = null;
this.clientId_ = null;
this.websocket_ = null;
this.registered_ = false;
this.onerror = null;
this.onmessage = null;
};
SignalingChannel.prototype.open = function() {
if (this.websocket_) {
trace("ERROR: SignalingChannel has already opened.");
return;
}
trace("Opening signaling channel.");
return new Promise(function(resolve, reject) {
this.websocket_ = new WebSocket(this.wssUrl_);
this.websocket_.onopen = function() {
trace("Signaling channel opened.");
this.websocket_.onerror = function() {
trace("Signaling channel error.");
};
this.websocket_.onclose = function(event) {
trace("Channel closed with code:" + event.code + " reason:" + event.reason);
this.websocket_ = null;
this.registered_ = false;
};
if (this.clientId_ && this.roomId_) {
this.register(this.roomId_, this.clientId_);
}
resolve();
}.bind(this);
this.websocket_.onmessage = function(event) {
trace("WSS->C: " + event.data);
var message = parseJSON(event.data);
if (!message) {
trace("Failed to parse WSS message: " + event.data);
return;
}
if (message.error) {
trace("Signaling server error message: " + message.error);
return;
}
this.onmessage(message.msg);
}.bind(this);
this.websocket_.onerror = function() {
reject(Error("WebSocket error."));
};
}.bind(this));
};
SignalingChannel.prototype.register = function(roomId, clientId) {
if (this.registered_) {
trace("ERROR: SignalingChannel has already registered.");
return;
}
this.roomId_ = roomId;
this.clientId_ = clientId;
if (!this.roomId_) {
trace("ERROR: missing roomId.");
}
if (!this.clientId_) {
trace("ERROR: missing clientId.");
}
if (!this.websocket_ || this.websocket_.readyState !== WebSocket.OPEN) {
trace("WebSocket not open yet; saving the IDs to register later.");
return;
}
trace("Registering signaling channel.");
var registerMessage = {
cmd: "register",
roomid: this.roomId_,
clientid: this.clientId_
};
this.websocket_.send(JSON.stringify(registerMessage));
this.registered_ = true;
trace("Signaling channel registered.");
};
SignalingChannel.prototype.close = function() {
if (this.websocket_) {
this.websocket_.close();
this.websocket_ = null;
}
if (!this.clientId_ || !this.roomId_) {
return;
}
var path = this.wssPostUrl_ + "/" + this.roomId_ + "/" + this.clientId_;
var xhr = new XMLHttpRequest;
xhr.open("DELETE", path, false);
xhr.send();
this.clientId_ = null;
this.roomId_ = null;
this.registered_ = false;
};
SignalingChannel.prototype.send = function(message) {
if (!this.roomId_ || !this.clientId_) {
trace("ERROR: SignalingChannel has not registered.");
return;
}
trace("C->WSS: " + message);
var wssMessage = {
cmd: "send",
msg: message
};
var msgString = JSON.stringify(wssMessage);
if (this.websocket_ && this.websocket_.readyState === WebSocket.OPEN) {
this.websocket_.send(msgString);
} else {
var path = this.wssPostUrl_ + "/" + this.roomId_ + "/" + this.clientId_;
var xhr = new XMLHttpRequest;
xhr.open("POST", path, true);
xhr.send(wssMessage.msg);
}
};
function extractStatAsInt(stats, statObj, statName) {
var str = extractStat(stats, statObj, statName);
if (str) {
var val = parseInt(str);
if (val !== -1) {
return val;
}
}
return null;
}
function extractStat(stats, statObj, statName) {
var report = getStatsReport(stats, statObj, statName);
if (report && report.names().indexOf(statName) !== -1) {
return report.stat(statName);
}
return null;
}
function getStatsReport(stats, statObj, statName, statVal) {
if (stats) {
for (var i = 0; i < stats.length; ++i) {
var report = stats[i];
if (report.type === statObj) {
var found = true;
if (statName) {
var val = report.stat(statName);
found = statVal !== undefined ? val === statVal : val;
}
if (found) {
return report;
}
}
}
}
}
function computeRate(newReport, oldReport, statName) {
var newVal = newReport.stat(statName);
var oldVal = oldReport ? oldReport.stat(statName) : null;
if (newVal === null || oldVal === null) {
return null;
}
return (newVal - oldVal) / (newReport.timestamp - oldReport.timestamp) * 1E3;
}
function computeBitrate(newReport, oldReport, statName) {
return computeRate(newReport, oldReport, statName) * 8;
}
function computeE2EDelay(captureStart, remoteVideoCurrentTime) {
if (!captureStart) {
return null;
}
var nowNTP = Date.now() + 22089888E5;
return nowNTP - captureStart - remoteVideoCurrentTime * 1E3;
};
var Storage = function() {};
Storage.prototype.getStorage = function(key, callback) {
if (isChromeApp()) {
chrome.storage.local.get(key, function(values) {
if (callback) {
window.setTimeout(function() {
callback(values[key]);
}, 0);
}
});
} else {
var value = localStorage.getItem(key);
if (callback) {
window.setTimeout(function() {
callback(value);
}, 0);
}
}
};
Storage.prototype.setStorage = function(key, value, callback) {
if (isChromeApp()) {
var data = {};
data[key] = value;
chrome.storage.local.set(data, callback);
} else {
localStorage.setItem(key, value);
if (callback) {
window.setTimeout(callback, 0);
}
}
};
function $(selector) {
return document.querySelector(selector);
}
function queryStringToDictionary(queryString) {
var pairs = queryString.slice(1).split("&");
var result = {};
pairs.forEach(function(pair) {
if (pair) {
pair = pair.split("=");
if (pair[0]) {
result[pair[0]] = decodeURIComponent(pair[1] || "");
}
}
});
return result;
}
function sendAsyncUrlRequest(method, url, body) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest;
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) {
return;
}
if (xhr.status !== 200) {
reject(Error("Status=" + xhr.status + ", response=" + xhr.responseText));
return;
}
resolve(xhr.responseText);
};
xhr.open(method, url, true);
xhr.send(body);
});
}
function requestTurnServers(turnRequestUrl, turnTransports) {
return new Promise(function(resolve, reject) {
sendAsyncUrlRequest("GET", turnRequestUrl).then(function(response) {
var turnServerResponse = parseJSON(response);
if (!turnServerResponse) {
reject(Error("Error parsing response JSON: " + response));
return;
}
if (turnTransports.length > 0) {
filterTurnUrls(turnServerResponse.uris, turnTransports);
}
var turnServers = createIceServers(turnServerResponse.uris, turnServerResponse.username, turnServerResponse.password);
if (!turnServers) {
reject(Error("Error creating ICE servers from response."));
return;
}
trace("Retrieved TURN server information.");
resolve(turnServers);
}).catch(function(error) {
reject(Error("TURN server request error: " + error.message));
return;
});
});
}
function parseJSON(json) {
try {
return JSON.parse(json);
} catch (e) {
trace("Error parsing json: " + json);
}
return null;
}
function filterTurnUrls(urls, protocol) {
for (var i = 0; i < urls.length;) {
var parts = urls[i].split("?");
if (parts.length > 1 && parts[1] !== "transport=" + protocol) {
urls.splice(i, 1);
} else {
++i;
}
}
}
function setUpFullScreen() {
if (isChromeApp()) {
document.cancelFullScreen = function() {
chrome.app.window.current().restore();
};
} else {
document.cancelFullScreen = document.webkitCancelFullScreen || document.mozCancelFullScreen || document.cancelFullScreen;
}
if (isChromeApp()) {
document.body.requestFullScreen = function() {
chrome.app.window.current().fullscreen();
};
} else {
document.body.requestFullScreen = document.body.webkitRequestFullScreen || document.body.mozRequestFullScreen || document.body.requestFullScreen;
}
document.onfullscreenchange = document.onfullscreenchange || document.onwebkitfullscreenchange || document.onmozfullscreenchange;
}
function isFullScreen() {
if (isChromeApp()) {
return chrome.app.window.current().isFullscreen();
}
return !!(document.webkitIsFullScreen || document.mozFullScreen || document.isFullScreen);
}
function fullScreenElement() {
return document.webkitFullScreenElement || document.webkitCurrentFullScreenElement || document.mozFullScreenElement || document.fullScreenElement;
}
function randomString(strLength) {
var result = [];
strLength = strLength || 5;
var charSet = "0123456789";
while (strLength--) {
result.push(charSet.charAt(Math.floor(Math.random() * charSet.length)));
}
return result.join("");
}
function isChromeApp() {
return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.local !== "undefined";
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment