diff --git a/source/js/libs/adapter.js b/source/js/libs/adapter.js index 047efa4..99ab9ba 100644 --- a/source/js/libs/adapter.js +++ b/source/js/libs/adapter.js @@ -1,4 +1,4 @@ -/*! adapterjs - v0.11.0 - 2015-06-08 */ +/*! adapterjs - v0.13.4 - 2016-09-22 */ // Adapter's interface. var AdapterJS = AdapterJS || {}; @@ -17,7 +17,7 @@ AdapterJS.options = AdapterJS.options || {}; // AdapterJS.options.hidePluginInstallPrompt = true; // AdapterJS version -AdapterJS.VERSION = '0.11.0'; +AdapterJS.VERSION = '0.13.4'; // This function will be called when the WebRTC API is ready to be used // Whether it is the native implementation (Chrome, Firefox, Opera) or @@ -34,6 +34,9 @@ AdapterJS.onwebrtcready = AdapterJS.onwebrtcready || function(isUsingPlugin) { // Override me and do whatever you want here }; +// New interface to store multiple callbacks, private +AdapterJS._onwebrtcreadies = []; + // Sets a callback function to be called when the WebRTC interface is ready. // The first argument is the function to callback.\ // Throws an error if the first argument is not a function @@ -47,7 +50,7 @@ AdapterJS.webRTCReady = function (callback) { callback(null !== AdapterJS.WebRTCPlugin.plugin); } else { // will be triggered automatically when your browser/plugin is ready. - AdapterJS.onwebrtcready = callback; + AdapterJS._onwebrtcreadies.push(callback); } }; @@ -55,7 +58,8 @@ AdapterJS.webRTCReady = function (callback) { AdapterJS.WebRTCPlugin = AdapterJS.WebRTCPlugin || {}; // The object to store plugin information -AdapterJS.WebRTCPlugin.pluginInfo = { +/* jshint ignore:start */ +AdapterJS.WebRTCPlugin.pluginInfo = AdapterJS.WebRTCPlugin.pluginInfo || { prefix : 'Tem', plugName : 'TemWebRTCPlugin', pluginId : 'plugin0', @@ -63,15 +67,29 @@ AdapterJS.WebRTCPlugin.pluginInfo = { onload : '__TemWebRTCReady0', portalLink : 'http://skylink.io/plugin/', downloadLink : null, //set below - companyName: 'Temasys' + companyName: 'Temasys', + downloadLinks : { + mac: 'http://bit.ly/1n77hco', + win: 'http://bit.ly/1kkS4FN' + } }; -if(!!navigator.platform.match(/^Mac/i)) { - AdapterJS.WebRTCPlugin.pluginInfo.downloadLink = 'http://bit.ly/1n77hco'; -} -else if(!!navigator.platform.match(/^Win/i)) { - AdapterJS.WebRTCPlugin.pluginInfo.downloadLink = 'http://bit.ly/1kkS4FN'; +if(typeof AdapterJS.WebRTCPlugin.pluginInfo.downloadLinks !== "undefined" && AdapterJS.WebRTCPlugin.pluginInfo.downloadLinks !== null) { + if(!!navigator.platform.match(/^Mac/i)) { + AdapterJS.WebRTCPlugin.pluginInfo.downloadLink = AdapterJS.WebRTCPlugin.pluginInfo.downloadLinks.mac; + } + else if(!!navigator.platform.match(/^Win/i)) { + AdapterJS.WebRTCPlugin.pluginInfo.downloadLink = AdapterJS.WebRTCPlugin.pluginInfo.downloadLinks.win; + } } +/* jshint ignore:end */ + +AdapterJS.WebRTCPlugin.TAGS = { + NONE : 'none', + AUDIO : 'audio', + VIDEO : 'video' +}; + // Unique identifier of each opened page AdapterJS.WebRTCPlugin.pageId = Math.random().toString(36).slice(2); @@ -153,15 +171,13 @@ AdapterJS.WebRTCPlugin.callWhenPluginReady = null; __TemWebRTCReady0 = function () { if (document.readyState === 'complete') { AdapterJS.WebRTCPlugin.pluginState = AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY; - AdapterJS.maybeThroughWebRTCReady(); } else { - AdapterJS.WebRTCPlugin.documentReadyInterval = setInterval(function () { + var timer = setInterval(function () { if (document.readyState === 'complete') { // TODO: update comments, we wait for the document to be ready - clearInterval(AdapterJS.WebRTCPlugin.documentReadyInterval); + clearInterval(timer); AdapterJS.WebRTCPlugin.pluginState = AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY; - AdapterJS.maybeThroughWebRTCReady(); } }, 100); @@ -172,7 +188,15 @@ AdapterJS.maybeThroughWebRTCReady = function() { if (!AdapterJS.onwebrtcreadyDone) { AdapterJS.onwebrtcreadyDone = true; - if (typeof(AdapterJS.onwebrtcready) === 'function') { + // If new interface for multiple callbacks used + if (AdapterJS._onwebrtcreadies.length) { + AdapterJS._onwebrtcreadies.forEach(function (callback) { + if (typeof(callback) === 'function') { + callback(AdapterJS.WebRTCPlugin.plugin !== null); + } + }); + // Else if no callbacks on new interface assuming user used old(deprecated) way to set callback through AdapterJS.onwebrtcready = ... + } else if (typeof(AdapterJS.onwebrtcready) === 'function') { AdapterJS.onwebrtcready(AdapterJS.WebRTCPlugin.plugin !== null); } } @@ -223,65 +247,69 @@ AdapterJS.isDefined = null; // This sets: // - webrtcDetectedBrowser: The browser agent name. // - webrtcDetectedVersion: The browser version. +// - webrtcMinimumVersion: The minimum browser version still supported by AJS. // - webrtcDetectedType: The types of webRTC support. // - 'moz': Mozilla implementation of webRTC. // - 'webkit': WebKit implementation of webRTC. // - 'plugin': Using the plugin implementation. AdapterJS.parseWebrtcDetectedBrowser = function () { - var hasMatch, checkMatch = navigator.userAgent.match( - /(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; - if (/trident/i.test(checkMatch[1])) { - hasMatch = /\brv[ :]+(\d+)/g.exec(navigator.userAgent) || []; + var hasMatch = null; + if ((!!window.opr && !!opr.addons) || + !!window.opera || + navigator.userAgent.indexOf(' OPR/') >= 0) { + // Opera 8.0+ + webrtcDetectedBrowser = 'opera'; + webrtcDetectedType = 'webkit'; + webrtcMinimumVersion = 26; + hasMatch = /OPR\/(\d+)/i.exec(navigator.userAgent) || []; + webrtcDetectedVersion = parseInt(hasMatch[1], 10); + } else if (typeof InstallTrigger !== 'undefined') { + // Firefox 1.0+ + // Bowser and Version set in Google's adapter + webrtcDetectedType = 'moz'; + } else if (Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0) { + // Safari + webrtcDetectedBrowser = 'safari'; + webrtcDetectedType = 'plugin'; + webrtcMinimumVersion = 7; + hasMatch = /version\/(\d+)/i.exec(navigator.userAgent) || []; + webrtcDetectedVersion = parseInt(hasMatch[1], 10); + } else if (/*@cc_on!@*/false || !!document.documentMode) { + // Internet Explorer 6-11 webrtcDetectedBrowser = 'IE'; + webrtcDetectedType = 'plugin'; + webrtcMinimumVersion = 9; + hasMatch = /\brv[ :]+(\d+)/g.exec(navigator.userAgent) || []; webrtcDetectedVersion = parseInt(hasMatch[1] || '0', 10); - } else if (checkMatch[1] === 'Chrome') { - hasMatch = navigator.userAgent.match(/\bOPR\/(\d+)/); - if (hasMatch !== null) { - webrtcDetectedBrowser = 'opera'; - webrtcDetectedVersion = parseInt(hasMatch[1], 10); - } - } - if (navigator.userAgent.indexOf('Safari')) { - if (typeof InstallTrigger !== 'undefined') { - webrtcDetectedBrowser = 'firefox'; - } else if (/*@cc_on!@*/ false || !!document.documentMode) { - webrtcDetectedBrowser = 'IE'; - } else if ( - Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0) { - webrtcDetectedBrowser = 'safari'; - } else if (!!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0) { - webrtcDetectedBrowser = 'opera'; - } else if (!!window.chrome) { - webrtcDetectedBrowser = 'chrome'; + if (!webrtcDetectedVersion) { + hasMatch = /\bMSIE[ :]+(\d+)/g.exec(navigator.userAgent) || []; + webrtcDetectedVersion = parseInt(hasMatch[1] || '0', 10); } + } else if (!!window.StyleMedia) { + // Edge 20+ + // Bowser and Version set in Google's adapter + webrtcDetectedType = ''; + } else if (!!window.chrome && !!window.chrome.webstore) { + // Chrome 1+ + // Bowser and Version set in Google's adapter + webrtcDetectedType = 'webkit'; + } else if ((webrtcDetectedBrowser === 'chrome'|| webrtcDetectedBrowser === 'opera') && + !!window.CSS) { + // Blink engine detection + webrtcDetectedBrowser = 'blink'; + // TODO: detected WebRTC version } - if (!webrtcDetectedBrowser) { - webrtcDetectedVersion = checkMatch[1]; - } - if (!webrtcDetectedVersion) { - try { - checkMatch = (checkMatch[2]) ? [checkMatch[1], checkMatch[2]] : - [navigator.appName, navigator.appVersion, '-?']; - if ((hasMatch = navigator.userAgent.match(/version\/(\d+)/i)) !== null) { - checkMatch.splice(1, 1, hasMatch[1]); - } - webrtcDetectedVersion = parseInt(checkMatch[1], 10); - } catch (error) { } - } -}; - -// To fix configuration as some browsers does not support -// the 'urls' attribute. -AdapterJS.maybeFixConfiguration = function (pcConfig) { - if (pcConfig === null) { - return; - } - 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; - } + if ((navigator.userAgent.match(/android/ig) || []).length === 0 && + (navigator.userAgent.match(/chrome/ig) || []).length === 0 && + navigator.userAgent.indexOf('Safari/') > 0) { + webrtcDetectedBrowser = 'safari'; + webrtcDetectedVersion = parseInt((navigator.userAgent.match(/Version\/(.*)\ /) || ['', '0'])[1], 10); + webrtcMinimumVersion = 7; + webrtcDetectedType = 'plugin'; } + window.webrtcDetectedBrowser = webrtcDetectedBrowser; + window.webrtcDetectedVersion = webrtcDetectedVersion; + window.webrtcMinimumVersion = webrtcMinimumVersion; }; AdapterJS.addEvent = function(elem, evnt, func) { @@ -302,6 +330,7 @@ AdapterJS.renderNotificationBar = function (text, buttonText, buttonLink, openNe var w = window; var i = document.createElement('iframe'); + i.name = 'adapterjs-alert'; i.style.position = 'fixed'; i.style.top = '-41px'; i.style.left = 0; @@ -318,36 +347,55 @@ AdapterJS.renderNotificationBar = function (text, buttonText, buttonLink, openNe i.style.transition = 'all .5s ease-out'; } document.body.appendChild(i); - c = (i.contentWindow) ? i.contentWindow : + var c = (i.contentWindow) ? i.contentWindow : (i.contentDocument.document) ? i.contentDocument.document : i.contentDocument; c.document.open(); c.document.write('' + text + ''); if(buttonText && buttonLink) { - c.document.write('' + buttonText + 'Cancel'); + c.document.write('' + buttonText + 'Cancel'); c.document.close(); + // On click on okay AdapterJS.addEvent(c.document.getElementById('okay'), 'click', function(e) { if (!!displayRefreshBar) { AdapterJS.renderNotificationBar(AdapterJS.TEXT.EXTENSION ? AdapterJS.TEXT.EXTENSION.REQUIRE_REFRESH : AdapterJS.TEXT.REFRESH.REQUIRE_REFRESH, - AdapterJS.TEXT.REFRESH.BUTTON, 'javascript:location.reload()'); + AdapterJS.TEXT.REFRESH.BUTTON, 'javascript:location.reload()'); // jshint ignore:line } window.open(buttonLink, !!openNewTab ? '_blank' : '_top'); e.preventDefault(); try { - event.cancelBubble = true; + e.cancelBubble = true; } catch(error) { } + + var pluginInstallInterval = setInterval(function(){ + if(! isIE) { + navigator.plugins.refresh(false); + } + AdapterJS.WebRTCPlugin.isPluginInstalled( + AdapterJS.WebRTCPlugin.pluginInfo.prefix, + AdapterJS.WebRTCPlugin.pluginInfo.plugName, + AdapterJS.WebRTCPlugin.pluginInfo.type, + function() { // plugin now installed + clearInterval(pluginInstallInterval); + AdapterJS.WebRTCPlugin.defineWebRTCInterface(); + }, + function() { + // still no plugin detected, nothing to do + }); + } , 500); }); - } - else { + + // On click on Cancel + AdapterJS.addEvent(c.document.getElementById('cancel'), 'click', function(e) { + w.document.body.removeChild(i); + }); + } else { c.document.close(); } - AdapterJS.addEvent(c.document, 'click', function() { - w.document.body.removeChild(i); - }); setTimeout(function() { if(typeof i.style.webkitTransform === 'string') { i.style.webkitTransform = 'translateY(40px)'; @@ -366,11 +414,6 @@ AdapterJS.renderNotificationBar = function (text, buttonText, buttonLink, openNe // - 'plugin': Using the plugin implementation. webrtcDetectedType = null; -// Detected webrtc datachannel support. Types are: -// - 'SCTP': SCTP datachannel support. -// - 'RTP': RTP datachannel support. -webrtcDetectedDCSupport = null; - // Set the settings for creating DataChannels, MediaStream for // Cross-browser compability. // - This is only for SCTP based support browsers. @@ -437,282 +480,2046 @@ checkIceConnectionState = function (peerId, iceConnectionState, callback) { iceConnectionState === AdapterJS._iceConnectionStates.closed) { AdapterJS._iceConnectionFiredStates[peerId] = []; } - iceConnectionState = AdapterJS._iceConnectionStates[iceConnectionState]; - if (AdapterJS._iceConnectionFiredStates[peerId].indexOf(iceConnectionState) < 0) { - AdapterJS._iceConnectionFiredStates[peerId].push(iceConnectionState); - if (iceConnectionState === AdapterJS._iceConnectionStates.connected) { - setTimeout(function () { - AdapterJS._iceConnectionFiredStates[peerId] - .push(AdapterJS._iceConnectionStates.done); - callback(AdapterJS._iceConnectionStates.done); - }, 1000); + iceConnectionState = AdapterJS._iceConnectionStates[iceConnectionState]; + if (AdapterJS._iceConnectionFiredStates[peerId].indexOf(iceConnectionState) < 0) { + AdapterJS._iceConnectionFiredStates[peerId].push(iceConnectionState); + if (iceConnectionState === AdapterJS._iceConnectionStates.connected) { + setTimeout(function () { + AdapterJS._iceConnectionFiredStates[peerId] + .push(AdapterJS._iceConnectionStates.done); + callback(AdapterJS._iceConnectionStates.done); + }, 1000); + } + callback(iceConnectionState); + } + return; +}; + +// Firefox: +// - Creates iceServer from the url for Firefox. +// - Create iceServer with stun url. +// - Create iceServer with turn url. +// - Ignore the transport parameter from TURN url for FF version <=27. +// - Return null for createIceServer if transport=tcp. +// - FF 27 and above supports transport parameters in TURN url, +// - So passing in the full url to create iceServer. +// Chrome: +// - Creates iceServer from the url for Chrome M33 and earlier. +// - Create iceServer with stun url. +// - Chrome M28 & above uses below TURN format. +// Plugin: +// - Creates Ice Server for Plugin Browsers +// - If Stun - Create iceServer with stun url. +// - Else - Create iceServer with turn url +// - This is a WebRTC Function +createIceServer = null; + +// Firefox: +// - Creates IceServers for Firefox +// - Use .url for FireFox. +// - Multiple Urls support +// Chrome: +// - Creates iceServers from the urls for Chrome M34 and above. +// - .urls is supported since Chrome M34. +// - Multiple Urls support +// Plugin: +// - Creates Ice Servers for Plugin Browsers +// - Multiple Urls support +// - This is a WebRTC Function +createIceServers = null; +//------------------------------------------------------------ + +//The RTCPeerConnection object. +RTCPeerConnection = null; + +// Creates RTCSessionDescription object for Plugin Browsers +RTCSessionDescription = (typeof RTCSessionDescription === 'function') ? + RTCSessionDescription : null; + +// Creates RTCIceCandidate object for Plugin Browsers +RTCIceCandidate = (typeof RTCIceCandidate === 'function') ? + RTCIceCandidate : null; + +// Get UserMedia (only difference is the prefix). +// Code from Adam Barth. +getUserMedia = null; + +// Attach a media stream to an element. +attachMediaStream = null; + +// Re-attach a media stream to an element. +reattachMediaStream = null; + + +// Detected browser agent name. Types are: +// - 'firefox': Firefox browser. +// - 'chrome': Chrome browser. +// - 'opera': Opera browser. +// - 'safari': Safari browser. +// - 'IE' - Internet Explorer browser. +webrtcDetectedBrowser = null; + +// Detected browser version. +webrtcDetectedVersion = null; + +// The minimum browser version still supported by AJS. +webrtcMinimumVersion = null; + +// Check for browser types and react accordingly +if ( (navigator.mozGetUserMedia || + navigator.webkitGetUserMedia || + (navigator.mediaDevices && + navigator.userAgent.match(/Edge\/(\d+).(\d+)$/))) + && !((navigator.userAgent.match(/android/ig) || []).length === 0 && +     (navigator.userAgent.match(/chrome/ig) || []).length === 0 && navigator.userAgent.indexOf('Safari/') > 0)) { + + /////////////////////////////////////////////////////////////////// + // INJECTION OF GOOGLE'S ADAPTER.JS CONTENT + +/* jshint ignore:start */ + /* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + + /* More information about these options at jshint.com/docs/options */ + /* jshint browser: true, camelcase: true, curly: true, devel: true, + eqeqeq: true, forin: false, globalstrict: true, node: true, + quotmark: single, undef: true, unused: strict */ + /* global mozRTCIceCandidate, mozRTCPeerConnection, Promise, + mozRTCSessionDescription, webkitRTCPeerConnection, MediaStreamTrack, + MediaStream, RTCIceGatherer, RTCIceTransport, RTCDtlsTransport, + RTCRtpSender, RTCRtpReceiver*/ + /* exported trace,requestUserMedia */ + + 'use strict'; + + var getUserMedia = null; + var attachMediaStream = null; + var reattachMediaStream = null; + var webrtcDetectedBrowser = null; + var webrtcDetectedVersion = null; + var webrtcMinimumVersion = null; + var webrtcUtils = { + log: function() { + // suppress console.log output when being included as a module. + if (typeof module !== 'undefined' || + typeof require === 'function' && typeof define === 'function') { + return; + } + console.log.apply(console, arguments); + }, + extractVersion: function(uastring, expr, pos) { + var match = uastring.match(expr); + return match && match.length >= pos && parseInt(match[pos], 10); + } + }; + + function trace(text) { + // This function is used for logging. + if (text[text.length - 1] === '\n') { + text = text.substring(0, text.length - 1); + } + if (window.performance) { + var now = (window.performance.now() / 1000).toFixed(3); + webrtcUtils.log(now + ': ' + text); + } else { + webrtcUtils.log(text); + } + } + + if (typeof window === 'object') { + if (window.HTMLMediaElement && + !('srcObject' in window.HTMLMediaElement.prototype)) { + // Shim the srcObject property, once, when HTMLMediaElement is found. + Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', { + get: function() { + // If prefixed srcObject property exists, return it. + // Otherwise use the shimmed property, _srcObject + return 'mozSrcObject' in this ? this.mozSrcObject : this._srcObject; + }, + set: function(stream) { + if ('mozSrcObject' in this) { + this.mozSrcObject = stream; + } else { + // Use _srcObject as a private property for this shim + this._srcObject = stream; + // TODO: revokeObjectUrl(this.src) when !stream to release resources? + this.src = URL.createObjectURL(stream); + } + } + }); + } + // Proxy existing globals + getUserMedia = window.navigator && window.navigator.getUserMedia; + } + + // Attach a media stream to an element. + attachMediaStream = function(element, stream) { + element.srcObject = stream; + }; + + reattachMediaStream = function(to, from) { + to.srcObject = from.srcObject; + }; + + if (typeof window === 'undefined' || !window.navigator) { + webrtcUtils.log('This does not appear to be a browser'); + webrtcDetectedBrowser = 'not a browser'; + } else if (navigator.mozGetUserMedia) { + webrtcUtils.log('This appears to be Firefox'); + + webrtcDetectedBrowser = 'firefox'; + + // the detected firefox version. + webrtcDetectedVersion = webrtcUtils.extractVersion(navigator.userAgent, + /Firefox\/([0-9]+)\./, 1); + + // the minimum firefox version still supported by adapter. + webrtcMinimumVersion = 31; + + // Shim for RTCPeerConnection on older versions. + if (!window.RTCPeerConnection) { + window.RTCPeerConnection = function(pcConfig, pcConstraints) { + if (webrtcDetectedVersion < 38) { + // .urls is not supported in FF < 38. + // create RTCIceServers with a single url. + if (pcConfig && pcConfig.iceServers) { + var newIceServers = []; + for (var i = 0; i < pcConfig.iceServers.length; i++) { + var server = pcConfig.iceServers[i]; + if (server.hasOwnProperty('urls')) { + for (var j = 0; j < server.urls.length; j++) { + var newServer = { + url: server.urls[j] + }; + if (server.urls[j].indexOf('turn') === 0) { + newServer.username = server.username; + newServer.credential = server.credential; + } + newIceServers.push(newServer); + } + } else { + newIceServers.push(pcConfig.iceServers[i]); + } + } + pcConfig.iceServers = newIceServers; + } + } + return new mozRTCPeerConnection(pcConfig, pcConstraints); // jscs:ignore requireCapitalizedConstructors + }; + + // wrap static methods. Currently just generateCertificate. + if (mozRTCPeerConnection.generateCertificate) { + Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { + get: function() { + if (arguments.length) { + return mozRTCPeerConnection.generateCertificate.apply(null, + arguments); + } else { + return mozRTCPeerConnection.generateCertificate; + } + } + }); + } + + window.RTCSessionDescription = mozRTCSessionDescription; + window.RTCIceCandidate = mozRTCIceCandidate; + } + + // getUserMedia constraints shim. + getUserMedia = function(constraints, onSuccess, onError) { + var constraintsToFF37 = function(c) { + if (typeof c !== 'object' || c.require) { + return c; + } + var require = []; + Object.keys(c).forEach(function(key) { + if (key === 'require' || key === 'advanced' || key === 'mediaSource') { + return; + } + var r = c[key] = (typeof c[key] === 'object') ? + c[key] : {ideal: c[key]}; + if (r.min !== undefined || + r.max !== undefined || r.exact !== undefined) { + require.push(key); + } + if (r.exact !== undefined) { + if (typeof r.exact === 'number') { + r.min = r.max = r.exact; + } else { + c[key] = r.exact; + } + delete r.exact; + } + if (r.ideal !== undefined) { + c.advanced = c.advanced || []; + var oc = {}; + if (typeof r.ideal === 'number') { + oc[key] = {min: r.ideal, max: r.ideal}; + } else { + oc[key] = r.ideal; + } + c.advanced.push(oc); + delete r.ideal; + if (!Object.keys(r).length) { + delete c[key]; + } + } + }); + if (require.length) { + c.require = require; + } + return c; + }; + if (webrtcDetectedVersion < 38) { + webrtcUtils.log('spec: ' + JSON.stringify(constraints)); + if (constraints.audio) { + constraints.audio = constraintsToFF37(constraints.audio); + } + if (constraints.video) { + constraints.video = constraintsToFF37(constraints.video); + } + webrtcUtils.log('ff37: ' + JSON.stringify(constraints)); + } + return navigator.mozGetUserMedia(constraints, onSuccess, onError); + }; + + navigator.getUserMedia = getUserMedia; + + // Shim for mediaDevices on older versions. + if (!navigator.mediaDevices) { + navigator.mediaDevices = {getUserMedia: requestUserMedia, + addEventListener: function() { }, + removeEventListener: function() { } + }; + } + navigator.mediaDevices.enumerateDevices = + navigator.mediaDevices.enumerateDevices || function() { + return new Promise(function(resolve) { + var infos = [ + {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''}, + {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''} + ]; + resolve(infos); + }); + }; + + if (webrtcDetectedVersion < 41) { + // Work around http://bugzil.la/1169665 + var orgEnumerateDevices = + navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices); + navigator.mediaDevices.enumerateDevices = function() { + return orgEnumerateDevices().then(undefined, function(e) { + if (e.name === 'NotFoundError') { + return []; + } + throw e; + }); + }; + } + } else if (navigator.webkitGetUserMedia && window.webkitRTCPeerConnection) { + webrtcUtils.log('This appears to be Chrome'); + + webrtcDetectedBrowser = 'chrome'; + + // the detected chrome version. + webrtcDetectedVersion = webrtcUtils.extractVersion(navigator.userAgent, + /Chrom(e|ium)\/([0-9]+)\./, 2); + + // the minimum chrome version still supported by adapter. + webrtcMinimumVersion = 38; + + // The RTCPeerConnection object. + window.RTCPeerConnection = function(pcConfig, pcConstraints) { + // Translate iceTransportPolicy to iceTransports, + // see https://code.google.com/p/webrtc/issues/detail?id=4869 + if (pcConfig && pcConfig.iceTransportPolicy) { + pcConfig.iceTransports = pcConfig.iceTransportPolicy; + } + + var pc = new webkitRTCPeerConnection(pcConfig, pcConstraints); // jscs:ignore requireCapitalizedConstructors + var origGetStats = pc.getStats.bind(pc); + pc.getStats = function(selector, successCallback, errorCallback) { // jshint ignore: line + var self = this; + var args = arguments; + + // If selector is a function then we are in the old style stats so just + // pass back the original getStats format to avoid breaking old users. + if (arguments.length > 0 && typeof selector === 'function') { + return origGetStats(selector, successCallback); + } + + var fixChromeStats = function(response) { + var standardReport = {}; + var reports = response.result(); + reports.forEach(function(report) { + var standardStats = { + id: report.id, + timestamp: report.timestamp, + type: report.type + }; + report.names().forEach(function(name) { + standardStats[name] = report.stat(name); + }); + standardReport[standardStats.id] = standardStats; + }); + + return standardReport; + }; + + if (arguments.length >= 2) { + var successCallbackWrapper = function(response) { + args[1](fixChromeStats(response)); + }; + + return origGetStats.apply(this, [successCallbackWrapper, arguments[0]]); + } + + // promise-support + return new Promise(function(resolve, reject) { + if (args.length === 1 && selector === null) { + origGetStats.apply(self, [ + function(response) { + resolve.apply(null, [fixChromeStats(response)]); + }, reject]); + } else { + origGetStats.apply(self, [resolve, reject]); + } + }); + }; + + return pc; + }; + + // wrap static methods. Currently just generateCertificate. + if (webkitRTCPeerConnection.generateCertificate) { + Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { + get: function() { + if (arguments.length) { + return webkitRTCPeerConnection.generateCertificate.apply(null, + arguments); + } else { + return webkitRTCPeerConnection.generateCertificate; + } + } + }); + } + + // add promise support + ['createOffer', 'createAnswer'].forEach(function(method) { + var nativeMethod = webkitRTCPeerConnection.prototype[method]; + webkitRTCPeerConnection.prototype[method] = function() { + var self = this; + if (arguments.length < 1 || (arguments.length === 1 && + typeof(arguments[0]) === 'object')) { + var opts = arguments.length === 1 ? arguments[0] : undefined; + return new Promise(function(resolve, reject) { + nativeMethod.apply(self, [resolve, reject, opts]); + }); + } else { + return nativeMethod.apply(this, arguments); + } + }; + }); + + ['setLocalDescription', 'setRemoteDescription', + 'addIceCandidate'].forEach(function(method) { + var nativeMethod = webkitRTCPeerConnection.prototype[method]; + webkitRTCPeerConnection.prototype[method] = function() { + var args = arguments; + var self = this; + return new Promise(function(resolve, reject) { + nativeMethod.apply(self, [args[0], + function() { + resolve(); + if (args.length >= 2) { + args[1].apply(null, []); + } + }, + function(err) { + reject(err); + if (args.length >= 3) { + args[2].apply(null, [err]); + } + }] + ); + }); + }; + }); + + // getUserMedia constraints shim. + var constraintsToChrome = function(c) { + if (typeof c !== 'object' || c.mandatory || c.optional) { + return c; + } + var cc = {}; + Object.keys(c).forEach(function(key) { + if (key === 'require' || key === 'advanced' || key === 'mediaSource') { + return; + } + var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]}; + if (r.exact !== undefined && typeof r.exact === 'number') { + r.min = r.max = r.exact; + } + var oldname = function(prefix, name) { + if (prefix) { + return prefix + name.charAt(0).toUpperCase() + name.slice(1); + } + return (name === 'deviceId') ? 'sourceId' : name; + }; + if (r.ideal !== undefined) { + cc.optional = cc.optional || []; + var oc = {}; + if (typeof r.ideal === 'number') { + oc[oldname('min', key)] = r.ideal; + cc.optional.push(oc); + oc = {}; + oc[oldname('max', key)] = r.ideal; + cc.optional.push(oc); + } else { + oc[oldname('', key)] = r.ideal; + cc.optional.push(oc); + } + } + if (r.exact !== undefined && typeof r.exact !== 'number') { + cc.mandatory = cc.mandatory || {}; + cc.mandatory[oldname('', key)] = r.exact; + } else { + ['min', 'max'].forEach(function(mix) { + if (r[mix] !== undefined) { + cc.mandatory = cc.mandatory || {}; + cc.mandatory[oldname(mix, key)] = r[mix]; + } + }); + } + }); + if (c.advanced) { + cc.optional = (cc.optional || []).concat(c.advanced); + } + return cc; + }; + + getUserMedia = function(constraints, onSuccess, onError) { + if (constraints.audio) { + constraints.audio = constraintsToChrome(constraints.audio); + } + if (constraints.video) { + constraints.video = constraintsToChrome(constraints.video); + } + webrtcUtils.log('chrome: ' + JSON.stringify(constraints)); + return navigator.webkitGetUserMedia(constraints, onSuccess, onError); + }; + navigator.getUserMedia = getUserMedia; + + if (!navigator.mediaDevices) { + navigator.mediaDevices = {getUserMedia: requestUserMedia, + enumerateDevices: function() { + return new Promise(function(resolve) { + var kinds = {audio: 'audioinput', video: 'videoinput'}; + return MediaStreamTrack.getSources(function(devices) { + resolve(devices.map(function(device) { + return {label: device.label, + kind: kinds[device.kind], + deviceId: device.id, + groupId: ''}; + })); + }); + }); + }}; + } + + // A shim for getUserMedia method on the mediaDevices object. + // TODO(KaptenJansson) remove once implemented in Chrome stable. + if (!navigator.mediaDevices.getUserMedia) { + navigator.mediaDevices.getUserMedia = function(constraints) { + return requestUserMedia(constraints); + }; + } else { + // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia + // function which returns a Promise, it does not accept spec-style + // constraints. + var origGetUserMedia = navigator.mediaDevices.getUserMedia. + bind(navigator.mediaDevices); + navigator.mediaDevices.getUserMedia = function(c) { + webrtcUtils.log('spec: ' + JSON.stringify(c)); // whitespace for alignment + c.audio = constraintsToChrome(c.audio); + c.video = constraintsToChrome(c.video); + webrtcUtils.log('chrome: ' + JSON.stringify(c)); + return origGetUserMedia(c); + }; + } + + // Dummy devicechange event methods. + // TODO(KaptenJansson) remove once implemented in Chrome stable. + if (typeof navigator.mediaDevices.addEventListener === 'undefined') { + navigator.mediaDevices.addEventListener = function() { + webrtcUtils.log('Dummy mediaDevices.addEventListener called.'); + }; + } + if (typeof navigator.mediaDevices.removeEventListener === 'undefined') { + navigator.mediaDevices.removeEventListener = function() { + webrtcUtils.log('Dummy mediaDevices.removeEventListener called.'); + }; + } + + // Attach a media stream to an element. + attachMediaStream = function(element, stream) { + if (webrtcDetectedVersion >= 43) { + element.srcObject = stream; + } else if (typeof element.src !== 'undefined') { + element.src = URL.createObjectURL(stream); + } else { + webrtcUtils.log('Error attaching stream to element.'); + } + }; + reattachMediaStream = function(to, from) { + if (webrtcDetectedVersion >= 43) { + to.srcObject = from.srcObject; + } else { + to.src = from.src; + } + }; + + } else if (navigator.mediaDevices && navigator.userAgent.match( + /Edge\/(\d+).(\d+)$/)) { + webrtcUtils.log('This appears to be Edge'); + webrtcDetectedBrowser = 'edge'; + + webrtcDetectedVersion = webrtcUtils.extractVersion(navigator.userAgent, + /Edge\/(\d+).(\d+)$/, 2); + + // The minimum version still supported by adapter. + // This is the build number for Edge. + webrtcMinimumVersion = 10547; + + if (window.RTCIceGatherer) { + // Generate an alphanumeric identifier for cname or mids. + // TODO: use UUIDs instead? https://gist.github.com/jed/982883 + var generateIdentifier = function() { + return Math.random().toString(36).substr(2, 10); + }; + + // The RTCP CNAME used by all peerconnections from the same JS. + var localCName = generateIdentifier(); + + // SDP helpers - to be moved into separate module. + var SDPUtils = {}; + + // Splits SDP into lines, dealing with both CRLF and LF. + SDPUtils.splitLines = function(blob) { + return blob.trim().split('\n').map(function(line) { + return line.trim(); + }); + }; + + // Splits SDP into sessionpart and mediasections. Ensures CRLF. + SDPUtils.splitSections = function(blob) { + var parts = blob.split('\r\nm='); + return parts.map(function(part, index) { + return (index > 0 ? 'm=' + part : part).trim() + '\r\n'; + }); + }; + + // Returns lines that start with a certain prefix. + SDPUtils.matchPrefix = function(blob, prefix) { + return SDPUtils.splitLines(blob).filter(function(line) { + return line.indexOf(prefix) === 0; + }); + }; + + // Parses an ICE candidate line. Sample input: + // candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8 rport 55996" + SDPUtils.parseCandidate = function(line) { + var parts; + // Parse both variants. + if (line.indexOf('a=candidate:') === 0) { + parts = line.substring(12).split(' '); + } else { + parts = line.substring(10).split(' '); + } + + var candidate = { + foundation: parts[0], + component: parts[1], + protocol: parts[2].toLowerCase(), + priority: parseInt(parts[3], 10), + ip: parts[4], + port: parseInt(parts[5], 10), + // skip parts[6] == 'typ' + type: parts[7] + }; + + for (var i = 8; i < parts.length; i += 2) { + switch (parts[i]) { + case 'raddr': + candidate.relatedAddress = parts[i + 1]; + break; + case 'rport': + candidate.relatedPort = parseInt(parts[i + 1], 10); + break; + case 'tcptype': + candidate.tcpType = parts[i + 1]; + break; + default: // Unknown extensions are silently ignored. + break; + } + } + return candidate; + }; + + // Translates a candidate object into SDP candidate attribute. + SDPUtils.writeCandidate = function(candidate) { + var sdp = []; + sdp.push(candidate.foundation); + sdp.push(candidate.component); + sdp.push(candidate.protocol.toUpperCase()); + sdp.push(candidate.priority); + sdp.push(candidate.ip); + sdp.push(candidate.port); + + var type = candidate.type; + sdp.push('typ'); + sdp.push(type); + if (type !== 'host' && candidate.relatedAddress && + candidate.relatedPort) { + sdp.push('raddr'); + sdp.push(candidate.relatedAddress); // was: relAddr + sdp.push('rport'); + sdp.push(candidate.relatedPort); // was: relPort + } + if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') { + sdp.push('tcptype'); + sdp.push(candidate.tcpType); + } + return 'candidate:' + sdp.join(' '); + }; + + // Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input: + // a=rtpmap:111 opus/48000/2 + SDPUtils.parseRtpMap = function(line) { + var parts = line.substr(9).split(' '); + var parsed = { + payloadType: parseInt(parts.shift(), 10) // was: id + }; + + parts = parts[0].split('/'); + + parsed.name = parts[0]; + parsed.clockRate = parseInt(parts[1], 10); // was: clockrate + parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1; // was: channels + return parsed; + }; + + // Generate an a=rtpmap line from RTCRtpCodecCapability or RTCRtpCodecParameters. + SDPUtils.writeRtpMap = function(codec) { + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate + + (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n'; + }; + + // Parses an ftmp line, returns dictionary. Sample input: + // a=fmtp:96 vbr=on;cng=on + // Also deals with vbr=on; cng=on + SDPUtils.parseFmtp = function(line) { + var parsed = {}; + var kv; + var parts = line.substr(line.indexOf(' ') + 1).split(';'); + for (var j = 0; j < parts.length; j++) { + kv = parts[j].trim().split('='); + parsed[kv[0].trim()] = kv[1]; + } + return parsed; + }; + + // Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters. + SDPUtils.writeFtmp = function(codec) { + var line = ''; + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + if (codec.parameters && codec.parameters.length) { + var params = []; + Object.keys(codec.parameters).forEach(function(param) { + params.push(param + '=' + codec.parameters[param]); + }); + line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n'; + } + return line; + }; + + // Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input: + // a=rtcp-fb:98 nack rpsi + SDPUtils.parseRtcpFb = function(line) { + var parts = line.substr(line.indexOf(' ') + 1).split(' '); + return { + type: parts.shift(), + parameter: parts.join(' ') + }; + }; + // Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters. + SDPUtils.writeRtcpFb = function(codec) { + var lines = ''; + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + if (codec.rtcpFeedback && codec.rtcpFeedback.length) { + // FIXME: special handling for trr-int? + codec.rtcpFeedback.forEach(function(fb) { + lines += 'a=rtcp-fb:' + pt + ' ' + fb.type + ' ' + fb.parameter + + '\r\n'; + }); + } + return lines; + }; + + // Parses an RFC 5576 ssrc media attribute. Sample input: + // a=ssrc:3735928559 cname:something + SDPUtils.parseSsrcMedia = function(line) { + var sp = line.indexOf(' '); + var parts = { + ssrc: line.substr(7, sp - 7), + }; + var colon = line.indexOf(':', sp); + if (colon > -1) { + parts.attribute = line.substr(sp + 1, colon - sp - 1); + parts.value = line.substr(colon + 1); + } else { + parts.attribute = line.substr(sp + 1); + } + return parts; + }; + + // Extracts DTLS parameters from SDP media section or sessionpart. + // FIXME: for consistency with other functions this should only + // get the fingerprint line as input. See also getIceParameters. + SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) { + var lines = SDPUtils.splitLines(mediaSection); + lines = lines.concat(SDPUtils.splitLines(sessionpart)); // Search in session part, too. + var fpLine = lines.filter(function(line) { + return line.indexOf('a=fingerprint:') === 0; + })[0].substr(14); + // Note: a=setup line is ignored since we use the 'auto' role. + var dtlsParameters = { + role: 'auto', + fingerprints: [{ + algorithm: fpLine.split(' ')[0], + value: fpLine.split(' ')[1] + }] + }; + return dtlsParameters; + }; + + // Serializes DTLS parameters to SDP. + SDPUtils.writeDtlsParameters = function(params, setupType) { + var sdp = 'a=setup:' + setupType + '\r\n'; + params.fingerprints.forEach(function(fp) { + sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n'; + }); + return sdp; + }; + // Parses ICE information from SDP media section or sessionpart. + // FIXME: for consistency with other functions this should only + // get the ice-ufrag and ice-pwd lines as input. + SDPUtils.getIceParameters = function(mediaSection, sessionpart) { + var lines = SDPUtils.splitLines(mediaSection); + lines = lines.concat(SDPUtils.splitLines(sessionpart)); // Search in session part, too. + var iceParameters = { + usernameFragment: lines.filter(function(line) { + return line.indexOf('a=ice-ufrag:') === 0; + })[0].substr(12), + password: lines.filter(function(line) { + return line.indexOf('a=ice-pwd:') === 0; + })[0].substr(10) + }; + return iceParameters; + }; + + // Serializes ICE parameters to SDP. + SDPUtils.writeIceParameters = function(params) { + return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' + + 'a=ice-pwd:' + params.password + '\r\n'; + }; + + // Parses the SDP media section and returns RTCRtpParameters. + SDPUtils.parseRtpParameters = function(mediaSection) { + var description = { + codecs: [], + headerExtensions: [], + fecMechanisms: [], + rtcp: [] + }; + var lines = SDPUtils.splitLines(mediaSection); + var mline = lines[0].split(' '); + for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..] + var pt = mline[i]; + var rtpmapline = SDPUtils.matchPrefix( + mediaSection, 'a=rtpmap:' + pt + ' ')[0]; + if (rtpmapline) { + var codec = SDPUtils.parseRtpMap(rtpmapline); + var fmtps = SDPUtils.matchPrefix( + mediaSection, 'a=fmtp:' + pt + ' '); + // Only the first a=fmtp: is considered. + codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {}; + codec.rtcpFeedback = SDPUtils.matchPrefix( + mediaSection, 'a=rtcp-fb:' + pt + ' ') + .map(SDPUtils.parseRtcpFb); + description.codecs.push(codec); + } + } + // FIXME: parse headerExtensions, fecMechanisms and rtcp. + return description; + }; + + // Generates parts of the SDP media section describing the capabilities / parameters. + SDPUtils.writeRtpDescription = function(kind, caps) { + var sdp = ''; + + // Build the mline. + sdp += 'm=' + kind + ' '; + sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs. + sdp += ' UDP/TLS/RTP/SAVPF '; + sdp += caps.codecs.map(function(codec) { + if (codec.preferredPayloadType !== undefined) { + return codec.preferredPayloadType; + } + return codec.payloadType; + }).join(' ') + '\r\n'; + + sdp += 'c=IN IP4 0.0.0.0\r\n'; + sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n'; + + // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb. + caps.codecs.forEach(function(codec) { + sdp += SDPUtils.writeRtpMap(codec); + sdp += SDPUtils.writeFtmp(codec); + sdp += SDPUtils.writeRtcpFb(codec); + }); + // FIXME: add headerExtensions, fecMechanismş and rtcp. + sdp += 'a=rtcp-mux\r\n'; + return sdp; + }; + + SDPUtils.writeSessionBoilerplate = function() { + // FIXME: sess-id should be an NTP timestamp. + return 'v=0\r\n' + + 'o=thisisadapterortc 8169639915646943137 2 IN IP4 127.0.0.1\r\n' + + 's=-\r\n' + + 't=0 0\r\n'; + }; + + SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) { + var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps); + + // Map ICE parameters (ufrag, pwd) to SDP. + sdp += SDPUtils.writeIceParameters( + transceiver.iceGatherer.getLocalParameters()); + + // Map DTLS parameters to SDP. + sdp += SDPUtils.writeDtlsParameters( + transceiver.dtlsTransport.getLocalParameters(), + type === 'offer' ? 'actpass' : 'active'); + + sdp += 'a=mid:' + transceiver.mid + '\r\n'; + + if (transceiver.rtpSender && transceiver.rtpReceiver) { + sdp += 'a=sendrecv\r\n'; + } else if (transceiver.rtpSender) { + sdp += 'a=sendonly\r\n'; + } else if (transceiver.rtpReceiver) { + sdp += 'a=recvonly\r\n'; + } else { + sdp += 'a=inactive\r\n'; + } + + // FIXME: for RTX there might be multiple SSRCs. Not implemented in Edge yet. + if (transceiver.rtpSender) { + var msid = 'msid:' + stream.id + ' ' + + transceiver.rtpSender.track.id + '\r\n'; + sdp += 'a=' + msid; + sdp += 'a=ssrc:' + transceiver.sendSsrc + ' ' + msid; + } + // FIXME: this should be written by writeRtpDescription. + sdp += 'a=ssrc:' + transceiver.sendSsrc + ' cname:' + + localCName + '\r\n'; + return sdp; + }; + + // Gets the direction from the mediaSection or the sessionpart. + SDPUtils.getDirection = function(mediaSection, sessionpart) { + // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv. + var lines = SDPUtils.splitLines(mediaSection); + for (var i = 0; i < lines.length; i++) { + switch (lines[i]) { + case 'a=sendrecv': + case 'a=sendonly': + case 'a=recvonly': + case 'a=inactive': + return lines[i].substr(2); + } + } + if (sessionpart) { + return SDPUtils.getDirection(sessionpart); + } + return 'sendrecv'; + }; + + // ORTC defines an RTCIceCandidate object but no constructor. + // Not implemented in Edge. + if (!window.RTCIceCandidate) { + window.RTCIceCandidate = function(args) { + return args; + }; + } + // ORTC does not have a session description object but + // other browsers (i.e. Chrome) that will support both PC and ORTC + // in the future might have this defined already. + if (!window.RTCSessionDescription) { + window.RTCSessionDescription = function(args) { + return args; + }; + } + + window.RTCPeerConnection = function(config) { + var self = this; + + this.onicecandidate = null; + this.onaddstream = null; + this.onremovestream = null; + this.onsignalingstatechange = null; + this.oniceconnectionstatechange = null; + this.onnegotiationneeded = null; + this.ondatachannel = null; + + this.localStreams = []; + this.remoteStreams = []; + this.getLocalStreams = function() { return self.localStreams; }; + this.getRemoteStreams = function() { return self.remoteStreams; }; + + this.localDescription = new RTCSessionDescription({ + type: '', + sdp: '' + }); + this.remoteDescription = new RTCSessionDescription({ + type: '', + sdp: '' + }); + this.signalingState = 'stable'; + this.iceConnectionState = 'new'; + + this.iceOptions = { + gatherPolicy: 'all', + iceServers: [] + }; + if (config && config.iceTransportPolicy) { + switch (config.iceTransportPolicy) { + case 'all': + case 'relay': + this.iceOptions.gatherPolicy = config.iceTransportPolicy; + break; + case 'none': + // FIXME: remove once implementation and spec have added this. + throw new TypeError('iceTransportPolicy "none" not supported'); + } + } + if (config && config.iceServers) { + // Edge does not like + // 1) stun: + // 2) turn: that does not have all of turn:host:port?transport=udp + // 3) an array of urls + config.iceServers.forEach(function(server) { + if (server.urls) { + var url; + if (typeof(server.urls) === 'string') { + url = server.urls; + } else { + url = server.urls[0]; + } + if (url.indexOf('transport=udp') !== -1) { + self.iceServers.push({ + username: server.username, + credential: server.credential, + urls: url + }); + } + } + }); + } + + // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ... + // everything that is needed to describe a SDP m-line. + this.transceivers = []; + + // since the iceGatherer is currently created in createOffer but we + // must not emit candidates until after setLocalDescription we buffer + // them in this array. + this._localIceCandidatesBuffer = []; + }; + + window.RTCPeerConnection.prototype._emitBufferedCandidates = function() { + var self = this; + // FIXME: need to apply ice candidates in a way which is async but in-order + this._localIceCandidatesBuffer.forEach(function(event) { + if (self.onicecandidate !== null) { + self.onicecandidate(event); + } + }); + this._localIceCandidatesBuffer = []; + }; + + window.RTCPeerConnection.prototype.addStream = function(stream) { + // Clone is necessary for local demos mostly, attaching directly + // to two different senders does not work (build 10547). + this.localStreams.push(stream.clone()); + this._maybeFireNegotiationNeeded(); + }; + + window.RTCPeerConnection.prototype.removeStream = function(stream) { + var idx = this.localStreams.indexOf(stream); + if (idx > -1) { + this.localStreams.splice(idx, 1); + this._maybeFireNegotiationNeeded(); + } + }; + + // Determines the intersection of local and remote capabilities. + window.RTCPeerConnection.prototype._getCommonCapabilities = + function(localCapabilities, remoteCapabilities) { + var commonCapabilities = { + codecs: [], + headerExtensions: [], + fecMechanisms: [] + }; + localCapabilities.codecs.forEach(function(lCodec) { + for (var i = 0; i < remoteCapabilities.codecs.length; i++) { + var rCodec = remoteCapabilities.codecs[i]; + if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() && + lCodec.clockRate === rCodec.clockRate && + lCodec.numChannels === rCodec.numChannels) { + // push rCodec so we reply with offerer payload type + commonCapabilities.codecs.push(rCodec); + + // FIXME: also need to determine intersection between + // .rtcpFeedback and .parameters + break; + } + } + }); + + localCapabilities.headerExtensions.forEach(function(lHeaderExtension) { + for (var i = 0; i < remoteCapabilities.headerExtensions.length; i++) { + var rHeaderExtension = remoteCapabilities.headerExtensions[i]; + if (lHeaderExtension.uri === rHeaderExtension.uri) { + commonCapabilities.headerExtensions.push(rHeaderExtension); + break; + } + } + }); + + // FIXME: fecMechanisms + return commonCapabilities; + }; + + // Create ICE gatherer, ICE transport and DTLS transport. + window.RTCPeerConnection.prototype._createIceAndDtlsTransports = + function(mid, sdpMLineIndex) { + var self = this; + var iceGatherer = new RTCIceGatherer(self.iceOptions); + var iceTransport = new RTCIceTransport(iceGatherer); + iceGatherer.onlocalcandidate = function(evt) { + var event = {}; + event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex}; + + var cand = evt.candidate; + // Edge emits an empty object for RTCIceCandidateComplete‥ + if (!cand || Object.keys(cand).length === 0) { + // polyfill since RTCIceGatherer.state is not implemented in Edge 10547 yet. + if (iceGatherer.state === undefined) { + iceGatherer.state = 'completed'; + } + + // Emit a candidate with type endOfCandidates to make the samples work. + // Edge requires addIceCandidate with this empty candidate to start checking. + // The real solution is to signal end-of-candidates to the other side when + // getting the null candidate but some apps (like the samples) don't do that. + event.candidate.candidate = + 'candidate:1 1 udp 1 0.0.0.0 9 typ endOfCandidates'; + } else { + // RTCIceCandidate doesn't have a component, needs to be added + cand.component = iceTransport.component === 'RTCP' ? 2 : 1; + event.candidate.candidate = SDPUtils.writeCandidate(cand); + } + + var complete = self.transceivers.every(function(transceiver) { + return transceiver.iceGatherer && + transceiver.iceGatherer.state === 'completed'; + }); + // FIXME: update .localDescription with candidate and (potentially) end-of-candidates. + // To make this harder, the gatherer might emit candidates before localdescription + // is set. To make things worse, gather.getLocalCandidates still errors in + // Edge 10547 when no candidates have been gathered yet. + + if (self.onicecandidate !== null) { + // Emit candidate if localDescription is set. + // Also emits null candidate when all gatherers are complete. + if (self.localDescription && self.localDescription.type === '') { + self._localIceCandidatesBuffer.push(event); + if (complete) { + self._localIceCandidatesBuffer.push({}); + } + } else { + self.onicecandidate(event); + if (complete) { + self.onicecandidate({}); + } + } + } + }; + iceTransport.onicestatechange = function() { + self._updateConnectionState(); + }; + + var dtlsTransport = new RTCDtlsTransport(iceTransport); + dtlsTransport.ondtlsstatechange = function() { + self._updateConnectionState(); + }; + dtlsTransport.onerror = function() { + // onerror does not set state to failed by itself. + dtlsTransport.state = 'failed'; + self._updateConnectionState(); + }; + + return { + iceGatherer: iceGatherer, + iceTransport: iceTransport, + dtlsTransport: dtlsTransport + }; + }; + + // Start the RTP Sender and Receiver for a transceiver. + window.RTCPeerConnection.prototype._transceive = function(transceiver, + send, recv) { + var params = this._getCommonCapabilities(transceiver.localCapabilities, + transceiver.remoteCapabilities); + if (send && transceiver.rtpSender) { + params.encodings = [{ + ssrc: transceiver.sendSsrc + }]; + params.rtcp = { + cname: localCName, + ssrc: transceiver.recvSsrc + }; + transceiver.rtpSender.send(params); + } + if (recv && transceiver.rtpReceiver) { + params.encodings = [{ + ssrc: transceiver.recvSsrc + }]; + params.rtcp = { + cname: transceiver.cname, + ssrc: transceiver.sendSsrc + }; + transceiver.rtpReceiver.receive(params); + } + }; + + window.RTCPeerConnection.prototype.setLocalDescription = + function(description) { + var self = this; + if (description.type === 'offer') { + if (!this._pendingOffer) { + } else { + this.transceivers = this._pendingOffer; + delete this._pendingOffer; + } + } else if (description.type === 'answer') { + var sections = SDPUtils.splitSections(self.remoteDescription.sdp); + var sessionpart = sections.shift(); + sections.forEach(function(mediaSection, sdpMLineIndex) { + var transceiver = self.transceivers[sdpMLineIndex]; + var iceGatherer = transceiver.iceGatherer; + var iceTransport = transceiver.iceTransport; + var dtlsTransport = transceiver.dtlsTransport; + var localCapabilities = transceiver.localCapabilities; + var remoteCapabilities = transceiver.remoteCapabilities; + var rejected = mediaSection.split('\n', 1)[0] + .split(' ', 2)[1] === '0'; + + if (!rejected) { + var remoteIceParameters = SDPUtils.getIceParameters(mediaSection, + sessionpart); + iceTransport.start(iceGatherer, remoteIceParameters, 'controlled'); + + var remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection, + sessionpart); + dtlsTransport.start(remoteDtlsParameters); + + // Calculate intersection of capabilities. + var params = self._getCommonCapabilities(localCapabilities, + remoteCapabilities); + + // Start the RTCRtpSender. The RTCRtpReceiver for this transceiver + // has already been started in setRemoteDescription. + self._transceive(transceiver, + params.codecs.length > 0, + false); + } + }); + } + + this.localDescription = description; + switch (description.type) { + case 'offer': + this._updateSignalingState('have-local-offer'); + break; + case 'answer': + this._updateSignalingState('stable'); + break; + default: + throw new TypeError('unsupported type "' + description.type + '"'); + } + + // If a success callback was provided, emit ICE candidates after it has been + // executed. Otherwise, emit callback after the Promise is resolved. + var hasCallback = arguments.length > 1 && + typeof arguments[1] === 'function'; + if (hasCallback) { + var cb = arguments[1]; + window.setTimeout(function() { + cb(); + self._emitBufferedCandidates(); + }, 0); + } + var p = Promise.resolve(); + p.then(function() { + if (!hasCallback) { + window.setTimeout(self._emitBufferedCandidates.bind(self), 0); + } + }); + return p; + }; + + window.RTCPeerConnection.prototype.setRemoteDescription = + function(description) { + var self = this; + var stream = new MediaStream(); + var sections = SDPUtils.splitSections(description.sdp); + var sessionpart = sections.shift(); + sections.forEach(function(mediaSection, sdpMLineIndex) { + var lines = SDPUtils.splitLines(mediaSection); + var mline = lines[0].substr(2).split(' '); + var kind = mline[0]; + var rejected = mline[1] === '0'; + var direction = SDPUtils.getDirection(mediaSection, sessionpart); + + var transceiver; + var iceGatherer; + var iceTransport; + var dtlsTransport; + var rtpSender; + var rtpReceiver; + var sendSsrc; + var recvSsrc; + var localCapabilities; + + // FIXME: ensure the mediaSection has rtcp-mux set. + var remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection); + var remoteIceParameters; + var remoteDtlsParameters; + if (!rejected) { + remoteIceParameters = SDPUtils.getIceParameters(mediaSection, + sessionpart); + remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection, + sessionpart); + } + var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0].substr(6); + + var cname; + // Gets the first SSRC. Note that with RTX there might be multiple SSRCs. + var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') + .map(function(line) { + return SDPUtils.parseSsrcMedia(line); + }) + .filter(function(obj) { + return obj.attribute === 'cname'; + })[0]; + if (remoteSsrc) { + recvSsrc = parseInt(remoteSsrc.ssrc, 10); + cname = remoteSsrc.value; + } + + if (description.type === 'offer') { + var transports = self._createIceAndDtlsTransports(mid, sdpMLineIndex); + + localCapabilities = RTCRtpReceiver.getCapabilities(kind); + sendSsrc = (2 * sdpMLineIndex + 2) * 1001; + + rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind); + + // FIXME: not correct when there are multiple streams but that is + // not currently supported in this shim. + stream.addTrack(rtpReceiver.track); + + // FIXME: look at direction. + if (self.localStreams.length > 0 && + self.localStreams[0].getTracks().length >= sdpMLineIndex) { + // FIXME: actually more complicated, needs to match types etc + var localtrack = self.localStreams[0].getTracks()[sdpMLineIndex]; + rtpSender = new RTCRtpSender(localtrack, transports.dtlsTransport); + } + + self.transceivers[sdpMLineIndex] = { + iceGatherer: transports.iceGatherer, + iceTransport: transports.iceTransport, + dtlsTransport: transports.dtlsTransport, + localCapabilities: localCapabilities, + remoteCapabilities: remoteCapabilities, + rtpSender: rtpSender, + rtpReceiver: rtpReceiver, + kind: kind, + mid: mid, + cname: cname, + sendSsrc: sendSsrc, + recvSsrc: recvSsrc + }; + // Start the RTCRtpReceiver now. The RTPSender is started in setLocalDescription. + self._transceive(self.transceivers[sdpMLineIndex], + false, + direction === 'sendrecv' || direction === 'sendonly'); + } else if (description.type === 'answer' && !rejected) { + transceiver = self.transceivers[sdpMLineIndex]; + iceGatherer = transceiver.iceGatherer; + iceTransport = transceiver.iceTransport; + dtlsTransport = transceiver.dtlsTransport; + rtpSender = transceiver.rtpSender; + rtpReceiver = transceiver.rtpReceiver; + sendSsrc = transceiver.sendSsrc; + //recvSsrc = transceiver.recvSsrc; + localCapabilities = transceiver.localCapabilities; + + self.transceivers[sdpMLineIndex].recvSsrc = recvSsrc; + self.transceivers[sdpMLineIndex].remoteCapabilities = + remoteCapabilities; + self.transceivers[sdpMLineIndex].cname = cname; + + iceTransport.start(iceGatherer, remoteIceParameters, 'controlling'); + dtlsTransport.start(remoteDtlsParameters); + + self._transceive(transceiver, + direction === 'sendrecv' || direction === 'recvonly', + direction === 'sendrecv' || direction === 'sendonly'); + + if (rtpReceiver && + (direction === 'sendrecv' || direction === 'sendonly')) { + stream.addTrack(rtpReceiver.track); + } else { + // FIXME: actually the receiver should be created later. + delete transceiver.rtpReceiver; + } + } + }); + + this.remoteDescription = description; + switch (description.type) { + case 'offer': + this._updateSignalingState('have-remote-offer'); + break; + case 'answer': + this._updateSignalingState('stable'); + break; + default: + throw new TypeError('unsupported type "' + description.type + '"'); + } + window.setTimeout(function() { + if (self.onaddstream !== null && stream.getTracks().length) { + self.remoteStreams.push(stream); + window.setTimeout(function() { + self.onaddstream({stream: stream}); + }, 0); + } + }, 0); + if (arguments.length > 1 && typeof arguments[1] === 'function') { + window.setTimeout(arguments[1], 0); + } + return Promise.resolve(); + }; + + window.RTCPeerConnection.prototype.close = function() { + this.transceivers.forEach(function(transceiver) { + /* not yet + if (transceiver.iceGatherer) { + transceiver.iceGatherer.close(); + } + */ + if (transceiver.iceTransport) { + transceiver.iceTransport.stop(); + } + if (transceiver.dtlsTransport) { + transceiver.dtlsTransport.stop(); + } + if (transceiver.rtpSender) { + transceiver.rtpSender.stop(); + } + if (transceiver.rtpReceiver) { + transceiver.rtpReceiver.stop(); + } + }); + // FIXME: clean up tracks, local streams, remote streams, etc + this._updateSignalingState('closed'); + }; + + // Update the signaling state. + window.RTCPeerConnection.prototype._updateSignalingState = + function(newState) { + this.signalingState = newState; + if (this.onsignalingstatechange !== null) { + this.onsignalingstatechange(); + } + }; + + // Determine whether to fire the negotiationneeded event. + window.RTCPeerConnection.prototype._maybeFireNegotiationNeeded = + function() { + // Fire away (for now). + if (this.onnegotiationneeded !== null) { + this.onnegotiationneeded(); + } + }; + + // Update the connection state. + window.RTCPeerConnection.prototype._updateConnectionState = + function() { + var self = this; + var newState; + var states = { + 'new': 0, + closed: 0, + connecting: 0, + checking: 0, + connected: 0, + completed: 0, + failed: 0 + }; + this.transceivers.forEach(function(transceiver) { + states[transceiver.iceTransport.state]++; + states[transceiver.dtlsTransport.state]++; + }); + // ICETransport.completed and connected are the same for this purpose. + states.connected += states.completed; + + newState = 'new'; + if (states.failed > 0) { + newState = 'failed'; + } else if (states.connecting > 0 || states.checking > 0) { + newState = 'connecting'; + } else if (states.disconnected > 0) { + newState = 'disconnected'; + } else if (states.new > 0) { + newState = 'new'; + } else if (states.connecting > 0 || states.completed > 0) { + newState = 'connected'; + } + + if (newState !== self.iceConnectionState) { + self.iceConnectionState = newState; + if (this.oniceconnectionstatechange !== null) { + this.oniceconnectionstatechange(); + } + } + }; + + window.RTCPeerConnection.prototype.createOffer = function() { + var self = this; + if (this._pendingOffer) { + throw new Error('createOffer called while there is a pending offer.'); + } + var offerOptions; + if (arguments.length === 1 && typeof arguments[0] !== 'function') { + offerOptions = arguments[0]; + } else if (arguments.length === 3) { + offerOptions = arguments[2]; + } + + var tracks = []; + var numAudioTracks = 0; + var numVideoTracks = 0; + // Default to sendrecv. + if (this.localStreams.length) { + numAudioTracks = this.localStreams[0].getAudioTracks().length; + numVideoTracks = this.localStreams[0].getVideoTracks().length; + } + // Determine number of audio and video tracks we need to send/recv. + if (offerOptions) { + // Reject Chrome legacy constraints. + if (offerOptions.mandatory || offerOptions.optional) { + throw new TypeError( + 'Legacy mandatory/optional constraints not supported.'); + } + if (offerOptions.offerToReceiveAudio !== undefined) { + numAudioTracks = offerOptions.offerToReceiveAudio; + } + if (offerOptions.offerToReceiveVideo !== undefined) { + numVideoTracks = offerOptions.offerToReceiveVideo; + } + } + if (this.localStreams.length) { + // Push local streams. + this.localStreams[0].getTracks().forEach(function(track) { + tracks.push({ + kind: track.kind, + track: track, + wantReceive: track.kind === 'audio' ? + numAudioTracks > 0 : numVideoTracks > 0 + }); + if (track.kind === 'audio') { + numAudioTracks--; + } else if (track.kind === 'video') { + numVideoTracks--; + } + }); + } + // Create M-lines for recvonly streams. + while (numAudioTracks > 0 || numVideoTracks > 0) { + if (numAudioTracks > 0) { + tracks.push({ + kind: 'audio', + wantReceive: true + }); + numAudioTracks--; + } + if (numVideoTracks > 0) { + tracks.push({ + kind: 'video', + wantReceive: true + }); + numVideoTracks--; + } + } + + var sdp = SDPUtils.writeSessionBoilerplate(); + var transceivers = []; + tracks.forEach(function(mline, sdpMLineIndex) { + // For each track, create an ice gatherer, ice transport, dtls transport, + // potentially rtpsender and rtpreceiver. + var track = mline.track; + var kind = mline.kind; + var mid = generateIdentifier(); + + var transports = self._createIceAndDtlsTransports(mid, sdpMLineIndex); + + var localCapabilities = RTCRtpSender.getCapabilities(kind); + var rtpSender; + var rtpReceiver; + + // generate an ssrc now, to be used later in rtpSender.send + var sendSsrc = (2 * sdpMLineIndex + 1) * 1001; + if (track) { + rtpSender = new RTCRtpSender(track, transports.dtlsTransport); + } + + if (mline.wantReceive) { + rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind); + } + + transceivers[sdpMLineIndex] = { + iceGatherer: transports.iceGatherer, + iceTransport: transports.iceTransport, + dtlsTransport: transports.dtlsTransport, + localCapabilities: localCapabilities, + remoteCapabilities: null, + rtpSender: rtpSender, + rtpReceiver: rtpReceiver, + kind: kind, + mid: mid, + sendSsrc: sendSsrc, + recvSsrc: null + }; + var transceiver = transceivers[sdpMLineIndex]; + sdp += SDPUtils.writeMediaSection(transceiver, + transceiver.localCapabilities, 'offer', self.localStreams[0]); + }); + + this._pendingOffer = transceivers; + var desc = new RTCSessionDescription({ + type: 'offer', + sdp: sdp + }); + if (arguments.length && typeof arguments[0] === 'function') { + window.setTimeout(arguments[0], 0, desc); + } + return Promise.resolve(desc); + }; + + window.RTCPeerConnection.prototype.createAnswer = function() { + var self = this; + var answerOptions; + if (arguments.length === 1 && typeof arguments[0] !== 'function') { + answerOptions = arguments[0]; + } else if (arguments.length === 3) { + answerOptions = arguments[2]; + } + + var sdp = SDPUtils.writeSessionBoilerplate(); + this.transceivers.forEach(function(transceiver) { + // Calculate intersection of capabilities. + var commonCapabilities = self._getCommonCapabilities( + transceiver.localCapabilities, + transceiver.remoteCapabilities); + + sdp += SDPUtils.writeMediaSection(transceiver, commonCapabilities, + 'answer', self.localStreams[0]); + }); + + var desc = new RTCSessionDescription({ + type: 'answer', + sdp: sdp + }); + if (arguments.length && typeof arguments[0] === 'function') { + window.setTimeout(arguments[0], 0, desc); + } + return Promise.resolve(desc); + }; + + window.RTCPeerConnection.prototype.addIceCandidate = function(candidate) { + var mLineIndex = candidate.sdpMLineIndex; + if (candidate.sdpMid) { + for (var i = 0; i < this.transceivers.length; i++) { + if (this.transceivers[i].mid === candidate.sdpMid) { + mLineIndex = i; + break; + } + } + } + var transceiver = this.transceivers[mLineIndex]; + if (transceiver) { + var cand = Object.keys(candidate.candidate).length > 0 ? + SDPUtils.parseCandidate(candidate.candidate) : {}; + // Ignore Chrome's invalid candidates since Edge does not like them. + if (cand.protocol === 'tcp' && cand.port === 0) { + return; + } + // Ignore RTCP candidates, we assume RTCP-MUX. + if (cand.component !== '1') { + return; + } + // A dirty hack to make samples work. + if (cand.type === 'endOfCandidates') { + cand = {}; + } + transceiver.iceTransport.addRemoteCandidate(cand); + } + if (arguments.length > 1 && typeof arguments[1] === 'function') { + window.setTimeout(arguments[1], 0); + } + return Promise.resolve(); + }; + + window.RTCPeerConnection.prototype.getStats = function() { + var promises = []; + this.transceivers.forEach(function(transceiver) { + ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport', + 'dtlsTransport'].forEach(function(method) { + if (transceiver[method]) { + promises.push(transceiver[method].getStats()); + } + }); + }); + var cb = arguments.length > 1 && typeof arguments[1] === 'function' && + arguments[1]; + return new Promise(function(resolve) { + var results = {}; + Promise.all(promises).then(function(res) { + res.forEach(function(result) { + Object.keys(result).forEach(function(id) { + results[id] = result[id]; + }); + }); + if (cb) { + window.setTimeout(cb, 0, results); + } + resolve(results); + }); + }); + }; + } + } else { + webrtcUtils.log('Browser does not appear to be WebRTC-capable'); + } + + // Returns the result of getUserMedia as a Promise. + function requestUserMedia(constraints) { + return new Promise(function(resolve, reject) { + getUserMedia(constraints, resolve, reject); + }); + } + + var webrtcTesting = {}; + try { + Object.defineProperty(webrtcTesting, 'version', { + set: function(version) { + webrtcDetectedVersion = version; + } + }); + } catch (e) {} + + /* Orginal exports removed in favor of AdapterJS custom export. + if (typeof module !== 'undefined') { + var RTCPeerConnection; + var RTCIceCandidate; + var RTCSessionDescription; + if (typeof window !== 'undefined') { + RTCPeerConnection = window.RTCPeerConnection; + RTCIceCandidate = window.RTCIceCandidate; + RTCSessionDescription = window.RTCSessionDescription; } - callback(iceConnectionState); + module.exports = { + RTCPeerConnection: RTCPeerConnection, + RTCIceCandidate: RTCIceCandidate, + RTCSessionDescription: RTCSessionDescription, + getUserMedia: getUserMedia, + attachMediaStream: attachMediaStream, + reattachMediaStream: reattachMediaStream, + webrtcDetectedBrowser: webrtcDetectedBrowser, + webrtcDetectedVersion: webrtcDetectedVersion, + webrtcMinimumVersion: webrtcMinimumVersion, + webrtcTesting: webrtcTesting, + webrtcUtils: webrtcUtils + //requestUserMedia: not exposed on purpose. + //trace: not exposed on purpose. + }; + } else if ((typeof require === 'function') && (typeof define === 'function')) { + // Expose objects and functions when RequireJS is doing the loading. + define([], function() { + return { + RTCPeerConnection: window.RTCPeerConnection, + RTCIceCandidate: window.RTCIceCandidate, + RTCSessionDescription: window.RTCSessionDescription, + getUserMedia: getUserMedia, + attachMediaStream: attachMediaStream, + reattachMediaStream: reattachMediaStream, + webrtcDetectedBrowser: webrtcDetectedBrowser, + webrtcDetectedVersion: webrtcDetectedVersion, + webrtcMinimumVersion: webrtcMinimumVersion, + webrtcTesting: webrtcTesting, + webrtcUtils: webrtcUtils + //requestUserMedia: not exposed on purpose. + //trace: not exposed on purpose. + }; + }); } - return; -}; - -// Firefox: -// - Creates iceServer from the url for Firefox. -// - Create iceServer with stun url. -// - Create iceServer with turn url. -// - Ignore the transport parameter from TURN url for FF version <=27. -// - Return null for createIceServer if transport=tcp. -// - FF 27 and above supports transport parameters in TURN url, -// - So passing in the full url to create iceServer. -// Chrome: -// - Creates iceServer from the url for Chrome M33 and earlier. -// - Create iceServer with stun url. -// - Chrome M28 & above uses below TURN format. -// Plugin: -// - Creates Ice Server for Plugin Browsers -// - If Stun - Create iceServer with stun url. -// - Else - Create iceServer with turn url -// - This is a WebRTC Function -createIceServer = null; - -// Firefox: -// - Creates IceServers for Firefox -// - Use .url for FireFox. -// - Multiple Urls support -// Chrome: -// - Creates iceServers from the urls for Chrome M34 and above. -// - .urls is supported since Chrome M34. -// - Multiple Urls support -// Plugin: -// - Creates Ice Servers for Plugin Browsers -// - Multiple Urls support -// - This is a WebRTC Function -createIceServers = null; -//------------------------------------------------------------ - -//The RTCPeerConnection object. -RTCPeerConnection = null; - -// Creates RTCSessionDescription object for Plugin Browsers -RTCSessionDescription = (typeof RTCSessionDescription === 'function') ? - RTCSessionDescription : null; - -// Creates RTCIceCandidate object for Plugin Browsers -RTCIceCandidate = (typeof RTCIceCandidate === 'function') ? - RTCIceCandidate : null; - -// Get UserMedia (only difference is the prefix). -// Code from Adam Barth. -getUserMedia = null; - -// Attach a media stream to an element. -attachMediaStream = null; - -// Re-attach a media stream to an element. -reattachMediaStream = null; + */ +/* jshint ignore:end */ -// Detected browser agent name. Types are: -// - 'firefox': Firefox browser. -// - 'chrome': Chrome browser. -// - 'opera': Opera browser. -// - 'safari': Safari browser. -// - 'IE' - Internet Explorer browser. -webrtcDetectedBrowser = null; + // END OF INJECTION OF GOOGLE'S ADAPTER.JS CONTENT + /////////////////////////////////////////////////////////////////// -// Detected browser version. -webrtcDetectedVersion = null; + AdapterJS.parseWebrtcDetectedBrowser(); -// Check for browser types and react accordingly -if (navigator.mozGetUserMedia) { - webrtcDetectedBrowser = 'firefox'; - webrtcDetectedVersion = parseInt(navigator - .userAgent.match(/Firefox\/([0-9]+)\./)[1], 10); - webrtcDetectedType = 'moz'; - webrtcDetectedDCSupport = 'SCTP'; - - RTCPeerConnection = function (pcConfig, pcConstraints) { - AdapterJS.maybeFixConfiguration(pcConfig); - return new mozRTCPeerConnection(pcConfig, pcConstraints); - }; + /////////////////////////////////////////////////////////////////// + // EXTENSION FOR CHROME, FIREFOX AND EDGE + // Includes legacy functions + // -- createIceServer + // -- createIceServers + // -- MediaStreamTrack.getSources + // + // and additional shims + // -- attachMediaStream + // -- reattachMediaStream + // -- requestUserMedia + // -- a call to AdapterJS.maybeThroughWebRTCReady (notifies WebRTC is ready) + + // Add support for legacy functions createIceServer and createIceServers + if ( navigator.mozGetUserMedia ) { + // Shim for MediaStreamTrack.getSources. + MediaStreamTrack.getSources = function(successCb) { + setTimeout(function() { + var infos = [ + { kind: 'audio', id: 'default', label:'', facing:'' }, + { kind: 'video', id: 'default', label:'', facing:'' } + ]; + successCb(infos); + }, 0); + }; - // The RTCSessionDescription object. - RTCSessionDescription = mozRTCSessionDescription; - window.RTCSessionDescription = RTCSessionDescription; - - // The RTCIceCandidate object. - RTCIceCandidate = mozRTCIceCandidate; - window.RTCIceCandidate = RTCIceCandidate; - - window.getUserMedia = navigator.mozGetUserMedia.bind(navigator); - navigator.getUserMedia = window.getUserMedia; - - // Shim for MediaStreamTrack.getSources. - MediaStreamTrack.getSources = function(successCb) { - setTimeout(function() { - var infos = [ - { kind: 'audio', id: 'default', label:'', facing:'' }, - { kind: 'video', id: 'default', label:'', facing:'' } - ]; - successCb(infos); - }, 0); - }; + createIceServer = function (url, username, password) { + console.warn('createIceServer is deprecated. It should be replaced with an application level implementation.'); + // Note: Google's import of AJS will auto-reverse to 'url': '...' for FF < 38 - createIceServer = function (url, username, password) { - var iceServer = null; - var url_parts = url.split(':'); - if (url_parts[0].indexOf('stun') === 0) { - iceServer = { url : url }; - } else if (url_parts[0].indexOf('turn') === 0) { - if (webrtcDetectedVersion < 27) { - var turn_url_parts = url.split('?'); - if (turn_url_parts.length === 1 || - turn_url_parts[1].indexOf('transport=udp') === 0) { + var iceServer = null; + var urlParts = url.split(':'); + if (urlParts[0].indexOf('stun') === 0) { + iceServer = { urls : [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 = { + urls : [turnUrlParts[0]], + credential : password, + username : username + }; + } + } else { iceServer = { - url : turn_url_parts[0], + urls : [url], credential : password, username : username }; } - } else { - iceServer = { - url : url, - credential : password, - username : username - }; } - } - return iceServer; - }; - - createIceServers = function (urls, username, password) { - var iceServers = []; - for (i = 0; i < urls.length; i++) { - var iceServer = createIceServer(urls[i], username, password); - if (iceServer !== null) { - iceServers.push(iceServer); - } - } - return iceServers; - }; + return iceServer; + }; - attachMediaStream = function (element, stream) { - element.mozSrcObject = stream; - if (stream !== null) - element.play(); + createIceServers = function (urls, username, password) { + console.warn('createIceServers is deprecated. It should be replaced with an application level implementation.'); - return element; - }; + var iceServers = []; + for (i = 0; i < urls.length; i++) { + var iceServer = createIceServer(urls[i], username, password); + if (iceServer !== null) { + iceServers.push(iceServer); + } + } + return iceServers; + }; + } else if ( navigator.webkitGetUserMedia ) { + createIceServer = function (url, username, password) { + console.warn('createIceServer is deprecated. It should be replaced with an application level implementation.'); - reattachMediaStream = function (to, from) { - to.mozSrcObject = from.mozSrcObject; - to.play(); - return to; - }; + 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; + }; - MediaStreamTrack.getSources = MediaStreamTrack.getSources || function (callback) { - if (!callback) { - throw new TypeError('Failed to execute \'getSources\' on \'MediaStreamTrack\'' + - ': 1 argument required, but only 0 present.'); - } - return callback([]); - }; + createIceServers = function (urls, username, password) { + console.warn('createIceServers is deprecated. It should be replaced with an application level implementation.'); - // Fake get{Video,Audio}Tracks - if (!MediaStream.prototype.getVideoTracks) { - MediaStream.prototype.getVideoTracks = function () { - return []; - }; - } - if (!MediaStream.prototype.getAudioTracks) { - MediaStream.prototype.getAudioTracks = function () { - return []; + var iceServers = []; + if (webrtcDetectedVersion >= 34) { + iceServers = { + 'urls' : urls, + 'credential' : password, + 'username' : username + }; + } else { + for (i = 0; i < urls.length; i++) { + var iceServer = createIceServer(urls[i], username, password); + if (iceServer !== null) { + iceServers.push(iceServer); + } + } + } + return iceServers; }; } - AdapterJS.maybeThroughWebRTCReady(); -} else if (navigator.webkitGetUserMedia) { - webrtcDetectedBrowser = 'chrome'; - webrtcDetectedType = 'webkit'; - webrtcDetectedVersion = parseInt(navigator - .userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10); - // check if browser is opera 20+ - var checkIfOpera = navigator.userAgent.match(/\bOPR\/(\d+)/); - if (checkIfOpera !== null) { - webrtcDetectedBrowser = 'opera'; - webrtcDetectedVersion = parseInt(checkIfOpera[1], 10); - } - // check browser datachannel support - if ((webrtcDetectedBrowser === 'chrome' && webrtcDetectedVersion >= 31) || - (webrtcDetectedBrowser === 'opera' && webrtcDetectedVersion >= 20)) { - webrtcDetectedDCSupport = 'SCTP'; - } else if (webrtcDetectedBrowser === 'chrome' && webrtcDetectedVersion < 30 && - webrtcDetectedVersion > 24) { - webrtcDetectedDCSupport = 'RTP'; - } else { - webrtcDetectedDCSupport = ''; + // adapter.js by Google currently doesn't suppport + // attachMediaStream and reattachMediaStream for Egde + if (navigator.mediaDevices && navigator.userAgent.match( + /Edge\/(\d+).(\d+)$/)) { + getUserMedia = window.getUserMedia = navigator.getUserMedia.bind(navigator); + attachMediaStream = function(element, stream) { + element.srcObject = stream; + return element; + }; + reattachMediaStream = function(to, from) { + to.srcObject = from.srcObject; + return to; + }; } - createIceServer = function (url, username, password) { - var iceServer = null; - var url_parts = url.split(':'); - if (url_parts[0].indexOf('stun') === 0) { - iceServer = { 'url' : url }; - } else if (url_parts[0].indexOf('turn') === 0) { - iceServer = { - 'url' : url, - 'credential' : password, - 'username' : username - }; - } - return iceServer; - }; + // Need to override attachMediaStream and reattachMediaStream + // to support the plugin's logic + attachMediaStream_base = attachMediaStream; - createIceServers = function (urls, username, password) { - var iceServers = []; - if (webrtcDetectedVersion >= 34) { - iceServers = { - 'urls' : urls, - 'credential' : password, - 'username' : username - }; - } else { - for (i = 0; i < urls.length; i++) { - var iceServer = createIceServer(urls[i], username, password); - if (iceServer !== null) { - iceServers.push(iceServer); - } + if (webrtcDetectedBrowser === 'opera') { + attachMediaStream_base = function (element, stream) { + if (webrtcDetectedVersion > 38) { + element.srcObject = stream; + } else if (typeof element.src !== 'undefined') { + element.src = URL.createObjectURL(stream); } - } - return iceServers; - }; - - RTCPeerConnection = function (pcConfig, pcConstraints) { - if (webrtcDetectedVersion < 34) { - AdapterJS.maybeFixConfiguration(pcConfig); - } - return new webkitRTCPeerConnection(pcConfig, pcConstraints); - }; - - window.getUserMedia = navigator.webkitGetUserMedia.bind(navigator); - navigator.getUserMedia = window.getUserMedia; + // Else it doesn't work + }; + } 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 = (stream === null ? '' : URL.createObjectURL(stream)); + if ((webrtcDetectedBrowser === 'chrome' || + webrtcDetectedBrowser === 'opera') && + !stream) { + // Chrome does not support "src = null" + element.src = ''; } else { - console.log('Error attaching stream to element.'); + attachMediaStream_base(element, stream); } return element; }; - + reattachMediaStream_base = reattachMediaStream; reattachMediaStream = function (to, from) { - to.src = from.src; + reattachMediaStream_base(to, from); return to; }; + // Propagate attachMediaStream and gUM in window and AdapterJS + window.attachMediaStream = attachMediaStream; + window.reattachMediaStream = reattachMediaStream; + window.getUserMedia = getUserMedia; + AdapterJS.attachMediaStream = attachMediaStream; + AdapterJS.reattachMediaStream = reattachMediaStream; + AdapterJS.getUserMedia = getUserMedia; + + // Removed Google defined promises when promise is not defined + if (typeof Promise === 'undefined') { + requestUserMedia = null; + } + AdapterJS.maybeThroughWebRTCReady(); + + // END OF EXTENSION OF CHROME, FIREFOX AND EDGE + /////////////////////////////////////////////////////////////////// + } else { // TRY TO USE PLUGIN + + /////////////////////////////////////////////////////////////////// + // WEBRTC PLUGIN SHIM + // Will automatically check if the plugin is available and inject it + // into the DOM if it is. + // When the plugin is not available, will prompt a banner to suggest installing it + // Use AdapterJS.options.hidePluginInstallPrompt to prevent this banner from popping + // + // Shims the follwing: + // -- getUserMedia + // -- MediaStreamTrack + // -- MediaStreamTrack.getSources + // -- RTCPeerConnection + // -- RTCSessionDescription + // -- RTCIceCandidate + // -- createIceServer + // -- createIceServers + // -- attachMediaStream + // -- reattachMediaStream + // -- webrtcDetectedBrowser + // -- webrtcDetectedVersion + // IE 9 is not offering an implementation of console.log until you open a console if (typeof console !== 'object' || typeof console.log !== 'function') { /* jshint -W020 */ @@ -736,8 +2543,6 @@ if (navigator.mozGetUserMedia) { console.groupEnd = function (arg) {}; /* jshint +W020 */ } - webrtcDetectedType = 'plugin'; - webrtcDetectedDCSupport = 'plugin'; AdapterJS.parseWebrtcDetectedBrowser(); isIE = webrtcDetectedBrowser === 'IE'; @@ -794,8 +2599,8 @@ if (navigator.mozGetUserMedia) { AdapterJS.WebRTCPlugin.pluginInfo.pluginId + '" /> ' + ' ' + ' ' + - '' + + '' + + '' + // uncomment to be able to use virtual cams (AdapterJS.options.getAllCams ? '':'') + @@ -829,7 +2634,8 @@ if (navigator.mozGetUserMedia) { AdapterJS.WebRTCPlugin.pluginInfo.pluginId + '">' + ' ' + (AdapterJS.options.getAllCams ? '':'') + - ''; + '' + + ''; document.body.appendChild(AdapterJS.WebRTCPlugin.plugin); } @@ -838,11 +2644,11 @@ if (navigator.mozGetUserMedia) { }; AdapterJS.WebRTCPlugin.isPluginInstalled = - function (comName, plugName, installedCb, notInstalledCb) { + function (comName, plugName, plugType, installedCb, notInstalledCb) { if (!isIE) { - var pluginArray = navigator.plugins; + var pluginArray = navigator.mimeTypes; for (var i = 0; i < pluginArray.length; i++) { - if (pluginArray[i].name.indexOf(plugName) >= 0) { + if (pluginArray[i].type.indexOf(plugType) >= 0) { installedCb(); return; } @@ -860,6 +2666,12 @@ if (navigator.mozGetUserMedia) { }; AdapterJS.WebRTCPlugin.defineWebRTCInterface = function () { + if (AdapterJS.WebRTCPlugin.pluginState === + AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY) { + console.error('AdapterJS - WebRTC interface has already been defined'); + return; + } + AdapterJS.WebRTCPlugin.pluginState = AdapterJS.WebRTCPlugin.PLUGIN_STATES.INITIALIZING; AdapterJS.isDefined = function (variable) { @@ -868,13 +2680,13 @@ if (navigator.mozGetUserMedia) { createIceServer = function (url, username, password) { var iceServer = null; - var url_parts = url.split(':'); - if (url_parts[0].indexOf('stun') === 0) { + var urlParts = url.split(':'); + if (urlParts[0].indexOf('stun') === 0) { iceServer = { 'url' : url, 'hasCredentials' : false }; - } else if (url_parts[0].indexOf('turn') === 0) { + } else if (urlParts[0].indexOf('turn') === 0) { iceServer = { 'url' : url, 'hasCredentials' : true, @@ -900,10 +2712,39 @@ if (navigator.mozGetUserMedia) { }; RTCPeerConnection = function (servers, constraints) { + // Validate server argumenr + if (!(servers === undefined || + servers === null || + Array.isArray(servers.iceServers))) { + throw new Error('Failed to construct \'RTCPeerConnection\': Malformed RTCConfiguration'); + } + + // Validate constraints argument + if (typeof constraints !== 'undefined' && constraints !== null) { + var invalidConstraits = false; + invalidConstraits |= typeof constraints !== 'object'; + invalidConstraits |= constraints.hasOwnProperty('mandatory') && + constraints.mandatory !== undefined && + constraints.mandatory !== null && + constraints.mandatory.constructor !== Object; + invalidConstraits |= constraints.hasOwnProperty('optional') && + constraints.optional !== undefined && + constraints.optional !== null && + !Array.isArray(constraints.optional); + if (invalidConstraits) { + throw new Error('Failed to construct \'RTCPeerConnection\': Malformed constraints object'); + } + } + + // Call relevant PeerConnection constructor according to plugin version + AdapterJS.WebRTCPlugin.WaitForPluginReady(); + + // RTCPeerConnection prototype from the old spec var iceServers = null; - if (servers) { + if (servers && Array.isArray(servers.iceServers)) { iceServers = servers.iceServers; for (var i = 0; i < iceServers.length; i++) { + // Legacy plugin versions compatibility if (iceServers[i].urls && !iceServers[i].url) { iceServers[i].url = iceServers[i].urls; } @@ -912,117 +2753,205 @@ if (navigator.mozGetUserMedia) { AdapterJS.isDefined(iceServers[i].credential); } } - var mandatory = (constraints && constraints.mandatory) ? - constraints.mandatory : null; - var optional = (constraints && constraints.optional) ? - constraints.optional : null; - AdapterJS.WebRTCPlugin.WaitForPluginReady(); - return AdapterJS.WebRTCPlugin.plugin. - PeerConnection(AdapterJS.WebRTCPlugin.pageId, - iceServers, mandatory, optional); + if (AdapterJS.WebRTCPlugin.plugin.PEER_CONNECTION_VERSION && + AdapterJS.WebRTCPlugin.plugin.PEER_CONNECTION_VERSION > 1) { + // RTCPeerConnection prototype from the new spec + if (iceServers) { + servers.iceServers = iceServers; + } + return AdapterJS.WebRTCPlugin.plugin.PeerConnection(servers); + } else { + var mandatory = (constraints && constraints.mandatory) ? + constraints.mandatory : null; + var optional = (constraints && constraints.optional) ? + constraints.optional : null; + return AdapterJS.WebRTCPlugin.plugin. + PeerConnection(AdapterJS.WebRTCPlugin.pageId, + iceServers, mandatory, optional); + } }; - MediaStreamTrack = {}; + MediaStreamTrack = function(){}; MediaStreamTrack.getSources = function (callback) { AdapterJS.WebRTCPlugin.callWhenPluginReady(function() { AdapterJS.WebRTCPlugin.plugin.GetSources(callback); }); }; - window.getUserMedia = function (constraints, successCallback, failureCallback) { - constraints.audio = constraints.audio || false; - constraints.video = constraints.video || false; + // getUserMedia constraints shim. + // Copied from Chrome + var constraintsToPlugin = function(c) { + if (typeof c !== 'object' || c.mandatory || c.optional) { + return c; + } + var cc = {}; + Object.keys(c).forEach(function(key) { + if (key === 'require' || key === 'advanced' || key === 'mediaSource') { + return; + } + var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]}; + if (r.exact !== undefined && typeof r.exact === 'number') { + r.min = r.max = r.exact; + } + var oldname = function(prefix, name) { + if (prefix) { + return prefix + name.charAt(0).toUpperCase() + name.slice(1); + } + return (name === 'deviceId') ? 'sourceId' : name; + }; + if (r.ideal !== undefined) { + cc.optional = cc.optional || []; + var oc = {}; + if (typeof r.ideal === 'number') { + oc[oldname('min', key)] = r.ideal; + cc.optional.push(oc); + oc = {}; + oc[oldname('max', key)] = r.ideal; + cc.optional.push(oc); + } else { + oc[oldname('', key)] = r.ideal; + cc.optional.push(oc); + } + } + if (r.exact !== undefined && typeof r.exact !== 'number') { + cc.mandatory = cc.mandatory || {}; + cc.mandatory[oldname('', key)] = r.exact; + } else { + ['min', 'max'].forEach(function(mix) { + if (r[mix] !== undefined) { + cc.mandatory = cc.mandatory || {}; + cc.mandatory[oldname(mix, key)] = r[mix]; + } + }); + } + }); + if (c.advanced) { + cc.optional = (cc.optional || []).concat(c.advanced); + } + return cc; + }; + + getUserMedia = function (constraints, successCallback, failureCallback) { + var cc = {}; + cc.audio = constraints.audio ? + constraintsToPlugin(constraints.audio) : false; + cc.video = constraints.video ? + constraintsToPlugin(constraints.video) : false; AdapterJS.WebRTCPlugin.callWhenPluginReady(function() { AdapterJS.WebRTCPlugin.plugin. - getUserMedia(constraints, successCallback, failureCallback); + getUserMedia(cc, successCallback, failureCallback); }); }; - window.navigator.getUserMedia = window.getUserMedia; + window.navigator.getUserMedia = getUserMedia; + + // Defined mediaDevices when promises are available + if ( !navigator.mediaDevices && + typeof Promise !== 'undefined') { + requestUserMedia = function(constraints) { + return new Promise(function(resolve, reject) { + getUserMedia(constraints, resolve, reject); + }); + }; + navigator.mediaDevices = {getUserMedia: requestUserMedia, + enumerateDevices: function() { + return new Promise(function(resolve) { + var kinds = {audio: 'audioinput', video: 'videoinput'}; + return MediaStreamTrack.getSources(function(devices) { + resolve(devices.map(function(device) { + return {label: device.label, + kind: kinds[device.kind], + id: device.id, + deviceId: device.id, + groupId: ''}; + })); + }); + }); + }}; + } attachMediaStream = function (element, stream) { if (!element || !element.parentNode) { return; } - var streamId + var streamId; if (stream === null) { streamId = ''; - } - else { - stream.enableSoundTracks(true); + } else { + if (typeof stream.enableSoundTracks !== 'undefined') { + stream.enableSoundTracks(true); + } streamId = stream.id; } - if (element.nodeName.toLowerCase() !== 'audio') { - var elementId = element.id.length === 0 ? Math.random().toString(36).slice(2) : element.id; - if (!element.isWebRTCPlugin || !element.isWebRTCPlugin()) { - var frag = document.createDocumentFragment(); - var temp = document.createElement('div'); - var classHTML = ''; - if (element.className) { - classHTML = 'class="' + element.className + '" '; - } else if (element.attributes && element.attributes['class']) { - classHTML = 'class="' + element.attributes['class'].value + '" '; + var elementId = element.id.length === 0 ? Math.random().toString(36).slice(2) : element.id; + var nodeName = element.nodeName.toLowerCase(); + if (nodeName !== 'object') { // not a plugin tag yet + var tag; + switch(nodeName) { + case 'audio': + tag = AdapterJS.WebRTCPlugin.TAGS.AUDIO; + break; + case 'video': + tag = AdapterJS.WebRTCPlugin.TAGS.VIDEO; + break; + default: + tag = AdapterJS.WebRTCPlugin.TAGS.NONE; } - temp.innerHTML = '' + - ' ' + - ' ' + - ' ' + - ' ' + - ''; - while (temp.firstChild) { - frag.appendChild(temp.firstChild); - } + var frag = document.createDocumentFragment(); + var temp = document.createElement('div'); + var classHTML = ''; + if (element.className) { + classHTML = 'class="' + element.className + '" '; + } else if (element.attributes && element.attributes['class']) { + classHTML = 'class="' + element.attributes['class'].value + '" '; + } - var height = ''; - var width = ''; - if (element.getBoundingClientRect) { - var rectObject = element.getBoundingClientRect(); - width = rectObject.width + 'px'; - height = rectObject.height + 'px'; - } - else if (element.width) { - width = element.width; - height = element.height; - } else { - // TODO: What scenario could bring us here? - } + temp.innerHTML = '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + while (temp.firstChild) { + frag.appendChild(temp.firstChild); + } - element.parentNode.insertBefore(frag, element); - frag = document.getElementById(elementId); - frag.width = width; - frag.height = height; - element.parentNode.removeChild(element); - } else { - var children = element.children; - for (var i = 0; i !== children.length; ++i) { - if (children[i].name === 'streamId') { - children[i].value = streamId; - break; - } + var height = ''; + var width = ''; + if (element.clientWidth || element.clientHeight) { + width = element.clientWidth; + height = element.clientHeight; + } + else if (element.width || element.height) { + width = element.width; + height = element.height; + } + + element.parentNode.insertBefore(frag, element); + frag = document.getElementById(elementId); + frag.width = width; + frag.height = height; + element.parentNode.removeChild(element); + } else { // already an tag, just change the stream id + var children = element.children; + for (var i = 0; i !== children.length; ++i) { + if (children[i].name === 'streamId') { + children[i].value = streamId; + break; } - element.setStreamId(streamId); - } - var newElement = document.getElementById(elementId); - newElement.onplaying = (element.onplaying) ? element.onplaying : function (arg) {}; - if (isIE) { // on IE the event needs to be plugged manually - newElement.attachEvent('onplaying', newElement.onplaying); - newElement.onclick = (element.onclick) ? element.onclick : function (arg) {}; - newElement._TemOnClick = function (id) { - var arg = { - srcElement : document.getElementById(id) - }; - newElement.onclick(arg); - }; } - return newElement; - } else { - return element; + element.setStreamId(streamId); } + var newElement = document.getElementById(elementId); + AdapterJS.forwardEventHandlers(newElement, element, Object.getPrototypeOf(element)); + + return newElement; }; reattachMediaStream = function (to, from) { @@ -1043,6 +2972,33 @@ if (navigator.mozGetUserMedia) { } }; + // Propagate attachMediaStream and gUM in window and AdapterJS + window.attachMediaStream = attachMediaStream; + window.reattachMediaStream = reattachMediaStream; + window.getUserMedia = getUserMedia; + AdapterJS.attachMediaStream = attachMediaStream; + AdapterJS.reattachMediaStream = reattachMediaStream; + AdapterJS.getUserMedia = getUserMedia; + + AdapterJS.forwardEventHandlers = function (destElem, srcElem, prototype) { + properties = Object.getOwnPropertyNames( prototype ); + for(var prop in properties) { + if (prop) { + propName = properties[prop]; + + if (typeof propName.slice === 'function' && + propName.slice(0,2) === 'on' && + typeof srcElem[propName] === 'function') { + AdapterJS.addEvent(destElem, propName.slice(2), srcElem[propName]); + } + } + } + var subPrototype = Object.getPrototypeOf(prototype); + if(!!subPrototype) { + AdapterJS.forwardEventHandlers(destElem, srcElem, subPrototype); + } + }; + RTCIceCandidate = function (candidate) { if (!candidate.sdpMid) { candidate.sdpMid = ''; @@ -1093,15 +3049,18 @@ if (navigator.mozGetUserMedia) { } }; + // Try to detect the plugin and act accordingly AdapterJS.WebRTCPlugin.isPluginInstalled( AdapterJS.WebRTCPlugin.pluginInfo.prefix, AdapterJS.WebRTCPlugin.pluginInfo.plugName, + AdapterJS.WebRTCPlugin.pluginInfo.type, AdapterJS.WebRTCPlugin.defineWebRTCInterface, AdapterJS.WebRTCPlugin.pluginNeededButNotInstalledCb); -} - + // END OF WEBRTC PLUGIN SHIM + /////////////////////////////////////////////////////////////////// +} (function () { @@ -1118,10 +3077,14 @@ if (navigator.mozGetUserMedia) { }; var clone = function(obj) { - if (null == obj || "object" != typeof obj) return obj; + if (null === obj || 'object' !== typeof obj) { + return obj; + } var copy = obj.constructor(); for (var attr in obj) { - if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr]; + if (obj.hasOwnProperty(attr)) { + copy[attr] = obj[attr]; + } } return copy; }; @@ -1134,8 +3097,10 @@ if (navigator.mozGetUserMedia) { if (constraints && constraints.video && !!constraints.video.mediaSource) { // intercepting screensharing requests + // Invalid mediaSource for firefox, only "screen" and "window" are supported if (constraints.video.mediaSource !== 'screen' && constraints.video.mediaSource !== 'window') { - throw new Error('Only "screen" and "window" option is available as mediaSource'); + failureCb(new Error('GetUserMedia: Only "screen" and "window" are supported as mediaSource constraints')); + return; } var updatedConstraints = clone(constraints); @@ -1150,11 +3115,10 @@ if (navigator.mozGetUserMedia) { clearInterval(checkIfReady); baseGetUserMedia(updatedConstraints, successCb, function (error) { - if (error.name === 'PermissionDeniedError' && window.parent.location.protocol === 'https:') { + if (['PermissionDeniedError', 'SecurityError'].indexOf(error.name) > -1 && window.parent.location.protocol === 'https:') { AdapterJS.renderNotificationBar(AdapterJS.TEXT.EXTENSION.REQUIRE_INSTALLATION_FF, AdapterJS.TEXT.EXTENSION.BUTTON_FF, - 'http://skylink.io/screensharing/ff_addon.php?domain=' + window.location.hostname, false, true); - //window.location.href = 'http://skylink.io/screensharing/ff_addon.php?domain=' + window.location.hostname; + 'https://addons.mozilla.org/en-US/firefox/addon/skylink-webrtc-tools/', true, true); } else { failureCb(error); } @@ -1167,16 +3131,22 @@ if (navigator.mozGetUserMedia) { } }; - getUserMedia = navigator.getUserMedia; + AdapterJS.getUserMedia = window.getUserMedia = navigator.getUserMedia; + navigator.mediaDevices.getUserMedia = function(constraints) { + return new Promise(function(resolve, reject) { + window.getUserMedia(constraints, resolve, reject); + }); + }; - } else if (window.navigator.webkitGetUserMedia) { + } else if (window.navigator.webkitGetUserMedia && window.webrtcDetectedBrowser !== 'safari') { baseGetUserMedia = window.navigator.getUserMedia; navigator.getUserMedia = function (constraints, successCb, failureCb) { - if (constraints && constraints.video && !!constraints.video.mediaSource) { if (window.webrtcDetectedBrowser !== 'chrome') { - throw new Error('Current browser does not support screensharing'); + // This is Opera, which does not support screensharing + failureCb(new Error('Current browser does not support screensharing')); + return; } // would be fine since no methods @@ -1197,11 +3167,13 @@ if (navigator.mozGetUserMedia) { baseGetUserMedia(updatedConstraints, successCb, failureCb); - } else { + } else { // GUM failed if (error === 'permission-denied') { - throw new Error('Permission denied for screen retrieval'); + failureCb(new Error('Permission denied for screen retrieval')); } else { - throw new Error('Failed retrieving selected screen'); + // NOTE(J-O): I don't think we ever pass in here. + // A failure to capture the screen does not lead here. + failureCb(new Error('Failed retrieving selected screen')); } } }; @@ -1244,7 +3216,16 @@ if (navigator.mozGetUserMedia) { } }; - getUserMedia = navigator.getUserMedia; + AdapterJS.getUserMedia = window.getUserMedia = navigator.getUserMedia; + navigator.mediaDevices.getUserMedia = function(constraints) { + return new Promise(function(resolve, reject) { + window.getUserMedia(constraints, resolve, reject); + }); + }; + + } else if (navigator.mediaDevices && navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { + // nothing here because edge does not support screensharing + console.warn('Edge does not support screensharing feature in getUserMedia'); } else { baseGetUserMedia = window.navigator.getUserMedia; @@ -1259,8 +3240,6 @@ if (navigator.mozGetUserMedia) { // check if screensharing feature is available if (!!AdapterJS.WebRTCPlugin.plugin.HasScreensharingFeature && !!AdapterJS.WebRTCPlugin.plugin.isScreensharingAvailable) { - - // set the constraints updatedConstraints.video.optional = updatedConstraints.video.optional || []; updatedConstraints.video.optional.push({ @@ -1269,7 +3248,8 @@ if (navigator.mozGetUserMedia) { delete updatedConstraints.video.mediaSource; } else { - throw new Error('Your WebRTC plugin does not support screensharing'); + failureCb(new Error('Your version of the WebRTC plugin does not support screensharing')); + return; } baseGetUserMedia(updatedConstraints, successCb, failureCb); }); @@ -1278,9 +3258,17 @@ if (navigator.mozGetUserMedia) { } }; - getUserMedia = window.navigator.getUserMedia; + AdapterJS.getUserMedia = getUserMedia = + window.getUserMedia = navigator.getUserMedia; + if ( navigator.mediaDevices && + typeof Promise !== 'undefined') { + navigator.mediaDevices.getUserMedia = requestUserMedia; + } } + // For chrome, use an iframe to load the screensharing extension + // in the correct domain. + // Modify here for custom screensharing extension in chrome if (window.webrtcDetectedBrowser === 'chrome') { var iframe = document.createElement('iframe'); @@ -1289,12 +3277,11 @@ if (navigator.mozGetUserMedia) { }; iframe.src = 'https://cdn.temasys.com.sg/skylink/extensions/detectRTC.html'; - //'https://temasys-cdn.s3.amazonaws.com/skylink/extensions/detection-script-dev/detectRTC.html'; iframe.style.display = 'none'; (document.body || document.documentElement).appendChild(iframe); - var postFrameMessage = function (object) { + var postFrameMessage = function (object) { // jshint ignore:line object = object || {}; if (!iframe.isLoaded) { @@ -1306,5 +3293,7 @@ if (navigator.mozGetUserMedia) { iframe.contentWindow.postMessage(object, '*'); }; + } else if (window.webrtcDetectedBrowser === 'opera') { + console.warn('Opera does not support screensharing feature in getUserMedia'); } -})(); +})(); \ No newline at end of file diff --git a/source/js/libs/skylink.js b/source/js/libs/skylink.js index b192fc6..38e275f 100644 --- a/source/js/libs/skylink.js +++ b/source/js/libs/skylink.js @@ -1,51 +1,208 @@ -/*! skylinkjs - v0.5.10 - Fri Jun 05 2015 16:56:39 GMT+0800 (SGT) */ +/*! skylinkjs - v0.6.15 - Thu Sep 22 2016 18:56:28 GMT+0800 (SGT) */ (function() { 'use strict'; /** - * Please refer to the {{#crossLink "Skylink/init:method"}}init(){{/crossLink}} - * method for a guide to initializing Skylink. - * Please Note: - * - You must subscribe Skylink events before calling - * {{#crossLink "Skylink/init:method"}}init(){{/crossLink}}. - * - You will need an API key to use Skylink, if you do not have one you can - * [register for a developer account](http:// - * developer.temasys.com.sg) in the Skylink Developer Console. + * Polyfill for Object.keys() from Mozilla + * From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys + */ +if (!Object.keys) { + Object.keys = (function() { + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({ + toString: null + }).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; + + return function(obj) { + if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) throw new TypeError('Object.keys called on non-object'); + + var result = []; + + for (var prop in obj) { + if (hasOwnProperty.call(obj, prop)) result.push(prop); + } + + if (hasDontEnumBug) { + for (var i = 0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]); + } + } + return result; + } + })() +} + +/** + * Polyfill for Date.getISOString() from Mozilla + * From https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString + */ +(function() { + function pad(number) { + if (number < 10) { + return '0' + number; + } + return number; + } + + Date.prototype.toISOString = function() { + return this.getUTCFullYear() + + '-' + pad(this.getUTCMonth() + 1) + + '-' + pad(this.getUTCDate()) + + 'T' + pad(this.getUTCHours()) + + ':' + pad(this.getUTCMinutes()) + + ':' + pad(this.getUTCSeconds()) + + '.' + (this.getUTCMilliseconds() / 1000).toFixed(3).slice(2, 5) + + 'Z'; + }; +})(); + +/** + * Polyfill for addEventListener() from Eirik Backer @eirikbacker (github.com). + * From https://gist.github.com/eirikbacker/2864711 + * MIT Licensed + */ +(function(win, doc){ + if(win.addEventListener) return; //No need to polyfill + + function docHijack(p){var old = doc[p];doc[p] = function(v){ return addListen(old(v)) }} + function addEvent(on, fn, self){ + return (self = this).attachEvent('on' + on, function(e){ + var e = e || win.event; + e.preventDefault = e.preventDefault || function(){e.returnValue = false} + e.stopPropagation = e.stopPropagation || function(){e.cancelBubble = true} + fn.call(self, e); + }); + } + function addListen(obj, i){ + if(i = obj.length)while(i--)obj[i].addEventListener = addEvent; + else obj.addEventListener = addEvent; + return obj; + } + + addListen([doc, win]); + if('Element' in win)win.Element.prototype.addEventListener = addEvent; //IE8 + else{ //IE < 8 + doc.attachEvent('onreadystatechange', function(){addListen(doc.all)}); //Make sure we also init at domReady + docHijack('getElementsByTagName'); + docHijack('getElementById'); + docHijack('createElement'); + addListen(doc.all); + } +})(window, document); + +/** + * Global function that clones an object. + */ +var clone = function (obj) { + if (obj === null || typeof obj !== 'object') { + return obj; + } + + var copy = function (data) { + var copy = data.constructor(); + for (var attr in data) { + if (data.hasOwnProperty(attr)) { + copy[attr] = data[attr]; + } + } + return copy; + }; + + if (typeof obj === 'object' && !Array.isArray(obj)) { + try { + return JSON.parse( JSON.stringify(obj) ); + } catch (err) { + return copy(obj); + } + } + + return copy(obj); +}; + +/** + * Prerequisites on using Skylink + * Before using any Skylink functionalities, you will need to authenticate your App Key using + * the `init()` method. + * + * To manage or create App Keys, you may access the [Skylink Developer Portal here](https://console.temasys.io). + * + * To view the list of supported browsers, visit [the list here]( + * /~https://github.com/Temasys/SkylinkJS#supported-browsers). + * + * Here are some articles to help you get started: + * - [How to setup a simple video call](https://temasys.com.sg/getting-started-with-webrtc-and-skylinkjs/) + * - [How to setup screensharing](https://temasys.com.sg/screensharing-with-skylinkjs/) + * - [How to create a chatroom like feature](https://temasys.com.sg/building-a-simple-peer-to-peer-webrtc-chat/) + * + * Here are some demos you may use to aid your development: + * - Getaroom.io [[Demo](https://getaroom.io) / [Source code](/~https://github.com/Temasys/getaroom)] + * - Creating a component [[Link](/~https://github.com/Temasys/skylink-call-button)] + * + * You may see the example below in the Constructor tab to have a general idea how event subscription + * and the ordering of init() and + * joinRoom() methods should be called. + * + * If you have any issues, you may find answers to your questions in the FAQ section on [our support portal]( + * http://support.temasys.com.sg), asks questions, request features or raise bug tickets as well. + * + * If you would like to contribute to our SkylinkJS codebase, see [the contributing README]( + * /~https://github.com/Temasys/SkylinkJS/blob/master/CONTRIBUTING.md). + * + * [See License (Apache 2.0)](/~https://github.com/Temasys/SkylinkJS/blob/master/LICENSE) + * * @class Skylink * @constructor * @example - * // Getting started on how to use Skylink - * var SkylinkDemo = new Skylink(); - * SkylinkDemo.init('apiKey', function () { - * SkylinkDemo.joinRoom('my_room', { - * userData: 'My Username', - * audio: true, - * video: true - * }); - * }); + * // Here's a simple example on how you can start using Skylink. + * var skylinkDemo = new Skylink(); * - * SkylinkDemo.on('incomingStream', function (peerId, stream, peerInfo, isSelf) { + * // Subscribe all events first as a general guideline + * skylinkDemo.on("incomingStream", function (peerId, stream, peerInfo, isSelf) { * if (isSelf) { - * attachMediaStream(document.getElementById('selfVideo'), stream); + * attachMediaStream(document.getElementById("selfVideo"), stream); * } else { - * var peerVideo = document.createElement('video'); + * var peerVideo = document.createElement("video"); * peerVideo.id = peerId; - * peerVideo.autoplay = 'autoplay'; - * document.getElementById('peersVideo').appendChild(peerVideo); + * peerVideo.autoplay = "autoplay"; + * document.getElementById("peersVideo").appendChild(peerVideo); * attachMediaStream(peerVideo, stream); * } * }); * - * SkylinkDemo.on('peerLeft', function (peerId, peerInfo, isSelf) { - * if (isSelf) { - * document.getElementById('selfVideo').src = ''; - * } else { + * skylinkDemo.on("peerLeft", function (peerId, peerInfo, isSelf) { + * if (!isSelf) { * var peerVideo = document.getElementById(peerId); - * document.getElementById('peersVideo').removeChild(peerVideo); + * // do a check if peerVideo exists first + * if (peerVideo) { + * document.getElementById("peersVideo").removeChild(peerVideo); + * } else { + * console.error("Peer video for " + peerId + " is not found."); + * } * } * }); + * + * // init() should always be called first before other methods other than event methods like on() or off(). + * skylinkDemo.init("YOUR_APP_KEY_HERE", function (error, success) { + * if (success) { + * skylinkDemo.joinRoom("my_room", { + * userData: "My Username", + * audio: true, + * video: true + * }); + * } + * }); * @for Skylink * @since 0.5.0 */ @@ -53,33 +210,34 @@ function Skylink() { if (!(this instanceof Skylink)) { return new Skylink(); } +} - /** - * Version of Skylink - * @attribute VERSION - * @type String - * @readOnly - * @for Skylink - * @since 0.1.0 - */ - this.VERSION = '0.5.10'; +/** + * Contains the current version of Skylink Web SDK. + * @attribute VERSION + * @type String + * @readOnly + * @for Skylink + * @since 0.1.0 + */ +Skylink.prototype.VERSION = '0.6.15'; - /** - * Helper function to generate unique IDs for your application. - * @method generateUUID - * @return {String} The unique Id. - * @for Skylink - * @since 0.5.9 - */ - this.generateUUID = function () { - var d = new Date().getTime(); - var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = (d + Math.random()*16)%16 | 0; d = Math.floor(d/16); return (c=='x' ? r : (r&0x7|0x8)).toString(16); } - ); - return uuid; - }; -} -this.Skylink = Skylink; +/** + * Function that generates an UUID (Unique ID). + * @method generateUUID + * @return {String} Returns a generated UUID (Unique ID). + * @for Skylink + * @since 0.5.9 + */ +Skylink.prototype.generateUUID = function() { + var d = new Date().getTime(); + var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c == 'x' ? r : (r & 0x7 | 0x8)).toString(16); + }); + return uuid; +}; Skylink.prototype.DATA_CHANNEL_STATE = { CONNECTING: 'connecting', @@ -90,90 +248,174 @@ Skylink.prototype.DATA_CHANNEL_STATE = { }; /** - * The flag that indicates if DataChannel should be enabled. + * The list of Datachannel types. + * @attribute DATA_CHANNEL_TYPE + * @param {String} MESSAGING Value "messaging" + * The value of the Datachannel type that is used only for messaging in + * sendP2PMessage() method. + * However for Peers that do not support simultaneous data transfers, this Datachannel + * type will be used to do data transfers (1 at a time). + * Each Peer connections will only have one of this Datachannel type and the + * connection will only close when the Peer connection is closed (happens when + * peerConnectionState event triggers parameter payload state as + * CLOSED for Peer). + * @param {String} DATA Value "data" + * The value of the Datachannel type that is used only for a data transfer in + * sendURLData() method and + * sendBlobData() method. + * The connection will close after the data transfer has been completed or terminated (happens when + * dataTransferState event triggers parameter payload + * state as DOWNLOAD_COMPLETED, UPLOAD_COMPLETED, + * REJECTED, CANCEL or ERROR for Peer). + * @type JSON + * @readOnly + * @for Skylink + * @since 0.6.1 + */ +Skylink.prototype.DATA_CHANNEL_TYPE = { + MESSAGING: 'messaging', + DATA: 'data' +}; + +/** + * Stores the flag if Peers should have any Datachannel connections. * @attribute _enableDataChannel - * @type Boolean * @default true + * @type Boolean * @private - * @required - * @component DataChannel * @for Skylink * @since 0.3.0 */ Skylink.prototype._enableDataChannel = true; /** - * Stores the DataChannel received or created with peers. + * Stores the list of Peer Datachannel connections. * @attribute _dataChannels - * @param {Object} The DataChannel associated with peer. + * @param {JSON} (#peerId) The list of Datachannels associated with Peer ID. + * @param {RTCDataChannel} (#peerId).<#channelLabel> The Datachannel connection. + * The property name "main" is reserved for messaging Datachannel type. * @type JSON * @private - * @required - * @component DataChannel * @for Skylink * @since 0.2.0 */ Skylink.prototype._dataChannels = {}; /** - * Creates and binds events to a SCTP DataChannel. + * Function that starts a Datachannel connection with Peer. * @method _createDataChannel - * @param {String} peerId The peerId to tie the DataChannel to. - * @param {Object} [dataChannel] The datachannel object received. - * @trigger dataChannelState - * @return {Object} New DataChannel with events. * @private - * @component DataChannel * @for Skylink * @since 0.5.5 */ -Skylink.prototype._createDataChannel = function(peerId, dc) { +Skylink.prototype._createDataChannel = function(peerId, channelType, dc, customChannelName) { var self = this; - var channelName = (dc) ? dc.label : peerId; + + if (typeof dc === 'string') { + customChannelName = dc; + dc = null; + } + + if (!customChannelName) { + log.error([peerId, 'RTCDataChannel', null, 'Aborting of creating Datachannel as no ' + + 'channel name is provided for channel. Aborting of creating Datachannel'], { + channelType: channelType + }); + return; + } + + var channelName = (dc) ? dc.label : customChannelName; var pc = self._peerConnections[peerId]; - if (window.webrtcDetectedDCSupport !== 'SCTP' && - window.webrtcDetectedDCSupport !== 'plugin') { - log.warn([peerId, 'RTCDataChannel', channelName, 'SCTP not supported']); + var SctpSupported = + !(window.webrtcDetectedBrowser === 'chrome' && window.webrtcDetectedVersion < 30 || + window.webrtcDetectedBrowser === 'opera' && window.webrtcDetectedVersion < 20 ); + + if (!SctpSupported) { + log.warn([peerId, 'RTCDataChannel', channelName, 'SCTP not supported'], { + channelType: channelType + }); return; } var dcHasOpened = function () { - log.log([peerId, 'RTCDataChannel', channelName, 'Datachannel state ->'], 'open'); - log.log([peerId, 'RTCDataChannel', channelName, 'Binary type support ->'], dc.binaryType); - self._dataChannels[peerId] = dc; - self._trigger('dataChannelState', dc.readyState, peerId); + log.log([peerId, 'RTCDataChannel', channelName, 'Datachannel state ->'], { + readyState: 'open', + channelType: channelType + }); + + self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.OPEN, + peerId, null, channelName, channelType); }; if (!dc) { try { dc = pc.createDataChannel(channelName); - self._trigger('dataChannelState', dc.readyState, peerId); + if (dc.readyState === self.DATA_CHANNEL_STATE.OPEN) { + // the datachannel was not defined in array before it was triggered + // set a timeout to allow the dc objec to be returned before triggering "open" + setTimeout(dcHasOpened, 500); + } else { + self._trigger('dataChannelState', dc.readyState, peerId, null, + channelName, channelType); + + self._wait(function () { + log.log([peerId, 'RTCDataChannel', dc.label, 'Firing callback. ' + + 'Datachannel state has opened ->'], dc.readyState); + dcHasOpened(); + }, function () { + return dc.readyState === self.DATA_CHANNEL_STATE.OPEN; + }); + } - self._checkDataChannelReadyState(dc, dcHasOpened, self.DATA_CHANNEL_STATE.OPEN); + log.debug([peerId, 'RTCDataChannel', channelName, 'Datachannel RTC object is created'], { + readyState: dc.readyState, + channelType: channelType + }); } catch (error) { - log.error([peerId, 'RTCDataChannel', channelName, - 'Exception occurred in datachannel:'], error); - self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.ERROR, peerId, error); + log.error([peerId, 'RTCDataChannel', channelName, 'Exception occurred in datachannel:'], { + channelType: channelType, + error: error + }); + self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.ERROR, peerId, error, + channelName, channelType); return; } } else { if (dc.readyState === self.DATA_CHANNEL_STATE.OPEN) { - dcHasOpened(); + // the datachannel was not defined in array before it was triggered + // set a timeout to allow the dc objec to be returned before triggering "open" + setTimeout(dcHasOpened, 500); } else { dc.onopen = dcHasOpened; } } + log.log([peerId, 'RTCDataChannel', channelName, 'Binary type support ->'], { + binaryType: dc.binaryType, + readyState: dc.readyState, + channelType: channelType + }); + + dc.dcType = channelType; + dc.onerror = function(error) { - log.error([peerId, 'RTCDataChannel', channelName, 'Exception occurred in datachannel:'], error); - self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.ERROR, peerId, error); + log.error([peerId, 'RTCDataChannel', channelName, 'Exception occurred in datachannel:'], { + channelType: channelType, + readyState: dc.readyState, + error: error + }); + self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.ERROR, peerId, error, + channelName, channelType); }; dc.onclose = function() { - log.debug([peerId, 'RTCDataChannel', channelName, 'Datachannel state ->'], 'closed'); + log.debug([peerId, 'RTCDataChannel', channelName, 'Datachannel state ->'], { + readyState: 'closed', + channelType: channelType + }); dc.hasFiredClosed = true; @@ -183,95 +425,99 @@ Skylink.prototype._createDataChannel = function(peerId, dc) { pc = self._peerConnections[peerId]; // if closes because of firefox, reopen it again // if it is closed because of a restart, ignore - if (!!pc ? !pc.dataChannelClosed : false) { + + var checkIfChannelClosedDuringConn = !!pc ? !pc.dataChannelClosed : false; + + if (checkIfChannelClosedDuringConn && dc.dcType === self.DATA_CHANNEL_TYPE.MESSAGING) { log.debug([peerId, 'RTCDataChannel', channelName, 'Re-opening closed datachannel in ' + - 'on-going connection']); + 'on-going connection'], { + channelType: channelType, + readyState: dc.readyState, + isClosedDuringConnection: checkIfChannelClosedDuringConn + }); + + self._dataChannels[peerId].main = + self._createDataChannel(peerId, self.DATA_CHANNEL_TYPE.MESSAGING, null, peerId); - self._dataChannels[peerId] = self._createDataChannel(peerId); + log.debug([peerId, 'RTCDataChannel', channelName, 'Re-opened closed datachannel'], { + channelType: channelType, + readyState: dc.readyState, + isClosedDuringConnection: checkIfChannelClosedDuringConn + }); } else { - self._closeDataChannel(peerId); - self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.CLOSED, peerId); + self._closeDataChannel(peerId, channelName); + self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.CLOSED, peerId, null, + channelName, channelType); + + log.debug([peerId, 'RTCDataChannel', channelName, 'Datachannel has closed'], { + channelType: channelType, + readyState: dc.readyState, + isClosedDuringConnection: checkIfChannelClosedDuringConn + }); } }, 100); }; dc.onmessage = function(event) { - self._dataChannelProtocolHandler(event.data, peerId, channelName); + self._dataChannelProtocolHandler(event.data, peerId, channelName, channelType); }; + return dc; }; /** - * Checks and triggers provided callback when the current DataChannel readyState - * is the same as the readyState provided. - * @method _checkDataChannelReadyState - * @param {Object} dataChannel The DataChannel readyState to check on. - * @param {Function} callback The callback to be fired when DataChannel readyState - * matches the readyState provided. - * @param {String} readyState The DataChannel readystate to match. [Rel: DATA_CHANNEL_STATE] + * Function that sends data over the Datachannel connection. + * @method _sendDataChannelMessage * @private - * @component DataChannel * @for Skylink - * @since 0.5.5 + * @since 0.5.2 */ -Skylink.prototype._checkDataChannelReadyState = function(dc, callback, state) { +Skylink.prototype._sendDataChannelMessage = function(peerId, data, channelKey) { var self = this; - if (!self._enableDataChannel) { - log.debug('Datachannel not enabled. Returning callback'); - callback(); - return; - } - // fix for safari showing datachannel as function - if (typeof dc !== 'object' && (window.webrtcDetectedBrowser === 'safari' ? - typeof dc !== 'object' && typeof dc !== 'function' : true)) { - log.error('Datachannel not provided'); - return; - } - if (typeof callback !== 'function'){ - log.error('Callback not provided'); - return; - } - if (!state){ - log.error('State undefined'); - return; + var channelName; + + if (!channelKey || channelKey === peerId) { + channelKey = 'main'; } - self._wait(function () { - log.log([null, 'RTCDataChannel', dc.label, 'Firing callback. ' + - 'Datachannel state has met provided state ->'], state); - callback(); - }, function () { - return dc.readyState === state; - }); -}; -/** - * Sends a Message via the peer's DataChannel based on the peerId provided. - * @method _sendDataChannelMessage - * @param {String} peerId The peerId associated with the DataChannel to send from. - * @param {JSON} data The Message data to send. - * @trigger dataChannelState - * @private - * @component DataChannel - * @for Skylink - * @since 0.5.2 - */ -Skylink.prototype._sendDataChannelMessage = function(peerId, data) { - var dc = this._dataChannels[peerId]; + var dcList = self._dataChannels[peerId] || {}; + var dc = dcList[channelKey]; + if (!dc) { - log.error([peerId, 'RTCDataChannel', null, 'Datachannel connection ' + - 'to peer does not exist']); + log.error([peerId, 'RTCDataChannel', channelKey + '|' + channelName, + 'Datachannel connection to peer does not exist'], { + enabledState: self._enableDataChannel, + dcList: dcList, + dc: dc, + type: (data.type || 'DATA'), + data: data, + channelKey: channelKey + }); return; } else { + channelName = dc.label; + + log.debug([peerId, 'RTCDataChannel', channelKey + '|' + channelName, + 'Sending data using this channel key'], data); + if (dc.readyState === this.DATA_CHANNEL_STATE.OPEN) { var dataString = (typeof data === 'object') ? JSON.stringify(data) : data; - log.debug([peerId, 'RTCDataChannel', dc.label, 'Sending to peer ->'], - (data.type || 'DATA')); + log.debug([peerId, 'RTCDataChannel', channelKey + '|' + dc.label, + 'Sending to peer ->'], { + readyState: dc.readyState, + type: (data.type || 'DATA'), + data: data + }); dc.send(dataString); } else { - log.error([peerId, 'RTCDataChannel', dc.label, 'Datachannel is not opened'], - 'State: ' + dc.readyState); + log.error([peerId, 'RTCDataChannel', channelKey + '|' + dc.label, + 'Datachannel is not opened'], { + readyState: dc.readyState, + type: (data.type || 'DATA'), + data: data + }); this._trigger('dataChannelState', this.DATA_CHANNEL_STATE.ERROR, peerId, 'Datachannel is not ready.\nState is: ' + dc.readyState); } @@ -279,74 +525,98 @@ Skylink.prototype._sendDataChannelMessage = function(peerId, data) { }; /** - * Closes the peer's DataChannel based on the peerId provided. + * Function that stops the Datachannel connection and removes object references. * @method _closeDataChannel - * @param {String} peerId The peerId associated with the DataChannel to be closed. * @private - * @component DataChannel * @for Skylink * @since 0.1.0 */ -Skylink.prototype._closeDataChannel = function(peerId) { +Skylink.prototype._closeDataChannel = function(peerId, channelName) { var self = this; - var dc = self._dataChannels[peerId]; - if (dc) { - if (dc.readyState !== self.DATA_CHANNEL_STATE.CLOSED) { - dc.close(); - } else { - if (!dc.hasFiredClosed && window.webrtcDetectedBrowser === 'firefox') { - self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.CLOSED, peerId); + var dcList = self._dataChannels[peerId] || {}; + var dcKeysList = Object.keys(dcList); + + + if (channelName) { + dcKeysList = [channelName]; + } + + for (var i = 0; i < dcKeysList.length; i++) { + var channelKey = dcKeysList[i]; + var dc = dcList[channelKey]; + + if (dc) { + if (dc.readyState !== self.DATA_CHANNEL_STATE.CLOSED) { + log.log([peerId, 'RTCDataChannel', channelKey + '|' + dc.label, + 'Closing datachannel']); + dc.close(); + } else { + if (!dc.hasFiredClosed && window.webrtcDetectedBrowser === 'firefox') { + log.log([peerId, 'RTCDataChannel', channelKey + '|' + dc.label, + 'Closed Firefox datachannel']); + self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.CLOSED, peerId, + null, channelName, channelKey === 'main' ? self.DATA_CHANNEL_TYPE.MESSAGING : + self.DATA_CHANNEL_TYPE.DATA); + } } - } - delete self._dataChannels[peerId]; + delete self._dataChannels[peerId][channelKey]; - log.log([peerId, 'RTCDataChannel', dc.label, 'Sucessfully removed datachannel']); + log.log([peerId, 'RTCDataChannel', channelKey + '|' + dc.label, + 'Sucessfully removed datachannel']); + } else { + log.log([peerId, 'RTCDataChannel', channelKey + '|' + channelName, + 'Unable to close Datachannel as it does not exists'], { + dc: dc, + dcList: dcList + }); + } } }; +Skylink.prototype.DATA_TRANSFER_DATA_TYPE = { + BINARY_STRING: 'binaryString', + ARRAY_BUFFER: 'arrayBuffer', + BLOB: 'blob' +}; + +/** + * Stores the data chunk size for Blob transfers. + * @attribute _CHUNK_FILE_SIZE + * @type Number + * @private + * @readOnly + * @for Skylink + * @since 0.5.2 + */ Skylink.prototype._CHUNK_FILE_SIZE = 49152; /** - * The size of a chunk that DataTransfer should chunk a Blob into specifically for Firefox - * based browsers. - * - Tested: Sends 49152 kb | Receives 16384 kb. + * Stores the data chunk size for Blob transfers transferring from/to + * Firefox browsers due to limitation tested in the past in some PCs (linx predominatly). * @attribute _MOZ_CHUNK_FILE_SIZE * @type Number * @private - * @final - * @required - * @component DataProcess + * @readOnly * @for Skylink * @since 0.5.2 */ -Skylink.prototype._MOZ_CHUNK_FILE_SIZE = 16384; +Skylink.prototype._MOZ_CHUNK_FILE_SIZE = 12288; /** - * The list of DataTransfer native data types that would be transfered with. - * - Not Implemented: ARRAY_BUFFER, BLOB. - * @attribute DATA_TRANSFER_DATA_TYPE - * @type JSON - * @param {String} BINARY_STRING BinaryString data type. - * @param {String} ARRAY_BUFFER ArrayBuffer data type. - * @param {String} BLOB Blob data type. + * Stores the data chunk size for data URI string transfers. + * @attribute _CHUNK_DATAURL_SIZE + * @type Number + * @private * @readOnly - * @component DataProcess * @for Skylink - * @since 0.1.0 + * @since 0.5.2 */ -Skylink.prototype.DATA_TRANSFER_DATA_TYPE = { - BINARY_STRING: 'binaryString', - ARRAY_BUFFER: 'arrayBuffer', - BLOB: 'blob' -}; +Skylink.prototype._CHUNK_DATAURL_SIZE = 1212; /** - * Converts a Base64 encoded string to a Blob. - * - Not Implemented: Handling of URLEncoded DataURIs. - * @author devnull69@stackoverflow.com #6850276 + * Function that converts Base64 string into Blob object. + * This is referenced from devnull69@stackoverflow.com #6850276. * @method _base64ToBlob - * @param {String} dataURL Blob base64 dataurl. * @private - * @component DataProcess * @for Skylink * @since 0.1.0 */ @@ -363,25 +633,43 @@ Skylink.prototype._base64ToBlob = function(dataURL) { }; /** - * Chunks a Blob into Blob chunks based on a fixed size. + * Function that converts a Blob object into Base64 string. + * @method _blobToBase64 + * @private + * @for Skylink + * @since 0.1.0 + */ +Skylink.prototype._blobToBase64 = function(data, callback) { + var fileReader = new FileReader(); + fileReader.onload = function() { + // Load Blob as dataurl base64 string + var base64BinaryString = fileReader.result.split(',')[1]; + callback(base64BinaryString); + }; + fileReader.readAsDataURL(data); +}; + +/** + * Function that chunks Blob object based on the data chunk size provided. + * If provided Blob object size is lesser than or equals to the chunk size, it should return an array + * of length of 1. * @method _chunkBlobData - * @param {Blob} blob The Blob data to chunk. - * @param {Number} blobByteSize The original Blob data size. * @private - * @component DataProcess * @for Skylink * @since 0.5.2 */ -Skylink.prototype._chunkBlobData = function(blob, blobByteSize) { - var chunksArray = [], - startCount = 0, - endCount = 0; - if (blobByteSize > this._CHUNK_FILE_SIZE) { +Skylink.prototype._chunkBlobData = function(blob, chunkSize) { + var chunksArray = []; + var startCount = 0; + var endCount = 0; + var blobByteSize = blob.size; + + if (blobByteSize > chunkSize) { // File Size greater than Chunk size while ((blobByteSize - 1) > endCount) { - endCount = startCount + this._CHUNK_FILE_SIZE; + endCount = startCount + chunkSize; chunksArray.push(blob.slice(startCount, endCount)); - startCount += this._CHUNK_FILE_SIZE; + startCount += chunkSize; } if ((blobByteSize - (startCount + 1)) > 0) { chunksArray.push(blob.slice(startCount, blobByteSize - 1)); @@ -392,46 +680,72 @@ Skylink.prototype._chunkBlobData = function(blob, blobByteSize) { } return chunksArray; }; -Skylink.prototype.DT_PROTOCOL_VERSION = '0.1.0'; /** - * The DataTransfer protocol list. The data object is an - * indicator of the expected parameters to be given and received. - * @attribute _DC_PROTOCOL_TYPE - * @type JSON - * @param {String} WRQ Send to initiate a DataTransfer request. - * @param {String} ACK Send to acknowledge the DataTransfer request. - * @param {String} DATA Send as the raw Blob chunk data based on the ackN - * received. - * - Handle the logic based on parsing the data received as JSON. If it should fail, - * the expected data received should be a DATA request. - * @param {String} CANCEL Send to cancel or terminate a DataTransfer. - * @param {String} ERROR Sent when a timeout waiting for a DataTransfer response - * has reached its limit. - * @param {String} MESSAGE Sends a Message object. - * @final - * @private - * @for Skylink - * @component DataTransfer - * @since 0.5.2 + * Function that chunks large string into string chunks based on the data chunk size provided. + * If provided string length is lesser than or equals to the chunk size, it should return an array + * of length of 1. + * @method _chunkDataURL + * @private + * @for Skylink + * @since 0.6.1 */ -Skylink.prototype._DC_PROTOCOL_TYPE = { - WRQ: 'WRQ', - ACK: 'ACK', - ERROR: 'ERROR', - CANCEL: 'CANCEL', - MESSAGE: 'MESSAGE' +Skylink.prototype._chunkDataURL = function(dataURL, chunkSize) { + var outputStr = dataURL; //encodeURIComponent(dataURL); + var dataURLArray = []; + var startCount = 0; + var endCount = 0; + var dataByteSize = dataURL.size || dataURL.length; + + if (dataByteSize > chunkSize) { + // File Size greater than Chunk size + while ((dataByteSize - 1) > endCount) { + endCount = startCount + chunkSize; + dataURLArray.push(outputStr.slice(startCount, endCount)); + startCount += chunkSize; + } + if ((dataByteSize - (startCount + 1)) > 0) { + chunksArray.push(outputStr.slice(startCount, dataByteSize - 1)); + } + } else { + // File Size below Chunk size + dataURLArray.push(outputStr); + } + + return dataURLArray; +}; + +/** + * Function that assembles the data string chunks into a large string. + * @method _assembleDataURL + * @private + * @for Skylink + * @since 0.6.1 + */ +Skylink.prototype._assembleDataURL = function(dataURLArray) { + var outputStr = ''; + + for (var i = 0; i < dataURLArray.length; i++) { + try { + outputStr += dataURLArray[i]; + } catch (error) { + console.error('Malformed', i, dataURLArray[i]); + } + } + + return outputStr; }; +Skylink.prototype.DT_PROTOCOL_VERSION = '0.1.0'; /** - * The list of DataTransfer streamming types to indicate an upload stream - * or download stream. + * The list of data transfers directions. * @attribute DATA_TRANSFER_TYPE + * @param {String} UPLOAD Value "upload" + * The value of the data transfer direction when User is uploading data to Peer. + * @param {String} DOWNLOAD Value "download" + * The value of the data transfer direction when User is downloading data from Peer. * @type JSON - * @param {String} UPLOAD An upload stream. - * @param {String} DOWNLOAD A download stream. * @readOnly - * @component DataTransfer * @for Skylink * @since 0.1.0 */ @@ -441,21 +755,57 @@ Skylink.prototype.DATA_TRANSFER_TYPE = { }; /** - * The list of DataTransfer states that would be triggered. + * The list of data transfers session types. + * @attribute DATA_TRANSFER_SESSION_TYPE + * @param {String} BLOB Value "blob" + * The value of the session type for + * sendURLData() method data transfer. + * @param {String} DATA_URL Value "dataURL" + * The value of the session type for + * method_sendBlobData() method data transfer. + * @type JSON + * @readOnly + * @for Skylink + * @since 0.1.0 + */ +Skylink.prototype.DATA_TRANSFER_SESSION_TYPE = { + BLOB: 'blob', + DATA_URL: 'dataURL' +}; + +/** + * The list of data transfer states. * @attribute DATA_TRANSFER_STATE + * @param {String} UPLOAD_REQUEST Value "request" + * The value of the state when receiving an upload data transfer request from Peer to User. + * At this stage, the upload data transfer request from Peer may be accepted or rejected with the + * acceptDataTransfer() method. + * @param {String} UPLOAD_STARTED Value "uploadStarted" + * The value of the state when the data transfer request has been accepted + * and data transfer will start uploading data to Peer. + * At this stage, the data transfer may be terminated with the + * cancelDataTransfer() method. + * @param {String} DOWNLOAD_STARTED Value "downloadStarted" + * The value of the state when the data transfer request has been accepted + * and data transfer will start downloading data from Peer. + * At this stage, the data transfer may be terminated with the + * cancelDataTransfer() method. + * @param {String} REJECTED Value "rejected" + * The value of the state when upload data transfer request to Peer has been rejected and terminated. + * @param {String} UPLOADING Value "uploading" + * The value of the state when data transfer is uploading data to Peer. + * @param {String} DOWNLOADING Value "downloading" + * The value of the state when data transfer is downloading data from Peer. + * @param {String} UPLOAD_COMPLETED Value "uploadCompleted" + * The value of the state when data transfer has uploaded successfully to Peer. + * @param {String} DOWNLOAD_COMPLETED Value "downloadCompleted" + * The value of the state when data transfer has downloaded successfully from Peer. + * @param {String} CANCEL Value "cancel" + * The value of the state when data transfer has been terminated from / to Peer. + * @param {String} ERROR Value "error" + * The value of the state when data transfer has errors and has been terminated from / to Peer. * @type JSON - * @param {String} UPLOAD_REQUEST A DataTransfer request to start a transfer is received. - * @param {String} UPLOAD_STARTED The request has been accepted and upload is starting. - * @param {String} DOWNLOAD_STARTED The request has been accepted and download is starting. - * @param {String} UPLOADING An ongoing upload DataTransfer is occuring. - * @param {String} DOWNLOADING An ongoing download DataTransfer is occuring. - * @param {String} UPLOAD_COMPLETED The upload is completed. - * @param {String} DOWNLOAD_COMPLETED The download is completed. - * @param {String} REJECTED A DataTransfer request is rejected by a peer. - * @param {String} ERROR DataTransfer has waiting longer than timeout is specified. - * DataTransfer is aborted. * @readOnly - * @component DataTransfer * @for Skylink * @since 0.4.0 */ @@ -473,1074 +823,2423 @@ Skylink.prototype.DATA_TRANSFER_STATE = { }; /** - * Stores the list of DataTransfer uploading chunks. - * @attribute _uploadDataTransfers - * @type JSON + * Stores the fixed delimiter that concats the Datachannel label and actual transfer ID. + * @attribute _TRANSFER_DELIMITER + * @type String + * @readOnly * @private - * @required - * @component DataTransfer * @for Skylink - * @since 0.4.1 + * @since 0.5.10 */ -Skylink.prototype._uploadDataTransfers = {}; +Skylink.prototype._TRANSFER_DELIMITER = '_skylink__'; /** - * Stores the list of DataTransfer uploading sessions. - * @attribute _uploadDataSessions + * Stores the list of data transfer protocols. + * @attribute _DC_PROTOCOL_TYPE + * @param {String} WRQ The protocol to initiate data transfer. + * @param {String} ACK The protocol to request for data transfer chunk. + * Give -1 to reject the request at the beginning and 0 to accept + * the data transfer request. + * @param {String} CANCEL The protocol to terminate data transfer. + * @param {String} ERROR The protocol when data transfer has errors and has to be terminated. + * @param {String} MESSAGE The protocol that is used to send P2P messages. * @type JSON + * @readOnly * @private - * @required - * @component DataTransfer * @for Skylink - * @since 0.4.1 + * @since 0.5.2 */ -Skylink.prototype._uploadDataSessions = {}; +Skylink.prototype._DC_PROTOCOL_TYPE = { + WRQ: 'WRQ', + ACK: 'ACK', + ERROR: 'ERROR', + CANCEL: 'CANCEL', + MESSAGE: 'MESSAGE' +}; /** - * Stores the list of DataTransfer downloading chunks. - * @attribute _downloadDataTransfers - * @type JSON + * Stores the list of types of SDKs that do not support simultaneous data transfers. + * @attribute _INTEROP_MULTI_TRANSFERS + * @type Array + * @readOnly + * @private + * @for Skylink + * @since 0.6.1 + */ +Skylink.prototype._INTEROP_MULTI_TRANSFERS = ['Android', 'iOS']; + +/** + * Stores the list of uploading data transfers chunks to Peers. + * @attribute _uploadDataTransfers + * @param {Array} <#transferId> The uploading data transfer chunks. + * @type JSON * @private - * @required - * @component DataTransfer * @for Skylink * @since 0.4.1 */ -Skylink.prototype._downloadDataTransfers = {}; +Skylink.prototype._uploadDataTransfers = {}; /** - * Stores the list of DataTransfer downloading sessions. - * @attribute _downloadDataSessions + * Stores the list of uploading data transfer sessions to Peers. + * @attribute _uploadDataSessions + * @param {JSON} <#transferId> The uploading data transfer session. * @type JSON * @private - * @required - * @component DataTransfer * @for Skylink * @since 0.4.1 */ -Skylink.prototype._downloadDataSessions = {}; +Skylink.prototype._uploadDataSessions = {}; /** - * Stores all the setTimeout objects for each - * request received. - * @attribute _dataTransfersTimeout + * Stores the list of downloading data transfers chunks to Peers. + * @attribute _downloadDataTransfers + * @param {Array} <#transferId> The downloading data transfer chunks. * @type JSON * @private - * @required - * @component DataTransfer * @for Skylink * @since 0.4.1 */ -Skylink.prototype._dataTransfersTimeout = {}; +Skylink.prototype._downloadDataTransfers = {}; /** - * Sets a waiting timeout for the request received from the peer. Once - * timeout has expired, an error would be thrown. - * @method _setDataChannelTimeout - * @param {String} peerId The responding peerId of the peer to await for - * response during the DataTransfer. - * @param {Number} timeout The timeout to set in seconds. - * @param {Boolean} [isSender=false] The flag to indicate if the response - * received is from the sender or the receiver. + * Stores the list of downloading data transfer sessions to Peers. + * @attribute _downloadDataSessions + * @param {JSON} <#transferId> The downloading data transfer session. + * @type JSON * @private - * @component DataTransfer * @for Skylink - * @since 0.5.0 + * @since 0.4.1 */ -Skylink.prototype._setDataChannelTimeout = function(peerId, timeout, isSender) { - var self = this; - if (!self._dataTransfersTimeout[peerId]) { - self._dataTransfersTimeout[peerId] = []; - } - var type = (isSender) ? self.DATA_TRANSFER_TYPE.UPLOAD : - self.DATA_TRANSFER_TYPE.DOWNLOAD; - self._dataTransfersTimeout[peerId][type] = setTimeout(function() { - var name; - if (self._dataTransfersTimeout[peerId][type]) { - if (isSender) { - name = self._uploadDataSessions[peerId].name; - delete self._uploadDataTransfers[peerId]; - delete self._uploadDataSessions[peerId]; - } else { - name = self._downloadDataSessions[peerId].name; - delete self._downloadDataTransfers[peerId]; - delete self._downloadDataSessions[peerId]; - } - self._sendDataChannelMessage(peerId, { - type: self._DC_PROTOCOL_TYPE.ERROR, - sender: self._user.sid, - name: name, - content: 'Connection Timeout. Longer than ' + timeout + - ' seconds. Connection is abolished.', - isUploadError: isSender - }); - // TODO: Find a way to add channel name so it's more specific - log.error([peerId, 'RTCDataChannel', null, 'Failed transfering data:'], - 'Transfer ' + ((isSender) ? 'for': 'from') + ' ' + peerId + - ' failed. Connection timeout'); - self._clearDataChannelTimeout(peerId, isSender); - } - }, 1000 * timeout); -}; +Skylink.prototype._downloadDataSessions = {}; /** - * Clears the timeout set for the DataTransfer. - * @method _clearDataChannelTimeout - * @param {String} peerId The responding peerId of the peer to await for - * response during the DataTransfer. - * @param {Boolean} [isSender=false] The flag to indicate if the response - * received is from the sender or the receiver. + * Stores the list of data transfer "wait-for-response" timeouts. + * @attribute _dataTransfersTimeout + * @param {Object} <#transferId> The data transfer session "wait-for-response" timeout. + * @type JSON * @private - * @component DataTransfer * @for Skylink - * @since 0.5.0 + * @since 0.4.1 */ -Skylink.prototype._clearDataChannelTimeout = function(peerId, isSender) { - if (this._dataTransfersTimeout[peerId]) { - var type = (isSender) ? this.DATA_TRANSFER_TYPE.UPLOAD : - this.DATA_TRANSFER_TYPE.DOWNLOAD; - clearTimeout(this._dataTransfersTimeout[peerId][type]); - delete this._dataTransfersTimeout[peerId][type]; - } -}; +Skylink.prototype._dataTransfersTimeout = {}; /** - * Initiates a DataTransfer with the peer. - * @method _sendBlobDataToPeer - * @param {Blob} data The Blob data to send. - * @param {JSON} dataInfo The Blob data information. - * @param {String} dataInfo.transferId The transferId of the DataTransfer. - * @param {String} dataInfo.name The Blob data name. - * @param {Number} [dataInfo.timeout=60] The timeout set to await for response from peer. - * @param {Number} dataInfo.size The Blob data size. - * @param {Boolean} data.target The real peerId to send data to, in the case where MCU is enabled. - * @param {String} [targetPeerId] The peerId of the peer to start the DataTransfer. - * To start the DataTransfer to all peers, set as false. - * @param {Boolean} isPrivate The flag to indicate if the DataTransfer is broadcasted to other - * peers or sent to the peer privately. - * @private - * @component DataTransfer + * + * Note that Android and iOS SDKs do not support simultaneous data transfers. + * + * Function that starts an uploading data transfer from User to Peers. + * @method sendBlobData + * @param {Blob} data The Blob object. + * @param {Number} [timeout=60] The timeout to wait for response from Peer. + * @param {String|Array} [targetPeerId] The target Peer ID to start data transfer with. + * - When provided as an Array, it will start uploading data transfers with all connections + * with all the Peer IDs provided. + * - When not provided, it will start uploading data transfers with all the currently connected Peers in the Room. + * @param {Function} [callback] The callback function fired when request has completed. + * Function parameters signature is function (error, success) + * Function request completion is determined by the + * dataTransferState event triggering state parameter payload + * as UPLOAD_COMPLETED for all Peers targeted for request success. + * @param {JSON} callback.error The error result in request. + * Defined as null when there are no errors in request + * @param {String} callback.error.transferId + * Deprecation Warning! This property has been deprecated. + * Please use callback.error.transferInfo.transferId instead. + * The data transfer ID. + * Defined only for single targeted Peer data transfer. + * @param {String} callback.error.state + * Deprecation Warning! This property has been deprecated. + * Please use dataTransferState + * event instead. The data transfer state that resulted in error. + * Defined only for single targeted Peer data transfer. [Rel: Skylink.DATA_TRANSFER_STATE] + * @param {String} callback.error.peerId + * Deprecation Warning! This property has been deprecated. + * Please use callback.error.listOfPeers instead. + * The targeted Peer ID for data transfer. + * Defined only for single targeted Peer data transfer. + * @param {Boolean} callback.error.isPrivate + * Deprecation Warning! This property has been deprecated. + * Please use callback.error.transferInfo.isPrivate instead. + * The flag if data transfer is targeted or not, basing + * off the targetPeerId parameter being defined. + * Defined only for single targeted Peer data transfer. + * @param {Error|String} callback.error.error + * Deprecation Warning! This property has been deprecated. + * Please use callback.error.transferErrors instead. + * The error received that resulted in error. + * Defined only for single targeted Peer data transfer. + * @param {Array} callback.error.listOfPeers The list Peer IDs targeted for the data transfer. + * @param {JSON} callback.error.transferErrors The list of data transfer errors. + * @param {Error|String} callback.error.transferErrors.#peerId The data transfer error associated + * with the Peer ID defined in #peerId property. + * If #peerId value is "self", it means that it is the error when there + * are no Peer connections to start data transfer with. + * @param {JSON} callback.error.transferInfo The data transfer information. + * Object signature matches the transferInfo parameter payload received in the + * dataTransferState event. + * @param {JSON} callback.success The success result in request. + * Defined as null when there are errors in request + * @param {String} callback.success.transferId + * Deprecation Warning! This property has been deprecated. + * Please use callback.success.transferInfo.transferId instead. + * The data transfer ID. + * @param {String} callback.success.state + * Deprecation Warning! This property has been deprecated. + * Please use dataTransferState + * event instead. The data transfer state that resulted in error. + * Defined only for single targeted Peer data transfer. [Rel: Skylink.DATA_TRANSFER_STATE] + * @param {String} callback.success.peerId + * Deprecation Warning! This property has been deprecated. + * Please use callback.success.listOfPeers instead. + * The targeted Peer ID for data transfer. + * Defined only for single targeted Peer data transfer. + * @param {Boolean} callback.success.isPrivate + * Deprecation Warning! This property has been deprecated. + * Please use callback.success.transferInfo.isPrivate instead. + * The flag if data transfer is targeted or not, basing + * off the targetPeerId parameter being defined. + * Defined only for single targeted Peer data transfer. + * @param {Array} callback.success.listOfPeers The list Peer IDs targeted for the data transfer. + * @param {JSON} callback.success.transferInfo The data transfer information. + * Object signature matches the transferInfo parameter payload received in the + * dataTransferState event. + * @trigger + * Checks if should open a new Datachannel + * If Peer connection has closed: This can be checked with + * peerConnectionState event triggering parameter payload state as CLOSED + * for Peer. ABORT step and return error. + * If Peer supports simultaneous data transfer, open new Datachannel: + * dataChannelState event triggers parameter + * payload state as CONNECTING and channelType as DATA. + * If Datachannel has opened successfully: + * dataChannelState event triggers parameter payload + * state as OPEN and channelType as DATA. + * Else: If Peer connection Datachannel has not been opened This can be checked with + * dataChannelState event triggering parameter + * payload state as OPEN and channelType as + * MESSAGING for Peer. + * ABORT step and return error. + * Starts the data transfer to Peer + * For Peer only dataTransferState event + * triggers parameter payload state as UPLOAD_REQUEST. + * incomingDataRequest event triggers. + * Peer invokes acceptDataTransfer() method. + * If parameter accept value is true: + * User starts upload data transfer to Peer + * For User only dataTransferState event + * triggers parameter payload state as UPLOAD_STARTED. + * For Peer only dataTransferState event + * triggers parameter payload state as DOWNLOAD_STARTED. + * If Peer / User invokes cancelDataTransfer() method: + * dataTransferState event triggers parameter + * state as CANCEL.ABORT step and return error. + * If data transfer has errors: + * dataTransferState event triggers parameter + * state as ERROR.ABORT step and return error. + * If Datachannel has closed abruptly during data transfer: + * This can be checked with dataChannelState event + * triggering parameter payload state as CLOSED and channelType + * as DATA for Peer that supports simultaneous data transfer or MESSAGING + * for Peer that do not support it. + * dataTransferState event triggers parameter + * state as ERROR.ABORT step and return error. + * If data transfer is still progressing: + * For User only dataTransferState event + * triggers parameter payload state as UPLOADING. + * For Peer only dataTransferState event + * triggers parameter payload state as DOWNLOADING. + * If data transfer has completed + * For User only dataTransferState event + * triggers parameter payload state as UPLOAD_COMPLETED. + * For Peer only dataTransferState event + * triggers parameter payload state as DOWNLOAD_COMPLETED. + * incomingData event triggers. + * If parameter accept value is false: + * For User only dataTransferState event + * triggers parameter payload state as REJECTED. + * ABORT step and return error. + * @example + * <body> + * <input type="radio" name="timeout" onchange="setTransferTimeout(0)"> 1s timeout (Default) + * <input type="radio" name="timeout" onchange="setTransferTimeout(120)"> 2s timeout + * <input type="radio" name="timeout" onchange="setTransferTimeout(300)"> 5s timeout + * <hr> + * <input type="file" onchange="uploadFile(this.Files[0], this.getAttribute('data'))" data="peerId"> + * <input type="file" onchange="uploadFileGroup(this.Files[0], this.getAttribute('data').split(',')))" data="peerIdA,peerIdB"> + * <input type="file" onchange="uploadFileAll(this.Files[0])" data=""> + * <script> + * var transferTimeout = 0; + * + * function setTransferTimeout (timeout) { + * transferTimeout = timeout; + * } + * + * // Example 1: Upload data to a Peer + * function uploadFile (file, peerId) { + * var cb = function (error, success) { + * if (error) return; + * console.info("File has been transferred to '" + peerId + "' successfully"); + * }; + * if (transferTimeout > 0) { + * skylinkDemo.sendBlobData(file, peerId, transferTimeout, cb); + * } else { + * skylinkDemo.sendBlobData(file, peerId, cb); + * } + * } + * + * // Example 2: Upload data to a list of Peers + * function uploadFileGroup (file, peerIds) { + * var cb = function (error, success) { + * var listOfPeers = error ? error.listOfPeers : success.listOfPeers; + * var listOfPeersErrors = error ? error.transferErrors : {}; + * for (var i = 0; i < listOfPeers.length; i++) { + * if (listOfPeersErrors[listOfPeers[i]]) { + * console.error("Failed file transfer to '" + listOfPeers[i] + "'"); + * } else { + * console.info("File has been transferred to '" + listOfPeers[i] + "' successfully"); + * } + * } + * }; + * if (transferTimeout > 0) { + * skylinkDemo.sendBlobData(file, peerIds, transferTimeout, cb); + * } else { + * skylinkDemo.sendBlobData(file, peerIds, cb); + * } + * } + * + * // Example 2: Upload data to a list of Peers + * function uploadFileAll (file) { + * var cb = function (error, success) { + * var listOfPeers = error ? error.listOfPeers : success.listOfPeers; + * var listOfPeersErrors = error ? error.transferErrors : {}; + * for (var i = 0; i < listOfPeers.length; i++) { + * if (listOfPeersErrors[listOfPeers[i]]) { + * console.error("Failed file transfer to '" + listOfPeers[i] + "'"); + * } else { + * console.info("File has been transferred to '" + listOfPeers[i] + "' successfully"); + * } + * } + * }; + * if (transferTimeout > 0) { + * skylinkDemo.sendBlobData(file, transferTimeout, cb); + * } else { + * skylinkDemo.sendBlobData(file, cb); + * } + * } + * </script> + * </body> * @for Skylink * @since 0.5.5 */ -Skylink.prototype._sendBlobDataToPeer = function(data, dataInfo, targetPeerId, isPrivate) { - //If there is MCU then directs all messages to MCU - if(this._hasMCU && targetPeerId !== 'MCU'){ - //TODO It can be possible that even if we have a MCU in - //the room we are directly connected to the peer (hybrid/Threshold MCU) - if(isPrivate){ - this._sendBlobDataToPeer(data, dataInfo, 'MCU', isPrivate); - } - return; - } - var ongoingTransfer = null; - var binarySize = parseInt((dataInfo.size * (4 / 3)).toFixed(), 10); - var chunkSize = parseInt((this._CHUNK_FILE_SIZE * (4 / 3)).toFixed(), 10); +Skylink.prototype.sendBlobData = function(data, timeout, targetPeerId, callback) { + var listOfPeers = Object.keys(this._peerConnections); + var isPrivate = false; + var dataInfo = {}; + var transferId = this._user.sid + this.DATA_TRANSFER_TYPE.UPLOAD + + (((new Date()).toISOString().replace(/-/g, '').replace(/:/g, ''))).replace('.', ''); + // for error case + var errorMsg, errorPayload, i, peerId; // for jshint + var singleError = null; + var transferErrors = {}; + var stateError = null; + var singlePeerId = null; - if (window.webrtcDetectedBrowser === 'firefox' && - window.webrtcDetectedVersion < 30) { - chunkSize = this._MOZ_CHUNK_FILE_SIZE; + //Shift parameters + // timeout + if (typeof timeout === 'function') { + callback = timeout; + + } else if (typeof timeout === 'string') { + listOfPeers = [timeout]; + isPrivate = true; + + } else if (Array.isArray(timeout)) { + listOfPeers = timeout; + isPrivate = true; } - log.log([targetPeerId, null, null, 'Chunk size of data:'], chunkSize); - if (this._uploadDataSessions[targetPeerId]) { - ongoingTransfer = this.DATA_TRANSFER_TYPE.UPLOAD; - } else if (this._downloadDataSessions[targetPeerId]) { - ongoingTransfer = this.DATA_TRANSFER_TYPE.DOWNLOAD; + // targetPeerId + if (typeof targetPeerId === 'function'){ + callback = targetPeerId; + + // data, timeout, target [array], callback + } else if(Array.isArray(targetPeerId)) { + listOfPeers = targetPeerId; + isPrivate = true; + + // data, timeout, target [string], callback + } else if (typeof targetPeerId === 'string') { + listOfPeers = [targetPeerId]; + isPrivate = true; } - if (ongoingTransfer) { - log.error([targetPeerId, null, null, 'User have ongoing ' + ongoingTransfer + ' ' + - 'transfer session with peer. Unable to send data'], dataInfo); - // data transfer state - this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.ERROR, - dataInfo.transferId, targetPeerId, { - name: dataInfo.name, - message: dataInfo.content, - transferType: ongoingTransfer - },{ - message: 'Another transfer is ongoing. Unable to send data.', - transferType: ongoingTransfer - }); + //state: String, Deprecated. But for consistency purposes. Null if not a single peer + //error: Object, Deprecated. But for consistency purposes. Null if not a single peer + //transferId: String, + //peerId: String, Deprecated. But for consistency purposes. Null if not a single peer + //listOfPeers: Array, NEW!! + //isPrivate: isPrivate, NEW!! + //transferErrors: JSON, NEW!! - Array of errors + //transferInfo: JSON The same payload as dataTransferState transferInfo payload + + // check if it's blob data + if (!(typeof data === 'object' && data instanceof Blob)) { + errorMsg = 'Provided data is not a Blob data'; + + if (listOfPeers.length === 0) { + transferErrors.self = errorMsg; + + } else { + for (i = 0; i < listOfPeers.length; i++) { + peerId = listOfPeers[i]; + transferErrors[peerId] = errorMsg; + } + + // Deprecated but for consistency purposes. Null if not a single peer. + if (listOfPeers.length === 1 && isPrivate) { + stateError = self.DATA_TRANSFER_STATE.ERROR; + singleError = errorMsg; + singlePeerId = listOfPeers[0]; + } + } + + errorPayload = { + state: stateError, + error: singleError, + transferId: transferId, + peerId: singlePeerId, + listOfPeers: listOfPeers, + transferErrors: transferErrors, + transferInfo: dataInfo, + isPrivate: isPrivate + }; + + log.error(errorMsg, errorPayload); + + if (typeof callback === 'function'){ + log.log([null, 'RTCDataChannel', null, 'Error occurred. Firing callback ' + + 'with error -> '],errorPayload); + callback(errorPayload, null); + } return; } - this._uploadDataTransfers[targetPeerId] = this._chunkBlobData(data, dataInfo.size); - this._uploadDataSessions[targetPeerId] = { - name: dataInfo.name, - size: binarySize, - transferId: dataInfo.transferId, - timeout: dataInfo.timeout - }; - this._sendDataChannelMessage(targetPeerId, { - type: this._DC_PROTOCOL_TYPE.WRQ, - sender: this._user.sid, - agent: window.webrtcDetectedBrowser, - version: window.webrtcDetectedVersion, - name: dataInfo.name, - size: binarySize, - chunkSize: chunkSize, - timeout: dataInfo.timeout, - target: targetPeerId, - isPrivate: !!isPrivate - }); - this._setDataChannelTimeout(targetPeerId, dataInfo.timeout, true); -}; + // populate data + dataInfo.name = data.name || transferId; + dataInfo.size = data.size; + dataInfo.timeout = typeof timeout === 'number' ? timeout : 60; + dataInfo.transferId = transferId; + dataInfo.dataType = 'blob'; + dataInfo.isPrivate = isPrivate; -/** - * Handles the DataTransfer protocol stage and invokes the related handler function. - * @method _dataChannelProtocolHandler - * @param {String|Object} data The DataTransfer data received from the DataChannel. - * @param {String} senderPeerId The peerId of the sender. - * @param {String} channelName The DataChannel name related to the DataTransfer. - * @private - * @component DataTransfer - * @for Skylink - * @since 0.5.2 - */ -Skylink.prototype._dataChannelProtocolHandler = function(dataString, peerId, channelName) { - // PROTOCOL ESTABLISHMENT - if (typeof dataString === 'string') { - var data = {}; - try { - data = JSON.parse(dataString); - } catch (error) { - log.debug([peerId, 'RTCDataChannel', channelName, 'Received from peer ->'], 'DATA'); - this._DATAProtocolHandler(peerId, dataString, - this.DATA_TRANSFER_DATA_TYPE.BINARY_STRING, channelName); - return; + // check if datachannel is enabled first or not + if (!this._enableDataChannel) { + errorMsg = 'Unable to send any blob data. Datachannel is disabled'; + + if (listOfPeers.length === 0) { + transferErrors.self = errorMsg; + + } else { + for (i = 0; i < listOfPeers.length; i++) { + peerId = listOfPeers[i]; + transferErrors[peerId] = errorMsg; + } + + // Deprecated but for consistency purposes. Null if not a single peer. + if (listOfPeers.length === 1 && isPrivate) { + stateError = self.DATA_TRANSFER_STATE.ERROR; + singleError = errorMsg; + singlePeerId = listOfPeers[0]; + } } - log.debug([peerId, 'RTCDataChannel', channelName, 'Received from peer ->'], data.type); - switch (data.type) { - case this._DC_PROTOCOL_TYPE.WRQ: - this._WRQProtocolHandler(peerId, data, channelName); - break; - case this._DC_PROTOCOL_TYPE.ACK: - this._ACKProtocolHandler(peerId, data, channelName); - break; - case this._DC_PROTOCOL_TYPE.ERROR: - this._ERRORProtocolHandler(peerId, data, channelName); - break; - case this._DC_PROTOCOL_TYPE.CANCEL: - this._CANCELProtocolHandler(peerId, data, channelName); - break; - case this._DC_PROTOCOL_TYPE.MESSAGE: // Not considered a protocol actually? - this._MESSAGEProtocolHandler(peerId, data, channelName); - break; - default: - log.error([peerId, 'RTCDataChannel', channelName, 'Unsupported message ->'], data.type); + + errorPayload = { + state: stateError, + error: singleError, + transferId: transferId, + peerId: singlePeerId, + listOfPeers: listOfPeers, + transferErrors: transferErrors, + transferInfo: dataInfo, + isPrivate: isPrivate + }; + + log.error(errorMsg, errorPayload); + + if (typeof callback === 'function'){ + log.log([null, 'RTCDataChannel', null, 'Error occurred. Firing callback ' + + 'with error -> '], errorPayload); + callback(errorPayload, null); } + return; } + + this._startDataTransfer(data, dataInfo, listOfPeers, callback); }; /** - * Handles the WRQ request. - * @method _WRQProtocolHandler - * @param {String} senderPeerId The peerId of the sender. - * @param {JSON} data The WRQ data object. - * @param {String} data.agent The peer's browser agent. - * @param {Number} data.version The peer's browser version. - * @param {String} data.name The Blob name. - * @param {Number} data.size The Blob size. - * @param {Number} data.chunkSize The Blob chunk size expected to receive. - * @param {Number} data.timeout The timeout to wait for the packet response. - * @param {Boolean} data.isPrivate The flag to indicate if the data is - * sent as a private request. - * @param {String} data.sender The sender's peerId. - * @param {String} data.type Protocol step: "WRQ". - * @param {String} channelName The DataChannel name related to the DataTransfer. - * @trigger dataTransferState - * @private - * @component DataTransfer + * + * Deprecation Warning! This method has been deprecated, please use + * acceptDataTransfer() method instead. + * + * Function that accepts or rejects an upload data transfer request from Peer to User. + * Parameter signature follows + * acceptDataTransfer() method. + * @method respondBlobRequest + * @example + * // Example 1: Accept Peer upload data transfer request + * skylinkDemo.on("incomingDataRequest", function (transferId, peerId, transferInfo, isSelf) { + * if (!isSelf) { + * skylinkDemo.respondBlobRequest(peerId, transferId, true); + * } + * }); + * + * // Example 2: Reject Peer upload data transfer request + * skylinkDemo.on("incomingDataRequest", function (transferId, peerId, transferInfo, isSelf) { + * if (!isSelf) { + * skylinkDemo.respondBlobRequest(peerId, transferId, false); + * } + * }); + * @deprecated true + * @trigger Event sequence follows + * sendBlobData() method after acceptDataTransfer() method is invoked. * @for Skylink - * @since 0.5.2 + * @since 0.5.0 */ -Skylink.prototype._WRQProtocolHandler = function(peerId, data, channelName) { - var transferId = this._user.sid + this.DATA_TRANSFER_TYPE.DOWNLOAD + - (((new Date()).toISOString().replace(/-/g, '').replace(/:/g, ''))).replace('.', ''); - log.log([peerId, 'RTCDataChannel', [channelName, 'WRQ'], - 'Received file request from peer:'], data); - var name = data.name; - var binarySize = data.size; - var expectedSize = data.chunkSize; - var timeout = data.timeout; - this._downloadDataSessions[peerId] = { - transferId: transferId, - name: name, - size: binarySize, - ackN: 0, - receivedSize: 0, - chunkSize: expectedSize, - timeout: timeout - }; - this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.UPLOAD_REQUEST, - transferId, peerId, { - name: name, - size: binarySize, - senderPeerId: peerId - }); -}; - +Skylink.prototype.respondBlobRequest = /** - * Handles the ACK request. - * @method _ACKProtocolHandler - * @param {String} senderPeerId The peerId of the sender. - * @param {JSON} data The ACK data object. - * @param {String} data.ackN The current index of the Blob chunk array to - * receive from. - * - * 0 The request is accepted and sender sends the first packet. - * >0 The current packet number from Blob array being sent. - * -1 The request is rejected and sender cancels the transfer. - * - * @param {String} data.sender The sender's peerId. - * @param {String} data.type Protocol step: "ACK". - * @param {String} channelName The DataChannel name related to the DataTransfer. - * @trigger dataTransferState - * @private - * @component DataTransfer + * Function that accepts or rejects an upload data transfer request from Peer to User. + * @method acceptDataTransfer + * @param {String} peerId The Peer ID. + * @param {String} transferId The data transfer ID. + * @param {Boolean} [accept=false] The flag if User accepts the upload data transfer request from Peer. + * @example + * // Example 1: Accept Peer upload data transfer request + * skylinkDemo.on("incomingDataRequest", function (transferId, peerId, transferInfo, isSelf) { + * if (!isSelf) { + * skylinkDemo.acceptDataTransfer(peerId, transferId, true); + * } + * }); + * + * // Example 2: Reject Peer upload data transfer request + * skylinkDemo.on("incomingDataRequest", function (transferId, peerId, transferInfo, isSelf) { + * if (!isSelf) { + * skylinkDemo.acceptDataTransfer(peerId, transferId, false); + * } + * }); + * @trigger Event sequence follows + * sendBlobData() method after acceptDataTransfer() method is invoked. * @for Skylink - * @since 0.5.2 + * @since 0.6.1 */ -Skylink.prototype._ACKProtocolHandler = function(peerId, data, channelName) { - var self = this; - var ackN = data.ackN; - //peerId = (peerId === 'MCU') ? data.sender : peerId; +Skylink.prototype.acceptDataTransfer = function (peerId, transferId, accept) { + if (typeof transferId !== 'string' && typeof peerId !== 'string') { + log.error([peerId, 'RTCDataChannel', null, 'Aborting accept data transfer as ' + + 'transfer ID and peer ID is not provided'], { + accept: accept, + peerId: peerId, + transferId: transferId + }); + return; + } + + if (transferId.indexOf(this._TRANSFER_DELIMITER) === -1) { + log.error([peerId, 'RTCDataChannel', null, 'Aborting accept data transfer as ' + + 'invalid transfer ID is provided'], { + accept: accept, + transferId: transferId + }); + return; + } + var channelName = transferId.split(this._TRANSFER_DELIMITER)[0]; - var chunksLength = self._uploadDataTransfers[peerId].length; - var uploadedDetails = self._uploadDataSessions[peerId]; - var transferId = uploadedDetails.transferId; - var timeout = uploadedDetails.timeout; + if (accept) { - self._clearDataChannelTimeout(peerId, true); - log.log([peerId, 'RTCDataChannel', [channelName, 'ACK'], 'ACK stage ->'], - ackN + ' / ' + chunksLength); + log.info([peerId, 'RTCDataChannel', channelName, 'User accepted peer\'s request'], { + accept: accept, + transferId: transferId + }); - if (ackN > -1) { - // Still uploading - if (ackN < chunksLength) { - var fileReader = new FileReader(); - fileReader.onload = function() { - // Load Blob as dataurl base64 string - var base64BinaryString = fileReader.result.split(',')[1]; - self._sendDataChannelMessage(peerId, base64BinaryString); - self._setDataChannelTimeout(peerId, timeout, true); - self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.UPLOADING, - transferId, peerId, { - percentage: (((ackN + 1) / chunksLength) * 100).toFixed() - }); - }; - fileReader.readAsDataURL(self._uploadDataTransfers[peerId][ackN]); - } else if (ackN === chunksLength) { - log.log([peerId, 'RTCDataChannel', [channelName, 'ACK'], 'Upload completed']); - self._trigger('dataTransferState', - self.DATA_TRANSFER_STATE.UPLOAD_COMPLETED, transferId, peerId, { - name: uploadedDetails.name + if (!this._peerInformations[peerId] && !this._peerInformations[peerId].agent) { + log.error([peerId, 'RTCDataChannel', channelName, 'Aborting accept data transfer as ' + + 'Peer informations for peer is missing'], { + accept: accept, + transferId: transferId }); - delete self._uploadDataTransfers[peerId]; - delete self._uploadDataSessions[peerId]; + return; } + + this._downloadDataTransfers[channelName] = []; + + var data = this._downloadDataSessions[channelName]; + this._sendDataChannelMessage(peerId, { + type: this._DC_PROTOCOL_TYPE.ACK, + sender: this._user.sid, + ackN: 0, + agent: window.webrtcDetectedBrowser + }, channelName); + this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.DOWNLOAD_STARTED, + data.transferId, peerId, { + name: data.name, + size: data.size, + data: null, + dataType: data.dataType, + percentage: 0, + senderPeerId: peerId, + timeout: data.timeout, + isPrivate: data.isPrivate + }); } else { - self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.REJECTED, - transferId, peerId, { - name: self._uploadDataSessions[peerId].name, - size: self._uploadDataSessions[peerId].size - }); - delete self._uploadDataTransfers[peerId]; - delete self._uploadDataSessions[peerId]; + log.info([peerId, 'RTCDataChannel', channelName, 'User rejected peer\'s request'], { + accept: accept, + transferId: transferId + }); + this._sendDataChannelMessage(peerId, { + type: this._DC_PROTOCOL_TYPE.ACK, + sender: this._user.sid, + ackN: -1 + }, channelName); + delete this._downloadDataSessions[channelName]; + delete this._downloadDataTransfers[channelName]; } }; /** - * Handles the MESSAGE request. - * @method _MESSAGEProtocolHandler - * @param {String} senderPeerId The peerId of the sender. - * @param {JSON} data The ACK data object. - * @param {String} data.target The peerId of the peer to send the Message to. - * @param {String|JSON} data.data The Message object to send. - * @param {String} data.sender The sender's peerId. - * @param {String} data.type Protocol step: "MESSAGE". - * @param {String} channelName The DataChannel name related to the DataTransfer. - * @trigger incomingMessage - * @private - * @component DataTransfer + * + * Deprecation Warning! This method has been deprecated, please use + * method_cancelDataTransfer() method instead. + * + * Function that terminates a currently uploading / downloading data transfer from / to Peer. + * Parameter signature follows + * cancelDataTransfer() method. + * @method cancelBlobTransfer + * @trigger Event sequence follows + * @example + * // Example 1: Cancel Peer data transfer + * var transferSessions = {}; + * + * skylinkDemo.on("dataTransferState", function (state, transferId, peerId) { + * if ([skylinkDemo.DATA_TRANSFER_STATE.DOWNLOAD_STARTED, + * skylinkDemo.DATA_TRANSFER_STATE.UPLOAD_STARTED].indexOf(state) > -1) { + * if (!Array.isArray(transferSessions[transferId])) { + * transferSessions[transferId] = []; + * } + * transferSessions[transferId].push(peerId); + * } else { + * transferSessions[transferId].splice(transferSessions[transferId].indexOf(peerId), 1); + * } + * }); + * + * function cancelTransfer (peerId, transferId) { + * skylinkDemo.cancelBlobTransfer(peerId, transferId); + * } + * @trigger Event sequence follows + * sendBlobData() method after cancelDataTransfer() method is invoked. * @for Skylink - * @since 0.5.2 + * @deprecated true + * @for Skylink + * @since 0.5.7 */ -Skylink.prototype._MESSAGEProtocolHandler = function(peerId, data, channelName) { - var targetMid = data.sender; - log.log([channelName, 'RTCDataChannel', [targetMid, 'MESSAGE'], - 'Received P2P message from peer:'], data); - this._trigger('incomingMessage', { - content: data.data, - isPrivate: data.isPrivate, - isDataChannel: true, - targetPeerId: this._user.sid, - senderPeerId: targetMid - }, targetMid, this._peerInformations[targetMid], false); -}; - -/** - * Handles the ERROR request. - * @method _ERRORProtocolHandler - * @param {String} senderPeerId The peerId of the sender. - * @param {JSON} data The ERROR data object. - * @param {String} data.name The Blob data name. - * @param {String} data.content The error message. - * @param {Boolean} [data.isUploadError=false] The flag to indicate if the - * exception is thrown from the sender or receiving peer. - * @param {String} data.sender The sender's peerId. - * @param {String} data.type Protocol step: "ERROR". - * @param {String} channelName The DataChannel name related to the DataTransfer. - * @trigger dataTransferState - * @private - * @for Skylink - * @since 0.5.2 - */ -Skylink.prototype._ERRORProtocolHandler = function(peerId, data, channelName) { - var isUploader = data.isUploadError; - var transferId = (isUploader) ? this._uploadDataSessions[peerId].transferId : - this._downloadDataSessions[peerId].transferId; - log.error([peerId, 'RTCDataChannel', [channelName, 'ERROR'], - 'Received an error from peer:'], data); - this._clearDataChannelTimeout(peerId, isUploader); - this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.ERROR, - transferId, peerId, {}, { - name: data.name, - message: data.content, - transferType: ((isUploader) ? this.DATA_TRANSFER_TYPE.UPLOAD : - this.DATA_TRANSFER_TYPE.DOWNLOAD) - }); -}; - +Skylink.prototype.cancelBlobTransfer = /** - * Handles the CANCEL request. - * @method _CANCELProtocolHandler - * @param {String} senderPeerId The peerId of the sender. - * @param {JSON} data The CANCEL data object. - * @param {String} data.name The Blob data name. - * @param {String} data.content The reason for termination. - * @param {String} data.sender The sender's peerId. - * @param {String} data.type Protocol step: "CANCEL". - * @param {String} channelName The DataChannel name related to the DataTransfer. - * @trigger dataTransferState - * @private - * @component DataTransfer + * Function that terminates a currently uploading / downloading data transfer from / to Peer. + * @method cancelDataTransfer + * @param {String} peerId The Peer ID. + * @param {String} transferId The data transfer ID. + * @example + * // Example 1: Cancel Peer data transfer + * var transferSessions = {}; + * + * skylinkDemo.on("dataTransferState", function (state, transferId, peerId) { + * if ([skylinkDemo.DATA_TRANSFER_STATE.DOWNLOAD_STARTED, + * skylinkDemo.DATA_TRANSFER_STATE.UPLOAD_STARTED].indexOf(state) > -1) { + * if (!Array.isArray(transferSessions[transferId])) { + * transferSessions[transferId] = []; + * } + * transferSessions[transferId].push(peerId); + * } else { + * transferSessions[transferId].splice(transferSessions[transferId].indexOf(peerId), 1); + * } + * }); + * + * function cancelTransfer (peerId, transferId) { + * skylinkDemo.cancelDataTransfer(peerId, transferId); + * } + * @trigger Event sequence follows + * sendBlobData() method after cancelDataTransfer() method is invoked. * @for Skylink - * @since 0.5.0 + * @since 0.6.1 */ -Skylink.prototype._CANCELProtocolHandler = function(peerId, data, channelName) { - var isUpload = !!this._uploadDataSessions[peerId]; - var isDownload = !!this._downloadDataSessions[peerId]; +Skylink.prototype.cancelDataTransfer = function (peerId, transferId) { + var data; - var transferId = (isUpload) ? this._uploadDataSessions[peerId].transferId : - this._downloadDataSessions[peerId].transferId; + // targetPeerId + '-' + transferId + var channelName = peerId + '-' + transferId; - log.log([peerId, 'RTCDataChannel', [channelName, 'CANCEL'], - 'Received file transfer cancel request:'], data); + if (transferId.indexOf(this._TRANSFER_DELIMITER) > 0) { + channelName = transferId.split(this._TRANSFER_DELIMITER)[0]; + } else { - this._clearDataChannelTimeout(peerId, isUploader); + var peerAgent = (this._peerInformations[peerId] || {}).agent; - this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.CANCEL, - transferId, peerId, {}, { - name: data.name, - content: data.content, - senderPeerId: data.sender, - transferType: ((isUpload) ? this.DATA_TRANSFER_TYPE.UPLOAD : - this.DATA_TRANSFER_TYPE.DOWNLOAD) - }); + if (!peerAgent && !peerAgent.name) { + log.error([peerId, 'RTCDataChannel', null, 'Cancel transfer to peer ' + + 'failed as peer agent information for peer does not exists'], transferId); + return; + } - try { - if (isUpload) { - delete this._uploadDataSessions[peerId]; - delete this._uploadDataTransfers[peerId]; - } else { - delete this._downloadDataSessions[peerId]; - delete this._downloadDataTransfers[peerId]; + if (self._INTEROP_MULTI_TRANSFERS.indexOf(peerAgent.name) > -1) { + channelName = peerId; } - this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.CANCEL, transferId, peerId, { + } + + if (this._uploadDataSessions[channelName]) { + data = this._uploadDataSessions[channelName]; + + delete this._uploadDataSessions[channelName]; + delete this._uploadDataTransfers[channelName]; + + // send message + this._sendDataChannelMessage(peerId, { + type: this._DC_PROTOCOL_TYPE.CANCEL, + sender: this._user.sid, name: data.name, - content: data.content, - senderPeerId: data.sender, - transferType: ((isUpload) ? this.DATA_TRANSFER_TYPE.UPLOAD : - this.DATA_TRANSFER_TYPE.DOWNLOAD) - }); - } catch (error) { - this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.ERROR, {}, { - message: 'Failed cancelling data request from peer', - transferType: ((isUpload) ? this.DATA_TRANSFER_TYPE.UPLOAD : - this.DATA_TRANSFER_TYPE.DOWNLOAD) - }); + content: 'Peer cancelled upload transfer' + }, channelName); + + log.debug([peerId, 'RTCDataChannel', channelName, + 'Cancelling upload data transfers'], transferId); + + } else if (this._downloadDataSessions[channelName]) { + data = this._downloadDataSessions[channelName]; + + delete this._downloadDataSessions[channelName]; + delete this._downloadDataTransfers[channelName]; + + // send message + this._sendDataChannelMessage(peerId, { + type: this._DC_PROTOCOL_TYPE.CANCEL, + sender: this._user.sid, + name: data.name, + content: 'Peer cancelled download transfer' + }, channelName); + + log.debug([peerId, 'RTCDataChannel', channelName, + 'Cancelling download data transfers'], transferId); + + } else { + log.error([peerId, 'RTCDataChannel', null, 'Cancel transfer to peer ' + + 'failed as transfer session with peer does not exists'], transferId); + return; } + + this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.CANCEL, + data.transferId, peerId, { + name: data.name, + size: data.size, + percentage: data.percentage, + data: null, + dataType: data.dataType, + senderPeerId: data.senderPeerId, + timeout: data.timeout, + isPrivate: data.isPrivate + }); }; /** - * Handles the DATA request. - * @method _DATAProtocolHandler - * @param {String} senderPeerId The peerId of the sender. - * @param {ArrayBuffer|Blob|String} dataString The data received. - * [Rel: Skylink._DC_PROTOCOL_TYPE.DATA.data] - * @param {String} dataType The data type received from datachannel. - * [Rel: Skylink.DATA_TRANSFER_DATA_TYPE] - * @param {String} channelName The DataChannel name related to the DataTransfer. - * @trigger dataTransferState - * @private - * @component DataTransfer + * Function that sends a message to Peers via the Datachannel connection. + * Consider using sendURLData() method if you are + * sending large strings to Peers. + * @method sendP2PMessage + * @param {String|JSON} message The message. + * @param {String|Array} [targetPeerId] The target Peer ID to send message to. + * - When provided as an Array, it will send the message to only Peers which IDs are in the list. + * - When not provided, it will broadcast the message to all connected Peers in the Room. + * @trigger + * Sends P2P message to all targeted Peers. + * If Peer connection Datachannel has not been opened: This can be checked with + * dataChannelState event + * triggering parameter payload state as OPEN and + * channelType as MESSAGING for Peer. + * ABORT step and return error. + * incomingMessage event triggers + * parameter payload message.isDataChannel value as true and + * isSelf value as true. + * @example + * // Example 1: Broadcasting to all Peers + * skylinkDemo.on("dataChannelState", function (state, peerId, error, channelName, channelType) { + * if (state === skylinkDemo.DATA_CHANNEL_STATE.OPEN && + * channelType === skylinkDemo.DATA_CHANNEL_TYPE.MESSAGING) { + * skylinkDemo.sendP2PMessage("Hi all!"); + * } + * }); + * + * // Example 2: Sending to specific Peers + * var peersInExclusiveParty = []; + * + * skylinkDemo.on("peerJoined", function (peerId, peerInfo, isSelf) { + * if (isSelf) return; + * if (peerInfo.userData.exclusive) { + * peersInExclusiveParty[peerId] = false; + * } + * }); + * + * skylinkDemo.on("dataChannelState", function (state, peerId, error, channelName, channelType) { + * if (state === skylinkDemo.DATA_CHANNEL_STATE.OPEN && + * channelType === skylinkDemo.DATA_CHANNEL_TYPE.MESSAGING) { + * peersInExclusiveParty[peerId] = true; + * } + * }); + * + * function updateExclusivePartyStatus (message) { + * var readyToSend = []; + * for (var p in peersInExclusiveParty) { + * if (peersInExclusiveParty.hasOwnProperty(p)) { + * readyToSend.push(p); + * } + * } + * skylinkDemo.sendP2PMessage(message, readyToSend); + * } * @for Skylink * @since 0.5.5 */ -Skylink.prototype._DATAProtocolHandler = function(peerId, dataString, dataType, channelName) { - var chunk, error = ''; - var transferStatus = this._downloadDataSessions[peerId]; - log.log([peerId, 'RTCDataChannel', [channelName, 'DATA'], - 'Received data chunk from peer. Data type:'], dataType); +Skylink.prototype.sendP2PMessage = function(message, targetPeerId) { + var self = this; - if (!transferStatus) { - log.log([peerId, 'RTCDataChannel', [channelName, 'DATA'], - 'Ignoring data received as download data session is empty']); + // check if datachannel is enabled first or not + if (!self._enableDataChannel) { + log.warn('Unable to send any P2P message. Datachannel is disabled'); return; } - var transferId = transferStatus.transferId; + var listOfPeers = Object.keys(self._dataChannels); + var isPrivate = false; - this._clearDataChannelTimeout(peerId, false); + //targetPeerId is defined -> private message + if (Array.isArray(targetPeerId)) { + listOfPeers = targetPeerId; + isPrivate = true; - if (dataType === this.DATA_TRANSFER_DATA_TYPE.BINARY_STRING) { - chunk = this._base64ToBlob(dataString); - } else if (dataType === this.DATA_TRANSFER_DATA_TYPE.ARRAY_BUFFER) { - chunk = new Blob(dataString); - } else if (dataType === this.DATA_TRANSFER_DATA_TYPE.BLOB) { - chunk = dataString; - } else { - error = 'Unhandled data exception: ' + dataType; - log.error([peerId, 'RTCDataChannel', [channelName, 'DATA'], - 'Failed downloading data packets:'], error); - this._trigger('dataTransferState', - this.DATA_TRANSFER_STATE.ERROR, transferId, peerId, {}, { - message: error, - transferType: this.DATA_TRANSFER_TYPE.DOWNLOAD - }); - return; + } else if (typeof targetPeerId === 'string') { + listOfPeers = [targetPeerId]; + isPrivate = true; } - var receivedSize = (chunk.size * (4 / 3)); - log.log([peerId, 'RTCDataChannel', [channelName, 'DATA'], - 'Received data chunk size:'], receivedSize); - log.log([peerId, 'RTCDataChannel', [channelName, 'DATA'], - 'Expected data chunk size:'], transferStatus.chunkSize); - - if (transferStatus.chunkSize >= receivedSize) { - this._downloadDataTransfers[peerId].push(chunk); - transferStatus.ackN += 1; - transferStatus.receivedSize += receivedSize; - var totalReceivedSize = transferStatus.receivedSize; - var percentage = ((totalReceivedSize / transferStatus.size) * 100).toFixed(); - this._sendDataChannelMessage(peerId, { - type: this._DC_PROTOCOL_TYPE.ACK, - sender: this._user.sid, - ackN: transferStatus.ackN - }); - if (transferStatus.chunkSize === receivedSize) { - log.log([peerId, 'RTCDataChannel', [channelName, 'DATA'], - 'Transfer in progress ACK n:'],transferStatus.ackN); - this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.DOWNLOADING, - transferId, peerId, { - percentage: percentage + // sending public message to MCU to relay. MCU case only + if (self._hasMCU) { + if (isPrivate) { + log.log(['MCU', null, null, 'Relaying private P2P message to peers'], listOfPeers); + self._sendDataChannelMessage('MCU', { + type: self._DC_PROTOCOL_TYPE.MESSAGE, + isPrivate: isPrivate, + sender: self._user.sid, + target: listOfPeers, + data: message }); - this._setDataChannelTimeout(peerId, transferStatus.timeout, false); - this._downloadDataTransfers[peerId].info = transferStatus; } else { - log.log([peerId, 'RTCDataChannel', [channelName, 'DATA'], - 'Download complete']); - var blob = new Blob(this._downloadDataTransfers[peerId]); - this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.DOWNLOAD_COMPLETED, - transferId, peerId, { - data: blob + log.log(['MCU', null, null, 'Relaying P2P message to peers']); + + self._sendDataChannelMessage('MCU', { + type: self._DC_PROTOCOL_TYPE.MESSAGE, + isPrivate: isPrivate, + sender: self._user.sid, + target: 'MCU', + data: message }); - delete this._downloadDataTransfers[peerId]; - delete this._downloadDataSessions[peerId]; } } else { - error = 'Packet not match - [Received]' + receivedSize + - ' / [Expected]' + transferStatus.chunkSize; - this._trigger('dataTransferState', - this.DATA_TRANSFER_STATE.ERROR, transferId, peerId, {}, { - message: error, - transferType: this.DATA_TRANSFER_TYPE.DOWNLOAD - }); - log.error([peerId, 'RTCDataChannel', [channelName, 'DATA'], - 'Failed downloading data packets:'], error); + for (var i = 0; i < listOfPeers.length; i++) { + var peerId = listOfPeers[i]; + var useChannel = (self._hasMCU) ? 'MCU' : peerId; + + // Ignore MCU peer + if (peerId === 'MCU') { + continue; + } + + log.log([peerId, null, useChannel, 'Sending P2P message to peer']); + + self._sendDataChannelMessage(useChannel, { + type: self._DC_PROTOCOL_TYPE.MESSAGE, + isPrivate: isPrivate, + sender: self._user.sid, + target: peerId, + data: message + }); + } } + + self._trigger('incomingMessage', { + content: message, + isPrivate: isPrivate, + targetPeerId: targetPeerId || null, + isDataChannel: true, + senderPeerId: self._user.sid + }, self._user.sid, self.getPeerInfo(), true); }; /** - * Starts a DataTransfer request to the peers based on the peerIds provided. - * Peers have the option to accept or reject the receiving data. - * DataTransfers are encrypted. - * @method sendBlobData - * @param {Object} data The Blob data to be sent over. - * @param {JSON} dataInfo Information required about the data transferred - * @param {String} dataInfo.name The request name (name of the file for example). - * @param {Number} [dataInfo.timeout=60] The time (in seconds) before the transfer - * request is cancelled if not answered. - * @param {Number} dataInfo.size The Blob data size (in bytes). - * @param {String} [targetPeerId] The peerId of the peer targeted to receive data. - * To send to all peers, leave this option blank. - * @param {Function} [callback] The callback fired after data was uploaded. - * @param {Object} [callback.error] The error received in the callback. - * @param {Object} [callback.success] The result received in the callback. + * + * Currently, the Android and iOS SDKs do not support this type of data transfer. + * + * Function that starts an uploading string data transfer from User to Peers. + * @method sendURLData + * @param {String} data The data string to transfer to Peer. + * Parameter signature follows + * sendBlobData() method except data parameter. + * @trigger Event sequence follows + * sendBlobData() method. * @example + * <body> + * <input type="radio" name="timeout" onchange="setTransferTimeout(0)"> 1s timeout (Default) + * <input type="radio" name="timeout" onchange="setTransferTimeout(120)"> 2s timeout + * <input type="radio" name="timeout" onchange="setTransferTimeout(300)"> 5s timeout + * <hr> + * <input type="file" onchange="showImage(this.Files[0], this.getAttribute('data'))" data="peerId"> + * <input type="file" onchange="showImageGroup(this.Files[0], this.getAttribute('data').split(',')))" data="peerIdA,peerIdB"> + * <input type="file" onchange="showImageAll(this.Files[0])" data=""> + * <image id="target-1" src=""> + * <image id="target-2" src=""> + * <image id="target-3" src=""> + * <script> + * var transferTimeout = 0; * - * // Example 1: Send file to all peers connected - * SkylinkDemo.sendBlobData(file, 67); + * function setTransferTimeout (timeout) { + * transferTimeout = timeout; + * } * - * // Example 2: Send file to individual peer - * SkylinkDemo.sendBlobData(blob, 87, targetPeerId); + * function retrieveImageDataURL(file, cb) { + * var fr = new FileReader(); + * fr.onload = function () { + * cb(fr.result); + * }; + * fr.readAsDataURL(files[0]); + * } * - * // Example 3: Send file with callback - * SkylinkDemo.sendBlobData(data,{ - * name: data.name, - * size: data.size - * },function(error, success){ - * if (error){ - * console.log('Error happened. Can not send file')); - * } - * else{ - * console.log('Successfully uploaded file'); - * } - * }); + * // Example 1: Send image data URL to a Peer + * function showImage (file, peerId) { + * var cb = function (error, success) { + * if (error) return; + * console.info("Image has been transferred to '" + peerId + "' successfully"); + * }; + * retrieveImageDataURL(file, function (str) { + * if (transferTimeout > 0) { + * skylinkDemo.sendURLData(str, peerId, transferTimeout, cb); + * } else { + * skylinkDemo.sendURLData(str, peerId, cb); + * } + * document.getElementById("target-1").src = str; + * }); + * } * - * @trigger dataTransferState - * @since 0.5.5 - * @component DataTransfer + * // Example 2: Send image data URL to a list of Peers + * function showImageGroup (file, peerIds) { + * var cb = function (error, success) { + * var listOfPeers = error ? error.listOfPeers : success.listOfPeers; + * var listOfPeersErrors = error ? error.transferErrors : {}; + * for (var i = 0; i < listOfPeers.length; i++) { + * if (listOfPeersErrors[listOfPeers[i]]) { + * console.error("Failed image transfer to '" + listOfPeers[i] + "'"); + * } else { + * console.info("Image has been transferred to '" + listOfPeers[i] + "' successfully"); + * } + * } + * }; + * retrieveImageDataURL(file, function (str) { + * if (transferTimeout > 0) { + * skylinkDemo.sendURLData(str, peerIds, transferTimeout, cb); + * } else { + * skylinkDemo.sendURLData(str, peerIds, cb); + * } + * document.getElementById("target-2").src = str; + * }); + * } + * + * // Example 2: Send image data URL to a list of Peers + * function uploadFileAll (file) { + * var cb = function (error, success) { + * var listOfPeers = error ? error.listOfPeers : success.listOfPeers; + * var listOfPeersErrors = error ? error.transferErrors : {}; + * for (var i = 0; i < listOfPeers.length; i++) { + * if (listOfPeersErrors[listOfPeers[i]]) { + * console.error("Failed image transfer to '" + listOfPeers[i] + "'"); + * } else { + * console.info("Image has been transferred to '" + listOfPeers[i] + "' successfully"); + * } + * } + * }; + * retrieveImageDataURL(file, function (str) { + * if (transferTimeout > 0) { + * skylinkDemo.sendURLData(str, transferTimeout, cb); + * } else { + * skylinkDemo.sendURLData(str, cb); + * } + * document.getElementById("target-3").src = str; + * }); + * } + * </script> + * </body> * @for Skylink + * @since 0.6.1 */ -Skylink.prototype.sendBlobData = function(data, dataInfo, targetPeerId, callback) { - var self = this; - var error = ''; +Skylink.prototype.sendURLData = function(data, timeout, targetPeerId, callback) { + var listOfPeers = Object.keys(this._peerConnections); + var isPrivate = false; + var dataInfo = {}; + var transferId = this._user.sid + this.DATA_TRANSFER_TYPE.UPLOAD + + (((new Date()).toISOString().replace(/-/g, '').replace(/:/g, ''))).replace('.', ''); + // for error case + var errorMsg, errorPayload, i, peerId; // for jshint + var singleError = null; + var transferErrors = {}; + var stateError = null; + var singlePeerId = null; + //Shift parameters + // timeout + if (typeof timeout === 'function') { + callback = timeout; + + } else if (typeof timeout === 'string') { + listOfPeers = [timeout]; + isPrivate = true; + + } else if (Array.isArray(timeout)) { + listOfPeers = timeout; + isPrivate = true; + } + + // targetPeerId if (typeof targetPeerId === 'function'){ callback = targetPeerId; - targetPeerId = undefined; + + // data, timeout, target [array], callback + } else if(Array.isArray(targetPeerId)) { + listOfPeers = targetPeerId; + isPrivate = true; + + // data, timeout, target [string], callback + } else if (typeof targetPeerId === 'string') { + listOfPeers = [targetPeerId]; + isPrivate = true; } - // check if datachannel is enabled first or not - if (!self._enableDataChannel) { - error = 'Unable to send any blob data. Datachannel is disabled'; - log.error(error); + //state: String, Deprecated. But for consistency purposes. Null if not a single peer + //error: Object, Deprecated. But for consistency purposes. Null if not a single peer + //transferId: String, + //peerId: String, Deprecated. But for consistency purposes. Null if not a single peer + //listOfPeers: Array, NEW!! + //isPrivate: isPrivate, NEW!! + //transferErrors: JSON, NEW!! - Array of errors + //transferInfo: JSON The same payload as dataTransferState transferInfo payload + + // check if it's blob data + if (typeof data !== 'string') { + errorMsg = 'Provided data is not a dataURL'; + + if (listOfPeers.length === 0) { + transferErrors.self = errorMsg; + + } else { + for (i = 0; i < listOfPeers.length; i++) { + peerId = listOfPeers[i]; + transferErrors[peerId] = errorMsg; + } + + // Deprecated but for consistency purposes. Null if not a single peer. + if (listOfPeers.length === 1 && isPrivate) { + stateError = self.DATA_TRANSFER_STATE.ERROR; + singleError = errorMsg; + singlePeerId = listOfPeers[0]; + } + } + + errorPayload = { + state: stateError, + error: singleError, + transferId: transferId, + peerId: singlePeerId, + listOfPeers: listOfPeers, + transferErrors: transferErrors, + transferInfo: dataInfo, + isPrivate: isPrivate + }; + + log.error(errorMsg, errorPayload); + if (typeof callback === 'function'){ log.log([null, 'RTCDataChannel', null, 'Error occurred. Firing callback ' + - 'with error -> '],error); - callback(error,null); + 'with error -> '],errorPayload); + callback(errorPayload, null); } return; } - //Both data and dataInfo are required as objects - if (arguments.length < 2 || typeof data !== 'object' || typeof dataInfo !== 'object'){ - error = 'Either data or dataInfo was not supplied.'; - log.error(error); - if (typeof callback === 'function'){ - log.log([null, 'RTCDataChannel', null, 'Error occurred. Firing callback with ' + - 'error -> '],error); - callback(error,null); + // populate data + dataInfo.name = data.name || transferId; + dataInfo.size = data.size || data.length; + dataInfo.timeout = typeof timeout === 'number' ? timeout : 60; + dataInfo.transferId = transferId; + dataInfo.dataType = 'dataURL'; + dataInfo.isPrivate = isPrivate; + + // check if datachannel is enabled first or not + if (!this._enableDataChannel) { + errorMsg = 'Unable to send any dataURL. Datachannel is disabled'; + + if (listOfPeers.length === 0) { + transferErrors.self = errorMsg; + + } else { + for (i = 0; i < listOfPeers.length; i++) { + peerId = listOfPeers[i]; + transferErrors[peerId] = errorMsg; + } + + // Deprecated but for consistency purposes. Null if not a single peer. + if (listOfPeers.length === 1 && isPrivate) { + stateError = self.DATA_TRANSFER_STATE.ERROR; + singleError = errorMsg; + singlePeerId = listOfPeers[0]; + } } - return; - } - //Name and size and required properties of dataInfo - if (!dataInfo.hasOwnProperty('name') || !dataInfo.hasOwnProperty('size')){ - error = 'Either name or size is missing in dataInfo'; - log.error(error); + errorPayload = { + state: stateError, + error: singleError, + transferId: transferId, + peerId: singlePeerId, + listOfPeers: listOfPeers, + transferErrors: transferErrors, + transferInfo: dataInfo, + isPrivate: isPrivate + }; + + log.error(errorMsg, errorPayload); + if (typeof callback === 'function'){ log.log([null, 'RTCDataChannel', null, 'Error occurred. Firing callback ' + - 'with error -> '],error); - callback(error,null); + 'with error -> '], errorPayload); + callback(errorPayload, null); } return; } - var noOfPeersSent = 0; - dataInfo.timeout = dataInfo.timeout || 60; - dataInfo.transferId = self._user.sid + self.DATA_TRANSFER_TYPE.UPLOAD + - (((new Date()).toISOString().replace(/-/g, '').replace(/:/g, ''))).replace('.', ''); - - //Send file to specific peer only - if (targetPeerId) { - if (self._dataChannels.hasOwnProperty(targetPeerId)) { - log.log([targetPeerId, null, null, 'Sending blob data ->'], dataInfo); + this._startDataTransfer(data, dataInfo, listOfPeers, callback); +}; - self._sendBlobDataToPeer(data, dataInfo, targetPeerId, true); - noOfPeersSent = 1; - } else { - log.error([targetPeerId, null, null, 'Datachannel does not exist']); - } - } - //No peer specified --> send to all peers - else - { - targetPeerId = self._user.sid; - for (var peerId in self._dataChannels) - { - if (self._dataChannels.hasOwnProperty(peerId)) - { - // Binary String filesize [Formula n = 4/3] - self._sendBlobDataToPeer(data, dataInfo, peerId); - noOfPeersSent++; - } - else - { - log.error([peerId, null, null, 'Datachannel does not exist']); - } - } - } - if (noOfPeersSent > 0) { - self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.UPLOAD_STARTED, - dataInfo.transferId, targetPeerId, { - transferId: dataInfo.transferId, - senderPeerId: self._user.sid, - name: dataInfo.name, - size: dataInfo.size, - timeout: dataInfo.timeout || 60, - data: data - }); - } else { - error = 'No available datachannels to send data.'; - self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.ERROR, - dataInfo.transferId, targetPeerId, {}, { - message: error, - transferType: self.DATA_TRANSFER_TYPE.UPLOAD - }); - log.error('Failed sending data: ', error); - self._uploadDataTransfers = []; - self._uploadDataSessions = []; +/** + * Function that sets the data transfer "wait-for-response" timeout. + * When there is not response after timeout, the data transfer will be terminated. + * @method _setDataChannelTimeout + * @private + * @for Skylink + * @since 0.5.0 + */ +Skylink.prototype._setDataChannelTimeout = function(peerId, timeout, isSender, channelName) { + var self = this; + if (!self._dataTransfersTimeout[channelName]) { + self._dataTransfersTimeout[channelName] = null; } + var type = (isSender) ? self.DATA_TRANSFER_TYPE.UPLOAD : + self.DATA_TRANSFER_TYPE.DOWNLOAD; - if (typeof callback === 'function'){ - self.once('dataTransferState',function(state, transferId, peerId, transferInfo, error){ - log.log([null, 'RTCDataChannel', null, 'Firing callback. ' + - 'Data transfer state has met provided state ->'], state); - callback(null,{ - state: state, - transferId: transferId, - peerId: peerId, - transferInfo: transferInfo - }); - },function(state, transferId){ - return state === self.DATA_TRANSFER_STATE.UPLOAD_COMPLETED && - transferId === dataInfo.transferId; - },false); - - self.once('dataTransferState',function(state, transferId, peerId, transferInfo, error){ - log.log([null, 'RTCDataChannel', null, 'Firing callback. ' + - 'Data transfer state has met provided state ->'], state); - callback({ - state: state, - error: error - },null); - },function(state, transferId){ - return (state === self.DATA_TRANSFER_STATE.REJECTED || - state === self.DATA_TRANSFER_STATE.CANCEL || - state === self.DATA_TRANSFER_STATE.ERROR); - },false); - - self.once('dataChannelState', function(state, peerId, error){ - log.log([null, 'RTCDataChannel', null, 'Firing callback. ' + - 'Data channel state has met provided state ->'], state); - callback({ - state: state, - peerId: peerId, - error: error - },null); - },function(state, peerId, error){ - return state === self.DATA_CHANNEL_STATE.ERROR && - (targetPeerId ? (peerId === targetPeerId) : true); - },false); - } + self._dataTransfersTimeout[channelName] = setTimeout(function() { + var name; + if (self._dataTransfersTimeout[channelName][type]) { + if (isSender) { + name = self._uploadDataSessions[channelName].name; + delete self._uploadDataTransfers[channelName]; + delete self._uploadDataSessions[channelName]; + } else { + name = self._downloadDataSessions[channelName].name; + delete self._downloadDataTransfers[channelName]; + delete self._downloadDataSessions[channelName]; + } + + self._sendDataChannelMessage(peerId, { + type: self._DC_PROTOCOL_TYPE.ERROR, + sender: self._user.sid, + name: name, + content: 'Connection Timeout. Longer than ' + timeout + + ' seconds. Connection is abolished.', + isUploadError: isSender + }, channelName); + // TODO: Find a way to add channel name so it's more specific + log.error([peerId, 'RTCDataChannel', channelName, 'Failed transfering data:'], + 'Transfer ' + ((isSender) ? 'for': 'from') + ' ' + peerId + + ' failed. Connection timeout'); + self._clearDataChannelTimeout(peerId, isSender, channelName); + } + }, 1000 * timeout); }; /** - * Responds to a DataTransfer request initiated by a peer. - * @method respondBlobRequest - * @param {String} [peerId] The peerId of the peer to respond the request to. - * @param {Boolean} [accept=false] The flag to accept or reject the request. - * @trigger dataTransferState - * @component DataTransfer + * Function that stops and clears the data transfer "wait-for-response" timeout. + * @method _clearDataChannelTimeout + * @private * @for Skylink * @since 0.5.0 */ -Skylink.prototype.respondBlobRequest = function (peerId, accept) { - if (accept) { - log.info([peerId, null, null, 'User accepted peer\'s request']); - this._downloadDataTransfers[peerId] = []; - var data = this._downloadDataSessions[peerId]; - this._sendDataChannelMessage(peerId, { - type: this._DC_PROTOCOL_TYPE.ACK, - sender: this._user.sid, - ackN: 0, - agent: window.webrtcDetectedBrowser +Skylink.prototype._clearDataChannelTimeout = function(peerId, isSender, channelName) { + if (this._dataTransfersTimeout[channelName]) { + clearTimeout(this._dataTransfersTimeout[channelName]); + delete this._dataTransfersTimeout[channelName]; + log.debug([peerId, 'RTCDataChannel', channelName, 'Clear datachannel timeout']); + } else { + log.debug([peerId, 'RTCDataChannel', channelName, 'Unable to find timeouts. ' + + 'Not clearing the datachannel timeouts']); + } +}; + +/** + * Function that starts a data transfer to Peer. + * This will open a new data type of Datachannel connection with Peer if + * simultaneous data transfers is supported by Peer. + * @method _sendBlobDataToPeer + * @private + * @for Skylink + * @since 0.5.5 + */ +Skylink.prototype._sendBlobDataToPeer = function(data, dataInfo, targetPeerId) { + var self = this; + //If there is MCU then directs all messages to MCU + var targetChannel = targetPeerId;//(self._hasMCU) ? 'MCU' : targetPeerId; + var targetPeerList = []; + + var binarySize = parseInt((dataInfo.size * (4 / 3)).toFixed(), 10); + var binaryChunkSize = 0; + var chunkSize = 0; + var i; + var hasSend = false; + + // move list of peers to targetPeerList + if (self._hasMCU) { + if (Array.isArray(targetPeerList)) { + targetPeerList = targetPeerId; + } else { + targetPeerList = [targetPeerId]; + } + targetPeerId = 'MCU'; + } + + if (dataInfo.dataType !== 'blob') { + // output: 1616 + binaryChunkSize = self._CHUNK_DATAURL_SIZE; + chunkSize = self._CHUNK_DATAURL_SIZE; + binarySize = dataInfo.size; + } else if (window.webrtcDetectedBrowser === 'firefox') { + // output: 16384 + binaryChunkSize = self._MOZ_CHUNK_FILE_SIZE * (4 / 3); + chunkSize = self._MOZ_CHUNK_FILE_SIZE; + } else { + // output: 65536 + binaryChunkSize = parseInt((self._CHUNK_FILE_SIZE * (4 / 3)).toFixed(), 10); + chunkSize = self._CHUNK_FILE_SIZE; + } + + var throwTransferErrorFn = function (message) { + // MCU targetPeerId case - list of peers + if (self._hasMCU) { + for (i = 0; i < targetPeerList.length; i++) { + var peerId = targetPeerList[i]; + self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.ERROR, + dataInfo.transferId, peerId, { + name: dataInfo.name, + size: dataInfo.size, + percentage: 0, + data: null, + dataType: dataInfo.dataType, + senderPeerId: self._user.sid, + timeout: dataInfo.timeout, + isPrivate: dataInfo.isPrivate + },{ + message: message, + transferType: self.DATA_TRANSFER_TYPE.UPLOAD + }); + } + } else { + self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.ERROR, + dataInfo.transferId, targetPeerId, { + name: dataInfo.name, + size: dataInfo.size, + percentage: 0, + data: null, + dataType: dataInfo.dataType, + senderPeerId: self._user.sid, + timeout: dataInfo.timeout, + isPrivate: dataInfo.isPrivate + },{ + message: message, + transferType: self.DATA_TRANSFER_TYPE.UPLOAD + }); + } + }; + + var startTransferFn = function (targetId, channel) { + if (!hasSend) { + hasSend = true; + var payload = { + type: self._DC_PROTOCOL_TYPE.WRQ, + sender: self._user.sid, + agent: window.webrtcDetectedBrowser, + version: window.webrtcDetectedVersion, + name: dataInfo.name, + size: binarySize, + dataType: dataInfo.dataType, + chunkSize: binaryChunkSize, + timeout: dataInfo.timeout, + target: self._hasMCU ? targetPeerList : targetPeerId, + isPrivate: dataInfo.isPrivate + }; + + if (self._hasMCU) { + // if has MCU and is public, do not send individually + self._sendDataChannelMessage('MCU', payload, channel); + try { + var mainChannel = self._dataChannels.MCU.main.label; + self._setDataChannelTimeout('MCU', dataInfo.timeout, true, mainChannel); + } catch (error) { + log.error(['MCU', 'RTCDataChannel', 'MCU', 'Failed setting datachannel ' + + 'timeout for MCU'], error); + } + } else { + // if has MCU and is public, do not send individually + self._sendDataChannelMessage(targetId, payload, channel); + self._setDataChannelTimeout(targetId, dataInfo.timeout, true, channel); + } + + } + }; + + log.log([targetPeerId, 'RTCDataChannel', targetChannel, 'Chunk size of data:'], { + chunkSize: chunkSize, + binaryChunkSize: binaryChunkSize, + transferId: dataInfo.transferId, + dataType: dataInfo.dataType + }); + + + var supportMulti = false; + var peerAgent = (self._peerInformations[targetPeerId] || {}).agent || {}; + + if (!peerAgent && !peerAgent.name) { + log.error([targetPeerId, 'RTCDataChannel', targetChannel, 'Aborting transfer to peer ' + + 'as peer agent information for peer does not exists'], dataInfo); + throwTransferErrorFn('Peer agent information for peer does not exists'); + return; + } + + if (self._INTEROP_MULTI_TRANSFERS.indexOf(peerAgent.name) === -1) { + + targetChannel = targetPeerId + '-' + dataInfo.transferId; + supportMulti = true; + + if (!(self._dataChannels[targetPeerId] || {}).main) { + log.error([targetPeerId, 'RTCDataChannel', targetChannel, + 'Main datachannel does not exists'], dataInfo); + throwTransferErrorFn('Main datachannel does not exists'); + return; + + } else if (self._dataChannels[targetPeerId].main.readyState !== + self.DATA_CHANNEL_STATE.OPEN) { + log.error([targetPeerId, 'RTCDataChannel', targetChannel, + 'Main datachannel is not opened'], { + transferId: dataInfo.transferId, + readyState: self._dataChannels[targetPeerId].main.readyState + }); + throwTransferErrorFn('Main datachannel is not opened'); + return; + } + + self._dataChannels[targetPeerId][targetChannel] = + self._createDataChannel(targetPeerId, self.DATA_CHANNEL_TYPE.DATA, null, targetChannel); + + } else { + var ongoingTransfer = null; + + if (self._uploadDataSessions[targetChannel]) { + ongoingTransfer = self.DATA_TRANSFER_TYPE.UPLOAD; + } else if (self._downloadDataSessions[targetChannel]) { + ongoingTransfer = self.DATA_TRANSFER_TYPE.DOWNLOAD; + } + + if (ongoingTransfer) { + log.error([targetPeerId, 'RTCDataChannel', targetChannel, 'User have ongoing ' + + ongoingTransfer + ' transfer session with peer. Unable to send data'], dataInfo); + throwTransferErrorFn('Another ' + ongoingTransfer + + ' transfer is ongoing. Unable to send data.'); + return; + } + } + + if (dataInfo.dataType === 'blob') { + self._uploadDataTransfers[targetChannel] = self._chunkBlobData(data, chunkSize); + } else { + self._uploadDataTransfers[targetChannel] = self._chunkDataURL(data, chunkSize); + } + + self._uploadDataSessions[targetChannel] = { + name: dataInfo.name, + size: binarySize, + isUpload: true, + senderPeerId: self._user.sid, + transferId: dataInfo.transferId, + percentage: 0, + timeout: dataInfo.timeout, + chunkSize: chunkSize, + dataType: dataInfo.dataType, + isPrivate: dataInfo.isPrivate + }; + + if (supportMulti) { + self._condition('dataChannelState', function () { + startTransferFn(targetPeerId, targetChannel); + }, function () { + return self._dataChannels[targetPeerId][targetChannel].readyState === + self.DATA_CHANNEL_STATE.OPEN; + }, function (state) { + return state === self.DATA_CHANNEL_STATE.OPEN; }); - this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.DOWNLOAD_STARTED, - data.transferId, peerId, { - name: data.name, - size: data.size, - senderPeerId: peerId + } else { + startTransferFn(targetChannel, targetChannel); + } + + return targetChannel; +}; + +/** + * Function that handles the data received from Datachannel and + * routes to the relevant data transfer protocol handler. + * @method _dataChannelProtocolHandler + * @private + * @for Skylink + * @since 0.5.2 + */ +Skylink.prototype._dataChannelProtocolHandler = function(dataString, peerId, channelName, channelType) { + // PROTOCOL ESTABLISHMENT + + if (!(this._peerInformations[peerId] || {}).agent) { + log.error([peerId, 'RTCDataChannel', channelName, 'Peer informations is missing during protocol ' + + 'handling. Dropping packet'], dataString); + return; + } + + /*var useChannel = channelName; + var peerAgent = this._peerInformations[peerId].agent.name; + + if (channelType === this.DATA_CHANNEL_TYPE.MESSAGING || + this._INTEROP_MULTI_TRANSFERS[peerAgent] > -1) { + useChannel = peerId; + }*/ + + if (typeof dataString === 'string') { + var data = {}; + try { + data = JSON.parse(dataString); + } catch (error) { + log.debug([peerId, 'RTCDataChannel', channelName, 'Received from peer ->'], { + type: 'DATA', + data: dataString + }); + this._DATAProtocolHandler(peerId, dataString, + this.DATA_TRANSFER_DATA_TYPE.BINARY_STRING, channelName); + return; + } + log.debug([peerId, 'RTCDataChannel', channelName, 'Received from peer ->'], { + type: data.type, + data: data }); + switch (data.type) { + case this._DC_PROTOCOL_TYPE.WRQ: + this._WRQProtocolHandler(peerId, data, channelName); + break; + case this._DC_PROTOCOL_TYPE.ACK: + this._ACKProtocolHandler(peerId, data, channelName); + break; + case this._DC_PROTOCOL_TYPE.ERROR: + this._ERRORProtocolHandler(peerId, data, channelName); + break; + case this._DC_PROTOCOL_TYPE.CANCEL: + this._CANCELProtocolHandler(peerId, data, channelName); + break; + case this._DC_PROTOCOL_TYPE.MESSAGE: // Not considered a protocol actually? + this._MESSAGEProtocolHandler(peerId, data, channelName); + break; + default: + log.error([peerId, 'RTCDataChannel', channelName, 'Unsupported message ->'], { + type: data.type, + data: data + }); + } + } +}; + +/** + * Function that handles the "WRQ" data transfer protocol. + * @method _WRQProtocolHandler + * @private + * @for Skylink + * @since 0.5.2 + */ +Skylink.prototype._WRQProtocolHandler = function(peerId, data, channelName) { + var transferId = channelName + this._TRANSFER_DELIMITER + (new Date()).getTime(); + + log.log([peerId, 'RTCDataChannel', channelName, + 'Received file request from peer:'], data); + + var name = data.name; + var binarySize = data.size; + var expectedSize = data.chunkSize; + var timeout = data.timeout; + + this._downloadDataSessions[channelName] = { + transferId: transferId, + name: name, + isUpload: false, + senderPeerId: peerId, + size: binarySize, + percentage: 0, + dataType: data.dataType, + ackN: 0, + receivedSize: 0, + chunkSize: expectedSize, + timeout: timeout, + isPrivate: data.isPrivate + }; + this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.UPLOAD_REQUEST, + transferId, peerId, { + name: name, + size: binarySize, + percentage: 0, + data: null, + dataType: data.dataType, + senderPeerId: peerId, + timeout: timeout, + isPrivate: data.isPrivate + }); + this._trigger('incomingDataRequest', transferId, peerId, { + name: name, + size: binarySize, + percentage: 0, + dataType: data.dataType, + senderPeerId: peerId, + timeout: timeout, + isPrivate: data.isPrivate + }, false); +}; + +/** + * Function that handles the "ACK" data transfer protocol. + * @method _ACKProtocolHandler + * @private + * @for Skylink + * @since 0.5.2 + */ +Skylink.prototype._ACKProtocolHandler = function(peerId, data, channelName) { + var self = this; + var ackN = data.ackN; + var transferStatus = self._uploadDataSessions[channelName]; + + if (!transferStatus) { + log.error([peerId, 'RTCDataChannel', channelName, 'Ignoring data received as ' + + 'upload data transfers is empty'], { + status: transferStatus, + data: data + }); + return; + } + + if (!this._uploadDataTransfers[channelName]) { + log.error([peerId, 'RTCDataChannel', channelName, + 'Ignoring data received as upload data transfers array is missing'], { + data: data + }); + return; + } + + //peerId = (peerId === 'MCU') ? data.sender : peerId; + var chunksLength = self._uploadDataTransfers[channelName].length; + var transferId = transferStatus.transferId; + var timeout = transferStatus.timeout; + + self._clearDataChannelTimeout(peerId, true, channelName); + log.log([peerId, 'RTCDataChannel', channelName, 'ACK stage (' + + transferStatus.transferId + ') ->'], ackN + ' / ' + chunksLength); + + if (ackN > -1) { + // Still uploading + if (ackN < chunksLength) { + var sendDataFn = function (base64BinaryString) { + var percentage = parseFloat((((ackN + 1) / chunksLength) * 100).toFixed(2), 10); + + if (!self._uploadDataSessions[channelName]) { + log.error([peerId, 'RTCDataChannel', channelName, + 'Failed uploading as data session is empty'], { + status: transferStatus, + data: data + }); + return; + } + + self._uploadDataSessions[channelName].percentage = percentage; + + self._sendDataChannelMessage(peerId, base64BinaryString, channelName); + self._setDataChannelTimeout(peerId, timeout, true, channelName); + + // to prevent from firing upload = 100; + if (percentage !== 100) { + self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.UPLOADING, + transferId, peerId, { + name: transferStatus.name, + size: transferStatus.size, + percentage: percentage, + data: null, + dataType: transferStatus.dataType, + senderPeerId: transferStatus.senderPeerId, + timeout: transferStatus.timeout, + isPrivate: transferStatus.isPrivate + }); + } + }; + + if (transferStatus.dataType === 'blob') { + self._blobToBase64(self._uploadDataTransfers[channelName][ackN], sendDataFn); + } else { + sendDataFn(self._uploadDataTransfers[channelName][ackN]); + } + } else if (ackN === chunksLength) { + log.log([peerId, 'RTCDataChannel', channelName, 'Upload completed (' + + transferStatus.transferId + ')'], transferStatus); + + self._trigger('dataTransferState', + self.DATA_TRANSFER_STATE.UPLOAD_COMPLETED, transferId, peerId, { + name: transferStatus.name, + size: transferStatus.size, + percentage: 100, + data: null, + dataType: transferStatus.dataType, + senderPeerId: transferStatus.senderPeerId, + timeout: transferStatus.timeout, + isPrivate: transferStatus.isPrivate + }); + + var blob = null; + + if (transferStatus.dataType === 'blob') { + blob = new Blob(self._uploadDataTransfers[channelName]); + } else { + blob = self._assembleDataURL(self._uploadDataTransfers[channelName]); + } + + self._trigger('incomingData', blob, transferId, peerId, { + name: transferStatus.name, + size: transferStatus.size, + percentage: 100, + dataType: transferStatus.dataType, + senderPeerId: transferStatus.senderPeerId, + timeout: transferStatus.timeout, + isPrivate: transferStatus.isPrivate + }, true); + delete self._uploadDataTransfers[channelName]; + delete self._uploadDataSessions[channelName]; + + // close datachannel after transfer + if (self._dataChannels[peerId] && self._dataChannels[peerId][channelName]) { + log.debug([peerId, 'RTCDataChannel', channelName, 'Closing datachannel for upload transfer']); + self._closeDataChannel(peerId, channelName); + } + } + } else { + log.debug([peerId, 'RTCDataChannel', channelName, 'Upload rejected (' + + transferStatus.transferId + ')'], transferStatus); + + self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.REJECTED, + transferId, peerId, { + name: transferStatus.name, //self._uploadDataSessions[channelName].name, + size: transferStatus.size, //self._uploadDataSessions[channelName].size, + percentage: 0, + data: null, + dataType: transferStatus.dataType, + senderPeerId: transferStatus.senderPeerId, + timeout: transferStatus.timeout, + isPrivate: transferStatus.isPrivate + }); + delete self._uploadDataTransfers[channelName]; + delete self._uploadDataSessions[channelName]; + + // close datachannel if rejected + if (self._dataChannels[peerId] && self._dataChannels[peerId][channelName]) { + log.debug([peerId, 'RTCDataChannel', channelName, 'Closing datachannel for upload transfer']); + self._closeDataChannel(peerId, channelName); + } + } +}; + +/** + * Function that handles the "MESSAGE" data transfer protocol. + * @method _MESSAGEProtocolHandler + * @private + * @for Skylink + * @since 0.5.2 + */ +Skylink.prototype._MESSAGEProtocolHandler = function(peerId, data, channelName) { + var targetMid = data.sender; + log.log([targetMid, 'RTCDataChannel', channelName, + 'Received P2P message from peer:'], data); + this._trigger('incomingMessage', { + content: data.data, + isPrivate: data.isPrivate, + isDataChannel: true, + targetPeerId: this._user.sid, + senderPeerId: targetMid + }, targetMid, this.getPeerInfo(targetMid), false); +}; + +/** + * Function that handles the "ERROR" data transfer protocol. + * @method _ERRORProtocolHandler + * @private + * @for Skylink + * @since 0.5.2 + */ +Skylink.prototype._ERRORProtocolHandler = function(peerId, data, channelName) { + var isUploader = data.isUploadError; + var transferStatus = (isUploader) ? this._uploadDataSessions[channelName] : + this._downloadDataSessions[channelName]; + + if (!transferStatus) { + log.error([peerId, 'RTCDataChannel', channelName, 'Ignoring data received as ' + + (isUploader ? 'upload' : 'download') + ' data session is empty'], data); + return; + } + + var transferId = transferStatus.transferId; + + log.error([peerId, 'RTCDataChannel', channelName, + 'Received an error from peer:'], data); + this._clearDataChannelTimeout(peerId, isUploader, channelName); + this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.ERROR, + transferId, peerId, { + name: transferStatus.name, + size: transferStatus.size, + percentage: transferStatus.percentage, + data: null, + dataType: transferStatus.dataType, + senderPeerId: transferStatus.senderPeerId, + timeout: transferStatus.timeout, + isPrivate: transferStatus.isPrivate + }, { + message: data.content, + transferType: ((isUploader) ? this.DATA_TRANSFER_TYPE.UPLOAD : + this.DATA_TRANSFER_TYPE.DOWNLOAD) + }); +}; + +/** + * Function that handles the "CANCEL" data transfer protocol. + * @method _CANCELProtocolHandler + * @private + * @for Skylink + * @since 0.5.0 + */ +Skylink.prototype._CANCELProtocolHandler = function(peerId, data, channelName) { + var isUpload = !!this._uploadDataSessions[channelName]; + var isDownload = !!this._downloadDataSessions[channelName]; + var transferStatus = (isUpload) ? this._uploadDataSessions[channelName] : + this._downloadDataSessions[channelName]; + + if (!transferStatus) { + log.error([peerId, 'RTCDataChannel', channelName, 'Ignoring data received as ' + + (isUpload ? 'upload' : 'download') + ' data session is empty'], data); + return; + } + + var transferId = transferStatus.transferId; + + log.log([peerId, 'RTCDataChannel', channelName, + 'Received file transfer cancel request:'], data); + + this._clearDataChannelTimeout(peerId, isUpload, channelName); + + try { + if (isUpload) { + delete this._uploadDataSessions[channelName]; + delete this._uploadDataTransfers[channelName]; + } else { + delete this._downloadDataSessions[channelName]; + delete this._downloadDataTransfers[channelName]; + } + + this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.CANCEL, + transferId, peerId, { + name: transferStatus.name, + size: transferStatus.size, + data: null, + dataType: transferStatus.dataType, + percentage: transferStatus.percentage, + senderPeerId: transferStatus.senderPeerId, + timeout: transferStatus.timeout, + isPrivate: transferStatus.isPrivate + }, { + message: data.content, + transferType: ((isUpload) ? this.DATA_TRANSFER_TYPE.UPLOAD : + this.DATA_TRANSFER_TYPE.DOWNLOAD) + }); + + log.log([peerId, 'RTCDataChannel', channelName, + 'Emptied file transfer session:'], data); + + } catch (error) { + this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.ERROR, + transferId, peerId, { + name: transferStatus.name, + size: transferStatus.size, + data: null, + dataType: transferStatus.dataType, + percentage: transferStatus.percentage, + senderPeerId: transferStatus.senderPeerId, + timeout: transferStatus.timeout, + isPrivate: transferStatus.isPrivate + }, { + message: 'Failed cancelling data request from peer', + transferType: ((isUpload) ? this.DATA_TRANSFER_TYPE.UPLOAD : + this.DATA_TRANSFER_TYPE.DOWNLOAD) + }); + + log.error([peerId, 'RTCDataChannel', channelName, + 'Failed emptying file transfer session:'], { + data: data, + error: error + }); + } +}; + +/** + * Function that handles the data transfer chunk received. + * @method _DATAProtocolHandler + * @private + * @for Skylink + * @since 0.5.5 + */ +Skylink.prototype._DATAProtocolHandler = function(peerId, dataString, dataType, channelName) { + var chunk, error = ''; + var transferStatus = this._downloadDataSessions[channelName]; + log.log([peerId, 'RTCDataChannel', channelName, + 'Received data chunk from peer ->'], { + dataType: dataType, + data: dataString, + type: 'DATA' + }); + + if (!transferStatus) { + log.error([peerId, 'RTCDataChannel', channelName, + 'Ignoring data received as download data session is empty'], { + dataType: dataType, + data: dataString, + type: 'DATA' + }); + return; + } + + if (!this._downloadDataTransfers[channelName]) { + log.error([peerId, 'RTCDataChannel', channelName, + 'Ignoring data received as download data transfers array is missing'], { + dataType: dataType, + data: dataString, + type: 'DATA' + }); + return; + } + + var transferId = transferStatus.transferId; + var dataTransferType = transferStatus.dataType; + var receivedSize = 0; + + this._clearDataChannelTimeout(peerId, false, channelName); + + if (dataType === this.DATA_TRANSFER_DATA_TYPE.BINARY_STRING) { + if (dataTransferType === 'blob') { + chunk = this._base64ToBlob(dataString); + receivedSize = (chunk.size * (4 / 3)); + } else { + chunk = dataString; + receivedSize = dataString.length; + } + } else if (dataType === this.DATA_TRANSFER_DATA_TYPE.ARRAY_BUFFER) { + chunk = new Blob(dataString); + } else if (dataType === this.DATA_TRANSFER_DATA_TYPE.BLOB) { + chunk = dataString; } else { - log.info([peerId, null, null, 'User rejected peer\'s request']); + error = 'Unhandled data exception: ' + dataType; + log.error([peerId, 'RTCDataChannel', channelName, 'Failed downloading data packets:'], { + dataType: dataType, + data: dataString, + type: 'DATA', + error: error + }); + this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.ERROR, + transferId, peerId, { + name: transferStatus.name, + size: transferStatus.size, + percentage: transferStatus.percentage, + data: null, + dataType: dataTransferType, + senderPeerId: transferStatus.senderPeerId, + timeout: transferStatus.timeout, + isPrivate: transferStatus.isPrivate + }, { + message: error, + transferType: this.DATA_TRANSFER_TYPE.DOWNLOAD + }); + return; + } + + log.log([peerId, 'RTCDataChannel', channelName, + 'Received and expected data chunk size (' + receivedSize + ' === ' + + transferStatus.chunkSize + ')'], { + dataType: dataType, + data: dataString, + receivedSize: receivedSize, + expectedSize: transferStatus.chunkSize, + type: 'DATA' + }); + + if (transferStatus.chunkSize >= receivedSize) { + this._downloadDataTransfers[channelName].push(chunk); + transferStatus.ackN += 1; + transferStatus.receivedSize += receivedSize; + var totalReceivedSize = transferStatus.receivedSize; + var percentage = parseFloat(((totalReceivedSize / transferStatus.size) * 100).toFixed(2), 10); + this._sendDataChannelMessage(peerId, { type: this._DC_PROTOCOL_TYPE.ACK, sender: this._user.sid, - ackN: -1 + ackN: transferStatus.ackN + }, channelName); + + // update the percentage + this._downloadDataSessions[channelName].percentage = percentage; + + if (transferStatus.chunkSize === receivedSize && percentage < 100) { + log.log([peerId, 'RTCDataChannel', channelName, + 'Transfer in progress ACK n (' + transferStatus.ackN + ')'], { + dataType: dataType, + data: dataString, + ackN: transferStatus.ackN, + type: 'DATA' + }); + this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.DOWNLOADING, + transferId, peerId, { + name: transferStatus.name, + size: transferStatus.size, + percentage: percentage, + data: null, + dataType: dataTransferType, + senderPeerId: transferStatus.senderPeerId, + timeout: transferStatus.timeout, + isPrivate: transferStatus.isPrivate + }); + this._setDataChannelTimeout(peerId, transferStatus.timeout, false, channelName); + + if (!this._downloadDataSessions[channelName]) { + log.error([peerId, 'RTCDataChannel', channelName, + 'Failed downloading as data session is empty'], { + dataType: dataType, + data: dataString, + type: 'DATA' + }); + return; + } + + this._downloadDataSessions[channelName].info = transferStatus; + + } else { + log.log([peerId, 'RTCDataChannel', channelName, + 'Download complete'], { + dataType: dataType, + data: dataString, + type: 'DATA', + transferInfo: transferStatus + }); + + var blob = null; + + if (dataTransferType === 'blob') { + blob = new Blob(this._downloadDataTransfers[channelName]); + } else { + blob = this._assembleDataURL(this._downloadDataTransfers[channelName]); + } + this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.DOWNLOAD_COMPLETED, + transferId, peerId, { + name: transferStatus.name, + size: transferStatus.size, + percentage: 100, + data: blob, + dataType: dataTransferType, + senderPeerId: transferStatus.senderPeerId, + timeout: transferStatus.timeout, + isPrivate: transferStatus.isPrivate + }); + + this._trigger('incomingData', blob, transferId, peerId, { + name: transferStatus.name, + size: transferStatus.size, + percentage: 100, + dataType: dataTransferType, + senderPeerId: transferStatus.senderPeerId, + timeout: transferStatus.timeout, + isPrivate: transferStatus.isPrivate + }, false); + + delete this._downloadDataTransfers[channelName]; + delete this._downloadDataSessions[channelName]; + + log.log([peerId, 'RTCDataChannel', channelName, + 'Converted to Blob as download'], { + dataType: dataType, + data: dataString, + type: 'DATA', + transferInfo: transferStatus + }); + + // close datachannel after transfer + if (this._dataChannels[peerId] && this._dataChannels[peerId][channelName]) { + log.debug([peerId, 'RTCDataChannel', channelName, 'Closing datachannel for download transfer']); + this._closeDataChannel(peerId, channelName); + } + } + + } else { + error = 'Packet not match - [Received]' + receivedSize + + ' / [Expected]' + transferStatus.chunkSize; + + this._trigger('dataTransferState', + this.DATA_TRANSFER_STATE.ERROR, transferId, peerId, { + name: transferStatus.name, + size: transferStatus.size, + percentage: transferStatus.percentage, + data: null, + dataType: dataTransferType, + senderPeerId: transferStatus.senderPeerId, + timeout: transferStatus.timeout, + isPrivate: transferStatus.isPrivate + }, { + message: error, + transferType: this.DATA_TRANSFER_TYPE.DOWNLOAD + }); + + log.error([peerId, 'RTCDataChannel', channelName, + 'Failed downloading data packets:'], { + dataType: dataType, + data: dataString, + type: 'DATA', + transferInfo: transferStatus, + error: error }); - delete this._downloadDataSessions[peerId]; } }; /** - * Cancels or terminates an ongoing DataTransfer request. - * @method cancelBlobTransfer - * @param {String} [peerId] The peerId of the peer associated with the DataTransfer to cancel. - * @param {String} [transferType] The transfer type of the request. Is it an ongoing uploading - * stream to reject or an downloading stream. - * If not transfer type is provided, it cancels all DataTransfer associated with the peer. - * [Rel: Skylink.DATA_TRANSFER_TYPE] - * @trigger dataTransferState. - * @since 0.5.7 - * @component DataTransfer + * Function that start the data transfer with the list of targeted Peer IDs provided. + * At this stage, it will open a new Datachannel connection if simultaneous data transfers is + * supported by Peer, or it will using the messaging type Datachannel connection. + * Note that 1 data transfer can occur at a time in 1 Datachannel connection. + * @method _startDataTransfer + * @private * @for Skylink + * @since 0.6.1 */ -Skylink.prototype.cancelBlobTransfer = function (peerId, transferType) { - var data; +Skylink.prototype._startDataTransfer = function(data, dataInfo, listOfPeers, callback) { + var self = this; + var error = ''; + var noOfPeersSent = 0; + var transferId = dataInfo.transferId; + var dataType = dataInfo.dataType; + var isPrivate = dataInfo.isPrivate; + var i; + var peerId; + + // for callback + var listOfPeersTransferState = {}; + var transferSuccess = true; + var listOfPeersTransferErrors = {}; + var listOfPeersChannels = {}; + var successfulPeerTransfers = []; + + var triggerCallbackFn = function () { + for (i = 0; i < listOfPeers.length; i++) { + var transferPeerId = listOfPeers[i]; + + if (!listOfPeersTransferState[transferPeerId]) { + // if error, make as false and break + transferSuccess = false; + break; + } + } - // cancel upload - if (transferType === this.DATA_TRANSFER_TYPE.UPLOAD && !transferType) { - data = this._uploadDataSessions[peerId]; + if (transferSuccess) { + log.log([null, 'RTCDataChannel', transferId, 'Firing success callback for data transfer'], dataInfo); + // should we even support this? maybe keeping to not break older impl + if (listOfPeers.length === 1 && isPrivate) { + callback(null,{ + state: self.DATA_TRANSFER_STATE.UPLOAD_COMPLETED, + peerId: listOfPeers[0], + listOfPeers: listOfPeers, + transferId: transferId, + isPrivate: isPrivate, // added new flag to indicate privacy + transferInfo: dataInfo + }); + } else { + callback(null,{ + state: null, + peerId: null, + transferId: transferId, + listOfPeers: listOfPeers, + isPrivate: isPrivate, // added new flag to indicate privacy + transferInfo: dataInfo + }); + } + } else { + log.log([null, 'RTCDataChannel', transferId, 'Firing failure callback for data transfer'], dataInfo); + + // should we even support this? maybe keeping to not break older impl + if (listOfPeers.length === 1 && isPrivate) { + callback({ + state: self.DATA_TRANSFER_STATE.ERROR, + error: listOfPeersTransferErrors[listOfPeers[0]], + peerId: listOfPeers[0], + transferId: transferId, + transferErrors: listOfPeersTransferErrors, + transferInfo: dataInfo, + isPrivate: isPrivate, // added new flag to indicate privacy + listOfPeers: listOfPeers + }, null); + } else { + callback({ + state: null, + peerId: null, + error: null, + transferId: transferId, + listOfPeers: listOfPeers, + isPrivate: isPrivate, // added new flag to indicate privacy + transferInfo: dataInfo, + transferErrors: listOfPeersTransferErrors + }, null); + } + } + }; - if (data) { - delete this._uploadDataSessions[peerId]; - delete this._uploadDataTransfers[peerId]; + for (i = 0; i < listOfPeers.length; i++) { + peerId = listOfPeers[i]; - // send message - this._sendDataChannelMessage(peerId, { - type: this._DC_PROTOCOL_TYPE.CANCEL, - sender: this._user.sid, - name: data.name, - content: 'Peer cancelled upload transfer' - }); - } else { - this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.ERROR, - dataInfo.transferId, targetPeerId, {}, { - name: dataInfo.name, - message: 'Unable to cancel upload transfer. There is ' + - 'not ongoing upload sessions with the peer', - transferType: this.DATA_TRANSFER_TYPE.UPLOAD + if (peerId === 'MCU') { + continue; + } + + if (self._dataChannels[peerId] && self._dataChannels[peerId].main) { + log.log([peerId, 'RTCDataChannel', null, 'Sending blob data ->'], dataInfo); + + self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.UPLOAD_STARTED, + transferId, peerId, { + name: dataInfo.name, + size: dataInfo.size, + percentage: 0, + data: data, + dataType: dataType, + senderPeerId: self._user.sid, + timeout: dataInfo.timeout, + isPrivate: isPrivate }); - if (!!transferType) { - return; + self._trigger('incomingDataRequest', transferId, peerId, { + name: dataInfo.name, + size: dataInfo.size, + percentage: 0, + dataType: dataType, + senderPeerId: self._user.sid, + timeout: dataInfo.timeout, + isPrivate: isPrivate + }, true); + + if (!self._hasMCU) { + listOfPeersChannels[peerId] = + self._sendBlobDataToPeer(data, dataInfo, peerId); + } else { + listOfPeersChannels[peerId] = self._dataChannels[peerId].main.label; } + + noOfPeersSent++; + + } else { + error = 'Datachannel does not exist. Unable to start data transfer with peer'; + log.error([peerId, 'RTCDataChannel', null, error]); + listOfPeersTransferErrors[peerId] = error; } } - if (transferType === this.DATA_TRANSFER_TYPE.DOWNLOAD) { - data = this._downloadDataSessions[peerId]; - if (data) { - delete this._downloadDataSessions[peerId]; - delete this._downloadDataTransfers[peerId]; + // if has MCU + if (self._hasMCU) { + self._sendBlobDataToPeer(data, dataInfo, listOfPeers, isPrivate, transferId); + } - // send message - this._sendDataChannelMessage(peerId, { - type: this._DC_PROTOCOL_TYPE.CANCEL, - sender: this._user.sid, - name: data.name, - content: 'Peer cancelled download transfer' - }); - } else { - this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.ERROR, - dataInfo.transferId, targetPeerId, {}, { - name: dataInfo.name, - message: 'Unable to cancel download transfer. There is ' + - 'not ongoing download sessions with the peer', - transferType: this.DATA_TRANSFER_TYPE.DOWNLOAD + if (noOfPeersSent === 0) { + error = 'Failed sending data as there is no available datachannels to send data'; + + for (i = 0; i < listOfPeers.length; i++) { + peerId = listOfPeers[i]; + + self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.ERROR, + transferId, peerId, { + name: dataInfo.name, + size: dataInfo.size, + data: null, + dataType: dataType, + percentage: 0, + senderPeerId: self._user.sid, + timeout: dataInfo.timeout, + isPrivate: isPrivate + }, { + message: error, + transferType: self.DATA_TRANSFER_TYPE.UPLOAD }); + + listOfPeersTransferErrors[peerId] = error; } - } -}; -/** - * Send a Message object via the DataChannel established with peers. - * - Maximum size: 16Kb - * @method sendP2PMessage - * @param {String|JSON} message The Message object to send. - * @param {String} [targetPeerId] The peerId of the targeted peer to - * send the Message object only. To send to all peers, leave this - * option blank. - * @example - * // Example 1: Send to all peers - * SkylinkDemo.sendP2PMessage('Hi there! This is from a DataChannel!'); - * - * // Example 2: Send to specific peer - * SkylinkDemo.sendP2PMessage('Hi there peer! This is from a DataChannel!', targetPeerId); - * @trigger incomingMessage - * @since 0.5.5 - * @component DataTransfer - * @for Skylink - */ -Skylink.prototype.sendP2PMessage = function(message, targetPeerId) { - // check if datachannel is enabled first or not - if (!this._enableDataChannel) { - log.warn('Unable to send any P2P message. Datachannel is disabled'); + log.error([null, 'RTCDataChannel', null, error]); + self._uploadDataTransfers = []; + self._uploadDataSessions = []; + + transferSuccess = false; + + if (typeof callback === 'function') { + triggerCallbackFn(); + } return; } - //targetPeerId is defined -> private message - if (targetPeerId) { - //If there is MCU then directs all messages to MCU - var useChannel = (this._hasMCU) ? 'MCU' : targetPeerId; - //send private P2P message - log.log([targetPeerId, null, useChannel, 'Sending private P2P message to peer']); - this._sendDataChannelMessage(useChannel, { - type: this._DC_PROTOCOL_TYPE.MESSAGE, - isPrivate: true, - sender: this._user.sid, - target: targetPeerId, - data: message - }); - } - //targetPeerId is null or undefined -> public message - else { - //If has MCU, only need to send once to MCU then it will forward to all peers - if (this._hasMCU) { - log.log(['MCU', null, null, 'Relaying P2P message to peers']); - this._sendDataChannelMessage('MCU', { - type: this._DC_PROTOCOL_TYPE.MESSAGE, - isPrivate: false, - sender: this._user.sid, - target: 'MCU', - data: message - }); - //If no MCU -> need to send to every peers - } else { - // send to all datachannels - for (var peerId in this._dataChannels){ - if (this._dataChannels.hasOwnProperty(peerId)) { - log.log([peerId, null, null, 'Sending P2P message to peer']); - - this._sendDataChannelMessage(peerId, { - type: this._DC_PROTOCOL_TYPE.MESSAGE, - isPrivate: false, - sender: this._user.sid, - target: peerId, - data: message + + if (typeof callback === 'function') { + var dataChannelStateFn = function(state, transferringPeerId, errorObj, channelName, channelType){ + // check if error or closed halfway, if so abort + if (state === self.DATA_CHANNEL_STATE.ERROR && + state === self.DATA_CHANNEL_STATE.CLOSED && + listOfPeersChannels[peerId] === channelName) { + // if peer has already been inside, ignore + if (successfulPeerTransfers.indexOf(transferringPeerId) === -1) { + listOfPeersTransferState[transferringPeerId] = false; + listOfPeersTransferErrors[transferringPeerId] = errorObj; + + log.error([transferringPeerId, 'RTCDataChannel', null, + 'Data channel state has met a failure state for peer (datachannel) ->'], { + state: state, + error: errorObj }); } } - } + + if (Object.keys(listOfPeersTransferState).length === listOfPeers.length) { + self.off('dataTransferState', dataTransferStateFn); + self.off('dataChannelState', dataChannelStateFn); + + log.log([null, 'RTCDataChannel', transferId, + 'Transfer states have been gathered completely in dataChannelState'], state); + + triggerCallbackFn(); + } + }; + + var dataTransferStateFn = function(state, stateTransferId, transferringPeerId, transferInfo, errorObj){ + // check if transfer is related to this transfer + if (stateTransferId === transferId) { + // check if state upload has completed + if (state === self.DATA_TRANSFER_STATE.UPLOAD_COMPLETED) { + + log.debug([transferringPeerId, 'RTCDataChannel', stateTransferId, + 'Data transfer state has met a success state for peer ->'], state); + + // if peer has already been inside, ignore + if (successfulPeerTransfers.indexOf(transferringPeerId) === -1) { + listOfPeersTransferState[transferringPeerId] = true; + } + } else if(state === self.DATA_TRANSFER_STATE.REJECTED || + state === self.DATA_TRANSFER_STATE.CANCEL || + state === self.DATA_TRANSFER_STATE.ERROR) { + + if (state === self.DATA_TRANSFER_STATE.REJECTED) { + errorObj = new Error('Peer has rejected data transfer request'); + } + + log.error([transferringPeerId, 'RTCDataChannel', stateTransferId, + 'Data transfer state has met a failure state for peer ->'], { + state: state, + error: errorObj + }); + + // if peer has already been inside, ignore + if (successfulPeerTransfers.indexOf(transferringPeerId) === -1) { + listOfPeersTransferState[transferringPeerId] = false; + listOfPeersTransferErrors[transferringPeerId] = errorObj; + } + } + } + + if (Object.keys(listOfPeersTransferState).length === listOfPeers.length) { + self.off('dataTransferState', dataTransferStateFn); + self.off('dataChannelState', dataChannelStateFn); + + log.log([null, 'RTCDataChannel', stateTransferId, + 'Transfer states have been gathered completely in dataTransferState'], state); + + triggerCallbackFn(); + } + }; + self.on('dataTransferState', dataTransferStateFn); + self.on('dataChannelState', dataChannelStateFn); } - this._trigger('incomingMessage', { - content: message, - isPrivate: !!targetPeerId, - targetPeerId: targetPeerId, - isDataChannel: true, - senderPeerId: this._user.sid - }, this._user.sid, this.getPeerInfo(), true); }; -Skylink.prototype._peerCandidatesQueue = {}; +Skylink.prototype.CANDIDATE_GENERATION_STATE = { + NEW: 'new', + GATHERING: 'gathering', + COMPLETED: 'completed' +}; /** - * Stores the list of ICE Candidates to disable ICE trickle - * to ensure stability of ICE connection. - * @attribute _peerIceTrickleDisabled + * Stores the list of buffered ICE candidates that is received before + * remote session description is received and set. + * @attribute _peerCandidatesQueue + * @param {Array} <#peerId> The list of the Peer connection buffered ICE candidates received. + * @param {Object} <#peerId>.<#index> The Peer connection buffered ICE candidate received. * @type JSON * @private - * @required - * @since 0.5.8 - * @component ICE * @for Skylink + * @since 0.5.1 */ -Skylink.prototype._peerIceTrickleDisabled = {}; +Skylink.prototype._peerCandidatesQueue = {}; /** - * The list of ICE candidate generation states that would be triggered. - * @attribute CANDIDATE_GENERATION_STATE + * Stores the list of Peer connection ICE candidates. + * @attribute _gatheredCandidates + * @param {JSON} <#peerId> The list of the Peer connection ICE candidates. + * @param {JSON} <#peerId>.sending The list of the Peer connection ICE candidates sent. + * @param {JSON} <#peerId>.receiving The list of the Peer connection ICE candidates received. * @type JSON - * @param {String} NEW The object was just created, and no networking - * has occurred yet. - * @param {String} GATHERING The ICE engine is in the process of gathering - * candidates for connection. - * @param {String} COMPLETED The ICE engine has completed gathering. Events - * such as adding a new interface or a new TURN server will cause the - * state to go back to gathering. - * @readOnly - * @since 0.4.1 - * @component ICE + * @private * @for Skylink + * @since 0.6.14 */ -Skylink.prototype.CANDIDATE_GENERATION_STATE = { - NEW: 'new', - GATHERING: 'gathering', - COMPLETED: 'completed' -}; +Skylink.prototype._gatheredCandidates = {}; /** - * An ICE candidate has just been generated (ICE gathering) and will be sent to the peer. - * Part of connection establishment. + * Function that handles the Peer connection gathered ICE candidate to be sent. * @method _onIceCandidate - * @param {String} targetMid The peerId of the target peer. - * @param {Event} event This is provided directly by the peerconnection API. - * @trigger candidateGenerationState * @private - * @since 0.1.0 - * @component ICE * @for Skylink + * @since 0.1.0 */ -Skylink.prototype._onIceCandidate = function(targetMid, event) { - if (event.candidate) { - if (this._enableIceTrickle && !this._peerIceTrickleDisabled[targetMid]) { - var messageCan = event.candidate.candidate.split(' '); - var candidateType = messageCan[7]; - log.debug([targetMid, 'RTCIceCandidate', null, 'Created and sending ' + - candidateType + ' candidate:'], event); +Skylink.prototype._onIceCandidate = function(targetMid, candidate) { + var self = this; - this._sendChannelMessage({ - type: this._SIG_MESSAGE_TYPE.CANDIDATE, - label: event.candidate.sdpMLineIndex, - id: event.candidate.sdpMid, - candidate: event.candidate.candidate, - mid: this._user.sid, - target: targetMid, - rid: this._room.id - }); + if (candidate.candidate) { + var messageCan = candidate.candidate.split(' '); + var candidateType = messageCan[7]; + log.debug([targetMid, 'RTCIceCandidate', null, 'Created and sending ' + + candidateType + ' candidate:'], candidate); + + if (self._forceTURN && candidateType !== 'relay') { + if (!self._hasMCU) { + log.warn([targetMid, 'RTCICECandidate', null, 'Ignoring sending of "' + candidateType + + '" candidate as TURN connections is forced'], candidate); + return; + } + + log.warn([targetMid, 'RTCICECandidate', null, 'Not ignoring sending of "' + candidateType + + '" candidate although TURN connections is forced as MCU is present'], candidate); } + + if (!self._gatheredCandidates[targetMid]) { + self._gatheredCandidates[targetMid] = { + sending: { host: [], srflx: [], relay: [] }, + receiving: { host: [], srflx: [], relay: [] } + }; + } + + self._gatheredCandidates[targetMid].sending[candidateType].push({ + sdpMid: candidate.sdpMid, + sdpMLineIndex: candidate.sdpMLineIndex, + candidate: candidate.candidate + }); + + if (!self._enableIceTrickle) { + log.warn([targetMid, 'RTCICECandidate', null, 'Ignoring sending of "' + candidateType + + '" candidate as trickle ICE is disabled'], candidate); + return; + } + + self._sendChannelMessage({ + type: self._SIG_MESSAGE_TYPE.CANDIDATE, + label: candidate.sdpMLineIndex, + id: candidate.sdpMid, + candidate: candidate.candidate, + mid: self._user.sid, + target: targetMid, + rid: self._room.id + }); + } else { log.debug([targetMid, 'RTCIceCandidate', null, 'End of gathering']); - this._trigger('candidateGenerationState', this.CANDIDATE_GENERATION_STATE.COMPLETED, + self._trigger('candidateGenerationState', self.CANDIDATE_GENERATION_STATE.COMPLETED, targetMid); // Disable Ice trickle option - if (!this._enableIceTrickle || this._peerIceTrickleDisabled[targetMid]) { - var sessionDescription = this._peerConnections[targetMid].localDescription; - this._sendChannelMessage({ + if (!self._enableIceTrickle) { + var sessionDescription = self._peerConnections[targetMid].localDescription; + + // make checks for firefox session description + if (sessionDescription.type === self.HANDSHAKE_PROGRESS.ANSWER && window.webrtcDetectedBrowser === 'firefox') { + sessionDescription.sdp = self._addSDPSsrcFirefoxAnswer(targetMid, sessionDescription.sdp); + } + + self._sendChannelMessage({ type: sessionDescription.type, sdp: sessionDescription.sdp, - mid: this._user.sid, - agent: window.webrtcDetectedBrowser, + mid: self._user.sid, + //agent: window.webrtcDetectedBrowser, + userInfo: self._getUserInfo(), target: targetMid, - rid: this._room.id + rid: self._room.id }); } + + // We should remove this.. this could be due to ICE failures + // Adding this fix is bad + // Does the restart in the case when the candidates are extremely a lot + /*var doACandidateRestart = self._addedCandidates[targetMid].relay.length > 20 && + (window.webrtcDetectedBrowser === 'chrome' || window.webrtcDetectedBrowser === 'opera'); + + log.debug([targetMid, 'RTCIceCandidate', null, 'Relay candidates generated length'], self._addedCandidates[targetMid].relay.length); + + if (doACandidateRestart) { + setTimeout(function () { + if (self._peerConnections[targetMid]) { + if(self._peerConnections[targetMid].iceConnectionState !== self.ICE_CONNECTION_STATE.CONNECTED && + self._peerConnections[targetMid].iceConnectionState !== self.ICE_CONNECTION_STATE.COMPLETED) { + // restart + self._restartPeerConnection(targetMid, true, true, null, false); + } + } + }, self._addedCandidates[targetMid].relay.length * 50); + }*/ } }; /** - * Stores an ICE Candidate received before handshaking + * Function that buffers the Peer connection ICE candidate when received + * before remote session description is received and set. * @method _addIceCandidateToQueue - * @param {String} targetMid The peerId of the target peer. - * @param {Object} candidate The ICE Candidate object. * @private - * @since 0.5.2 - * @component ICE * @for Skylink + * @since 0.5.2 */ Skylink.prototype._addIceCandidateToQueue = function(targetMid, candidate) { log.debug([targetMid, null, null, 'Queued candidate to add after ' + @@ -1551,39 +3250,38 @@ Skylink.prototype._addIceCandidateToQueue = function(targetMid, candidate) { }; /** - * Handles the event when adding ICE Candidate passes. + * Function that handles when the Peer connection received ICE candidate + * has been added or processed successfully. + * Separated in a function to prevent jshint errors. * @method _onAddIceCandidateSuccess * @private - * @since 0.5.9 - * @component ICE * @for Skylink + * @since 0.5.9 */ Skylink.prototype._onAddIceCandidateSuccess = function () { - log.debug([null, 'RTCICECandidate', null, - 'Successfully added ICE candidate']); + log.debug([null, 'RTCICECandidate', null, 'Successfully added ICE candidate']); }; /** - * Handles the event when adding ICE Candidate fails. + * Function that handles when the Peer connection received ICE candidate + * has failed adding or processing. + * Separated in a function to prevent jshint errors. * @method _onAddIceCandidateFailure * @private - * @since 0.5.9 - * @component ICE * @for Skylink + * @since 0.5.9 */ Skylink.prototype._onAddIceCandidateFailure = function (error) { - log.error([null, 'RTCICECandidate', - null, 'Error'], error); + log.error([null, 'RTCICECandidate', null, 'Error'], error); }; /** - * Adds all stored ICE Candidates received before handshaking. + * Function that adds all the Peer connection buffered ICE candidates received. + * This should be called only after the remote session description is received and set. * @method _addIceCandidateFromQueue - * @param {String} targetMid The peerId of the target peer. * @private - * @since 0.5.2 - * @component ICE * @for Skylink + * @since 0.5.2 */ Skylink.prototype._addIceCandidateFromQueue = function(targetMid) { this._peerCandidatesQueue[targetMid] = @@ -1612,228 +3310,323 @@ Skylink.prototype.ICE_CONNECTION_STATE = { }; /** - * The list of TURN server transports. + * + * Note that configuring the protocol may not necessarily result in the desired network transports protocol + * used in the actual TURN network traffic as it depends which protocol the browser selects and connects with. + * This simply configures the TURN ICE server urls query option when constructing + * the Peer connection. When all protocols are selected, the ICE servers urls are duplicated with all protocols. + * + * The list of TURN network transport protocols options when constructing Peer connections + * configured in the init() method. + * Example .urls inital input: ["turn:server.com?transport=tcp", + * "turn:server1.com:3478", "turn:server.com?transport=udp"] * @attribute TURN_TRANSPORT + * @param {String} TCP Value "tcp" + * The value of the option to configure using only TCP network transport protocol. + * Example .urls output: ["turn:server.com?transport=tcp", + * "turn:server1.com:3478?transport=tcp"] + * @param {String} UDP Value "udp" + * The value of the option to configure using only UDP network transport protocol. + * Example .urls output: ["turn:server.com?transport=udp", + * "turn:server1.com:3478?transport=udp"] + * @param {String} ANY Value "any" + * The value of the option to configure using any network transport protocols configured from the Signaling server. + * Example .urls output: ["turn:server.com?transport=tcp", + * "turn:server1.com:3478", "turn:server.com?transport=udp"] + * @param {String} NONE Value "none" + * The value of the option to not configure using any network transport protocols. + * Example .urls output: ["turn:server.com", "turn:server1.com:3478"] + * Configuring this does not mean that no protocols will be used, but + * rather removing ?transport=(protocol) query option in + * the TURN ICE server .urls when constructing the Peer connection. + * @param {String} ALL Value "all" + * The value of the option to configure using both TCP and UDP network transport protocols. + * Example .urls output: ["turn:server.com?transport=tcp", + * "turn:server.com?transport=udp", "turn:server1.com:3478?transport=tcp", + * "turn:server1.com:3478?transport=udp"] * @type JSON - * @param {String} TCP Use only TCP transport option. - * @param {String} UDP Use only UDP transport option. - * @param {String} ANY Use both TCP and UDP transport option. - * @param {String} NONE Set no transport option in TURN servers * @readOnly - * @since 0.5.4 - * @component ICE * @for Skylink + * @since 0.5.4 */ Skylink.prototype.TURN_TRANSPORT = { UDP: 'udp', TCP: 'tcp', ANY: 'any', - NONE: 'none' + NONE: 'none', + ALL: 'all' }; /** - * The flag that indicates if ICE trickle is enabled. + * Stores the flag that indicates if Peer connections should trickle ICE. * @attribute _enableIceTrickle * @type Boolean * @default true * @private - * @required - * @since 0.3.0 - * @component ICE * @for Skylink + * @since 0.3.0 */ Skylink.prototype._enableIceTrickle = true; /** - * The flag that indicates if STUN server is to be used. + * Stores the flag that indicates if STUN ICE servers should be used when constructing Peer connection. * @attribute _enableSTUN * @type Boolean * @default true * @private - * @required - * @component ICE + * @for Skylink * @since 0.5.4 */ Skylink.prototype._enableSTUN = true; /** - * The flag that indicates if TURN server is to be used. + * Stores the flag that indicates if TURN ICE servers should be used when constructing Peer connection. * @attribute _enableTURN * @type Boolean * @default true * @private - * @required - * @component ICE + * @for Skylink * @since 0.5.4 */ Skylink.prototype._enableTURN = true; /** - * The flag that indicates if SSL is used in STUN server connection. - * @attribute _STUNSSL - * @type Boolean - * @default false - * @private - * @required - * @development true - * @unsupported true - * @since 0.5.4 - * @component ICE - * @for Skylink - */ -//Skylink.prototype._STUNSSL = false; - -/** - * The flag that indicates if SSL is used in TURN server connection. - * @attribute _TURNSSL + * Stores the flag that indicates if public STUN ICE servers should be used when constructing Peer connection. + * @attribute _usePublicSTUN * @type Boolean - * @default false + * @default true * @private - * @required - * @development true - * @unsupported true - * @since 0.5.4 - * @component ICE * @for Skylink + * @since 0.6.1 */ -//Skylink.prototype._TURNSSL = false; +Skylink.prototype._usePublicSTUN = true; /** - * The option of transport protocol for TURN servers. + * Stores the option for the TURN protocols to use. + * This should configure the TURN ICE servers urls ?transport=protocol flag. * @attribute _TURNTransport * @type String - * @default Skylink.TURN_TRANSPORT.ANY + * @default "any" * @private * @required - * @since 0.5.4 - * @component ICE * @for Skylink + * @since 0.5.4 */ Skylink.prototype._TURNTransport = 'any'; /** - * Stores the list of ICE connection failures. + * Stores the list of Peer connections ICE failures counter. * @attribute _ICEConnectionFailures + * @param {Number} <#peerId> The Peer connection ICE failures counter. * @type JSON * @private - * @required - * @component Peer * @for Skylink * @since 0.5.8 */ Skylink.prototype._ICEConnectionFailures = {}; -/** - * Sets the STUN server specifically for Firefox ICE Connection. - * @method _setFirefoxIceServers - * @param {JSON} config Ice configuration servers url object. - * @return {JSON} Updated configuration - * @private - * @since 0.1.0 - * @component ICE - * @for Skylink - */ -Skylink.prototype._setFirefoxIceServers = function(config) { - if (window.webrtcDetectedType === 'moz') { - log.log('Updating firefox Ice server configuration', config); - // NOTE ALEX: shoul dbe given by the server - var newIceServers = [{ - 'url': 'stun:stun.services.mozilla.com' - }]; - for (var i = 0; i < config.iceServers.length; i++) { - var iceServer = config.iceServers[i]; - var iceServerType = iceServer.url.split(':')[0]; - if (iceServerType === 'stun') { - if (iceServer.url.indexOf('google')) { - continue; - } - iceServer.url = [iceServer.url]; - newIceServers.push(iceServer); +/** + * Function that filters and configures the ICE servers received from Signaling + * based on the init() configuration and returns the updated + * list of ICE servers to be used when constructing Peer connection. + * @method _setIceServers + * @private + * @for Skylink + * @since 0.5.4 + */ +Skylink.prototype._setIceServers = function(givenConfig) { + var givenIceServers = clone(givenConfig.iceServers); + var iceServersList = {}; + var newIceServers = []; + // TURN SSL config + var useTURNSSLProtocol = false; + var useTURNSSLPort = false; + + + + if (window.location.protocol === 'https:' || this._forceTURNSSL) { + if (window.webrtcDetectedBrowser === 'chrome' || + window.webrtcDetectedBrowser === 'safari' || + window.webrtcDetectedBrowser === 'IE') { + useTURNSSLProtocol = true; + useTURNSSLPort = false; + } else { + useTURNSSLPort = true; + } + } + + log.log('TURN server connections SSL configuration', { + useTURNSSLProtocol: useTURNSSLProtocol, + useTURNSSLPort: useTURNSSLPort + }); + + var pushIceServer = function (username, credential, url, index) { + if (!iceServersList[username]) { + iceServersList[username] = {}; + } + + if (!iceServersList[username][credential]) { + iceServersList[username][credential] = []; + } + + if (iceServersList[username][credential].indexOf(url) === -1) { + if (typeof index === 'number') { + iceServersList[username][credential].splice(index, 0, url); } else { - var newIceServer = {}; - newIceServer.credential = iceServer.credential; - newIceServer.url = iceServer.url.split(':')[0]; - newIceServer.username = iceServer.url.split(':')[1].split('@')[0]; - newIceServer.url += ':' + iceServer.url.split(':')[1].split('@')[1]; - newIceServers.push(newIceServer); + iceServersList[username][credential].push(url); } } - config.iceServers = newIceServers; - log.debug('Updated firefox Ice server configuration: ', config); - } - return config; -}; + }; -/** - * Sets the STUN server specially for Firefox for ICE Connection. - * @method _setIceServers - * @param {JSON} config Ice configuration servers url object. - * @return {JSON} Updated configuration - * @private - * @since 0.5.4 - * @component ICE - * @for Skylink - */ -Skylink.prototype._setIceServers = function(config) { - // firstly, set the STUN server specially for firefox - config = this._setFirefoxIceServers(config); + var i, serverItem; - var newConfig = { - iceServers: [] - }; + for (i = 0; i < givenIceServers.length; i++) { + var server = givenIceServers[i]; - for (var i = 0; i < config.iceServers.length; i++) { - var iceServer = config.iceServers[i]; - var iceServerParts = iceServer.url.split(':'); - // check for stun servers - if (iceServerParts[0] === 'stun' || iceServerParts[0] === 'stuns') { + if (typeof server.url !== 'string') { + log.warn('Ignoring ICE server provided at index ' + i, clone(server)); + continue; + } + + if (server.url.indexOf('stun') === 0) { if (!this._enableSTUN) { - log.log('Removing STUN Server support'); + log.warn('Ignoring STUN server provided at index ' + i, clone(server)); continue; - } else { - // STUNS is unsupported - iceServerParts[0] = (this._STUNSSL) ? 'stuns' : 'stun'; } - iceServer.url = iceServerParts.join(':'); - } - // check for turn servers - if (iceServerParts[0] === 'turn' || iceServerParts[0] === 'turns') { + + if (!this._usePublicSTUN && server.url.indexOf('temasys') === -1) { + log.warn('Ignoring public STUN server provided at index ' + i, clone(server)); + continue; + } + + } else if (server.url.indexOf('turn') === 0) { if (!this._enableTURN) { - log.log('Removing TURN Server support'); + log.warn('Ignoring TURN server provided at index ' + i, clone(server)); continue; + } + + if (server.url.indexOf(':443') === -1 && useTURNSSLPort) { + log.log('Ignoring TURN Server (non-SSL port) provided at index ' + i, clone(server)); + continue; + } + + if (useTURNSSLProtocol) { + var parts = server.url.split(':'); + parts[0] = 'turns'; + server.url = parts.join(':'); + } + } + + // parse "@" settings + if (server.url.indexOf('@') > 0) { + var protocolParts = server.url.split(':'); + var urlParts = protocolParts[1].split('@'); + server.username = urlParts[0]; + server.url = protocolParts[0] + ':' + urlParts[1]; + + // add the ICE server port + if (protocolParts[2]) { + server.url += ':' + protocolParts[2]; + } + } + + var username = typeof server.username === 'string' ? server.username : 'none'; + var credential = typeof server.credential === 'string' ? server.credential : 'none'; + + if (server.url.indexOf('turn') === 0) { + if (this._TURNTransport === this.TURN_TRANSPORT.ANY) { + pushIceServer(username, credential, server.url); + } else { - iceServerParts[0] = (this._TURNSSL) ? 'turns' : 'turn'; - iceServer.url = iceServerParts.join(':'); - // check if requires SSL - log.log('Transport option:', this._TURNTransport); - if (this._TURNTransport !== this.TURN_TRANSPORT.ANY) { - // this has a transport attached to it - if (iceServer.url.indexOf('?transport=') > -1) { - // remove transport because user does not want it - if (this._TURNTransport === this.TURN_TRANSPORT.NONE) { - log.log('Removing transport option'); - iceServer.url = iceServer.url.split('?')[0]; - } else { - // UDP or TCP - log.log('Setting transport option'); - var urlProtocolParts = iceServer.url.split('=')[1]; - urlProtocolParts = this._TURNTransport; - iceServer.url = urlProtocolParts.join('='); + var rawUrl = server.url; + + if (rawUrl.indexOf('?transport=') > 0) { + rawUrl = rawUrl.split('?transport=')[0]; + } + + if (this._TURNTransport === this.TURN_TRANSPORT.NONE) { + pushIceServer(username, credential, rawUrl); + } else if (this._TURNTransport === this.TURN_TRANSPORT.UDP) { + pushIceServer(username, credential, rawUrl + '?transport=udp'); + } else if (this._TURNTransport === this.TURN_TRANSPORT.TCP) { + pushIceServer(username, credential, rawUrl + '?transport=tcp'); + } else if (this._TURNTransport === this.TURN_TRANSPORT.ALL) { + pushIceServer(username, credential, rawUrl + '?transport=tcp'); + pushIceServer(username, credential, rawUrl + '?transport=udp'); + } else { + log.warn('Invalid TURN transport option "' + this._TURNTransport + + '". Ignoring TURN server at index' + i, clone(server)); + continue; + } + } + } else { + pushIceServer(username, credential, server.url); + } + } + + // add mozilla STUN for firefox + if (this._enableSTUN && this._usePublicSTUN && window.webrtcDetectedBrowser === 'firefox') { + pushIceServer('none', 'none', 'stun:stun.services.mozilla.com', 0); + } + + var hasUrlsSupport = false; + + if (window.webrtcDetectedBrowser === 'chrome' && window.webrtcDetectedVersion > 34) { + hasUrlsSupport = true; + } + + if (window.webrtcDetectedBrowser === 'firefox' && window.webrtcDetectedVersion > 38) { + hasUrlsSupport = true; + } + + if (window.webrtcDetectedBrowser === 'opera' && window.webrtcDetectedVersion > 31) { + hasUrlsSupport = true; + } + + // plugin supports .urls + if (window.webrtcDetectedBrowser === 'safari' || window.webrtcDetectedBrowser === 'IE') { + hasUrlsSupport = true; + } + + for (var serverUsername in iceServersList) { + if (iceServersList.hasOwnProperty(serverUsername)) { + for (var serverCred in iceServersList[serverUsername]) { + if (iceServersList[serverUsername].hasOwnProperty(serverCred)) { + if (hasUrlsSupport) { + var urlsItem = { + urls: iceServersList[serverUsername][serverCred] + }; + if (serverUsername !== 'none') { + urlsItem.username = serverUsername; } + if (serverCred !== 'none') { + urlsItem.credential = serverCred; + } + newIceServers.push(urlsItem); } else { - if (this._TURNTransport !== this.TURN_TRANSPORT.NONE) { - log.log('Setting transport option'); - // no transport here. manually add - iceServer.url += '?transport=' + this._TURNTransport; + for (var j = 0; j < iceServersList[serverUsername][serverCred].length; j++) { + var urlItem = { + url: iceServersList[serverUsername][serverCred][j] + }; + if (serverUsername !== 'none') { + urlItem.username = serverUsername; + } + if (serverCred !== 'none') { + urlItem.credential = serverCred; + } + newIceServers.push(urlItem); } } } } } - newConfig.iceServers.push(iceServer); } - log.log('Output iceServers configuration:', newConfig.iceServers); - return newConfig; + + log.log('Output iceServers configuration:', newIceServers); + + return { + iceServers: newIceServers + }; }; Skylink.prototype.PEER_CONNECTION_STATE = { STABLE: 'stable', @@ -1843,69 +3636,734 @@ Skylink.prototype.PEER_CONNECTION_STATE = { }; /** - * Timestamp of the moment when last restart happened. + * The list of getConnectionStatus() + * method retrieval states. + * @attribute GET_CONNECTION_STATUS_STATE + * @param {Number} RETRIEVING Value 0 + * The value of the state when getConnectionStatus() is retrieving the Peer connection stats. + * @param {Number} RETRIEVE_SUCCESS Value 1 + * The value of the state when getConnectionStatus() has retrieved the Peer connection stats successfully. + * @param {Number} RETRIEVE_ERROR Value -1 + * The value of the state when getConnectionStatus() has failed retrieving the Peer connection stats. + * @type JSON + * @readOnly + * @for Skylink + * @since 0.1.0 + */ +Skylink.prototype.GET_CONNECTION_STATUS_STATE = { + RETRIEVING: 0, + RETRIEVE_SUCCESS: 1, + RETRIEVE_ERROR: -1 +}; + +/** + * + * As there are more features getting implemented, there will be eventually more different types of + * server Peers. + * + * The list of available types of server Peer connections. + * @attribute SERVER_PEER_TYPE + * @param {String} MCU Value "mcu" + * The value of the server Peer type that is used for MCU connection. + * @type JSON + * @readOnly + * @for Skylink + * @since 0.6.1 + */ +Skylink.prototype.SERVER_PEER_TYPE = { + MCU: 'mcu' + //SIP: 'sip' +}; + +/** + * Stores the restart initiated timestamp to throttle the refreshConnection functionality. * @attribute _lastRestart * @type Object - * @required * @private - * @component Peer * @for Skylink * @since 0.5.9 */ Skylink.prototype._lastRestart = null; /** - * Counter of the number of consecutive retries. + * Stores the global number of Peer connection retries that would increase the wait-for-response timeout + * for the Peer connection health timer. * @attribute _retryCount - * @type Integer - * @required + * @type Number * @private - * @component Peer * @for Skylink * @since 0.5.10 */ Skylink.prototype._retryCount = 0; /** - * Internal array of Peer connections. + * Stores the list of the Peer connections. * @attribute _peerConnections - * @type Object - * @required + * @param {Object} <#peerId> The Peer connection. + * @type JSON * @private - * @component Peer * @for Skylink * @since 0.1.0 */ -Skylink.prototype._peerConnections = []; +Skylink.prototype._peerConnections = {}; /** - * Stores the list of restart weights received that would be compared against - * to indicate if User should initiates a restart or Peer should. - * In general, the one that sends restart later is the one who initiates the restart. - * @attribute _peerRestartPriorities - * @type JSON + * + * For MCU enabled Peer connections, the restart functionality may differ, you may learn more about how to workaround + * it in this article here. + * For restarts with Peers connecting from Android or iOS SDKs, restarts might not work as written in + * in this article here. + * Note that this functionality should be used when Peer connection stream freezes during a connection, + * and is throttled when invoked many times in less than 3 seconds interval. + * + * Function that refreshes Peer connections to update with the current streaming. + * @method refreshConnection + * @param {String|Array} [targetPeerId] + * Note that this is ignored if MCU is enabled for the App Key provided in + * init() method. refreshConnection() will "refresh" + * all Peer connections. See the Event Sequence for more information. + * The target Peer ID to refresh connection with. + * - When provided as an Array, it will refresh all connections with all the Peer IDs provided. + * - When not provided, it will refresh all the currently connected Peers in the Room. + * @param {Function} [callback] The callback function fired when request has completed. + * Function parameters signature is function (error, success) + * Function request completion is determined by the + * peerRestart event triggering isSelfInitiateRestart parameter payload + * value as true for all Peers targeted for request success. + * @param {JSON} callback.error The error result in request. + * Defined as null when there are no errors in request + * @param {Array} callback.error.listOfPeers The list of Peer IDs targeted. + * @param {JSON} callback.error.refreshErrors The list of Peer connection refresh errors. + * @param {Error|String} callback.error.refreshErrors.#peerId The Peer connection refresh error associated + * with the Peer ID defined in #peerId property. + * If #peerId value is "self", it means that it is the error when there + * is no Peer connections to refresh with. + * @param {JSON} callback.success The success result in request. + * Defined as null when there are errors in request + * @param {Array} callback.success.listOfPeers The list of Peer IDs targeted. + * @trigger + * Checks if MCU is enabled for App Key provided in init() method + * If MCU is enabled: If there are connected Peers in the Room: + * peerRestart event triggers parameter payload + * isSelfInitiateRestart value as true for all connected Peer connections. + * serverPeerRestart event triggers for + * connected MCU server Peer connection. + * Invokes joinRoom() method refreshConnection() + * will retain the User session information except the Peer ID will be a different assigned ID due to restarting the + * Room session. If request has errors ABORT and return error. + * + * Else: If there are connected Peers in the Room: + * Refresh connections for all targeted Peers. + * If Peer connection exists: + * peerRestart event triggers parameter payload + * isSelfInitiateRestart value as true for all targeted Peer connections. + * Else: ABORT and return error. + * + * @example + * // Example 1: Refreshing a Peer connection + * function refreshFrozenVideoStream (peerId) { + * skylinkDemo.refreshConnection(peerId, function (error, success) { + * if (error) return; + * console.log("Refreshing connection for '" + peerId + "'"); + * }); + * } + * + * // Example 2: Refreshing a list of Peer connections + * function refreshFrozenVideoStreamGroup (peerIdA, peerIdB) { + * skylinkDemo.refreshConnection([peerIdA, peerIdB], function (error, success) { + * if (error) { + * if (error.transferErrors[peerIdA]) { + * console.error("Failed refreshing connection for '" + peerIdA + "'"); + * } else { + * console.log("Refreshing connection for '" + peerIdA + "'"); + * } + * if (error.transferErrors[peerIdB]) { + * console.error("Failed refreshing connection for '" + peerIdB + "'"); + * } else { + * console.log("Refreshing connection for '" + peerIdB + "'"); + * } + * } else { + * console.log("Refreshing connection for '" + peerIdA + "' and '" + peerIdB + "'"); + * } + * }); + * } + * + * // Example 3: Refreshing all Peer connections + * function refreshFrozenVideoStreamAll () { + * skylinkDemo.refreshConnection(function (error, success) { + * if (error) { + * for (var i = 0; i < error.listOfPeers.length; i++) { + * if (error.refreshErrors[error.listOfPeers[i]]) { + * console.error("Failed refreshing connection for '" + error.listOfPeers[i] + "'"); + * } else { + * console.info("Refreshing connection for '" + error.listOfPeers[i] + "'"); + * } + * } + * } else { + * console.log("Refreshing connection for all Peers", success.listOfPeers); + * } + * }); + * } + * @for Skylink + * @since 0.5.5 + */ +Skylink.prototype.refreshConnection = function(targetPeerId, callback) { + var self = this; + + var listOfPeers = Object.keys(self._peerConnections); + var listOfPeerRestarts = []; + var error = ''; + var listOfPeerRestartErrors = {}; + + if(Array.isArray(targetPeerId)) { + listOfPeers = targetPeerId; + + } else if (typeof targetPeerId === 'string') { + listOfPeers = [targetPeerId]; + } else if (typeof targetPeerId === 'function') { + callback = targetPeerId; + } + + if (listOfPeers.length === 0) { + error = 'There is currently no peer connections to restart'; + log.warn([null, 'PeerConnection', null, error]); + + listOfPeerRestartErrors.self = new Error(error); + + if (typeof callback === 'function') { + callback({ + refreshErrors: listOfPeerRestartErrors, + listOfPeers: listOfPeers + }, null); + } + return; + } + + self._throttle(function () { + self._refreshPeerConnection(listOfPeers, true, callback); + },5000)(); + +}; + +/** + * Function that refresh connections. + * @method _refreshPeerConnection * @private - * @required * @for Skylink - * @since 0.5.11 + * @since 0.6.15 */ -Skylink.prototype._peerRestartPriorities = {}; +Skylink.prototype._refreshPeerConnection = function(listOfPeers, shouldThrottle, callback) { + var self = this; + var listOfPeerRestarts = []; + var error = ''; + var listOfPeerRestartErrors = {}; + + // To fix jshint dont put functions within a loop + var refreshSinglePeerCallback = function (peerId) { + return function (error, success) { + if (listOfPeerRestarts.indexOf(peerId) === -1) { + if (error) { + log.error([peerId, 'RTCPeerConnection', null, 'Failed restarting for peer'], error); + listOfPeerRestartErrors[peerId] = error; + } + listOfPeerRestarts.push(peerId); + } + + if (listOfPeerRestarts.length === listOfPeers.length) { + if (typeof callback === 'function') { + log.log([null, 'PeerConnection', null, 'Invoked all peers to restart. Firing callback']); + + if (Object.keys(listOfPeerRestartErrors).length > 0) { + callback({ + refreshErrors: listOfPeerRestartErrors, + listOfPeers: listOfPeers + }, null); + } else { + callback(null, { + listOfPeers: listOfPeers + }); + } + } + } + }; + }; + + var refreshSinglePeer = function(peerId, peerCallback){ + if (!self._peerConnections[peerId]) { + error = 'There is currently no existing peer connection made ' + + 'with the peer. Unable to restart connection'; + log.error([peerId, null, null, error]); + listOfPeerRestartErrors[peerId] = new Error(error); + return; + } + + if (shouldThrottle) { + var now = Date.now() || function() { return +new Date(); }; + + if (now - self.lastRestart < 3000) { + error = 'Last restart was so tight. Aborting.'; + log.error([peerId, null, null, error]); + listOfPeerRestartErrors[peerId] = new Error(error); + return; + } + } + + log.log([peerId, 'PeerConnection', null, 'Restarting peer connection']); + + // do a hard reset on variable object + self._restartPeerConnection(peerId, true, false, peerCallback, true); + }; + + if(!self._hasMCU) { + var i; + + for (i = 0; i < listOfPeers.length; i++) { + var peerId = listOfPeers[i]; + + if (Object.keys(self._peerConnections).indexOf(peerId) > -1) { + refreshSinglePeer(peerId, refreshSinglePeerCallback(peerId)); + } else { + error = 'Peer connection with peer does not exists. Unable to restart'; + log.error([peerId, 'PeerConnection', null, error]); + listOfPeerRestartErrors[peerId] = new Error(error); + } + + // there's an error to trigger for + if (i === listOfPeers.length - 1 && Object.keys(listOfPeerRestartErrors).length > 0) { + if (typeof callback === 'function') { + callback({ + refreshErrors: listOfPeerRestartErrors, + listOfPeers: listOfPeers + }, null); + } + } + } + } else { + self._restartMCUConnection(callback); + } +}; + +/** + * Function that retrieves Peer connection bandwidth and ICE connection stats. + * @method getConnectionStatus + * @param {String|Array} [targetPeerId] The target Peer ID to retrieve connection stats from. + * - When provided as an Array, it will retrieve all connection stats from all the Peer IDs provided. + * - When not provided, it will retrieve all connection stats from the currently connected Peers in the Room. + * @param {Function} [callback] The callback function fired when request has completed. + * Function parameters signature is function (error, success) + * Function request completion is determined by the + * getConnectionStatusStateChange event triggering state parameter payload + * value as RETRIEVE_SUCCESS for all Peers targeted for request success. + * [Rel: Skylink.GET_CONNECTION_STATUS_STATE] + * @param {JSON} callback.error The error result in request. + * Defined as null when there are no errors in request + * @param {Array} callback.error.listOfPeers The list of Peer IDs targeted. + * @param {JSON} callback.error.retrievalErrors The list of Peer connection stats retrieval errors. + * @param {Error|String} callback.error.retrievalErrors.#peerId The Peer connection stats retrieval error associated + * with the Peer ID defined in #peerId property. + * If #peerId value is "self", it means that it is the error when there + * are no Peer connections to refresh with. + * @param {JSON} callback.error.connectionStats The list of Peer connection stats. + * These are the Peer connection stats that has been managed to be successfully retrieved. + * @param {JSON} callback.error.connectionStats.#peerId The Peer connection stats associated with + * the Peer ID defined in #peerId property. + * Object signature matches the stats parameter payload received in the + * getConnectionStatusStateChange event. + * @param {JSON} callback.success The success result in request. + * Defined as null when there are errors in request + * @param {Array} callback.success.listOfPeers The list of Peer IDs targeted. + * @param {JSON} callback.success.connectionStats The list of Peer connection stats. + * @param {JSON} callback.success.connectionStats.#peerId The Peer connection stats associated with + * the Peer ID defined in #peerId property. + * Object signature matches the stats parameter payload received in the + * getConnectionStatusStateChange event. + * @trigger + * Retrieves Peer connection stats for all targeted Peers. + * If Peer connection has closed or does not exists: This can be checked with + * peerConnectionState event + * triggering parameter payload state as CLOSED for Peer. + * getConnectionStatusStateChange event + * triggers parameter payload state as RETRIEVE_ERROR. + * ABORT and return error. + * getConnectionStatusStateChange event + * triggers parameter payload state as RETRIEVING. + * Received response from retrieval. + * If retrieval was successful: + * getConnectionStatusStateChange event + * triggers parameter payload state as RETRIEVE_SUCCESS. + * Else: + * getConnectionStatusStateChange event + * triggers parameter payload state as RETRIEVE_ERROR. + * + * @example + * // Example 1: Retrieve a Peer connection stats + * function startBWStatsInterval (peerId) { + * setInterval(function () { + * skylinkDemo.getConnectionStatus(peerId, function (error, success) { + * if (error) return; + * var sendVideoBytes = success.connectionStats[peerId].video.sending.bytes; + * var sendAudioBytes = success.connectionStats[peerId].audio.sending.bytes; + * var recvVideoBytes = success.connectionStats[peerId].video.receiving.bytes; + * var recvAudioBytes = success.connectionStats[peerId].audio.receiving.bytes; + * var localCandidate = success.connectionStats[peerId].selectedCandidate.local; + * var remoteCandidate = success.connectionStats[peerId].selectedCandidate.remote; + * console.log("Sending audio (" + sendAudioBytes + "bps) video (" + sendVideoBytes + ")"); + * console.log("Receiving audio (" + recvAudioBytes + "bps) video (" + recvVideoBytes + ")"); + * console.log("Local candidate: " + localCandidate.ipAddress + ":" + localCandidate.portNumber + + * "?transport=" + localCandidate.transport + " (type: " + localCandidate.candidateType + ")"); + * console.log("Remote candidate: " + remoteCandidate.ipAddress + ":" + remoteCandidate.portNumber + + * "?transport=" + remoteCandidate.transport + " (type: " + remoteCandidate.candidateType + ")"); + * }); + * }, 1000); + * } + * + * // Example 2: Retrieve a list of Peer connection stats + * function printConnStats (peerId, data) { + * if (!data.connectionStats[peerId]) return; + * var sendVideoBytes = data.connectionStats[peerId].video.sending.bytes; + * var sendAudioBytes = data.connectionStats[peerId].audio.sending.bytes; + * var recvVideoBytes = data.connectionStats[peerId].video.receiving.bytes; + * var recvAudioBytes = data.connectionStats[peerId].audio.receiving.bytes; + * var localCandidate = data.connectionStats[peerId].selectedCandidate.local; + * var remoteCandidate = data.connectionStats[peerId].selectedCandidate.remote; + * console.log(peerId + " - Sending audio (" + sendAudioBytes + "bps) video (" + sendVideoBytes + ")"); + * console.log(peerId + " - Receiving audio (" + recvAudioBytes + "bps) video (" + recvVideoBytes + ")"); + * console.log(peerId + " - Local candidate: " + localCandidate.ipAddress + ":" + localCandidate.portNumber + + * "?transport=" + localCandidate.transport + " (type: " + localCandidate.candidateType + ")"); + * console.log(peerId + " - Remote candidate: " + remoteCandidate.ipAddress + ":" + remoteCandidate.portNumber + + * "?transport=" + remoteCandidate.transport + " (type: " + remoteCandidate.candidateType + ")"); + * } + * + * function startBWStatsInterval (peerIdA, peerIdB) { + * setInterval(function () { + * skylinkDemo.getConnectionStatus([peerIdA, peerIdB], function (error, success) { + * if (error) { + * printConnStats(peerIdA, error.connectionStats); + * printConnStats(peerIdB, error.connectionStats); + * } else { + * printConnStats(peerIdA, success.connectionStats); + * printConnStats(peerIdB, success.connectionStats); + * } + * }); + * }, 1000); + * } + * + * // Example 3: Retrieve all Peer connection stats + * function printConnStats (listOfPeers, data) { + * listOfPeers.forEach(function (peerId) { + * if (!data.connectionStats[peerId]) return; + * var sendVideoBytes = data.connectionStats[peerId].video.sending.bytes; + * var sendAudioBytes = data.connectionStats[peerId].audio.sending.bytes; + * var recvVideoBytes = data.connectionStats[peerId].video.receiving.bytes; + * var recvAudioBytes = data.connectionStats[peerId].audio.receiving.bytes; + * var localCandidate = data.connectionStats[peerId].selectedCandidate.local; + * var remoteCandidate = data.connectionStats[peerId].selectedCandidate.remote; + * console.log(peerId + " - Sending audio (" + sendAudioBytes + "bps) video (" + sendVideoBytes + ")"); + * console.log(peerId + " - Receiving audio (" + recvAudioBytes + "bps) video (" + recvVideoBytes + ")"); + * console.log(peerId + " - Local candidate: " + localCandidate.ipAddress + ":" + localCandidate.portNumber + + * "?transport=" + localCandidate.transport + " (type: " + localCandidate.candidateType + ")"); + * console.log(peerId + " - Remote candidate: " + remoteCandidate.ipAddress + ":" + remoteCandidate.portNumber + + * "?transport=" + remoteCandidate.transport + " (type: " + remoteCandidate.candidateType + ")"); + * }); + * } + * + * function startBWStatsInterval (peerIdA, peerIdB) { + * setInterval(function () { + * skylinkDemo.getConnectionStatus(function (error, success) { + * if (error) { + * printConnStats(error.listOfPeers, error.connectionStats); + * } else { + * printConnStats(success.listOfPeers, success.connectionStats); + * } + * }); + * }, 1000); + * } + * @for Skylink + * @since 0.6.14 + */ +Skylink.prototype.getConnectionStatus = function (targetPeerId, callback) { + var self = this; + var listOfPeers = Object.keys(self._peerConnections); + var listOfPeerStats = {}; + var listOfPeerErrors = {}; + + // getConnectionStatus([]) + if (Array.isArray(targetPeerId)) { + listOfPeers = targetPeerId; + + // getConnectionStatus('...') + } else if (typeof targetPeerId === 'string' && !!targetPeerId) { + listOfPeers = [targetPeerId]; + + // getConnectionStatus(function () {}) + } else if (typeof targetPeerId === 'function') { + callback = targetPeerId; + targetPeerId = undefined; + } + + // Check if Peers list is empty, in which we throw an Error if there isn't any + if (listOfPeers.length === 0) { + listOfPeerErrors.self = new Error('There is currently no peer connections to retrieve connection status'); + + log.error([null, 'RTCStatsReport', null, 'Retrieving request failure ->'], listOfPeerErrors.self); + + if (typeof callback === 'function') { + callback({ + listOfPeers: listOfPeers, + retrievalErrors: listOfPeerErrors, + connectionStats: listOfPeerStats + }, null); + } + return; + } + + var completedTaskCounter = []; + + var checkCompletedFn = function (peerId) { + if (completedTaskCounter.indexOf(peerId) === -1) { + completedTaskCounter.push(peerId); + } + + if (completedTaskCounter.length === listOfPeers.length) { + if (typeof callback === 'function') { + if (Object.keys(listOfPeerErrors).length > 0) { + callback({ + listOfPeers: listOfPeers, + retrievalErrors: listOfPeerErrors, + connectionStats: listOfPeerStats + }, null); + + } else { + callback(null, { + listOfPeers: listOfPeers, + connectionStats: listOfPeerStats + }); + } + } + } + }; + + var statsFn = function (peerId) { + log.debug([peerId, 'RTCStatsReport', null, 'Retrieivng connection status']); + + var pc = self._peerConnections[peerId]; + var result = { + raw: null, + connection: { + iceConnectionState: pc.iceConnectionState, + iceGatheringState: pc.iceGatheringState, + signalingState: pc.signalingState, + remoteDescription: pc.remoteDescription, + localDescription: pc.localDescription, + candidates: clone(self._gatheredCandidates[peerId] || { + sending: { host: [], srflx: [], relay: [] }, + receiving: { host: [], srflx: [], relay: [] } + }) + }, + audio: { + sending: { + ssrc: null, + bytes: 0, + packets: 0, + packetsLost: 0, + rtt: 0 + }, + receiving: { + ssrc: null, + bytes: 0, + packets: 0, + packetsLost: 0 + } + }, + video: { + sending: { + ssrc: null, + bytes: 0, + packets: 0, + packetsLost: 0, + rtt: 0 + }, + receiving: { + ssrc: null, + bytes: 0, + packets: 0, + packetsLost: 0 + } + }, + selectedCandidate: { + local: { ipAddress: null, candidateType: null, portNumber: null, transport: null }, + remote: { ipAddress: null, candidateType: null, portNumber: null, transport: null } + } + }; + var loopFn = function (obj, fn) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop) && obj[prop]) { + fn(obj[prop], prop); + } + } + }; + var formatCandidateFn = function (candidateDirType, candidate) { + result.selectedCandidate[candidateDirType].ipAddress = candidate.ipAddress; + result.selectedCandidate[candidateDirType].candidateType = candidate.candidateType; + result.selectedCandidate[candidateDirType].portNumber = typeof candidate.portNumber !== 'number' ? + parseInt(candidate.portNumber, 10) || null : candidate.portNumber; + result.selectedCandidate[candidateDirType].transport = candidate.transport; + }; + + pc.getStats(null, function (stats) { + log.debug([peerId, 'RTCStatsReport', null, 'Retrieval success ->'], stats); + + result.raw = stats; + + if (window.webrtcDetectedBrowser === 'firefox') { + loopFn(stats, function (obj, prop) { + var dirType = ''; + + // Receiving/Sending RTP packets + if (prop.indexOf('inbound_rtp') === 0 || prop.indexOf('outbound_rtp') === 0) { + dirType = prop.indexOf('inbound_rtp') === 0 ? 'receiving' : 'sending'; + + result[obj.mediaType][dirType].bytes = dirType === 'sending' ? obj.bytesSent : obj.bytesReceived; + result[obj.mediaType][dirType].packets = dirType === 'sending' ? obj.packetsSent : obj.packetsReceived; + result[obj.mediaType][dirType].ssrc = obj.ssrc; + + if (dirType === 'receiving') { + result[obj.mediaType][dirType].packetsLost = obj.packetsLost || 0; + } + + // Sending RTP packets lost + } else if (prop.indexOf('outbound_rtcp') === 0) { + dirType = prop.indexOf('inbound_rtp') === 0 ? 'receiving' : 'sending'; + + result[obj.mediaType][dirType].packetsLost = obj.packetsLost || 0; + + if (dirType === 'sending') { + result[obj.mediaType].sending.rtt = obj.mozRtt || 0; + } + + // Candidates + } else if (obj.nominated && obj.selected) { + formatCandidateFn('remote', stats[obj.remoteCandidateId]); + formatCandidateFn('local', stats[obj.localCandidateId]); + } + }); + + } else if (window.webrtcDetectedBrowser === 'edge') { + if (pc.getRemoteStreams().length > 0) { + var tracks = pc.getRemoteStreams()[0].getTracks(); + + loopFn(tracks, function (track) { + loopFn(stats, function (obj, prop) { + if (obj.type === 'track' && obj.trackIdentifier === track.id) { + loopFn(stats, function (streamObj) { + if (streamObj.associateStatsId === obj.id && + ['outboundrtp', 'inboundrtp'].indexOf(streamObj.type) > -1) { + var dirType = streamObj.type === 'outboundrtp' ? 'sending' : 'receiving'; + + result[track.kind][dirType].bytes = dirType === 'sending' ? streamObj.bytesSent : streamObj.bytesReceived; + result[track.kind][dirType].packets = dirType === 'sending' ? streamObj.packetsSent : streamObj.packetsReceived; + result[track.kind][dirType].packetsLost = streamObj.packetsLost || 0; + result[track.kind][dirType].ssrc = parseInt(streamObj.ssrc || '0', 10); + + if (dirType === 'sending') { + result[track.kind].sending.rtt = obj.roundTripTime || 0; + } + } + }); + } + }); + }); + } + + } else { + var reportedCandidate = false; + + loopFn(stats, function (obj, prop) { + if (prop.indexOf('ssrc_') === 0) { + var dirType = prop.indexOf('_recv') > 0 ? 'receiving' : 'sending'; + + // Polyfill fix for plugin. Plugin should fix this though + if (!obj.mediaType) { + obj.mediaType = obj.hasOwnProperty('audioOutputLevel') || + obj.hasOwnProperty('audioInputLevel') ? 'audio' : 'video'; + } + + // Receiving/Sending RTP packets + result[obj.mediaType][dirType].bytes = parseInt((dirType === 'receiving' ? + obj.bytesReceived : obj.bytesSent) || '0', 10); + result[obj.mediaType][dirType].packets = parseInt((dirType === 'receiving' ? + obj.packetsReceived : obj.packetsSent) || '0', 10); + result[obj.mediaType][dirType].ssrc = parseInt(obj.ssrc || '0', 10); + result[obj.mediaType][dirType].packetsLost = parseInt(obj.packetsLost || '0', 10); + + if (dirType === 'sending') { + // NOTE: Chrome sending audio does have it but plugin has.. + result[obj.mediaType].sending.rtt = parseInt(obj.googRtt || '0', 10); + } + + if (!reportedCandidate) { + loopFn(stats, function (canObj, canProp) { + if (!reportedCandidate && canProp.indexOf('Conn-') === 0) { + if (obj.transportId === canObj.googChannelId) { + formatCandidateFn('local', stats[canObj.localCandidateId]); + formatCandidateFn('remote', stats[canObj.remoteCandidateId]); + reportedCandidate = true; + } + } + }); + } + } + }); + } + + listOfPeerStats[peerId] = result; + + self._trigger('getConnectionStatusStateChange', self.GET_CONNECTION_STATUS_STATE.RETRIEVE_SUCCESS, + peerId, listOfPeerStats[peerId], null); + + checkCompletedFn(peerId); + + }, function (error) { + log.error([peerId, 'RTCStatsReport', null, 'Retrieval failure ->'], error); + + listOfPeerErrors[peerId] = error; + + self._trigger('getConnectionStatusStateChange', self.GET_CONNECTION_STATUS_STATE.RETRIEVE_ERROR, + peerId, null, error); + + checkCompletedFn(peerId); + }); + }; + + // Loop through all the list of Peers selected to retrieve connection status + for (var i = 0; i < listOfPeers.length; i++) { + var peerId = listOfPeers[i]; + + self._trigger('getConnectionStatusStateChange', self.GET_CONNECTION_STATUS_STATE.RETRIEVING, + peerId, null, null); + + // Check if the Peer connection exists first + if (self._peerConnections.hasOwnProperty(peerId) && self._peerConnections[peerId]) { + statsFn(peerId); + + } else { + listOfPeerErrors[peerId] = new Error('The peer connection object does not exists'); + + log.error([peerId, 'RTCStatsReport', null, 'Retrieval failure ->'], listOfPeerErrors[peerId]); + + self._trigger('getConnectionStatusStateChange', self.GET_CONNECTION_STATUS_STATE.RETRIEVE_ERROR, + peerId, null, listOfPeerErrors[peerId]); + + checkCompletedFn(peerId); + } + } +}; /** - * Initiates a Peer connection with either a response to an answer or starts - * a connection with an offer. + * Function that starts the Peer connection session. + * Remember to remove previous method of reconnection (re-creating the Peer connection - destroy and create connection). * @method _addPeer - * @param {String} targetMid PeerId of the peer we should connect to. - * @param {JSON} peerBrowser The peer browser information. - * @param {String} peerBrowser.agent The peer browser agent. - * @param {Number} peerBrowser.version The peer browser version. - * @param {Number} peerBrowser.os The peer operating system. - * @param {Boolean} [toOffer=false] Whether we should start the O/A or wait. - * @param {Boolean} [restartConn=false] Whether connection is restarted. - * @param {Boolean} [receiveOnly=false] Should they only receive? - * @param {Boolean} [isSS=false] Should the incoming stream labelled as screensharing mode? - * @private - * @component Peer + * @private * @for Skylink * @since 0.5.4 */ @@ -1927,34 +4385,38 @@ Skylink.prototype._addPeer = function(targetMid, peerBrowser, toOffer, restartCo if (!restartConn) { self._peerConnections[targetMid] = self._createPeerConnection(targetMid, !!isSS); } + + if (!self._peerConnections[targetMid]) { + log.error([targetMid, null, null, 'Failed creating the connection to peer']); + return; + } + self._peerConnections[targetMid].receiveOnly = !!receiveOnly; self._peerConnections[targetMid].hasScreen = !!isSS; if (!receiveOnly) { self._addLocalMediaStreams(targetMid); } // I'm the callee I need to make an offer - if (toOffer) { - if (self._enableDataChannel) { - self._dataChannels[targetMid] = self._createDataChannel(targetMid); - } + /*if (toOffer) { self._doOffer(targetMid, peerBrowser); - } + }*/ // do a peer connection health check - this._startPeerConnectionHealthCheck(targetMid, toOffer); + // let MCU handle this case + if (!self._hasMCU) { + this._startPeerConnectionHealthCheck(targetMid, toOffer); + } else { + log.warn([targetMid, 'PeerConnectionHealth', null, 'Not setting health timer for MCU connection']); + return; + } }; /** - * Restarts a Peer connection. + * Function that re-negotiates a Peer connection. + * We currently do not implement the ICE restart functionality. + * Remember to remove previous method of reconnection (re-creating the Peer connection - destroy and create connection). * @method _restartPeerConnection - * @param {String} peerId PeerId of the peer to restart connection with. - * @param {Boolean} isSelfInitiatedRestart Indicates whether the restarting action - * was caused by self. - * @param {Boolean} isConnectionRestart The flag that indicates whether the restarting action - * is caused by connectivity issues. - * @param {Function} [callback] The callback once restart peer connection is completed. * @private - * @component Peer * @for Skylink * @since 0.5.8 */ @@ -1967,121 +4429,141 @@ Skylink.prototype._restartPeerConnection = function (peerId, isSelfInitiatedRest return; } - log.log([peerId, null, null, 'Restarting a peer connection']); - - // get the value of receiveOnly - var receiveOnly = self._peerConnections[peerId] ? - !!self._peerConnections[peerId].receiveOnly : false; - var hasScreenSharing = self._peerConnections[peerId] ? - !!self._peerConnections[peerId].hasScreen : false; - - // close the peer connection and remove the reference - var iceConnectionStateClosed = false; - var peerConnectionStateClosed = false; - var dataChannelStateClosed = !self._enableDataChannel; - - self._peerConnections[peerId].dataChannelClosed = true; - - self.once('iceConnectionState', function () { - iceConnectionStateClosed = true; - }, function (state, currentPeerId) { - return state === self.ICE_CONNECTION_STATE.CLOSED && peerId === currentPeerId; - }); - - self.once('peerConnectionState', function () { - peerConnectionStateClosed = true; - }, function (state, currentPeerId) { - return state === self.PEER_CONNECTION_STATE.CLOSED && peerId === currentPeerId; - }); - delete self._peerConnectionHealth[peerId]; - delete self._peerRestartPriorities[peerId]; self._stopPeerConnectionHealthCheck(peerId); - if (self._peerConnections[peerId].signalingState !== 'closed') { - self._peerConnections[peerId].close(); - } + var pc = self._peerConnections[peerId]; - if (self._peerConnections[peerId].hasStream) { - self._trigger('streamEnded', peerId, self.getPeerInfo(peerId), false); - } + var agent = (self.getPeerInfo(peerId) || {}).agent || {}; - self._wait(function () { + // prevent restarts for other SDK clients + if (['Android', 'iOS', 'cpp'].indexOf(agent.name) > -1) { + var notSupportedError = new Error('Failed restarting with other agents connecting from other SDKs as ' + + 're-negotiation is not supported by other SDKs'); - log.log([peerId, null, null, 'Ice and peer connections closed']); + log.warn([peerId, 'RTCPeerConnection', null, 'Ignoring restart request as agent\'s SDK does not support it'], + notSupportedError); - delete self._peerConnections[peerId]; + if (typeof callback === 'function') { + log.debug([peerId, 'RTCPeerConnection', null, 'Firing restart failure callback']); + callback(null, notSupportedError); + } + return; + } - log.log([peerId, null, null, 'Re-creating peer connection']); + // This is when the state is stable and re-handshaking is possible + // This could be due to previous connection handshaking that is already done + if (pc.signalingState === self.PEER_CONNECTION_STATE.STABLE) { + if (self._peerConnections[peerId] && !self._peerConnections[peerId].receiveOnly) { + self._addLocalMediaStreams(peerId); + } - self._peerConnections[peerId] = self._createPeerConnection(peerId, !!hasScreenSharing); + if (isSelfInitiatedRestart){ + log.log([peerId, null, null, 'Sending restart message to signaling server']); - // Set one second tiemout before sending the offer or the message gets received - setTimeout(function () { - self._peerConnections[peerId].receiveOnly = receiveOnly; - self._peerConnections[peerId].hasScreen = hasScreenSharing; + var lastRestart = Date.now() || function() { return +new Date(); }; - if (!receiveOnly) { - self._addLocalMediaStreams(peerId); - } + self._sendChannelMessage({ + type: self._SIG_MESSAGE_TYPE.RESTART, + mid: self._user.sid, + rid: self._room.id, + agent: window.webrtcDetectedBrowser, + version: window.webrtcDetectedVersion, + os: window.navigator.platform, + userInfo: self._getUserInfo(), + target: peerId, + isConnectionRestart: !!isConnectionRestart, + lastRestart: lastRestart, + // This will not be used based off the logic in _restartHandler + weight: self._peerPriorityWeight, + receiveOnly: self._peerConnections[peerId] && self._peerConnections[peerId].receiveOnly, + enableIceTrickle: self._enableIceTrickle, + enableDataChannel: self._enableDataChannel, + sessionType: !!self._streams.screenshare ? 'screensharing' : 'stream', + explicit: !!explicit, + temasysPluginVersion: AdapterJS.WebRTCPlugin.plugin ? AdapterJS.WebRTCPlugin.plugin.VERSION : null + }); - if (isSelfInitiatedRestart){ - log.log([peerId, null, null, 'Sending restart message to signaling server']); + self._trigger('peerRestart', peerId, self.getPeerInfo(peerId), false); - var lastRestart = Date.now() || function() { return +new Date(); }; + if (typeof callback === 'function') { + log.debug([peerId, 'RTCPeerConnection', null, 'Firing restart callback']); + callback(null, null); + } + } else { + if (typeof callback === 'function') { + log.debug([peerId, 'RTCPeerConnection', null, 'Firing restart callback (receiving peer)']); + callback(null, null); + } + } - var weight = (new Date()).valueOf(); - self._peerRestartPriorities[peerId] = weight; + // following the previous logic to do checker always + self._startPeerConnectionHealthCheck(peerId, false); + } else { + // Let's check if the signalingState is stable first. + // In another galaxy or universe, where the local description gets dropped.. + // In the offerHandler or answerHandler, do the appropriate flags to ignore or drop "extra" descriptions + if (pc.signalingState === self.PEER_CONNECTION_STATE.HAVE_LOCAL_OFFER) { + // Checks if the local description is defined first + var hasLocalDescription = pc.localDescription && pc.localDescription.sdp; + // By then it should have at least the local description.. + if (hasLocalDescription) { self._sendChannelMessage({ - type: self._SIG_MESSAGE_TYPE.RESTART, + type: pc.localDescription.type, + sdp: pc.localDescription.sdp, mid: self._user.sid, - rid: self._room.id, - agent: window.webrtcDetectedBrowser, - version: window.webrtcDetectedVersion, - os: window.navigator.platform, - userInfo: self.getPeerInfo(), target: peerId, - isConnectionRestart: !!isConnectionRestart, - lastRestart: lastRestart, - weight: weight, - receiveOnly: receiveOnly, - enableIceTrickle: self._enableIceTrickle, - enableDataChannel: self._enableDataChannel, - sessionType: !!self._mediaScreen ? 'screensharing' : 'stream', - explicit: !!explicit + rid: self._room.id, + restart: true + }); + } else { + var noLocalDescriptionError = 'Failed re-sending localDescription as there is ' + + 'no localDescription set to connection. There could be a handshaking step error'; + log.error([peerId, 'RTCPeerConnection', null, noLocalDescriptionError], { + localDescription: pc.localDescription, + remoteDescription: pc.remoteDescription }); + if (typeof callback === 'function') { + log.debug([peerId, 'RTCPeerConnection', null, 'Firing restart failure callback']); + callback(null, new Error(noLocalDescriptionError)); + } } - - self._trigger('peerRestart', peerId, self._peerInformations[peerId] || {}, true); - - if (typeof callback === 'function'){ - log.log('Firing callback'); - callback(); + // It could have connection state closed + } else { + var unableToRestartError = 'Failed restarting as peer connection state is ' + pc.signalingState; + log.warn([peerId, 'RTCPeerConnection', null, unableToRestartError]); + if (typeof callback === 'function') { + log.debug([peerId, 'RTCPeerConnection', null, 'Firing restart failure callback']); + callback(null, new Error(unableToRestartError)); } - }, 1000); - }, function () { - return iceConnectionStateClosed && peerConnectionStateClosed; - }); + } + } }; /** - * Removes and closes a Peer connection. + * Function that ends the Peer connection session. * @method _removePeer - * @param {String} peerId PeerId of the peer to close connection. - * @trigger peerLeft * @private - * @component Peer * @for Skylink * @since 0.5.5 */ Skylink.prototype._removePeer = function(peerId) { + var peerInfo = clone(this.getPeerInfo(peerId)) || { + userData: '', + settings: {}, + mediaStatus: {}, + agent: {}, + room: clone(this._selectedRoom) + }; + if (peerId !== 'MCU') { - this._trigger('peerLeft', peerId, this._peerInformations[peerId], false); + this._trigger('peerLeft', peerId, peerInfo, false); } else { this._hasMCU = false; log.log([peerId, null, null, 'MCU has stopped listening and left']); + this._trigger('serverPeerLeft', peerId, this.SERVER_PEER_TYPE.MCU); } // stop any existing peer health timer this._stopPeerConnectionHealthCheck(peerId); @@ -2101,17 +4583,15 @@ Skylink.prototype._removePeer = function(peerId) { delete this._peerConnections[peerId]; } - - // check the handshake priorities and remove them accordingly - if (typeof this._peerHSPriorities[peerId] !== 'undefined') { - delete this._peerHSPriorities[peerId]; - } - if (typeof this._peerRestartPriorities[peerId] !== 'undefined') { - delete this._peerRestartPriorities[peerId]; - } + // remove peer informations session if (typeof this._peerInformations[peerId] !== 'undefined') { delete this._peerInformations[peerId]; } + // remove peer messages stamps session + if (typeof this._peerMessagesStamps[peerId] !== 'undefined') { + delete this._peerMessagesStamps[peerId]; + } + if (typeof this._peerConnectionHealth[peerId] !== 'undefined') { delete this._peerConnectionHealth[peerId]; } @@ -2124,20 +4604,16 @@ Skylink.prototype._removePeer = function(peerId) { }; /** - * Creates a Peer connection to communicate with the peer whose ID is 'targetMid'. - * All the peerconnection callbacks are set up here. This is a quite central piece. + * Function that creates the Peer connection. * @method _createPeerConnection - * @param {String} targetMid The target peer Id. - * @param {Boolean} [isScreenSharing=false] The flag that indicates if incoming - * stream is screensharing mode. - * @return {Object} The created peer connection object. * @private - * @component Peer * @for Skylink * @since 0.5.1 */ Skylink.prototype._createPeerConnection = function(targetMid, isScreenSharing) { var pc, self = this; + // currently the AdapterJS 0.12.1-2 causes an issue to prevent firefox from + // using .urls feature try { pc = new window.RTCPeerConnection( self._room.connection.peerConfig, @@ -2156,28 +4632,78 @@ Skylink.prototype._createPeerConnection = function(targetMid, isScreenSharing) { pc.setAnswer = ''; pc.hasStream = false; pc.hasScreen = !!isScreenSharing; + pc.hasMainChannel = false; + pc.firefoxStreamId = ''; + pc.processingLocalSDP = false; + pc.processingRemoteSDP = false; + pc.gathered = false; + + // datachannels + self._dataChannels[targetMid] = {}; + // candidates + self._gatheredCandidates[targetMid] = { + sending: { host: [], srflx: [], relay: [] }, + receiving: { host: [], srflx: [], relay: [] } + }; + // callbacks // standard not implemented: onnegotiationneeded, pc.ondatachannel = function(event) { var dc = event.channel || event; log.debug([targetMid, 'RTCDataChannel', dc.label, 'Received datachannel ->'], dc); if (self._enableDataChannel) { - self._dataChannels[targetMid] = self._createDataChannel(targetMid, dc); + + var channelType = self.DATA_CHANNEL_TYPE.DATA; + var channelKey = dc.label; + + // if peer does not have main channel, the first item is main + if (!pc.hasMainChannel) { + channelType = self.DATA_CHANNEL_TYPE.MESSAGING; + channelKey = 'main'; + pc.hasMainChannel = true; + } + + self._dataChannels[targetMid][channelKey] = + self._createDataChannel(targetMid, channelType, dc, dc.label); + } else { - log.warn([targetMid, 'RTCDataChannel', dc.label, 'Not adding datachannel']); + log.warn([targetMid, 'RTCDataChannel', dc.label, 'Not adding datachannel as enable datachannel ' + + 'is set to false']); } }; pc.onaddstream = function(event) { + var stream = event.stream || event; + + if (targetMid === 'MCU') { + log.debug([targetMid, 'MediaStream', stream.id, 'Ignoring received remote stream from MCU ->'], stream); + return; + } + pc.hasStream = true; - log.info('Remote stream', event, !!pc.hasScreen); + var agent = (self.getPeerInfo(targetMid) || {}).agent || {}; + var timeout = 0; - self._onRemoteStreamAdded(targetMid, event, !!pc.hasScreen); + // NOTE: Add timeouts to the firefox stream received because it seems to have some sort of black stream rendering at first + // This may not be advisable but that it seems to work after 1500s. (tried with ICE established but it does not work and getStats) + if (agent.name === 'firefox' && window.webrtcDetectedBrowser !== 'firefox') { + timeout = 1500; + } + setTimeout(function () { + self._onRemoteStreamAdded(targetMid, stream, !!pc.hasScreen); + }, timeout); }; pc.onicecandidate = function(event) { - log.debug([targetMid, 'RTCIceCandidate', null, 'Ice candidate generated ->'], - event.candidate); - self._onIceCandidate(targetMid, event); + var candidate = event.candidate || event; + + if (candidate.candidate) { + pc.gathered = false; + } else { + pc.gathered = true; + } + + log.debug([targetMid, 'RTCIceCandidate', null, 'Ice candidate generated ->'], candidate); + self._onIceCandidate(targetMid, candidate); }; pc.oniceconnectionstatechange = function(evt) { checkIceConnectionState(targetMid, pc.iceConnectionState, @@ -2201,19 +4727,18 @@ Skylink.prototype._createPeerConnection = function(targetMid, isScreenSharing) { self._ICEConnectionFailures[targetMid] = 0; } - if (self._ICEConnectionFailures[targetMid] > 2) { - self._peerIceTrickleDisabled[targetMid] = true; - } - if (iceConnectionState === self.ICE_CONNECTION_STATE.FAILED) { self._ICEConnectionFailures[targetMid] += 1; - if (self._enableIceTrickle && !self._peerIceTrickleDisabled[targetMid]) { + if (self._enableIceTrickle) { self._trigger('iceConnectionState', self.ICE_CONNECTION_STATE.TRICKLE_FAILED, targetMid); } - // refresh when failed - self._restartPeerConnection(targetMid, true, true, null, false); + + // refresh when failed. ignore for MCU case since restart is handled by MCU in this case + if (!self._hasMCU) { + self._restartPeerConnection(targetMid, true, true, null, false); + } } /**** SJS-53: Revert of commit ****** @@ -2228,7 +4753,7 @@ Skylink.prototype._createPeerConnection = function(targetMid, isScreenSharing) { rid: self._room.id, agent: window.webrtcDetectedBrowser, version: window.webrtcDetectedVersion, - userInfo: self.getPeerInfo(), + userInfo: self._getUserInfo(), target: targetMid, restartNego: true, hsPriority: -1 @@ -2261,136 +4786,198 @@ Skylink.prototype._createPeerConnection = function(targetMid, isScreenSharing) { 'Ice gathering state changed ->'], pc.iceGatheringState); self._trigger('candidateGenerationState', pc.iceGatheringState, targetMid); }; + + if (window.webrtcDetectedBrowser === 'firefox') { + pc.removeStream = function (stream) { + var senders = pc.getSenders(); + for (var s = 0; s < senders.length; s++) { + var tracks = stream.getTracks(); + for (var t = 0; t < tracks.length; t++) { + if (tracks[t] === senders[s].track) { + pc.removeTrack(senders[s]); + } + } + } + }; + } + return pc; }; /** - * Refreshes a Peer connection with a connected peer. - * If there are more than 1 refresh during 5 seconds - * or refresh is less than 3 seconds since the last refresh - * initiated by the other peer, it will be aborted. - * @method refreshConnection - * @param {String} [peerId] The peerId of the peer to refresh the connection. - * @example - * SkylinkDemo.on('iceConnectionState', function (state, peerId)) { - * if (iceConnectionState === SkylinkDemo.ICE_CONNECTION_STATE.FAILED) { - * // Do a refresh - * SkylinkDemo.refreshConnection(peerId); - * } - * }); - * @component Peer + * Function that handles the _restartPeerConnection scenario + * for MCU enabled Peer connections. + * This is implemented currently by making the user leave and join the Room again. + * The Peer ID will not stay the same though. + * @method _restartMCUConnection + * @private * @for Skylink - * @since 0.5.5 + * @since 0.6.1 */ -Skylink.prototype.refreshConnection = function(peerId) { +Skylink.prototype._restartMCUConnection = function(callback) { var self = this; + log.info([self._user.sid, null, null, 'Restarting with MCU enabled']); + // Save room name + /*var roomName = (self._room.id).substring((self._room.id) + .indexOf('_api_') + 5, (self._room.id).length);*/ + var listOfPeers = Object.keys(self._peerConnections); + var listOfPeerRestartErrors = {}; + var peerId; // j shint is whinning + var receiveOnly = false; + // for MCU case, these dont matter at all + var lastRestart = Date.now() || function() { return +new Date(); }; + var weight = (new Date()).valueOf(); - if (self._hasMCU) { - log.warn([peerId, 'PeerConnection', null, 'Restart functionality for peer\'s connection ' + - 'for MCU is not yet supported']); - return; - } + self._trigger('serverPeerRestart', 'MCU', self.SERVER_PEER_TYPE.MCU); - var refreshSinglePeer = function(peer){ - var fn = function () { - if (!self._peerConnections[peer]) { - log.error([peer, null, null, 'There is currently no existing peer connection made ' + - 'with the peer. Unable to restart connection']); - return; - } + for (var i = 0; i < listOfPeers.length; i++) { + peerId = listOfPeers[i]; - var now = Date.now() || function() { return +new Date(); }; + if (!self._peerConnections[peerId]) { + var error = 'Peer connection with peer does not exists. Unable to restart'; + log.error([peerId, 'PeerConnection', null, error]); + listOfPeerRestartErrors[peerId] = new Error(error); + continue; + } - if (now - self.lastRestart < 3000) { - log.error([peer, null, null, 'Last restart was so tight. Aborting.']); - return; + if (peerId === 'MCU') { + receiveOnly = !!self._peerConnections[peerId].receiveOnly; + } + + if (peerId !== 'MCU') { + self._trigger('peerRestart', peerId, self.getPeerInfo(peerId), true); + + log.log([peerId, null, null, 'Sending restart message to signaling server']); + + self._sendChannelMessage({ + type: self._SIG_MESSAGE_TYPE.RESTART, + mid: self._user.sid, + rid: self._room.id, + agent: window.webrtcDetectedBrowser, + version: window.webrtcDetectedVersion, + os: window.navigator.platform, + userInfo: self._getUserInfo(), + target: peerId, //'MCU', + isConnectionRestart: false, + lastRestart: lastRestart, + weight: self._peerPriorityWeight, + receiveOnly: receiveOnly, + enableIceTrickle: self._enableIceTrickle, + enableDataChannel: self._enableDataChannel, + sessionType: !!self._streams.screenshare ? 'screensharing' : 'stream', + explicit: true, + temasysPluginVersion: AdapterJS.WebRTCPlugin.plugin ? AdapterJS.WebRTCPlugin.plugin.VERSION : null + }); + } + } + + // Restart with MCU = peer leaves then rejoins room + var peerJoinedFn = function (peerId, peerInfo, isSelf) { + log.log([null, 'PeerConnection', null, 'Invoked all peers to restart with MCU. Firing callback']); + + if (typeof callback === 'function') { + if (Object.keys(listOfPeerRestartErrors).length > 0) { + callback({ + refreshErrors: listOfPeerRestartErrors, + listOfPeers: listOfPeers + }, null); + } else { + callback(null, { + listOfPeers: listOfPeers + }); } - // do a hard reset on variable object - self._restartPeerConnection(peer, true, false, null, true); - }; - fn(); + } }; - var toRefresh = function(){ - if (typeof peerId !== 'string') { - for (var key in self._peerConnections) { - if (self._peerConnections.hasOwnProperty(key)) { - refreshSinglePeer(key); + self.once('peerJoined', peerJoinedFn, function (peerId, peerInfo, isSelf) { + return isSelf; + }); + + self.leaveRoom(false, function (error, success) { + if (error) { + if (typeof callback === 'function') { + for (var i = 0; i < listOfPeers.length; i++) { + listOfPeerRestartErrors[listOfPeers[i]] = error; } + callback({ + refreshErrors: listOfPeerRestartErrors, + listOfPeers: listOfPeers + }, null); } } else { - refreshSinglePeer(peerId); + //self._trigger('serverPeerLeft', 'MCU', self.SERVER_PEER_TYPE.MCU); + self.joinRoom(self._selectedRoom); } - }; - - self._throttle(toRefresh,5000)(); - + }); }; -Skylink.prototype._peerInformations = []; +Skylink.prototype._peerInformations = {}; /** - * Stores the User information, credential and the local stream(s). + * Stores the Signaling user credentials from the API response required for connecting to the Signaling server. * @attribute _user + * @param {String} uid The API result "username". + * @param {String} token The API result "userCred". + * @param {String} timeStamp The API result "timeStamp". + * @param {String} sid The Signaling server receive user Peer ID. * @type JSON - * @param {String} uid The user's session id. - * @param {String} sid The user's secret id. This is the id used as the peerId. - * @param {String} timestamp The user's timestamp. - * @param {String} token The user's access token. - * @required * @private - * @component User * @for Skylink * @since 0.5.6 */ Skylink.prototype._user = null; /** - * User's custom data set. + * Stores the User custom data. + * By default, if no custom user data is set, it is an empty string "". * @attribute _userData * @type JSON|String - * @required + * @default "" * @private - * @component User * @for Skylink * @since 0.5.6 */ Skylink.prototype._userData = ''; /** - * Update/Set the User custom data. This Data can be a simple string or a JSON data. - * It is let to user choice to decide how this information must be handled. - * The Skylink demos provided use this parameter as a string for displaying user name. - * - Please note that the custom data would be totally overwritten. - * - If you want to modify only some data, please call - * {{#crossLink "Skylink/getUserData:method"}}getUserData(){{/crossLink}} - * and then modify the information you want individually. - * - {{#crossLink "Skylink/peerUpdated:event"}}peerUpdated{{/crossLink}} - * event fires only if setUserData() is called after - * joining a room. + * Function that overwrites the User current custom data. * @method setUserData - * @param {JSON|String} userData User custom data. + * @param {JSON|String} userData The updated custom data. + * @trigger + * Updates User custom data. + * If User is in Room: + * peerUpdated event triggers with parameter payload + * isSelf value as true. * @example - * // Example 1: Intial way of setting data before user joins the room - * SkylinkDemo.setUserData({ - * displayName: 'Bobby Rays', - * fbUserId: '1234' + * // Example 1: Set/Update User custom data before joinRoom() + * var userData = "beforejoin"; + * + * skylinkDemo.setUserData(userData); + * + * skylinkDemo.joinRoom(function (error, success) { + * if (error) return; + * if (success.peerInfo.userData === userData) { + * console.log("User data is sent"); + * } * }); * - * // Example 2: Way of setting data after user joins the room - * var userData = SkylinkDemo.getUserData(); - * userData.displayName = 'New Name'; - * userData.fbUserId = '1234'; - * SkylinkDemo.setUserData(userData); - * @trigger peerUpdated - * @component User + * // Example 2: Update User custom data after joinRoom() + * var userData = "afterjoin"; + * + * skylinkDemo.joinRoom(function (error, success) { + * if (error) return; + * skylinkDemo.setUserData(userData); + * if (skylinkDemo.getPeerInfo().userData === userData) { + * console.log("User data is updated and sent"); + * } + * }); * @for Skylink * @since 0.5.5 */ Skylink.prototype.setUserData = function(userData) { var self = this; - // NOTE ALEX: be smarter and copy fields and only if different - self._parseUserData(userData); + + this._userData = userData || ''; if (self._inRoom) { log.log('Updated userData -> ', userData); @@ -2398,7 +4985,8 @@ Skylink.prototype.setUserData = function(userData) { type: self._SIG_MESSAGE_TYPE.UPDATE_USER, mid: self._user.sid, rid: self._room.id, - userData: self._userData + userData: self._userData, + stamp: (new Date()).getTime() }); self._trigger('peerUpdated', self._user.sid, self.getPeerInfo(), true); } else { @@ -2407,18 +4995,18 @@ Skylink.prototype.setUserData = function(userData) { }; /** - * Gets the User custom data. - * See {{#crossLink "Skylink/setUserData:method"}}setUserData(){{/crossLink}} - * for more information + * Function that returns the User / Peer current custom data. * @method getUserData - * @return {JSON|String} User custom data. + * @param {String} [peerId] The Peer ID to return the current custom data from. + * - When not provided or that the Peer ID is does not exists, it will return + * the User current custom data. + * @return {JSON|String} The User / Peer current custom data. * @example - * // Example 1: To get other peer's userData - * var peerData = SkylinkDemo.getUserData(peerId); + * // Example 1: Get Peer current custom data + * var peerUserData = skylinkDemo.getUserData(peerId); * - * // Example 2: To get own userData - * var userData = SkylinkDemo.getUserData(); - * @component User + * // Example 2: Get User current custom data + * var userUserData = skylinkDemo.getUserData(); * @for Skylink * @since 0.5.10 */ @@ -2437,70 +5025,88 @@ Skylink.prototype.getUserData = function(peerId) { }; /** - * Gets the Peer information (media settings,media status and personnal data set by the peer). - * @method _parseUserData - * @param {JSON} [userData] User custom data. - * @private - * @component User - * @for Skylink - * @since 0.5.6 - */ -Skylink.prototype._parseUserData = function(userData) { - log.debug('Parsing user data:', userData); - - this._userData = userData || ''; -}; - -/** - * Gets the Peer information. - * - If there is no information related to the peer, null would be returned. + * Function that returns the User / Peer current session information. * @method getPeerInfo - * @param {String} [peerId] The peerId of the peer retrieve we want to retrieve the information. - * Leave this blank to return the User information. - * @return {JSON} Peer information. Please reference - * {{#crossLink "Skylink/peerJoined:event"}}peerJoined{{/crossLink}} - * peerInfo parameter. + * @param {String} [peerId] The Peer ID to return the current session information from. + * - When not provided or that the Peer ID is does not exists, it will return + * the User current session information. + * @return {JSON} The User / Peer current session information. + * Object signature matches the peerInfo parameter payload received in the + * peerJoined event. * @example - * // Example 1: To get other peer's information - * var peerInfo = SkylinkDemo.getPeerInfo(peerId); + * // Example 1: Get Peer current session information + * var peerPeerInfo = skylinkDemo.getPeerInfo(peerId); * - * // Example 2: To get own information - * var userInfo = SkylinkDemo.getPeerInfo(); - * @component Peer + * // Example 2: Get User current session information + * var userPeerInfo = skylinkDemo.getPeerInfo(); * @for Skylink * @since 0.4.0 */ Skylink.prototype.getPeerInfo = function(peerId) { - if (peerId && peerId !== this._user.sid) { - // peer info - var peerInfo = this._peerInformations[peerId]; - - if (typeof peerInfo === 'object') { - return peerInfo; + var peerInfo = null; + + if (typeof peerId === 'string' && typeof this._peerInformations[peerId] === 'object') { + peerInfo = clone(this._peerInformations[peerId]); + peerInfo.room = clone(this._selectedRoom); + + if (peerInfo.settings.video && peerInfo.settings.video.frameRate === -1) { + delete peerInfo.settings.video.frameRate; } - return null; } else { - // user info - // prevent undefined error - this._user = this._user || {}; - this._userData = this._userData || ''; - - this._mediaStreamsStatus = this._mediaStreamsStatus || {}; - this._streamSettings = this._streamSettings || {}; - - return { - userData: this._userData, - settings: this._streamSettings, - mediaStatus: this._mediaStreamsStatus, + peerInfo = { + userData: clone(this._userData) || '', + settings: { + audio: false, + video: false + }, + mediaStatus: clone(this._streamsMutedSettings), agent: { name: window.webrtcDetectedBrowser, - version: window.webrtcDetectedVersion - } + version: window.webrtcDetectedVersion, + os: window.navigator.platform, + pluginVersion: AdapterJS.WebRTCPlugin.plugin ? AdapterJS.WebRTCPlugin.plugin.VERSION : null + }, + room: clone(this._selectedRoom) }; + + if (this._streams.screenshare) { + peerInfo.settings = clone(this._streams.screenshare.settings); + } else if (this._streams.userMedia) { + peerInfo.settings = clone(this._streams.userMedia.settings); + } + } + + if (!peerInfo.settings.audio) { + peerInfo.mediaStatus.audioMuted = true; + } + + if (!peerInfo.settings.video) { + peerInfo.mediaStatus.videoMuted = true; } + + return peerInfo; }; +/** + * Function that returns the User session information to be sent to Peers. + * @method _getUserInfo + * @private + * @for Skylink + * @since 0.4.0 + */ +Skylink.prototype._getUserInfo = function(peerId) { + var userInfo = clone(this.getPeerInfo()); + + if (userInfo.settings.video && !userInfo.settings.video.frameRate) { + userInfo.settings.video.frameRate = -1; + } + + delete userInfo.agent; + delete userInfo.room; + + return userInfo; +}; Skylink.prototype.HANDSHAKE_PROGRESS = { ENTER: 'enter', WELCOME: 'welcome', @@ -2510,133 +5116,145 @@ Skylink.prototype.HANDSHAKE_PROGRESS = { }; /** - * Stores the list of setTimeout awaiting for successful connection. + * Stores the list of Peer connection health timers. + * This timers sets a timeout which checks and waits if Peer connection is successfully established, + * or else it will attempt to re-negotiate with the Peer connection again. * @attribute _peerConnectionHealthTimers + * @param {Object} <#peerId> The Peer connection health timer. * @type JSON * @private - * @required - * @component Peer * @for Skylink * @since 0.5.5 */ Skylink.prototype._peerConnectionHealthTimers = {}; /** - * Stores the list of stable Peer connection. + * Stores the list of Peer connection "healthy" flags, which indicates if Peer connection is + * successfully established, and when the health timers expires, it will clear the timer + * and not attempt to re-negotiate with the Peer connection again. * @attribute _peerConnectionHealth + * @param {Boolean} <#peerId> The flag that indicates if Peer connection has been successfully established. * @type JSON * @private - * @required - * @component Peer * @since 0.5.5 */ Skylink.prototype._peerConnectionHealth = {}; /** - * Stores the list of handshaking weights received that would be compared against - * to indicate if User should send an "offer" or Peer should. - * @attribute _peerHSPriorities - * @type JSON + * Stores the User connection priority weight. + * If Peer has a higher connection weight, it will do the offer from its Peer connection first. + * @attribute _peerPriorityWeight + * @type Number * @private - * @required * @for Skylink * @since 0.5.0 */ -Skylink.prototype._peerHSPriorities = {}; +Skylink.prototype._peerPriorityWeight = 0; /** - * Creates an offer to Peer to initate Peer connection. + * Function that creates the Peer connection offer session description. * @method _doOffer - * @param {String} targetMid PeerId of the peer to send offer to. - * @param {JSON} peerBrowser The peer browser information. - * @param {String} peerBrowser.agent The peer browser agent. - * @param {Number} peerBrowser.version The peer browser version. - * @param {Number} peerBrowser.os The peer browser operating system. * @private * @for Skylink - * @component Peer * @since 0.5.2 */ Skylink.prototype._doOffer = function(targetMid, peerBrowser) { var self = this; var pc = self._peerConnections[targetMid] || self._addPeer(targetMid, peerBrowser); + log.log([targetMid, null, null, 'Checking caller status'], peerBrowser); - // NOTE ALEX: handle the pc = 0 case, just to be sure - var inputConstraints = self._room.connection.offerConstraints; - var sc = self._room.connection.sdpConstraints; - for (var name in sc.mandatory) { - if (sc.mandatory.hasOwnProperty(name)) { - inputConstraints.mandatory[name] = sc.mandatory[name]; - } - } - inputConstraints.optional.concat(sc.optional); - checkMediaDataChannelSettings(peerBrowser.agent, peerBrowser.version, - function(beOfferer, unifiedOfferConstraints) { - // attempt to force make firefox not to offer datachannel. - // we will not be using datachannel in MCU + + // Added checks to ensure that connection object is defined first + if (!pc) { + log.warn([targetMid, 'RTCSessionDescription', 'offer', 'Dropping of creating of offer ' + + 'as connection does not exists']); + return; + } + + // Added checks to ensure that state is "stable" if setting local "offer" + if (pc.signalingState !== self.PEER_CONNECTION_STATE.STABLE) { + log.warn([targetMid, 'RTCSessionDescription', 'offer', + 'Dropping of creating of offer as signalingState is not "' + + self.PEER_CONNECTION_STATE.STABLE + '" ->'], pc.signalingState); + return; + } + + var offerConstraints = { + offerToReceiveAudio: true, + offerToReceiveVideo: true + }; + + // NOTE: Removing ICE restart functionality as of now since Firefox does not support it yet + // Check if ICE connection failed or disconnected, and if so, do an ICE restart + /*if ([self.ICE_CONNECTION_STATE.DISCONNECTED, self.ICE_CONNECTION_STATE.FAILED].indexOf(pc.iceConnectionState) > -1) { + offerConstraints.iceRestart = true; + }*/ + + // Prevent undefined OS errors + peerBrowser.os = peerBrowser.os || ''; + + /* + Ignoring these old codes as Firefox 39 and below is no longer supported if (window.webrtcDetectedType === 'moz' && peerBrowser.agent === 'MCU') { unifiedOfferConstraints.mandatory = unifiedOfferConstraints.mandatory || {}; unifiedOfferConstraints.mandatory.MozDontOfferDataChannel = true; beOfferer = true; } - // for windows firefox to mac chrome interopability - if (window.webrtcDetectedBrowser === 'firefox' && - window.navigator.platform.indexOf('Win') === 0 && - peerBrowser.agent !== 'firefox' && - peerBrowser.os.indexOf('Mac') === 0) { - beOfferer = false; + if (window.webrtcDetectedBrowser === 'firefox' && window.webrtcDetectedVersion >= 32) { + unifiedOfferConstraints = { + offerToReceiveAudio: true, + offerToReceiveVideo: true + }; } + */ - if (beOfferer) { - if (window.webrtcDetectedBrowser === 'firefox' && window.webrtcDetectedVersion >= 32) { - unifiedOfferConstraints = { - offerToReceiveAudio: true, - offerToReceiveVideo: true - }; - - if (window.webrtcDetectedVersion > 37) { - unifiedOfferConstraints = {}; - } + // Fallback to use mandatory constraints for plugin based browsers + if (['IE', 'safari'].indexOf(window.webrtcDetectedBrowser) > -1) { + offerConstraints = { + mandatory: { + OfferToReceiveAudio: true, + OfferToReceiveVideo: true } + }; + } - log.debug([targetMid, null, null, 'Creating offer with config:'], unifiedOfferConstraints); + if (self._enableDataChannel) { + if (typeof self._dataChannels[targetMid] !== 'object') { + log.error([targetMid, 'RTCDataChannel', null, 'Create offer error as unable to create datachannel ' + + 'as datachannels array is undefined'], self._dataChannels[targetMid]); + return; + } - pc.createOffer(function(offer) { - log.debug([targetMid, null, null, 'Created offer'], offer); - self._setLocalAndSendMessage(targetMid, offer); - }, function(error) { - self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ERROR, - targetMid, error); - log.error([targetMid, null, null, 'Failed creating an offer:'], error); - }, unifiedOfferConstraints); - } else { - log.debug([targetMid, null, null, 'User\'s browser is not eligible to create ' + - 'the offer to the other peer. Requesting other peer to create the offer instead' - ], peerBrowser); - self._sendChannelMessage({ - type: self._SIG_MESSAGE_TYPE.WELCOME, - mid: self._user.sid, - rid: self._room.id, - agent: window.webrtcDetectedBrowser, - version: window.webrtcDetectedVersion, - os: window.navigator.platform, - userInfo: self.getPeerInfo(), - target: targetMid, - weight: -1, - sessionType: !!self._mediaScreen ? 'screensharing' : 'stream' - }); + // Edge doesn't support datachannels yet + if (!self._dataChannels[targetMid].main && window.webrtcDetectedBrowser !== 'edge') { + self._dataChannels[targetMid].main = + self._createDataChannel(targetMid, self.DATA_CHANNEL_TYPE.MESSAGING, null, targetMid); + self._peerConnections[targetMid].hasMainChannel = true; } - }, inputConstraints); + } + + log.debug([targetMid, null, null, 'Creating offer with config:'], offerConstraints); + + pc.createOffer(function(offer) { + log.debug([targetMid, null, null, 'Created offer'], offer); + + self._setLocalAndSendMessage(targetMid, offer); + + }, function(error) { + self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ERROR, targetMid, error); + + log.error([targetMid, null, null, 'Failed creating an offer:'], error); + + }, offerConstraints); }; /** - * Creates an answer to Peer as a response to Peer's offer. + * Function that creates the Peer connection answer session description. + * This comes after receiving and setting the offer session description. * @method _doAnswer - * @param {String} targetMid PeerId of the peer to send answer to. * @private * @for Skylink - * @component Peer * @since 0.1.0 */ Skylink.prototype._doAnswer = function(targetMid) { @@ -2644,47 +5262,56 @@ Skylink.prototype._doAnswer = function(targetMid) { log.log([targetMid, null, null, 'Creating answer with config:'], self._room.connection.sdpConstraints); var pc = self._peerConnections[targetMid]; - if (pc) { - pc.createAnswer(function(answer) { - log.debug([targetMid, null, null, 'Created answer'], answer); - self._setLocalAndSendMessage(targetMid, answer); - }, function(error) { - log.error([targetMid, null, null, 'Failed creating an answer:'], error); - self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ERROR, targetMid, error); - });//, self._room.connection.sdpConstraints); - } else { - /* Houston ..*/ - log.error([targetMid, null, null, 'Requested to create an answer but user ' + - 'does not have any existing connection to peer']); + + // Added checks to ensure that connection object is defined first + if (!pc) { + log.warn([targetMid, 'RTCSessionDescription', 'answer', 'Dropping of creating of answer ' + + 'as connection does not exists']); + return; + } + + // Added checks to ensure that state is "have-remote-offer" if setting local "answer" + if (pc.signalingState !== self.PEER_CONNECTION_STATE.HAVE_REMOTE_OFFER) { + log.warn([targetMid, 'RTCSessionDescription', 'answer', + 'Dropping of creating of answer as signalingState is not "' + + self.PEER_CONNECTION_STATE.HAVE_REMOTE_OFFER + '" ->'], pc.signalingState); return; } + + // No ICE restart constraints for createAnswer as it fails in chrome 48 + // { iceRestart: true } + pc.createAnswer(function(answer) { + log.debug([targetMid, null, null, 'Created answer'], answer); + self._setLocalAndSendMessage(targetMid, answer); + }, function(error) { + log.error([targetMid, null, null, 'Failed creating an answer:'], error); + self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ERROR, targetMid, error); + });//, self._room.connection.sdpConstraints); }; /** - * Starts a Peer connection health check. - * The health timers waits for connection, and within 1m if there is not connection, - * it attempts a reconnection. + * Function that starts the Peer connection health timer. + * To count as a "healthy" successful established Peer connection, the + * ICE connection state has to be "connected" or "completed", + * messaging Datachannel type state has to be "opened" (if Datachannel is enabled) + * and Signaling state has to be "stable". + * Should consider dropping of counting messaging Datachannel type being opened as + * it should not involve the actual Peer connection for media (audio/video) streaming. * @method _startPeerConnectionHealthCheck - * @param {String} peerId The peerId of the peer to set a connection timeout if connection failed. - * @param {Boolean} toOffer The flag to check if peer is offerer. If the peer is offerer, - * the restart check should be increased. * @private - * @component Peer * @for Skylink * @since 0.5.5 */ Skylink.prototype._startPeerConnectionHealthCheck = function (peerId, toOffer) { var self = this; + var timer = self._enableIceTrickle ? (toOffer ? 12500 : 10000) : 50000; + timer = (self._hasMCU) ? 105000 : timer; - if (self._hasMCU) { - log.warn([peerId, 'PeerConnectionHealth', null, 'Check for peer\'s connection health ' + - 'for MCU is not yet supported']); - return; - } - - var timer = (self._enableIceTrickle && !self._peerIceTrickleDisabled[peerId]) ? - (toOffer ? 12500 : 10000) : 50000; - //timer = (self._hasMCU) ? 85000 : timer; + // increase timeout for android/ios + /*var agent = self.getPeerInfo(peerId).agent; + if (['Android', 'iOS'].indexOf(agent.name) > -1) { + timer = 105000; + }*/ timer += self._retryCount*10000; @@ -2698,7 +5325,29 @@ Skylink.prototype._startPeerConnectionHealthCheck = function (peerId, toOffer) { self._peerConnectionHealthTimers[peerId] = setTimeout(function () { // re-handshaking should start here. - if (!self._peerConnectionHealth[peerId]) { + var connectionStable = false; + var pc = self._peerConnections[peerId]; + + if (pc) { + var dc = (self._dataChannels[peerId] || {}).main; + + var dcConnected = pc.hasMainChannel ? dc && dc.readyState === self.DATA_CHANNEL_STATE.OPEN : true; + var iceConnected = pc.iceConnectionState === self.ICE_CONNECTION_STATE.CONNECTED || + pc.iceConnectionState === self.ICE_CONNECTION_STATE.COMPLETED; + var signalingConnected = pc.signalingState === self.PEER_CONNECTION_STATE.STABLE; + + connectionStable = dcConnected && iceConnected && signalingConnected; + + log.debug([peerId, 'PeerConnectionHealth', null, 'Connection status'], { + dcConnected: dcConnected, + iceConnected: iceConnected, + signalingConnected: signalingConnected + }); + } + + log.debug([peerId, 'PeerConnectionHealth', null, 'Require reconnection?'], connectionStable); + + if (!connectionStable) { log.warn([peerId, 'PeerConnectionHealth', null, 'Peer\'s health timer ' + 'has expired'], 10000); @@ -2715,17 +5364,23 @@ Skylink.prototype._startPeerConnectionHealthCheck = function (peerId, toOffer) { } // do a complete clean - self._restartPeerConnection(peerId, true, true, null, false); + if (!self._hasMCU) { + self._restartPeerConnection(peerId, true, true, null, false); + } else { + self._restartMCUConnection(); + } + } else { + self._peerConnectionHealth[peerId] = true; } }, timer); }; /** - * Stops a Peer connection health check. + * Function that stops the Peer connection health timer. + * This happens when Peer connection has been successfully established or when + * Peer leaves the Room. * @method _stopPeerConnectionHealthCheck - * @param {String} peerId The peerId of the peer to clear the checking. * @private - * @component Peer * @for Skylink * @since 0.5.5 */ @@ -2746,14 +5401,11 @@ Skylink.prototype._stopPeerConnectionHealthCheck = function (peerId) { }; /** - * Sets a generated session description and sends to Peer. + * Function that sets the local session description and sends to Peer. + * If trickle ICE is disabled, the local session description will be sent after + * ICE gathering has been completed. * @method _setLocalAndSendMessage - * @param {String} targetMid PeerId of the peer to send offer/answer to. - * @param {JSON} sessionDescription This should be provided by the peerconnection API. - * User might 'tamper' with it, but then , the setLocal may fail. - * @trigger handshakeProgress * @private - * @component Peer * @for Skylink * @since 0.5.2 */ @@ -2761,7 +5413,7 @@ Skylink.prototype._setLocalAndSendMessage = function(targetMid, sessionDescripti var self = this; var pc = self._peerConnections[targetMid]; - if (sessionDescription.type === self.HANDSHAKE_PROGRESS.ANSWER && pc.setAnswer) { + /*if (sessionDescription.type === self.HANDSHAKE_PROGRESS.ANSWER && pc.setAnswer) { log.log([targetMid, 'RTCSessionDescription', sessionDescription.type, 'Ignoring session description. User has already set local answer'], sessionDescription); return; @@ -2770,29 +5422,57 @@ Skylink.prototype._setLocalAndSendMessage = function(targetMid, sessionDescripti log.log([targetMid, 'RTCSessionDescription', sessionDescription.type, 'Ignoring session description. User has already set local offer'], sessionDescription); return; + }*/ + + // Added checks to ensure that sessionDescription is defined first + if (!(!!sessionDescription && !!sessionDescription.sdp)) { + log.warn([targetMid, 'RTCSessionDescription', null, 'Dropping of setting local unknown sessionDescription ' + + 'as received sessionDescription is empty ->'], sessionDescription); + return; + } + + // Added checks to ensure that connection object is defined first + if (!pc) { + log.warn([targetMid, 'RTCSessionDescription', sessionDescription.type, 'Dropping of setting local "' + + sessionDescription.type + '" as connection does not exists']); + return; + } + + // Added checks to ensure that state is "stable" if setting local "offer" + if (sessionDescription.type === self.HANDSHAKE_PROGRESS.OFFER && + pc.signalingState !== self.PEER_CONNECTION_STATE.STABLE) { + log.warn([targetMid, 'RTCSessionDescription', sessionDescription.type, + 'Dropping of setting local "offer" as signalingState is not "' + + self.PEER_CONNECTION_STATE.STABLE + '" ->'], pc.signalingState); + return; + + // Added checks to ensure that state is "have-remote-offer" if setting local "answer" + } else if (sessionDescription.type === self.HANDSHAKE_PROGRESS.ANSWER && + pc.signalingState !== self.PEER_CONNECTION_STATE.HAVE_REMOTE_OFFER) { + log.warn([targetMid, 'RTCSessionDescription', sessionDescription.type, + 'Dropping of setting local "answer" as signalingState is not "' + + self.PEER_CONNECTION_STATE.HAVE_REMOTE_OFFER + '" ->'], pc.signalingState); + return; } + // NOTE ALEX: handle the pc = 0 case, just to be sure var sdpLines = sessionDescription.sdp.split('\r\n'); // remove h264 invalid pref sdpLines = self._removeSDPFirefoxH264Pref(sdpLines); + // Check if stereo was enabled - if (self._streamSettings.hasOwnProperty('audio')) { - if (self._streamSettings.audio.stereo) { + if (self._streams.userMedia && self._streams.userMedia.settings.audio) { + if (self._streams.userMedia.settings.stereo) { + log.info([targetMid, null, null, 'Enabling OPUS stereo flag']); self._addSDPStereo(sdpLines); } } - log.info([targetMid, null, null, 'Requested stereo:'], (self._streamSettings.audio ? - (self._streamSettings.audio.stereo ? self._streamSettings.audio.stereo : false) : - false)); - - // set sdp bitrate - if (self._streamSettings.hasOwnProperty('bandwidth')) { - var peerSettings = (self._peerInformations[targetMid] || {}).settings || {}; - - sdpLines = self._setSDPBitrate(sdpLines, peerSettings); + // Set SDP max bitrate + if (self._streamsBandwidthSettings) { + sdpLines = self._setSDPBitrate(sdpLines, self._streamsBandwidthSettings); } // set sdp resolution @@ -2800,24 +5480,20 @@ Skylink.prototype._setLocalAndSendMessage = function(targetMid, sessionDescripti sdpLines = self._setSDPVideoResolution(sdpLines, self._streamSettings.video); }*/ - self._streamSettings.bandwidth = self._streamSettings.bandwidth || {}; - - self._streamSettings.video = self._streamSettings.video || false; - - log.info([targetMid, null, null, 'Custom bandwidth settings:'], { + /*log.info([targetMid, null, null, 'Custom bandwidth settings:'], { audio: (self._streamSettings.bandwidth.audio || 'Not set') + ' kB/s', video: (self._streamSettings.bandwidth.video || 'Not set') + ' kB/s', data: (self._streamSettings.bandwidth.data || 'Not set') + ' kB/s' - }); + });*/ - if (self._streamSettings.video.hasOwnProperty('frameRate') && + /*if (self._streamSettings.video.hasOwnProperty('frameRate') && self._streamSettings.video.hasOwnProperty('resolution')){ log.info([targetMid, null, null, 'Custom resolution settings:'], { frameRate: (self._streamSettings.video.frameRate || 'Not set') + ' fps', width: (self._streamSettings.video.resolution.width || 'Not set') + ' px', height: (self._streamSettings.video.resolution.height || 'Not set') + ' px' }); - } + }*/ // set video codec if (self._selectedVideoCodec !== self.VIDEO_CODEC.AUTO) { @@ -2835,6 +5511,25 @@ Skylink.prototype._setLocalAndSendMessage = function(targetMid, sessionDescripti sessionDescription.sdp = sdpLines.join('\r\n'); + var removeVP9AptRtxPayload = false; + var agent = (self._peerInformations[targetMid] || {}).agent || {}; + + if (agent.pluginVersion) { + // 0.8.870 supports + var parts = agent.pluginVersion.split('.'); + removeVP9AptRtxPayload = parseInt(parts[0], 10) >= 0 && parseInt(parts[1], 10) >= 8 && + parseInt(parts[2], 10) >= 870; + } + + // Remove rtx or apt= lines that prevent connections for browsers without VP8 or VP9 support + // See: https://bugs.chromium.org/p/webrtc/issues/detail?id=3962 + if (['chrome', 'opera'].indexOf(window.webrtcDetectedBrowser) > -1 && removeVP9AptRtxPayload) { + log.warn([targetMid, null, null, 'Removing apt= and rtx payload lines causing connectivity issues']); + + sessionDescription.sdp = sessionDescription.sdp.replace(/a=rtpmap:\d+ rtx\/\d+\r\na=fmtp:\d+ apt=101\r\n/g, ''); + sessionDescription.sdp = sessionDescription.sdp.replace(/a=rtpmap:\d+ rtx\/\d+\r\na=fmtp:\d+ apt=107\r\n/g, ''); + } + // NOTE ALEX: opus should not be used for mobile // Set Opus as the preferred codec in SDP if Opus is present. //sessionDescription.sdp = preferOpus(sessionDescription.sdp); @@ -2843,431 +5538,921 @@ Skylink.prototype._setLocalAndSendMessage = function(targetMid, sessionDescripti log.log([targetMid, 'RTCSessionDescription', sessionDescription.type, 'Updated session description:'], sessionDescription); + // Added checks if there is a current local sessionDescription being processing before processing this one + if (pc.processingLocalSDP) { + log.warn([targetMid, 'RTCSessionDescription', sessionDescription.type, + 'Dropping of setting local ' + sessionDescription.type + ' as there is another ' + + 'sessionDescription being processed ->'], sessionDescription); + return; + } + + pc.processingLocalSDP = true; + pc.setLocalDescription(sessionDescription, function() { log.debug([targetMid, sessionDescription.type, 'Local description set']); + + pc.processingLocalSDP = false; + self._trigger('handshakeProgress', sessionDescription.type, targetMid); if (sessionDescription.type === self.HANDSHAKE_PROGRESS.ANSWER) { pc.setAnswer = 'local'; } else { pc.setOffer = 'local'; } - if (self._enableIceTrickle && !self._peerIceTrickleDisabled[targetMid]) { - self._sendChannelMessage({ - type: sessionDescription.type, - sdp: sessionDescription.sdp, - mid: self._user.sid, - target: targetMid, - rid: self._room.id - }); - } else { + + if (!self._enableIceTrickle && !pc.gathered) { log.log([targetMid, 'RTCSessionDescription', sessionDescription.type, 'Waiting for Ice gathering to complete to prevent Ice trickle']); + return; + } + + // make checks for firefox session description + if (sessionDescription.type === self.HANDSHAKE_PROGRESS.ANSWER && window.webrtcDetectedBrowser === 'firefox') { + sessionDescription.sdp = self._addSDPSsrcFirefoxAnswer(targetMid, sessionDescription.sdp); } + + self._sendChannelMessage({ + type: sessionDescription.type, + sdp: sessionDescription.sdp, + mid: self._user.sid, + target: targetMid, + rid: self._room.id, + userInfo: self._getUserInfo() + }); + }, function(error) { self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ERROR, targetMid, error); + + pc.processingLocalSDP = false; + log.error([targetMid, 'RTCSessionDescription', sessionDescription.type, 'Failed setting local description: '], error); }); }; + +Skylink.prototype.GET_PEERS_STATE = { + ENQUIRED: 'enquired', + RECEIVED: 'received' +}; + +/** + * + * Note that this feature requires "isPrivileged" flag to be enabled and + * "autoIntroduce" flag to be disabled for the App Key provided in the + * init() method, as only Users connecting using + * the App Key with this flag enabled (which we call privileged Users / Peers) can retrieve the list of + * Peer IDs from Rooms within the same App space. + * + * Read more about privileged App Key feature here. + * + * The list of introducePeer method Peer introduction request states. + * @attribute INTRODUCE_STATE + * @param {String} INTRODUCING Value "enquired" + * The value of the state when introduction request for the selected pair of Peers has been made to the Signaling server. + * @param {String} ERROR Value "error" + * The value of the state when introduction request made to the Signaling server + * for the selected pair of Peers has failed. + * @readOnly + * @for Skylink + * @since 0.6.1 + */ +Skylink.prototype.INTRODUCE_STATE = { + INTRODUCING: 'introducing', + ERROR: 'error' +}; + +/** + * Stores the flag that indicates if "autoIntroduce" is enabled. + * If enabled, the Peers connecting the same Room will receive each others "enter" message ping. + * @attribute _autoIntroduce + * @type Boolean + * @default true + * @private + * @for Skylink + * @since 0.6.1 + */ +Skylink.prototype._autoIntroduce = true; + +/** + * Stores the flag that indicates if "isPrivileged" is enabled. + * If enabled, the User has Privileged features which has the ability to retrieve the list of + * Peers in the same App space with getPeers() method + * and introduce Peers to each other with introducePeer method. + * @attribute isPrivileged + * @type Boolean + * @default false + * @private + * @for Skylink + * @since 0.6.1 + */ +Skylink.prototype._isPrivileged = false; + +/** + * Stores the list of Peers retrieved from the Signaling from getPeers() method. + * @attribute _peerList + * @type JSON + * @private + * @for Skylink + * @since 0.6.1 + */ +Skylink.prototype._peerList = null; + +/** + * + * Note that this feature requires "isPrivileged" flag to be enabled for the App Key + * provided in the init() method, as only Users connecting using + * the App Key with this flag enabled (which we call privileged Users / Peers) can retrieve the list of + * Peer IDs from Rooms within the same App space. + * + * Read more about privileged App Key feature here. + * + * Function that retrieves the list of Peer IDs from Rooms within the same App space. + * @method getPeers + * @param {Boolean} [showAll=false] The flag if Signaling server should also return the list of privileged Peer IDs. + * By default, the Signaling server does not include the list of privileged Peer IDs in the return result. + * @param {Function} [callback] The callback function fired when request has completed. + * Function parameters signature is function (error, success) + * Function request completion is determined by the + * getPeersStateChange event triggering state parameter payload value as + * RECEIVED for request success. + * [Rel: Skylink.GET_PEERS_STATE] + * @param {Error|String} callback.error The error result in request. + * Defined as null when there are no errors in request + * Object signature is the getPeers() error when retrieving list of Peer IDs from Rooms + * within the same App space. + * @param {JSON} callback.success The success result in request. + * Defined as null when there are errors in request + * Object signature matches the peerList parameter payload received in the + * getPeersStateChange event. + * @trigger + * If App Key provided in the init() method is not + * a Privileged enabled Key: ABORT and return error. + * Retrieves the list of Peer IDs from Rooms within the same App space. + * getPeersStateChange event triggers parameter + * payload state value as ENQUIRED. + * If received list from Signaling server successfully: + * getPeersStateChange event triggers parameter + * payload state value as RECEIVED. + * @example + * // Example 1: Retrieving the un-privileged Peers + * skylinkDemo.joinRoom(function (jRError, jRSuccess) { + * if (jRError) return; + * skylinkDemo.getPeers(function (error, success) { + * if (error) return; + * console.log("The list of only un-privileged Peers in the same App space ->", success); + * }); + * }); + * + * // Example 2: Retrieving the all Peers (privileged or un-privileged) + * skylinkDemo.joinRoom(function (jRError, jRSuccess) { + * if (jRError) return; + * skylinkDemo.getPeers(true, function (error, success) { + * if (error) return; + * console.log("The list of all Peers in the same App space ->", success); + * }); + * }); + * @for Skylink + * @since 0.6.1 + */ +Skylink.prototype.getPeers = function(showAll, callback){ + var self = this; + if (!self._isPrivileged){ + log.warn('Please upgrade your key to privileged to use this function'); + return; + } + if (!self._appKey){ + log.warn('App key is not defined. Please authenticate again.'); + return; + } + + // Only callback is provided + if (typeof showAll === 'function'){ + callback = showAll; + showAll = false; + } + + self._sendChannelMessage({ + type: self._SIG_MESSAGE_TYPE.GET_PEERS, + showAll: showAll || false + }); + + self._trigger('getPeersStateChange',self.GET_PEERS_STATE.ENQUIRED, self._user.sid, null); + + log.log('Enquired server for peers within the realm'); + + if (typeof callback === 'function'){ + self.once('getPeersStateChange', function(state, privilegedPeerId, peerList){ + callback(null, peerList); + }, function(state, privilegedPeerId, peerList){ + return state === self.GET_PEERS_STATE.RECEIVED; + }); + } + +}; + +/** + * + * Note that this feature requires "isPrivileged" flag to be enabled and + * "autoIntroduce" flag to be disabled for the App Key provided in the + * init() method, as only Users connecting using + * the App Key with this flag enabled (which we call privileged Users / Peers) can retrieve the list of + * Peer IDs from Rooms within the same App space. + * + * Read more about privileged App Key feature here. + * + * Function that selects and introduces a pair of Peers to start connection with each other. + * @method introducePeer + * @param {String} sendingPeerId The Peer ID to be connected with receivingPeerId. + * @param {String} receivingPeerId The Peer ID to be connected with sendingPeerId. + * @trigger + * If App Key provided in the init() method is not + * a Privileged enabled Key: ABORT and return error. + * Starts sending introduction request for the selected pair of Peers to the Signaling server. + * introduceStateChange event triggers parameter + * payload state value as INTRODUCING. + * If received errors from Signaling server: + * introduceStateChange event triggers parameter + * payload state value as ERROR. + * @example + * // Example 1: Introduce a pair of Peers + * skylinkDemo.on("introduceStateChange", function (state, privilegedPeerId, sendingPeerId, receivingPeerId) { + * if (state === skylinkDemo.INTRODUCE_STATE.INTRODUCING) { + * console.log("Peer '" + sendingPeerId + "' has been introduced to '" + receivingPeerId + "'"); + * } + * }); + * + * skylinkDemo.joinRoom(function (jRError, jRSuccess) { + * if (jRError) return; + * skylinkDemo.getPeers(function (gPError, gPSuccess) { + * if (gPError) return; + * skylinkDemo.introducePeer(gPSuccess.roomName[0], gPSuccess.roomName[1]); + * }); + * }); + * @for Skylink + * @since 0.6.1 + */ +Skylink.prototype.introducePeer = function(sendingPeerId, receivingPeerId){ + var self = this; + if (!self._isPrivileged){ + log.warn('Please upgrade your key to privileged to use this function'); + self._trigger('introduceStateChange', self.INTRODUCE_STATE.ERROR, self._user.sid, sendingPeerId, receivingPeerId, 'notPrivileged'); + return; + } + self._sendChannelMessage({ + type: self._SIG_MESSAGE_TYPE.INTRODUCE, + sendingPeerId: sendingPeerId, + receivingPeerId: receivingPeerId + }); + self._trigger('introduceStateChange', self.INTRODUCE_STATE.INTRODUCING, self._user.sid, sendingPeerId, receivingPeerId, null); + log.log('Introducing',sendingPeerId,'to',receivingPeerId); +}; + + Skylink.prototype.SYSTEM_ACTION = { WARNING: 'warning', REJECT: 'reject' }; /** - * The list of signaling actions to be taken upon received. + * The list of Signaling server reaction states reason of action code during + * joinRoom() method. * @attribute SYSTEM_ACTION_REASON + * @param {String} CREDENTIALS_EXPIRED Value "oldTimeStamp" + * The value of the reason code when Room session token has expired. + * Happens during joinRoom() method request. + * Results with: REJECT + * @param {String} CREDENTIALS_ERROR Value "credentialError" + * The value of the reason code when Room session token provided is invalid. + * Happens during joinRoom() method request. + * @param {String} DUPLICATED_LOGIN Value "duplicatedLogin" + * The value of the reason code when Room session token has been used already. + * Happens during joinRoom() method request. + * Results with: REJECT + * @param {String} ROOM_NOT_STARTED Value "notStart" + * The value of the reason code when Room session has not started. + * Happens during joinRoom() method request. + * Results with: REJECT + * @param {String} EXPIRED Value "expired" + * The value of the reason code when Room session has ended already. + * Happens during joinRoom() method request. + * Results with: REJECT + * @param {String} ROOM_LOCKED Value "locked" + * The value of the reason code when Room is locked. + * Happens during joinRoom() method request. + * Results with: REJECT + * @param {String} FAST_MESSAGE Value "fastmsg" + * The value of the reason code when User is flooding socket messages to the Signaling server + * that is sent too quickly within less than a second interval. + * Happens after Room session has started. This can be caused by various methods like + * sendMessage() method, + * setUserData() method, + * muteStream() method, + * enableAudio() method, + * enableVideo() method, + * disableAudio() method and + * disableVideo() method + * Results with: WARNING + * @param {String} ROOM_CLOSING Value "toClose" + * The value of the reason code when Room session is ending. + * Happens after Room session has started. This serves as a prerequisite warning before + * ROOM_CLOSED occurs. + * Results with: WARNING + * @param {String} ROOM_CLOSED Value "roomclose" + * The value of the reason code when Room session has just ended. + * Happens after Room session has started. + * Results with: REJECT + * @param {String} SERVER_ERROR Value "serverError" + * The value of the reason code when Room session fails to start due to some technical errors. + * Happens during joinRoom() method request. + * Results with: REJECT + * @param {String} KEY_ERROR Value "keyFailed" + * The value of the reason code when Room session fails to start due to some technical error pertaining to + * App Key initialization. + * Happens during joinRoom() method request. + * Results with: REJECT * @type JSON - * @param {String} FAST_MESSAGE User is not alowed to - * send too quick messages as it is used to prevent jam. - * @param {String} ROOM_LOCKED Room is locked and User is rejected from joining the Room. - * @param {String} ROOM_FULL The target Peers in a persistent room is full. - * @param {String} DUPLICATED_LOGIN The User is re-attempting to connect again with - * an userId that has been used. - * @param {String} SERVER_ERROR Server has an error. - * @param {String} VERIFICATION Verification is incomplete for roomId provided. - * @param {String} EXPIRED Persistent meeting. Room has - * expired and user is unable to join the room. - * @param {String} ROOM_CLOSED The persistent room is closed as it has been expired. - * @param {String} ROOM_CLOSING The persistent room is closing. - * @param {String} OVER_SEAT_LIMIT The seat limit has been reached. * @readOnly - * @component Room * @for Skylink * @since 0.5.2 */ Skylink.prototype.SYSTEM_ACTION_REASON = { - FAST_MESSAGE: 'fastmsg', - ROOM_LOCKED: 'locked', - ROOM_FULL: 'roomfull', + CREDENTIALS_EXPIRED: 'oldTimeStamp', + CREDENTIALS_ERROR: 'credentialError', DUPLICATED_LOGIN: 'duplicatedLogin', - SERVER_ERROR: 'serverError', - VERIFICATION: 'verification', + ROOM_NOT_STARTED: 'notStart', EXPIRED: 'expired', - ROOM_CLOSED: 'roomclose', + ROOM_LOCKED: 'locked', + FAST_MESSAGE: 'fastmsg', ROOM_CLOSING: 'toclose', - OVER_SEAT_LIMIT: 'seatquota' + ROOM_CLOSED: 'roomclose', + SERVER_ERROR: 'serverError', + KEY_ERROR: 'keyFailed' }; /** - * The room that the user is currently connected to. + * Stores the current Room name that User is connected to. * @attribute _selectedRoom * @type String - * @default Skylink._defaultRoom * @private - * @component Room * @for Skylink * @since 0.3.0 */ Skylink.prototype._selectedRoom = null; /** - * The flag that indicates whether room is currently locked. + * Stores the flag that indicates if Room is locked. * @attribute _roomLocked * @type Boolean * @private - * @component Room * @for Skylink * @since 0.5.2 */ Skylink.prototype._roomLocked = false; /** - * Connects the User to a Room. + * Stores the flag that indicates if User is connected to the Room. + * @attribute _inRoom + * @type Boolean + * @private + * @for Skylink + * @since 0.4.0 + */ +Skylink.prototype._inRoom = false; + +/** + * Function that starts the Room session. * @method joinRoom - * @param {String} [room=init.options.defaultRoom] Room name to join. - * If Room name is not provided, User would join the default room. - * @param {JSON} [options] Media Constraints - * @param {JSON|String} [options.userData] User custom data. See - * {{#crossLink "Skylink/setUserData:method"}}setUserData(){{/crossLink}} - * for more information - * @param {Boolean|JSON} [options.audio=false] Enable audio stream. - * @param {Boolean} [options.audio.stereo] Option to enable stereo - * during call. - * @param {Boolean} [options.audio.mute=false] If audio stream should be muted. - * @param {Boolean|JSON} [options.video=false] Enable video stream. - * @param {JSON} [options.video.resolution] The resolution of video stream. - * [Rel: Skylink.VIDEO_RESOLUTION] - * @param {Number} [options.video.resolution.width] - * The video stream resolution width (in px). - * @param {Number} [options.video.resolution.height] - * The video stream resolution height (in px). - * @param {Number} [options.video.frameRate] - * The video stream frameRate. - * @param {Boolean} [options.video.mute=false] If audio stream should be muted. - * @param {JSON} [options.bandwidth] Stream bandwidth settings. - * @param {Number} [options.bandwidth.audio=50] Audio stream bandwidth in kbps. - * @param {Number} [options.bandwidth.video=256] Video stream bandwidth in kbps. - * @param {Number} [options.bandwidth.data=1638400] Data stream bandwidth in kbps. - * @param {Boolean} [options.manualGetUserMedia] Get the user media manually. - * @param {Function} [callback] The callback fired after peer leaves the room. - * Default signature: function(error object, success object) + * @param {String} [room] The Room name. + * - When not provided, its value is the options.defaultRoom provided in the + * init() method. + * Note that if you are using credentials based authentication, you cannot switch the Room + * that is not the same as the options.defaultRoom defined in the + * init() method. + * @param {JSON} [options] The Room session configuration options. + * @param {JSON|String} [options.userData] The User custom data. + * This can be set after Room session has started using the + * setUserData() method. + * @param {Boolean} [options.useExactConstraints] The getUserMedia() + * method options.useExactConstraints parameter settings. + * See the options.useExactConstraints parameter in the + * getUserMedia() method for more information. + * @param {Boolean|JSON} [options.audio] The getUserMedia() + * method options.audio parameter settings. + * When value is defined as true or an object, + * getUserMedia() method to be invoked to retrieve new Stream. If + * options.video is not defined, it will be defined as false. + * Object signature matches the options.audio parameter in the + * getUserMedia() method. + * @param {Boolean|JSON} [options.video] The getUserMedia() + * method options.video parameter settings. + * When value is defined as true or an object, + * getUserMedia() method to be invoked to retrieve new Stream. If + * options.audio is not defined, it will be defined as false. + * Object signature matches the options.video parameter in the + * getUserMedia() method. + * @param {JSON} [options.bandwidth] Note that this is currently not supported + * with Firefox browsers versions 48 and below as noted in an existing + * bugzilla ticket here. + * The configuration to set the maximum streaming bandwidth sent to Peers. + * @param {Number} [options.bandwidth.audio] The maximum audio streaming bandwidth sent to Peers in kbps. + * Recommended values are 50 to 200. 50 is sufficient enough for + * an audio call. The higher you go if you want clearer audio and to be able to hear music streaming. + * @param {Number} [options.bandwidth.video] The maximum video streaming bandwidth sent to Peers. + * Recommended values are 256-500 for 180p quality, + * 500-1024 for 360p quality, 1024-2048 for 720p quality + * and 2048-4096 for 1080p quality. + * @param {Number} [options.bandwidth.data] The maximum data streaming bandwidth sent to Peers. + * This affects the P2P messaging in sendP2PMessage() method, + * and data transfers in sendBlobData() method and + * sendURLData() method. + * @param {Boolean} [options.manualGetUserMedia] The flag if joinRoom() should trigger + * mediaAccessRequired event in which the + * getUserMedia() Stream or + * shareScreen() Stream + * must be retrieved as a requirement before Room session may begin. + * This ignores the options.audio and options.video configuration. + * After 30 seconds without any Stream retrieved, this results in the `callback(error, ..)` result. + * @param {Function} [callback] The callback function fired when request has completed. + * Function parameters signature is function (error, success) + * Function request completion is determined by the + * peerJoined event triggering isSelf parameter payload value as true + * for request success. + * @param {JSON} callback.error The error result in request. + * Defined as null when there are no errors in request + * @param {Error|String} callback.error.error The error received when starting Room session has failed. + * @param {Number} callback.error.errorCode The current init() method ready state. + * [Rel: Skylink.READY_STATE_CHANGE] + * @param {String} callback.error.room The Room name. + * @param {JSON} callback.success The success result in request. + * Defined as null when there are errors in request + * @param {String} callback.success.room The Room name. + * @param {String} callback.success.peerId The User's Room session Peer ID. + * @param {JSON} callback.success.peerInfo The User's current Room session information. + * Object signature matches the peerInfo parameter payload received in the + * peerJoined event. * @example - * // To just join the default room without any video or audio - * // Note that calling joinRoom without any parameters - * // still sends any available existing MediaStreams allowed. - * // See Examples 2, 3, 4 and 5 etc to prevent video or audio stream - * SkylinkDemo.joinRoom(); - * - * // To just join the default room with bandwidth settings - * SkylinkDemo.joinRoom({ - * 'bandwidth': { - * 'data': 14440 - * } + * // Example 1: Connecting to the default Room without Stream + * skylinkDemo.joinRoom(function (error, success) { + * if (error) return; + * console.log("User connected."); * }); * - * // Example 1: To call getUserMedia and joinRoom seperately - * SkylinkDemo.getUserMedia(); - * SkylinkDemo.on('mediaAccessSuccess', function (stream)) { - * attachMediaStream($('.localVideo')[0], stream); - * SkylinkDemo.joinRoom(); + * // Example 2: Connecting to Room "testxx" with Stream + * skylinkDemo.joinRoom("testxx", { + * audio: true, + * video: true + * }, function (error, success) { + * if (error) return; + * console.log("User connected with getUserMedia() Stream.") * }); * - * // Example 2: Join a room without any video or audio - * SkylinkDemo.joinRoom('room'); - * - * // Example 3: Join a room with audio only - * SkylinkDemo.joinRoom('room', { - * 'audio' : true, - * 'video' : false + * // Example 3: Connecting to default Room with Stream retrieved earlier + * skylinkDemo.getUserMedia(function (gUMError, gUMSuccess) { + * if (gUMError) return; + * skylinkDemo.joinRoom(function (error, success) { + * if (error) return; + * console.log("User connected with getUserMedia() Stream."); + * }); * }); * - * // Example 4: Join a room with prefixed video width and height settings - * SkylinkDemo.joinRoom('room', { - * 'audio' : true, - * 'video' : { - * 'resolution' : { - * 'width' : 640, - * 'height' : 320 - * } - * } + * // Example 4: Connecting to "testxx" Room with shareScreen() Stream retrieved manually + * skylinkDemo.on("mediaAccessRequired", function () { + * skylinkDemo.shareScreen(function (sSError, sSSuccess) { + * if (sSError) return; + * }); * }); * - * // Example 5: Join a room with userData and settings with audio, video - * // and bandwidth - * SkylinkDemo.joinRoom({ - * 'userData': { - * 'item1': 'My custom data', - * 'item2': 'Put whatever, string or JSON or array' - * }, - * 'audio' : { - * 'stereo' : true - * }, - * 'video' : { - * 'res' : SkylinkDemo.VIDEO_RESOLUTION.VGA, - * 'frameRate' : 50 - * }, - * 'bandwidth' : { - * 'audio' : 48, - * 'video' : 256, - * 'data' : 14480 - * } + * skylinkDemo.joinRoom("testxx", { + * manualGetUserMedia: true + * }, function (error, success) { + * if (error) return; + * console.log("User connected with shareScreen() Stream."); * }); * - * //Example 6: joinRoom with callback - * SkylinkDemo.joinRoom(function(error, success){ - * if (error){ - * console.log('Error happened. Can not join room')); - * } - * else{ - * console.log('Successfully joined room'); - * } + * // Example 5: Connecting to "testxx" Room with User custom data + * var data = { username: "myusername" }; + * skylinkDemo.joinRoom("testxx", { + * userData: data + * }, function (error, success) { + * if (error) return; + * console.log("User connected with correct user data?", success.peerInfo.userData.username === data.username); * }); - * @trigger peerJoined, mediaAccessRequired - * @component Room + * @trigger + * If User is in a Room: + * Invoke leaveRoom() method + * to end current Room connection. Invoked leaveRoom() + * method stopMediaOptions parameter value will be false. + * Regardless of request errors, joinRoom() will still proceed. + * Check if Room name provided matches the Room name of the currently retrieved Room session token. + * If Room name does not matches: + * Invoke init() method to retrieve new Room session token. + * If request has errors: ABORT and return error. + * Open a new socket connection to Signaling server. If Socket connection fails: + * socketError event triggers parameter payload + * errorCode as CONNECTION_FAILED. + * Checks if there are fallback ports and transports to use. + * If there are still fallback ports and transports: + * Attempts to retry socket connection to Signaling server. + * channelRetry event triggers. + * socketError event triggers parameter + * payload errorCode as RECONNECTION_ATTEMPT. + * If attempt to retry socket connection to Signaling server has failed: + * socketError event triggers parameter payload + * errorCode as RECONNECTION_FAILED. + * Checks if there are still any more fallback ports and transports to use. + * If there are is no more fallback ports and transports to use: + * socketError event triggers + * parameter payload errorCode as RECONNECTION_ABORTED. + * ABORT and return error.Else: REPEAT attempt to retry socket connection + * to Signaling server step.Else: + * socketError event triggers + * parameter payload errorCode as CONNECTION_ABORTED. + * ABORT and return error. + * If socket connection to Signaling server has opened: + * channelOpen event triggers. + * Checks if there is options.manualGetUserMedia requested If it is requested: + * mediaAccessRequired event triggers. + * If more than 30 seconds has passed and no getUserMedia() Stream + * or shareScreen() Stream + * has been retrieved: ABORT and return error.Else: + * If there is options.audio or options.video requested: + * Invoke getUserMedia() method. + * If request has errors: ABORT and return error. + * Starts the Room session If Room session has started successfully: + * peerJoined event triggers parameter payload + * isSelf value as true. + * If MCU is enabled for the App Key provided in init() + * method and connected: serverPeerJoined + * event triggers serverPeerType as MCU. MCU has + * to be present in the Room in order for Peer connections to commence. + * Checks for any available Stream + * If shareScreen() Stream is available: + * incomingStream event + * triggers parameter payload isSelf value as true and stream + * as shareScreen() Stream. + * User will be sending shareScreen() Stream + * to Peers. + * Else if getUserMedia() Stream is available: + * incomingStream event triggers parameter + * payload isSelf value as true and stream as + * getUserMedia() Stream. + * User will be sending getUserMedia() Stream to Peers.Else: + * No Stream will be sent.Else: + * systemAction event triggers + * parameter payload action as REJECT. + * ABORT and return error. * @for Skylink * @since 0.5.5 */ Skylink.prototype.joinRoom = function(room, mediaOptions, callback) { var self = this; + var error; + var stopStream = false; + var previousRoom = self._selectedRoom; + + if (room === null) { + error = 'Invalid room name is provided'; + log.error(error, room); - if (typeof room === 'string'){ - //joinRoom(room, callback) - if (typeof mediaOptions === 'function'){ + if (typeof mediaOptions === 'function') { callback = mediaOptions; mediaOptions = undefined; } + + if (typeof callback === 'function') { + callback({ + room: room, + errorCode: self._readyState, + error: new Error(error) + }, null); + } + return; } - else if (typeof room === 'object'){ - //joinRoom(mediaOptions, callback); - if (typeof mediaOptions === 'function'){ + else if (typeof room === 'string') { + //joinRoom(room+); - skip + + //joinRoom(room+,mediaOptions+) - skip + + // joinRoom(room+,callback+) + if (typeof mediaOptions === 'function') { callback = mediaOptions; - mediaOptions = room; - room = undefined; + mediaOptions = undefined; + + // joinRoom(room+, mediaOptions-) + } else if (typeof mediaOptions !== 'undefined') { + if (mediaOptions === null || typeof mediaOptions !== 'object') { + error = 'Invalid mediaOptions is provided'; + log.error(error, mediaOptions); + + // joinRoom(room+,mediaOptions-,callback+) + if (typeof callback === 'function') { + callback({ + room: room, + errorCode: self._readyState, + error: new Error(error) + }, null); + } + return; + } } - //joinRoom(mediaOptions); - else{ - mediaOptions = room; + + } else if (typeof room === 'object') { + //joinRoom(mediaOptions+, callback); + if (typeof mediaOptions === 'function') { + callback = mediaOptions; } - } - else if (typeof room === 'function'){ + + //joinRoom(mediaOptions); + mediaOptions = room; + room = undefined; + + } else if (typeof room === 'function') { //joinRoom(callback); callback = room; room = undefined; mediaOptions = undefined; + + } else if (typeof room !== 'undefined') { + //joinRoom(mediaOptions-,callback?); + error = 'Invalid mediaOptions is provided'; + log.error(error, mediaOptions); + + if (typeof mediaOptions === 'function') { + callback = mediaOptions; + mediaOptions = undefined; + } + + if (typeof callback === 'function') { + callback({ + room: self._defaultRoom, + errorCode: self._readyState, + error: new Error(error) + }, null); + return; + } } + + // If no room provided, join the default room + if (!room) { + room = self._defaultRoom; + } + //if none of the above is true --> joinRoom() + var channelCallback = function (error, success) { + if (error) { + if (typeof callback === 'function') { + callback({ + error: error, + errorCode: null, + room: self._selectedRoom + }, null); + } + } else { + if (typeof callback === 'function') { + self.once('peerJoined', function(peerId, peerInfo, isSelf) { + // keep returning _inRoom false, so do a wait + self._wait(function () { + log.log([null, 'Socket', self._selectedRoom, 'Peer joined. Firing callback. ' + + 'PeerId ->' + ], peerId); + callback(null, { + room: self._selectedRoom, + peerId: peerId, + peerInfo: peerInfo + }); + }, function () { + return self._inRoom; + }, false); + }, function(peerId, peerInfo, isSelf) { + return isSelf; + }, false); + } - if (self._channelOpen) { - self.leaveRoom(function(){ + self._sendChannelMessage({ + type: self._SIG_MESSAGE_TYPE.JOIN_ROOM, + uid: self._user.uid, + cid: self._key, + rid: self._room.id, + userCred: self._user.token, + timeStamp: self._user.timeStamp, + apiOwner: self._appKeyOwner, + roomCred: self._room.token, + start: self._room.startDateTime, + len: self._room.duration, + isPrivileged: self._isPrivileged === true, // Default to false if undefined + autoIntroduce: self._autoIntroduce !== false, // Default to true if undefined + key: self._appKey + }); + } + }; + + if (self._inRoom) { + if (typeof mediaOptions === 'object') { + if (mediaOptions.audio === false && mediaOptions.video === false) { + stopStream = true; + log.warn([null, 'MediaStream', self._selectedRoom, 'Stopping current MediaStream ' + + 'as provided settings for audio and video is false (' + stopStream + ')'], mediaOptions); + } + } + + log.log([null, 'Socket', previousRoom, 'Leaving room before joining new room'], self._selectedRoom); + + self.leaveRoom(stopStream, function(error, success) { + log.log([null, 'Socket', previousRoom, 'Leave room callback result'], { + error: error, + success: success + }); log.log([null, 'Socket', self._selectedRoom, 'Joining room. Media options:'], mediaOptions); if (typeof room === 'string' ? room !== self._selectedRoom : false) { - self._initSelectedRoom(room, function () { - self._waitForOpenChannel(mediaOptions); + self._initSelectedRoom(room, function(errorObj) { + if (errorObj) { + if (typeof callback === 'function') { + callback({ + room: self._selectedRoom, + errorCode: self._readyState, + error: new Error(errorObj) + }, null); + } + } else { + self._waitForOpenChannel(mediaOptions, channelCallback); + } }); } else { - self._waitForOpenChannel(mediaOptions); + self._waitForOpenChannel(mediaOptions, channelCallback); } }); - if (typeof callback === 'function'){ - self.once('peerJoined',function(peerId, peerInfo, isSelf){ - log.log([null, 'Socket', self._selectedRoom, 'Peer joined. Firing callback. ' + - 'PeerId ->'], peerId); - callback(null,{ - room: self._selectedRoom, - peerId: peerId, - peerInfo: peerInfo - }); - },function(peerId, peerInfo, isSelf){ - return isSelf; - }, false); - } - - return; - } - log.log([null, 'Socket', self._selectedRoom, 'Joining room. Media options:'], - mediaOptions); - - if (typeof room === 'string' ? room !== self._selectedRoom : false) { - - self._initSelectedRoom(room, function () { - self._waitForOpenChannel(mediaOptions); - }); } else { - self._waitForOpenChannel(mediaOptions); - } + log.log([null, 'Socket', self._selectedRoom, 'Joining room. Media options:'], + mediaOptions); - if (typeof callback === 'function'){ - self.once('peerJoined',function(peerId, peerInfo, isSelf){ - log.log([null, 'Socket', self._selectedRoom, 'Peer joined. Firing callback. ' + - 'PeerId ->'], peerId); - callback(null,{ - room: self._selectedRoom, - peerId: peerId, - peerInfo: peerInfo + var isNotSameRoom = typeof room === 'string' ? room !== self._selectedRoom : false; + + if (isNotSameRoom) { + self._initSelectedRoom(room, function(errorObj) { + if (errorObj) { + if (typeof callback === 'function') { + callback({ + room: self._selectedRoom, + errorCode: self._readyState, + error: new Error(errorObj) + }, null); + } + } else { + self._waitForOpenChannel(mediaOptions, channelCallback); + } }); - },function(peerId, peerInfo, isSelf){ - return isSelf; - }, false); + } else { + self._waitForOpenChannel(mediaOptions, channelCallback); + } } -}; -/** - * Waits for room to ready, before starting the Room connection. - * @method _waitForOpenChannel - * @private - * @param {JSON} [options] Media Constraints. - * @param {JSON|String} [options.userData] User custom data. - * @param {Boolean|JSON} [options.audio=false] This call requires audio stream. - * @param {Boolean} [options.audio.stereo] Option to enable stereo - * during call. - * @param {Boolean} [options.audio.mute=false] If audio stream should be muted. - * @param {Boolean|JSON} [options.video=false] This call requires video stream. - * @param {JSON} [options.video.resolution] The resolution of video stream. - * @param {Number} [options.video.resolution.width] - * The video stream resolution width. - * @param {Number} [options.video.resolution.height] - * The video stream resolution height. - * @param {Number} [options.video.frameRate] - * The video stream maximum frameRate. - * @param {Boolean} [options.video.mute=false] If video stream should be muted. - * @param {JSON} [options.bandwidth] Stream bandwidth settings. - * @param {Number} [options.bandwidth.audio] Audio stream bandwidth in kbps. - * Recommended: 50 kbps. - * @param {Number} [options.bandwidth.video] Video stream bandwidth in kbps. - * Recommended: 256 kbps. - * @param {Number} [options.bandwidth.data] Data stream bandwidth in kbps. - * Recommended: 1638400 kbps. - * @trigger peerJoined, incomingStream, mediaAccessRequired - * @component Room - * @for Skylink - * @since 0.5.5 - */ -Skylink.prototype._waitForOpenChannel = function(mediaOptions) { - var self = this; - // when reopening room, it should stay as 0 - self._socketCurrentReconnectionAttempt = 0; - - // wait for ready state before opening - self._wait(function () { - self._condition('channelOpen', function () { - mediaOptions = mediaOptions || {}; - - // parse user data settings - self._parseUserData(mediaOptions.userData || self._userData); - self._parseBandwidthSettings(mediaOptions.bandwidth); - - // wait for local mediastream - self._waitForLocalMediaStream(function() { - // once mediastream is loaded, send channel message - self._sendChannelMessage({ - type: self._SIG_MESSAGE_TYPE.JOIN_ROOM, - uid: self._user.uid, - cid: self._key, - rid: self._room.id, - userCred: self._user.token, - timeStamp: self._user.timeStamp, - apiOwner: self._apiKeyOwner, - roomCred: self._room.token, - start: self._room.startDateTime, - len: self._room.duration - }); - }, mediaOptions); - }, function () { - // open channel first if it's not opened - if (!self._channelOpen) { - self._openChannel(); - } - return self._channelOpen; - }, function (state) { - return true; - }); - }, function () { - return self._readyState === self.READY_STATE_CHANGE.COMPLETED; - }); - -}; - -/** - * Disconnects a User from the room. - * @method leaveRoom - * @param {Function} [callback] The callback fired after peer leaves the room. - * Default signature: function(error object, success object) - * @example - * //Example 1: Just leaveRoom - * SkylinkDemo.leaveRoom(); - * - * //Example 2: leaveRoom with callback - * SkylinkDemo.leaveRoom(function(error, success){ - * if (error){ - * console.log('Error happened')); - * } - * else{ - * console.log('Successfully left room'); - * } - * }); - * @trigger peerLeft, channelClose, streamEnded - * @component Room +}; + +/** + * Function that stops Room session. + * @method leaveRoom + * @param {Boolean|JSON} [stopMediaOptions=true] The flag if leaveRoom() + * should stop both shareScreen() Stream + * and getUserMedia() Stream. + * - When provided as a boolean, this sets both stopMediaOptions.userMedia + * and stopMediaOptions.screenshare to its boolean value. + * @param {Boolean} [stopMediaOptions.userMedia=true] The flag if leaveRoom() + * should stop getUserMedia() Stream. + * This invokes stopStream() method. + * @param {Boolean} [stopMediaOptions.screenshare=true] The flag if leaveRoom() + * should stop shareScreen() Stream. + * This invokes stopScreen() method. + * @param {Function} [callback] The callback function fired when request has completed. + * Function parameters signature is function (error, success) + * Function request completion is determined by the + * peerLeft event triggering isSelf parameter payload value as true + * for request success. + * @param {Error|String} callback.error The error result in request. + * Defined as null when there are no errors in request + * Object signature is the leaveRoom() error when stopping Room session. + * @param {JSON} callback.success The success result in request. + * Defined as null when there are errors in request + * @param {String} callback.success.peerId The User's Room session Peer ID. + * @param {String} callback.success.previousRoom The Room name. + * @trigger + * Checks if User is in Room. If User is not in a Room: ABORT and return error. + * Else: If parameter stopMediaOptions.userMedia value is true: + * Invoke stopStream() method. + * Regardless of request errors, leaveRoom() will still proceed. + * If parameter stopMediaOptions.screenshare value is true: + * Invoke stopScreen() method. + * Regardless of request errors, leaveRoom() will still proceed. + * peerLeft event triggers for User and all connected Peers in Room. + * If MCU is enabled for the App Key provided in init() method + * and connected: serverPeerLeft event + * triggers parameter payload serverPeerType as MCU. + * channelClose event triggers. * @for Skylink * @since 0.5.5 */ -Skylink.prototype.leaveRoom = function(callback) { +Skylink.prototype.leaveRoom = function(stopMediaOptions, callback) { var self = this; + var error; // j-shint !!! + var stopUserMedia = true; + var stopScreenshare = true; + + // shift parameters + if (typeof stopMediaOptions === 'function') { + callback = stopMediaOptions; + stopMediaOptions = true; + } else if (typeof stopMediaOptions === 'undefined') { + stopMediaOptions = true; + } + + // stopMediaOptions === null or {} ? + if (typeof stopMediaOptions === 'object' && stopMediaOptions !== null) { + stopUserMedia = stopMediaOptions.userMedia !== false; + stopScreenshare = stopMediaOptions.screenshare !== false; + + } else if (typeof stopMediaOptions !== 'boolean') { + error = 'stopMediaOptions parameter provided is not a boolean or valid object'; + log.error(error, stopMediaOptions); + if (typeof callback === 'function') { + log.log([null, 'Socket', self._selectedRoom, 'Error occurred. ' + + 'Firing callback with error -> ' + ], error); + callback(new Error(error), null); + } + return; + + } else if (stopMediaOptions === false) { + stopUserMedia = false; + stopScreenshare = false; + } + if (!self._inRoom) { - var error = 'Unable to leave room as user is not in any room'; + error = 'Unable to leave room as user is not in any room'; log.error(error); - if (typeof callback === 'function'){ - log.log([null, 'Socket', self._selectedRoom, 'Error occurred. '+ - 'Firing callback with error -> '],error); - callback(error,null); + if (typeof callback === 'function') { + log.log([null, 'Socket', self._selectedRoom, 'Error occurred. ' + + 'Firing callback with error -> ' + ], error); + callback(new Error(error), null); } return; } - for (var pc_index in self._peerConnections) { - if (self._peerConnections.hasOwnProperty(pc_index)) { - self._removePeer(pc_index); + + // NOTE: ENTER/WELCOME made but no peerconnection... + // which may result in peerLeft not triggered.. + // WHY? but to ensure clear all + var peers = Object.keys(self._peerInformations); + var conns = Object.keys(self._peerConnections); + var i; + for (i = 0; i < conns.length; i++) { + if (peers.indexOf(conns[i]) === -1) { + peers.push(conns[i]); } } + for (i = 0; i < peers.length; i++) { + self._removePeer(peers[i]); + } self._inRoom = false; self._closeChannel(); - self.stopStream(); - self._wait(function(){ - if (typeof callback === 'function'){ + self._stopStreams({ + userMedia: stopUserMedia, + screenshare: stopScreenshare + }); + + self._wait(function() { + log.log([null, 'Socket', self._selectedRoom, 'User left the room. Callback fired.']); + self._trigger('peerLeft', self._user.sid, self.getPeerInfo(), true); + + if (typeof callback === 'function') { callback(null, { peerId: self._user.sid, - previousRoom: self._selectedRoom, - inRoom: self._inRoom + previousRoom: self._selectedRoom }); } - log.log([null, 'Socket', self._selectedRoom, 'User left the room. Callback fired.']); - self._trigger('peerLeft', self._user.sid, self.getPeerInfo(), true); - - }, function(){ + }, function() { return (Object.keys(self._peerConnections).length === 0 && - self._channelOpen === false && - self._readyState === self.READY_STATE_CHANGE.COMPLETED); + self._channelOpen === false); // && + //self._readyState === self.READY_STATE_CHANGE.COMPLETED); }, false); }; /** - * Locks the room to prevent other Peers from joining the room. + * + * Note that broadcasted events from muteStream() method, + * stopStream() method, + * stopScreen() method, + * sendMessage() method, + * unlockRoom() method and + * lockRoom() method may be queued when + * sent within less than an interval. + * + * Function that locks the current Room when in session to prevent other Peers from joining the Room. * @method lockRoom - * @example - * SkylinkDemo.lockRoom(); - * @trigger lockRoom - * @component Room + * @trigger + * Requests to Signaling server to lock Room + * roomLock event triggers parameter payload + * isLocked value as true. * @for Skylink * @since 0.5.0 */ @@ -3279,17 +6464,27 @@ Skylink.prototype.lockRoom = function() { rid: this._room.id, lock: true }); + this._roomLocked = true; this._trigger('roomLock', true, this._user.sid, this.getPeerInfo(), true); }; /** - * Unlocks the room to allow other Peers to join the room. + * + * Note that broadcasted events from muteStream() method, + * stopStream() method, + * stopScreen() method, + * sendMessage() method, + * unlockRoom() method and + * lockRoom() method may be queued when + * sent within less than an interval. + * + * Function that unlocks the current Room when in session to allow other Peers to join the Room. * @method unlockRoom - * @example - * SkylinkDemo.unlockRoom(); - * @trigger lockRoom - * @component Room + * @trigger + * Requests to Signaling server to unlock Room + * roomLock event triggers parameter payload + * isLocked value as false. * @for Skylink * @since 0.5.0 */ @@ -3301,9 +6496,105 @@ Skylink.prototype.unlockRoom = function() { rid: this._room.id, lock: false }); + this._roomLocked = false; this._trigger('roomLock', false, this._user.sid, this.getPeerInfo(), true); }; + +/** + * Function that waits for Socket connection to Signaling to be opened. + * @method _waitForOpenChannel + * @private + * @for Skylink + * @since 0.5.5 + */ +Skylink.prototype._waitForOpenChannel = function(mediaOptions, callback) { + var self = this; + // when reopening room, it should stay as 0 + self._socketCurrentReconnectionAttempt = 0; + + // wait for ready state before opening + self._wait(function() { + self._condition('channelOpen', function() { + mediaOptions = mediaOptions || {}; + + self._userData = mediaOptions.userData || self._userData || ''; + self._streamsBandwidthSettings = {}; + + if (mediaOptions.bandwidth) { + if (typeof mediaOptions.bandwidth.audio === 'number') { + self._streamsBandwidthSettings.audio = mediaOptions.bandwidth.audio; + } + + if (typeof mediaOptions.bandwidth.video === 'number') { + self._streamsBandwidthSettings.video = mediaOptions.bandwidth.video; + } + + if (typeof mediaOptions.bandwidth.data === 'number') { + self._streamsBandwidthSettings.data = mediaOptions.bandwidth.data; + } + } + + // get the stream + if (mediaOptions.manualGetUserMedia === true) { + self._trigger('mediaAccessRequired'); + + var current50Block = 0; + var mediaAccessRequiredFailure = false; + // wait for available audio or video stream + self._wait(function () { + if (mediaAccessRequiredFailure === true) { + self._onUserMediaError(new Error('Waiting for stream timeout'), false, false); + } else { + callback(null, self._streams.userMedia.stream); + } + }, function () { + current50Block += 1; + if (current50Block === 600) { + mediaAccessRequiredFailure = true; + return true; + } + + if (self._streams.userMedia && self._streams.userMedia.stream) { + return true; + } + }, 50); + return; + } + + if (mediaOptions.audio || mediaOptions.video) { + self.getUserMedia({ + useExactConstraints: !!mediaOptions.useExactConstraints, + audio: mediaOptions.audio, + video: mediaOptions.video + + }, function (error, success) { + if (error) { + callback(error, null); + } else { + callback(null, success); + } + }); + return; + } + + callback(null, null); + + }, function() { // open channel first if it's not opened + + if (!self._channelOpen) { + self._openChannel(); + } + return self._channelOpen; + }, function(state) { + return true; + }); + }, function() { + return self._readyState === self.READY_STATE_CHANGE.COMPLETED; + }); + +}; + Skylink.prototype.READY_STATE_CHANGE = { INIT: 0, LOADING: 1, @@ -3312,44 +6603,81 @@ Skylink.prototype.READY_STATE_CHANGE = { }; /** - * The list of ready state change errors. - * - These are the error states from the error object error code. - * - ROOM_LOCKED is deprecated in 0.5.2. Please use - * {{#crossLink "Skylink/:attr"}}leaveRoom(){{/crossLink}} - * - The states that would occur are: + * The list of init() method ready state failure codes. * @attribute READY_STATE_CHANGE_ERROR + * @param {Number} API_INVALID Value 4001 + * The value of the failure code when provided App Key in init() does not exists. + * To resolve this, check that the provided App Key exists in + * the Temasys Console. + * @param {Number} API_DOMAIN_NOT_MATCH Value 4002 + * The value of the failure code when "domainName" property in the App Key does not + * match the accessing server IP address. + * To resolve this, contact our support portal. + * @param {Number} API_CORS_DOMAIN_NOT_MATCH Value 4003 + * The value of the failure code when "corsurl" property in the App Key does not match accessing CORS. + * To resolve this, configure the App Key CORS in + * the Temasys Console. + * @param {Number} API_CREDENTIALS_INVALID Value 4004 + * The value of the failure code when there is no [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) + * present in the HTTP headers during the request to the Auth server present nor + * options.credentials.credentials configuration provided in the init(). + * To resolve this, ensure that CORS are present in the HTTP headers during the request to the Auth server. + * @param {Number} API_CREDENTIALS_NOT_MATCH Value 4005 + * The value of the failure code when the options.credentials.credentials configuration provided in the + * init() does not match up with the options.credentials.startDateTime, + * options.credentials.duration or that the "secret" used to generate + * options.credentials.credentials does not match the App Key's "secret property provided. + * To resolve this, check that the options.credentials.credentials is generated correctly and + * that the "secret" used to generate it is from the App Key provided in the init(). + * @param {Number} API_INVALID_PARENT_KEY Value 4006 + * The value of the failure code when the App Key provided does not belong to any existing App. + * To resolve this, check that the provided App Key exists in + * the Developer Console. + * @param {Number} API_NO_MEETING_RECORD_FOUND Value 4010 + * The value of the failure code when provided options.credentials + * does not match any scheduled meetings available for the "Persistent Room" enabled App Key provided. + * See the Persistent Room article to learn more. + * @param {Number} API_OVER_SEAT_LIMIT Value 4020 + * The value of the failure code when App Key has reached its current concurrent users limit. + * To resolve this, use another App Key. To create App Keys dynamically, see the + * Application REST API + * docs for more information. + * @param {Number} API_RETRIEVAL_FAILED Value 4021 + * The value of the failure code when App Key retrieval of authentication token fails. + * If this happens frequently, contact our support portal. + * @param {Number} API_WRONG_ACCESS_DOMAIN Value 5005 + * The value of the failure code when App Key makes request to the incorrect Auth server. + * To resolve this, ensure that the roomServer is not configured. If this persists even without + * roomServer configuration, contact our support portal. + * @param {Number} XML_HTTP_REQUEST_ERROR Value -1 + * The value of the failure code when requesting to Auth server has timed out. + * @param {Number} NO_SOCKET_IO Value 1 + * The value of the failure code when dependency Socket.IO client is not loaded. + * To resolve this, ensure that the Socket.IO client dependency is loaded before the Skylink SDK. + * You may use the provided Socket.IO client CDN here. + * @param {Number} NO_XMLHTTPREQUEST_SUPPORT Value 2 + * The value of the failure code when + * XMLHttpRequest API required to make request to Auth server is not supported. + * To resolve this, display in the Web UI to ask clients to switch to the list of supported browser + * as listed in here. + * @param {Number} NO_WEBRTC_SUPPORT Value 3 + * The value of the failure code when + * RTCPeerConnection API required for Peer connections is not supported. + * To resolve this, display in the Web UI to ask clients to switch to the list of supported browser + * as listed in here. + * For plugin supported browsers, if the clients + * does not have the plugin installed, there will be an installation toolbar that will prompt for installation + * to support the RTCPeerConnection API. + * @param {Number} NO_PATH Value 4 + * The value of the failure code when provided init() configuration has errors. + * @param {Number} ADAPTER_NO_LOADED Value 7 + * The value of the failure code when dependency AdapterJS + * is not loaded. + * To resolve this, ensure that the AdapterJS dependency is loaded before the Skylink dependency. + * You may use the provided AdapterJS CDN here. * @type JSON - * @param {Number} API_INVALID Api Key provided does not exist. - * @param {Number} API_DOMAIN_NOT_MATCH Api Key used in domain does - * not match. - * @param {Number} API_CORS_DOMAIN_NOT_MATCH Api Key used in CORS - * domain does not match. - * @param {Number} API_CREDENTIALS_INVALID Api Key credentials does - * not exist. - * @param {Number} API_CREDENTIALS_NOT_MATCH Api Key credentials does not - * match what is expected. - * @param {Number} API_INVALID_PARENT_KEY Api Key does not have a parent - * key nor is a root key. - * @param {Number} API_NOT_ENOUGH_CREDIT Api Key does not have enough - * credits to use. - * @param {Number} API_NOT_ENOUGH_PREPAID_CREDIT Api Key does not have - * enough prepaid credits to use. - * @param {Number} API_FAILED_FINDING_PREPAID_CREDIT Api Key preapid - * payments does not exist. - * @param {Number} API_NO_MEETING_RECORD_FOUND Api Key does not have a - * meeting record at this timing. This occurs when Api Key is a - * static one. - * @param {Number} ROOM_LOCKED Room is locked. - * @param {Number} NO_SOCKET_IO No socket.io dependency is loaded to use. - * @param {Number} NO_XMLHTTPREQUEST_SUPPORT Browser does not support - * XMLHttpRequest to use. - * @param {Number} NO_WEBRTC_SUPPORT Browser does not have WebRTC support. - * @param {Number} NO_PATH No path is loaded yet. - * @param {Number} INVALID_XMLHTTPREQUEST_STATUS Invalid XMLHttpRequest - * when retrieving information. - * @param {Number} ADAPTER_NO_LOADED AdapterJS dependency is not loaded. * @readOnly - * @component Room * @for Skylink * @since 0.4.0 */ @@ -3360,31 +6688,33 @@ Skylink.prototype.READY_STATE_CHANGE_ERROR = { API_CREDENTIALS_INVALID: 4004, API_CREDENTIALS_NOT_MATCH: 4005, API_INVALID_PARENT_KEY: 4006, - API_NOT_ENOUGH_CREDIT: 4007, - API_NOT_ENOUGH_PREPAID_CREDIT: 4008, - API_FAILED_FINDING_PREPAID_CREDIT: 4009, API_NO_MEETING_RECORD_FOUND: 4010, - ROOM_LOCKED: 5001, + API_OVER_SEAT_LIMIT: 4020, + API_RETRIEVAL_FAILED: 4021, + API_WRONG_ACCESS_DOMAIN: 5005, + XML_HTTP_REQUEST_ERROR: -1, NO_SOCKET_IO: 1, NO_XMLHTTPREQUEST_SUPPORT: 2, NO_WEBRTC_SUPPORT: 3, NO_PATH: 4, - INVALID_XMLHTTPREQUEST_STATUS: 5, - SCRIPT_ERROR: 6, ADAPTER_NO_LOADED: 7 }; /** - * The list of available regional servers. - * - This is for developers to set the nearest region server - * for Skylink to connect to for faster connectivity. - * - The available regional servers are: + * Deprecation Warning! + * This constant has been deprecated.Automatic nearest regional server has been implemented + * on the platform. + * + * The list of available Auth servers in these regions configured in the + * init() method. * @attribute REGIONAL_SERVER + * @param {String} APAC1 Value "sg" + * The value of the option to use the Auth server in Asia Pacific (APAC). + * @param {String} US1 Value "us2" + * The value of the option to use the Auth server in United States (US). + * @deprecated * @type JSON - * @param {String} APAC1 Asia pacific server 1. - * @param {String} US1 server 1. * @readOnly - * @component Room * @for Skylink * @since 0.5.0 */ @@ -3394,187 +6724,629 @@ Skylink.prototype.REGIONAL_SERVER = { }; /** - * Force an SSL connection to signalling and API server. + * Stores the flag if HTTPS connections should be enforced when connecting to + * the API or Signaling server if App is accessing from HTTP domain. + * HTTPS connections are enforced if App is accessing from HTTPS domains. * @attribute _forceSSL * @type Boolean * @default false - * @required * @private - * @component Room * @for Skylink * @since 0.5.4 */ Skylink.prototype._forceSSL = false; /** - * The path that user is currently connect to. - * - NOTE ALEX: check if last char is '/' + * Stores the flag if TURNS connections should be enforced when connecting to + * the TURN server if App is accessing from HTTP domain. + * TURNS connections are enforced if App is accessing from HTTPS domains. + * @attribute _forceTURNSSL + * @type Boolean + * @default false + * @private + * @for Skylink + * @since 0.6.1 + */ +Skylink.prototype._forceTURNSSL = false; + +/** + * Stores the flag if TURN connections should be enforced when connecting to Peers. + * This filters all non "relay" ICE candidates to enforce connections via the TURN server. + * @attribute _forceTURN + * @type Boolean + * @default false + * @private + * @for Skylink + * @since 0.6.1 + */ +Skylink.prototype._forceTURN = false; + +/** + * Stores the construct API REST path to obtain Room credentials. * @attribute _path * @type String - * @default Skylink._serverPath - * @final - * @required * @private - * @component Room * @for Skylink * @since 0.1.0 */ Skylink.prototype._path = null; /** - * The regional server that Skylink connects to. + * Stores the server region for the Signaling server to use. + * This is already deprecated an no longer useful. To discuss and remove. * @attribute _serverRegion * @type String * @private - * @component Room * @for Skylink * @since 0.5.0 */ Skylink.prototype._serverRegion = null; /** - * The server that user connects to to make - * api calls to. - * - The reason why users can input this value is to give - * users the chance to connect to any of our beta servers - * if available instead of the stable version. + * Stores the API server url. * @attribute _roomServer * @type String - * @default '//api.temasys.com.sg' * @private - * @component Room * @for Skylink * @since 0.5.2 */ Skylink.prototype._roomServer = '//api.temasys.com.sg'; /** - * The API Key ID. - * @attribute _apiKey + * Stores the App Key configured in init(). + * @attribute _appKey * @type String * @private - * @component Room * @for Skylink * @since 0.3.0 */ -Skylink.prototype._apiKey = null; +Skylink.prototype._appKey = null; /** - * The default room that the user connects to if no room is provided in - * {{#crossLink "Skylink/joinRoom:method"}}joinRoom(){{/crossLink}}. + * Stores the default Room name to connect to when joinRoom() does not provide a Room name. * @attribute _defaultRoom * @type String * @private - * @component Room * @for Skylink * @since 0.3.0 */ Skylink.prototype._defaultRoom = null; /** - * The static room's meeting starting date and time. - * - The value is in ISO formatted string. + * Stores the init() credentials starting DateTime stamp in ISO 8601. * @attribute _roomStart * @type String * @private - * @optional - * @component Room * @for Skylink * @since 0.3.0 */ Skylink.prototype._roomStart = null; /** - * The static room's meeting duration in hours. + * Stores the init() credentials duration counted in hours. * @attribute _roomDuration * @type Number * @private - * @optional - * @component Room * @for Skylink * @since 0.3.0 */ Skylink.prototype._roomDuration = null; /** - * The credentials required to set the start date and time - * and the duration. + * Stores the init() generated credentials string. * @attribute _roomCredentials * @type String * @private - * @optional - * @component Room * @for Skylink * @since 0.3.0 */ Skylink.prototype._roomCredentials = null; /** - * The current Skylink ready state change. - * [Rel: Skylink.READY_STATE_CHANGE] + * Stores the current init() readyState. * @attribute _readyState * @type Number * @private - * @required - * @component Room * @for Skylink * @since 0.1.0 */ Skylink.prototype._readyState = 0; /** - * The received server key. + * Stores the "cid" used for joinRoom(). * @attribute _key * @type String * @private - * @component Room * @for Skylink * @since 0.1.0 */ Skylink.prototype._key = null; /** - * The owner's username of the apiKey. - * @attribute _apiKeyOwner + * Stores the "apiOwner" used for joinRoom(). + * @attribute _appKeyOwner * @type String * @private - * @component Room * @for Skylink * @since 0.5.2 */ -Skylink.prototype._apiKeyOwner = null; +Skylink.prototype._appKeyOwner = null; /** - * The room connection information. + * Stores the Room credentials information for joinRoom(). * @attribute _room + * @param {String} id The "rid" for joinRoom(). + * @param {String} token The "roomCred" for joinRoom(). + * @param {String} startDateTime The "start" for joinRoom(). + * @param {String} duration The "len" for joinRoom(). + * @param {String} connection The RTCPeerConnection constraints and configuration. This is not used in the SDK + * except for the "mediaConstraints" property that sets the default getUserMedia() settings. * @type JSON - * @param {String} id The roomId of the room user is connected to. - * @param {String} token The token of the room user is connected to. - * @param {String} startDateTime The startDateTime in ISO string format of the room. - * @param {String} duration The duration of the room. - * @param {JSON} connection Connection constraints and configuration. - * @param {JSON} connection.peerConstraints The peerconnection constraints. - * @param {JSON} connection.peerConfig The peerconnection configuration. - * @param {JSON} connection.offerConstraints The offer constraints. - * @param {JSON} connection.sdpConstraints The sdp constraints. - * @required * @private - * @component Room * @for Skylink * @since 0.5.2 */ Skylink.prototype._room = null; /** - * Gets information from api server. + * Function that authenticates and initialises App Key used for Room connections. + * @method init + * @param {JSON|String} options The configuration options. + * - When provided as a string, it's configured as options.appKey. + * @param {String} options.appKey The App Key. + * By default, init() uses [HTTP CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) + * authentication. For credentials based authentication, see the options.credentials configuration + * below. You can know more about the in the authentication methods article here + * for more details on the various authentication methods. + * If you are using the Persistent Room feature for scheduled meetings, you will require to + * use the credential based authentication. See the Persistent Room article here + * for more information. + * @param {String} [options.defaultRoom] The default Room to connect to when no room parameter + * is provided in joinRoom() method. + * - When not provided, its value is options.appKey. + * Note that switching Rooms is not available when using options.credentials based authentication. + * The Room that User will be connected to is the defaultRoom provided. + * @param {String} [options.roomServer] The Auth server. + * Note that this is a debugging feature and is only used when instructed for debugging purposes. + * @param {String} [options.region] Deprecation Warning! + * This option has been deprecated.Automatic nearest regional server has been implemented + * on the platform. + * The Auth server in the various regions to connect to for better connectivity. + * [Rel: Skylink.REGIONAL_SERVER] + * @param {Boolean} [options.enableIceTrickle=true] The flag if Peer connections should + * trickle ICE for faster connectivity. + * @param {Boolean} [options.enableDataChannel=true] The flag if Datachannel connections should be enabled. + * This is required to be enabled for sendBlobData() method, + * sendURLData() method and + * sendP2PMessage() method. + * @param {Boolean} [options.enableTURNServer=true] The flag if TURN ICE servers should + * be used when constructing Peer connections to allow TURN connections when required and enabled for the App Key. + * @param {Boolean} [options.enableSTUNServer=true] The flag if STUN ICE servers should + * be used when constructing Peer connections to allow TURN connections when required. + * @param {Boolean} [options.forceTURN=false] The flag if Peer connections should enforce + * connections over the TURN server. + * This sets options.enableTURNServer value to true and + * options.enableSTUNServer value to false. + * During Peer connections, it filters out non "relay" ICE candidates to + * ensure that TURN connections is enforced. + * @param {Boolean} [options.usePublicSTUN=true] The flag if publicly available STUN ICE servers should + * be used if options.enableSTUNServer is enabled. + * @param {Boolean} [options.TURNServerTransport] + * Note that configuring the protocol may not necessarily result in the desired network transports protocol + * used in the actual TURN network traffic as it depends which protocol the browser selects and connects with. + * This simply configures the TURN ICE server urls query option when constructing + * the Peer connection. When all protocols are selected, the ICE servers urls are duplicated with all protocols. + * The option to configure the ?transport= + * query parameter in TURN ICE servers when constructing a Peer connections. + * - When not provided, its value is ANY. + * [Rel: Skylink.TURN_TRANSPORT] + * @param {JSON} [options.credentials] The credentials used for authenticating App Key with + * credentials to retrieve the Room session token used for connection in + * joinRoom() method. + * Note that switching of Rooms is not allowed when using credentials based authentication, unless + * init() is invoked again with a different set of credentials followed by invoking + * the joinRoom() method. + * @param {String} options.credentials.startDateTime The credentials User session in Room starting DateTime + * in ISO 8601 format. + * @param {Number} options.credentials.duration The credentials User session in Room duration in hours. + * @param {String} options.credentials.credentials The generated credentials used to authenticate + * the provided App Key with its "secret" property. + * To generate the credentials: + * Concatenate a string that consists of the Room name you provide in the options.defaultRoom, + * the options.credentials.duration and the options.credentials.startDateTime. + * Example: var concatStr = defaultRoom + "_" + duration + "_" + startDateTime; + * Hash the concatenated string with the App Key "secret" property using + * SHA-1. + * Example: var hash = CryptoJS.HmacSHA1(concatStr, appKeySecret); + * See the CryptoJS.HmacSHA1 library. + * Encode the hashed string using base64 + * Example: var b64Str = hash.toString(CryptoJS.enc.Base64); + * See the CryptoJS.enc.Base64 library. + * Encode the base64 encoded string to replace special characters using UTF-8 encoding. + * Example: var credentials = encodeURIComponent(base64String); + * See encodeURIComponent() API. + * @param {Boolean} [options.audioFallback=false] The flag if + * getUserMedia() method should fallback to retrieve only audio Stream when + * retrieving audio and video Stream fails. + * @param {Boolean} [options.forceSSL=false] The flag if HTTPS connections should be enforced + * during request to Auth server and socket connections to Signaling server + * when accessing window.location.protocol value is "http:". + * By default, "https:" protocol connections uses HTTPS connections. + * @param {String} [options.audioCodec] + * Note that if the audio codec is not supported, the SDK will not configure the local "offer" or + * "answer" session description to prefer the codec. + * The option to configure the preferred audio codec + * to use to encode sending audio data when available for Peer connection. + * - When not provided, its value is AUTO. + * [Rel: Skylink.AUDIO_CODEC] + * @param {String} [options.videoCodec] + * Note that if the video codec is not supported, the SDK will not configure the local "offer" or + * "answer" session description to prefer the codec. + * The option to configure the preferred video codec + * to use to encode sending video data when available for Peer connection. + * - When not provided, its value is AUTO. + * [Rel: Skylink.VIDEO_CODEC] + * @param {Number} [options.socketTimeout=20000] The timeout for each attempts for socket connection + * with the Signaling server to indicate that connection has timed out and has failed to establish. + * Note that the mininum timeout value is 5000. If less, this value will be 5000. + * @param {Boolean} [options.forceTURNSSL=false] + * Note that currently Firefox does not support the TURNS protocol, and that if TURNS is required, + * TURN ICE servers using port 443 will be used instead. + * The flag if TURNS protocol should be used when options.enableTURNServer is enabled. + * By default, "https:" protocol connections uses TURNS protocol. + * @param {Function} [callback] The callback function fired when request has completed. + * Function parameters signature is function (error, success) + * Function request completion is determined by the + * readyStateChange event state parameter payload value + * as COMPLETED for request success. + * [Rel: Skylink.READY_STATE_CHANGE] + * @param {JSON|String} callback.error The error result in request. + * - When defined as string, it's the error when required App Key is not provided. + * Defined as null when there are no errors in request + * @param {Number} callback.error.errorCode The readyStateChange + * event error.errorCode parameter payload value. + * [Rel: Skylink.READY_STATE_CHANGE_ERROR] + * @param {Object} callback.error.error The readyStateChange + * event error.content parameter payload value. + * @param {Number} callback.error.status The readyStateChange + * event error.status parameter payload value. + * @param {JSON} callback.success The success result in request. + * Defined as null when there are errors in request + * @param {String} callback.success.serverUrl The constructed REST URL requested to Auth server. + * @param {String} callback.success.readyState The current ready state. + * [Rel: Skylink.READY_STATE_CHANGE] + * @param {String} callback.success.selectedRoom The Room based on the current Room session token retrieved for. + * @param {String} callback.success.appKey The configured value of the options.appKey. + * @param {String} callback.success.defaultRoom The configured value of the options.defaultRoom. + * @param {String} callback.success.roomServer The configured value of the options.roomServer. + * @param {Boolean} callback.success.enableIceTrickle The configured value of the options.enableIceTrickle. + * @param {Boolean} callback.success.enableDataChannel The configured value of the options.enableDataChannel. + * @param {Boolean} callback.success.enableTURNServer The configured value of the options.enableTURNServer. + * @param {Boolean} callback.success.enableSTUNServer The configured value of the options.enableSTUNServer. + * @param {Boolean} callback.success.TURNTransport The configured value of the options.TURNServerTransport. + * @param {String} callback.success.serverRegion The configured value of the options.region. + * @param {Boolean} callback.success.audioFallback The configured value of the options.audioFallback. + * @param {Boolean} callback.success.forceSSL The configured value of the options.forceSSL. + * @param {String} callback.success.audioCodec The configured value of the options.audioCodec. + * @param {String} callback.success.videoCodec The configured value of the options.videoCodec. + * @param {Number} callback.success.socketTimeout The configured value of the options.socketTimeout. + * @param {Boolean} callback.success.forceTURNSSL The configured value of the options.forceTURNSSL. + * @param {Boolean} callback.success.forceTURN The configured value of the options.forceTURN. + * @param {Boolean} callback.success.usePublicSTUN The configured value of the options.usePublicSTUN. + * @example + * // Example 1: Using CORS authentication and connection to default Room + * skylinkDemo(appKey, function (error, success) { + * if (error) return; + * skylinkDemo.joinRoom(); // Goes to default Room + * }); + * + * // Example 2: Using CORS authentication and connection to a different Room + * skylinkDemo(appKey, function (error, success) { + * skylinkDemo.joinRoom("testxx"); // Goes to "testxx" Room + * }); + * + * // Example 3: Using credentials authentication and connection to only default Room + * var defaultRoom = "test", + * startDateTime = (new Date()).toISOString(), + * duration = 1, // Allows only User session to stay for 1 hour + * appKeySecret = "xxxxxxx", + * hash = CryptoJS.HmacSHA1(defaultRoom + "_" + duration + "_" + startDateTime, appKeySecret); + * credentials = encodeURIComponent(hash.toString(CryptoJS.enc.Base64)); + * + * skylinkDemo({ + * defaultRoom: defaultRoom, + * appKey: appKey, + * credentials: { + * duration: duration, + * startDateTime: startDateTime, + * credentials: credentials + * } + * }, function (error, success) { + * if (error) return; + * skylinkDemo.joinRoom(); // Goes to default Room (switching to different Room is not allowed for credentials authentication) + * }); + * @trigger + * If parameter options is not provided: ABORT and return error. + * Checks if dependecies and browser APIs are available. If AdapterJS is not loaded: + * readyStateChange event triggers + * parameter payload state as ERROR and error.errorCode as + * ADAPTER_NO_LOADED.ABORT and return error. + * If socket.io-client is not loaded: + * readyStateChange event triggers parameter payload state + * as ERROR and error.errorCode as NO_SOCKET_IO. + * ABORT and return error. + * If XMLHttpRequest API is not available: + * readyStateChange event triggers parameter payload state + * as ERROR and error.errorCode as NO_XMLHTTPREQUEST_SUPPORT. + * ABORT and return error.If WebRTC is not supported by device: + * readyStateChange event triggers parameter + * payload state as ERROR and error.errorCode as + * NO_WEBRTC_SUPPORT.ABORT and return error. + * Retrieves Room session token from Auth server. + * readyStateChange event triggers + * parameter payload state as LOADING. + * If retrieval was successful: readyStateChange event + * triggers parameter payload state as COMPLETED.Else: + * readyStateChange event triggers parameter + * payload state as ERROR.ABORT and return error. + * @for Skylink + * @since 0.5.5 + */ +Skylink.prototype.init = function(options, callback) { + var self = this; + + if (typeof options === 'function'){ + callback = options; + options = undefined; + } + + if (!options) { + var error = 'No API key provided'; + log.error(error); + if (typeof callback === 'function'){ + callback(error,null); + } + return; + } + + var appKey, room, defaultRoom, region; + var startDateTime, duration, credentials; + var roomServer = self._roomServer; + // NOTE: Should we get all the default values from the variables + // rather than setting it? + var enableIceTrickle = true; + var enableDataChannel = true; + var enableSTUNServer = true; + var enableTURNServer = true; + var TURNTransport = self.TURN_TRANSPORT.ANY; + var audioFallback = false; + var forceSSL = false; + var socketTimeout = 0; + var forceTURNSSL = false; + var audioCodec = self.AUDIO_CODEC.AUTO; + var videoCodec = self.VIDEO_CODEC.AUTO; + var forceTURN = false; + var usePublicSTUN = true; + + log.log('Provided init options:', options); + + if (typeof options === 'string') { + // set all the default api key, default room and room + appKey = options; + defaultRoom = appKey; + room = appKey; + } else { + // set the api key + appKey = options.appKey || options.apiKey; + // set the room server + roomServer = (typeof options.roomServer === 'string') ? + options.roomServer : roomServer; + // check room server if it ends with /. Remove the extra / + roomServer = (roomServer.lastIndexOf('/') === + (roomServer.length - 1)) ? roomServer.substring(0, + roomServer.length - 1) : roomServer; + // set the region + region = (typeof options.region === 'string') ? + options.region : region; + // set the default room + defaultRoom = (typeof options.defaultRoom === 'string') ? + options.defaultRoom : appKey; + // set the selected room + room = defaultRoom; + // set ice trickle option + enableIceTrickle = (typeof options.enableIceTrickle === 'boolean') ? + options.enableIceTrickle : enableIceTrickle; + // set data channel option + enableDataChannel = (typeof options.enableDataChannel === 'boolean') ? + options.enableDataChannel : enableDataChannel; + // set stun server option + enableSTUNServer = (typeof options.enableSTUNServer === 'boolean') ? + options.enableSTUNServer : enableSTUNServer; + // set turn server option + enableTURNServer = (typeof options.enableTURNServer === 'boolean') ? + options.enableTURNServer : enableTURNServer; + // set the force ssl always option + forceSSL = (typeof options.forceSSL === 'boolean') ? + options.forceSSL : forceSSL; + // set the socket timeout option + socketTimeout = (typeof options.socketTimeout === 'number') ? + options.socketTimeout : socketTimeout; + // set the socket timeout option to be above 5000 + socketTimeout = (socketTimeout < 5000) ? 5000 : socketTimeout; + // set the force turn ssl always option + forceTURNSSL = (typeof options.forceTURNSSL === 'boolean') ? + options.forceTURNSSL : forceTURNSSL; + // set the preferred audio codec + audioCodec = typeof options.audioCodec === 'string' ? + options.audioCodec : audioCodec; + // set the preferred video codec + videoCodec = typeof options.videoCodec === 'string' ? + options.videoCodec : videoCodec; + // set the force turn server option + forceTURN = (typeof options.forceTURN === 'boolean') ? + options.forceTURN : forceTURN; + // set the use public stun option + usePublicSTUN = (typeof options.usePublicSTUN === 'boolean') ? + options.usePublicSTUN : usePublicSTUN; + + // set turn transport option + if (typeof options.TURNServerTransport === 'string') { + // loop out for every transport option + for (var type in self.TURN_TRANSPORT) { + if (self.TURN_TRANSPORT.hasOwnProperty(type)) { + // do a check if the transport option is valid + if (self.TURN_TRANSPORT[type] === options.TURNServerTransport) { + TURNTransport = options.TURNServerTransport; + break; + } + } + } + } + // set audio fallback option + audioFallback = options.audioFallback || audioFallback; + // Custom default meeting timing and duration + // Fallback to default if no duration or startDateTime provided + if (options.credentials && + typeof options.credentials.credentials === 'string' && + typeof options.credentials.duration === 'number' && + typeof options.credentials.startDateTime === 'string') { + // set start data time + startDateTime = options.credentials.startDateTime; + // set the duration + duration = options.credentials.duration; + // set the credentials + credentials = options.credentials.credentials; + } + + // if force turn option is set to on + if (forceTURN === true) { + enableTURNServer = true; + enableSTUNServer = false; + } + } + // api key path options + self._appKey = appKey; + self._roomServer = roomServer; + self._defaultRoom = defaultRoom; + self._selectedRoom = room; + self._serverRegion = region || null; + self._path = roomServer + '/api/' + appKey + '/' + room; + // set credentials if there is + if (credentials && startDateTime && duration) { + self._roomStart = startDateTime; + self._roomDuration = duration; + self._roomCredentials = credentials; + self._path += (credentials) ? ('/' + startDateTime + '/' + + duration + '?&cred=' + credentials) : ''; + } + + self._path += ((credentials) ? '&' : '?') + 'rand=' + (new Date()).toISOString(); + + // check if there is a other query parameters or not + if (region) { + self._path += '&rg=' + region; + } + // skylink functionality options + self._enableIceTrickle = enableIceTrickle; + self._enableDataChannel = enableDataChannel; + self._enableSTUN = enableSTUNServer; + self._enableTURN = enableTURNServer; + self._TURNTransport = TURNTransport; + self._audioFallback = audioFallback; + self._forceSSL = forceSSL; + self._socketTimeout = socketTimeout; + self._forceTURNSSL = forceTURNSSL; + self._selectedAudioCodec = audioCodec; + self._selectedVideoCodec = videoCodec; + self._forceTURN = forceTURN; + self._usePublicSTUN = usePublicSTUN; + + log.log('Init configuration:', { + serverUrl: self._path, + readyState: self._readyState, + appKey: self._appKey, + roomServer: self._roomServer, + defaultRoom: self._defaultRoom, + selectedRoom: self._selectedRoom, + serverRegion: self._serverRegion, + enableDataChannel: self._enableDataChannel, + enableIceTrickle: self._enableIceTrickle, + enableTURNServer: self._enableTURN, + enableSTUNServer: self._enableSTUN, + TURNTransport: self._TURNTransport, + audioFallback: self._audioFallback, + forceSSL: self._forceSSL, + socketTimeout: self._socketTimeout, + forceTURNSSL: self._forceTURNSSL, + audioCodec: self._selectedAudioCodec, + videoCodec: self._selectedVideoCodec, + forceTURN: self._forceTURN, + usePublicSTUN: self._usePublicSTUN + }); + // trigger the readystate + self._readyState = 0; + self._trigger('readyStateChange', self.READY_STATE_CHANGE.INIT, null, self._selectedRoom); + + if (typeof callback === 'function'){ + var hasTriggered = false; + + var readyStateChangeFn = function (readyState, error) { + if (!hasTriggered) { + if (readyState === self.READY_STATE_CHANGE.COMPLETED) { + log.log([null, 'Socket', null, 'Firing callback. ' + + 'Ready state change has met provided state ->'], readyState); + hasTriggered = true; + self.off('readyStateChange', readyStateChangeFn); + callback(null,{ + serverUrl: self._path, + readyState: self._readyState, + appKey: self._appKey, + roomServer: self._roomServer, + defaultRoom: self._defaultRoom, + selectedRoom: self._selectedRoom, + serverRegion: self._serverRegion, + enableDataChannel: self._enableDataChannel, + enableIceTrickle: self._enableIceTrickle, + enableTURNServer: self._enableTURN, + enableSTUNServer: self._enableSTUN, + TURNTransport: self._TURNTransport, + audioFallback: self._audioFallback, + forceSSL: self._forceSSL, + socketTimeout: self._socketTimeout, + forceTURNSSL: self._forceTURNSSL, + audioCodec: self._selectedAudioCodec, + videoCodec: self._selectedVideoCodec, + forceTURN: self._forceTURN, + usePublicSTUN: self._usePublicSTUN + }); + } else if (readyState === self.READY_STATE_CHANGE.ERROR) { + log.log([null, 'Socket', null, 'Firing callback. ' + + 'Ready state change has met provided state ->'], readyState); + log.debug([null, 'Socket', null, 'Ready state met failure'], error); + hasTriggered = true; + self.off('readyStateChange', readyStateChangeFn); + callback({ + error: new Error(error), + errorCode: error.errorCode, + status: error.status + },null); + } + } + }; + + self.on('readyStateChange', readyStateChangeFn); + } + + self._loadInfo(); +}; + +/** + * Starts retrieving Room credentials information from API server. * @method _requestServerInfo - * @param {String} method The http method. - * @param {String} url The url to do a rest call. - * @param {Function} callback The callback fired after Skylink - * receives a response from the api server. - * @param {JSON} params HTTP Params * @private - * @component Room * @for Skylink * @since 0.5.2 */ @@ -3619,9 +7391,15 @@ Skylink.prototype._requestServerInfo = function(method, url, callback, params) { callback(status, JSON.parse(response || '{}')); }; - xhr.onerror = function () { + xhr.onerror = function (error) { log.error([null, 'XMLHttpRequest', method, 'Failed retrieving information:'], { status: xhr.status }); + self._readyState = -1; + self._trigger('readyStateChange', self.READY_STATE_CHANGE.ERROR, { + status: xhr.status || null, + content: 'Network error occurred. (Status: ' + xhr.status + ')', + errorCode: self.READY_STATE_CHANGE_ERROR.XML_HTTP_REQUEST_ERROR + }, self._selectedRoom); }; xhr.onprogress = function () { @@ -3640,13 +7418,9 @@ Skylink.prototype._requestServerInfo = function(method, url, callback, params) { }; /** - * Parse the information received from the api server. + * Parses the Room credentials information retrieved from API server. * @method _parseInfo - * @param {JSON} info The parsed information from the server. - * @trigger readyStateChange * @private - * @required - * @component Room * @for Skylink * @since 0.5.2 */ @@ -3657,7 +7431,7 @@ Skylink.prototype._parseInfo = function(info) { status: 200, content: info.info, errorCode: info.error - }); + }, self._selectedRoom); return; } @@ -3665,9 +7439,13 @@ Skylink.prototype._parseInfo = function(info) { log.debug('Offer constraints:', info.offer_constraints); this._key = info.cid; - this._apiKeyOwner = info.apiOwner; + this._appKeyOwner = info.apiOwner; this._signalingServer = info.ipSigserver; + this._signalingServerPort = null; + + this._isPrivileged = info.isPrivileged; + this._autoIntroduce = info.autoIntroduce; this._user = { uid: info.username, @@ -3694,7 +7472,7 @@ Skylink.prototype._parseInfo = function(info) { mediaConstraints: JSON.parse(info.media_constraints) } }; - this._parseDefaultMediaStreamSettings(this._room.connection.mediaConstraints); + //this._parseDefaultMediaStreamSettings(this._room.connection.mediaConstraints); // set the socket ports this._socketPorts = { @@ -3706,90 +7484,107 @@ Skylink.prototype._parseInfo = function(info) { //this._streamSettings.bandwidth = info.bandwidth; //this._streamSettings.video = info.video; this._readyState = 2; - this._trigger('readyStateChange', this.READY_STATE_CHANGE.COMPLETED); + this._trigger('readyStateChange', this.READY_STATE_CHANGE.COMPLETED, null, this._selectedRoom); log.info('Parsed parameters from webserver. ' + 'Ready for web-realtime communication'); }; /** - * Start the loading of information from the api server. + * Loads and checks the dependencies if they are loaded correctly. * @method _loadInfo - * @trigger readyStateChange * @private - * @required - * @component Room * @for Skylink * @since 0.5.2 */ Skylink.prototype._loadInfo = function() { var self = this; + // check if adapterjs has been loaded already first or not + var adapter = (function () { + try { + return window.AdapterJS || AdapterJS; + } catch (error) { + return false; + } + })(); + + if (!(!!adapter ? typeof adapter.webRTCReady === 'function' : false)) { + var noAdapterErrorMsg = 'AdapterJS dependency is not loaded or incorrect AdapterJS dependency is used'; + self._trigger('readyStateChange', self.READY_STATE_CHANGE.ERROR, { + status: null, + content: noAdapterErrorMsg, + errorCode: self.READY_STATE_CHANGE_ERROR.ADAPTER_NO_LOADED + }, self._selectedRoom); + return; + } if (!window.io) { log.error('Socket.io not loaded. Please load socket.io'); + self._readyState = -1; self._trigger('readyStateChange', self.READY_STATE_CHANGE.ERROR, { status: null, content: 'Socket.io not found', errorCode: self.READY_STATE_CHANGE_ERROR.NO_SOCKET_IO - }); + }, self._selectedRoom); return; } if (!window.XMLHttpRequest) { log.error('XMLHttpRequest not supported. Please upgrade your browser'); + self._readyState = -1; self._trigger('readyStateChange', self.READY_STATE_CHANGE.ERROR, { status: null, content: 'XMLHttpRequest not available', errorCode: self.READY_STATE_CHANGE_ERROR.NO_XMLHTTPREQUEST_SUPPORT - }); - return; - } - if (!window.RTCPeerConnection) { - log.error('WebRTC not supported. Please upgrade your browser'); - self._trigger('readyStateChange', self.READY_STATE_CHANGE.ERROR, { - status: null, - content: 'WebRTC not available', - errorCode: self.READY_STATE_CHANGE_ERROR.NO_WEBRTC_SUPPORT - }); + }, self._selectedRoom); return; } if (!self._path) { log.error('Skylink is not initialised. Please call init() first'); + self._readyState = -1; self._trigger('readyStateChange', self.READY_STATE_CHANGE.ERROR, { status: null, content: 'No API Path is found', errorCode: self.READY_STATE_CHANGE_ERROR.NO_PATH - }); + }, self._selectedRoom); return; } - self._readyState = 1; - self._trigger('readyStateChange', self.READY_STATE_CHANGE.LOADING); - self._requestServerInfo('GET', self._path, function(status, response) { - if (status !== 200) { - // 403 - Room is locked - // 401 - API Not authorized - // 402 - run out of credits - var errorMessage = 'XMLHttpRequest status not OK\nStatus was: ' + status; - self._readyState = 0; + adapter.webRTCReady(function () { + if (!window.RTCPeerConnection) { + log.error('WebRTC not supported. Please upgrade your browser'); + self._readyState = -1; self._trigger('readyStateChange', self.READY_STATE_CHANGE.ERROR, { - status: status, - content: (response) ? (response.info || errorMessage) : errorMessage, - errorCode: response.error || - self.READY_STATE_CHANGE_ERROR.INVALID_XMLHTTPREQUEST_STATUS - }); + status: null, + content: 'WebRTC not available', + errorCode: self.READY_STATE_CHANGE_ERROR.NO_WEBRTC_SUPPORT + }, self._selectedRoom); return; } - self._parseInfo(response); + self._readyState = 1; + self._trigger('readyStateChange', self.READY_STATE_CHANGE.LOADING, null, self._selectedRoom); + self._requestServerInfo('GET', self._path, function(status, response) { + if (status !== 200) { + // 403 - Room is locked + // 401 - API Not authorized + // 402 - run out of credits + var errorMessage = 'XMLHttpRequest status not OK\nStatus was: ' + status; + self._readyState = 0; + self._trigger('readyStateChange', self.READY_STATE_CHANGE.ERROR, { + status: status, + content: (response) ? (response.info || errorMessage) : errorMessage, + errorCode: response.error || + self.READY_STATE_CHANGE_ERROR.INVALID_XMLHTTPREQUEST_STATUS + }, self._selectedRoom); + return; + } + self._parseInfo(response); + }); }); }; /** - * Initialize Skylink to retrieve new connection information based on options. + * Starts initialising for Room credentials for room name provided in joinRoom() method. * @method _initSelectedRoom - * @param {String} [room=Skylink._defaultRoom] The room to connect to. - * @param {Function} callback The callback fired once Skylink is re-initialized. - * @trigger readyStateChange * @private - * @component Room * @for Skylink * @since 0.5.5 */ @@ -3803,7 +7598,7 @@ Skylink.prototype._initSelectedRoom = function(room, callback) { var initOptions = { roomServer: self._roomServer, defaultRoom: room || defaultRoom, - apiKey: self._apiKey, + appKey: self._appKey, region: self._serverRegion, enableDataChannel: self._enableDataChannel, enableIceTrickle: self._enableIceTrickle @@ -3815,341 +7610,14 @@ Skylink.prototype._initSelectedRoom = function(room, callback) { startDateTime: self._roomStart }; } - self.init(initOptions); - self._defaultRoom = defaultRoom; - - // wait for ready state to be completed - self._condition('readyStateChange', function () { - callback(); - }, function () { - return self._readyState === self.READY_STATE_CHANGE.COMPLETED; - }, function (state) { - return state === self.READY_STATE_CHANGE.COMPLETED; - }); -}; - -/** - * Initialize Skylink to retrieve connection information. - * This is the first method to invoke before using any of Skylink functionalities. - * - Credentials parsing is not usabel. - * @method init - * @param {String|JSON} options Connection options or API Key ID - * @param {String} options.apiKey API Key ID to identify with the Temasys - * backend server - * @param {String} [options.defaultRoom] The default room to connect - * to if there is no room provided in - * {{#crossLink "Skylink/joinRoom:method"}}joinRoom(){{/crossLink}}. - * @param {String} [options.roomServer] Path to the Temasys - * backend server. If there's no room provided, default room would be used. - * @param {String} [options.region] The regional server that user - * chooses to use. [Rel: Skylink.REGIONAL_SERVER] - * @param {Boolean} [options.enableIceTrickle=true] The option to enable - * ICE trickle or not. - * @param {Boolean} [options.enableDataChannel=true] The option to enable - * enableDataChannel or not. - * @param {Boolean} [options.enableTURNServer=true] To enable TURN servers in ice connection. - * Please do so at your own risk as it might disrupt the connection. - * @param {Boolean} [options.enableSTUNServer=true] To enable STUN servers in ice connection. - * Please do so at your own risk as it might disrupt the connection. - * @param {Boolean} [options.TURNServerTransport=Skylink.TURN_TRANSPORT.ANY] Transport - * to set the transport packet type. [Rel: Skylink.TURN_TRANSPORT] - * @param {JSON} [options.credentials] Credentials options for - * setting a static meeting. - * @param {String} options.credentials.startDateTime The start timing of the - * meeting in Date ISO String - * @param {Number} options.credentials.duration The duration of the meeting in hours. - * E.g. 0.5 for half an hour, 1.4 for 1 hour and 24 minutes - * @param {String} options.credentials.credentials The credentials required - * to set the timing and duration of a meeting. - * @param {Boolean} [options.audioFallback=false] To allow the option to fallback to - * audio if failed retrieving video stream. - * @param {Boolean} [options.forceSSL=false] To force SSL connections to the API server - * and signaling server. - * @param {String} [options.audioCodec=Skylink.AUDIO_CODEC.OPUS] The preferred audio codec to use. - * It is only used when available. - * @param {String} [options.audioCodec=Skylink.VIDEO_CODEC.OPUS] The preferred video codec to use. - * It is only used when available. - * @param {Number} [options.socketTimeout=20000] To set the timeout for socket to fail - * and attempt a reconnection. The mininum value is 5000. - * @param {Function} [callback] The callback fired after the room was initialized. - * Default signature: function(error object, success object) - * @example - * // Note: Default room is apiKey when no room - * // Example 1: To initalize without setting any default room. - * SkylinkDemo.init('apiKey'); - * - * // Example 2: To initialize with apikey, roomServer and defaultRoom - * SkylinkDemo.init({ - * 'apiKey' : 'apiKey', - * 'roomServer' : 'http://xxxx.com', - * 'defaultRoom' : 'mainHangout' - * }); - * - * // Example 3: To initialize with credentials to set startDateTime and - * // duration of the room - * var hash = CryptoJS.HmacSHA1(roomname + '_' + duration + '_' + - * (new Date()).toISOString(), token); - * var credentials = encodeURIComponent(hash.toString(CryptoJS.enc.Base64)); - * SkylinkDemo.init({ - * 'apiKey' : 'apiKey', - * 'roomServer' : 'http://xxxx.com', - * 'defaultRoom' : 'mainHangout' - * 'credentials' : { - * 'startDateTime' : (new Date()).toISOString(), - * 'duration' : 500, - * 'credentials' : credentials - * } - * }); - * - * // Example 4: To initialize with callback - * SkylinkDemo.init('apiKey',function(error,success){ - * if (error){ - * console.log('Init failed: '+JSON.stringify(error)); - * } - * else{ - * console.log('Init succeed: '+JSON.stringify(success)); - * } - * }); - * - * @trigger readyStateChange - * @required - * @component Room - * @for Skylink - * @since 0.5.5 - */ -Skylink.prototype.init = function(options, callback) { - var self = this; - - if (typeof options === 'function'){ - callback = options; - options = undefined; - } - - if (!options) { - var error = 'No API key provided'; - log.error(error); - if (typeof callback === 'function'){ - callback(error,null); - } - return; - } - - var adapter = (function () { - try { - return window.AdapterJS || AdapterJS; - } catch (error) { - return false; - } - })(); - - if (!!adapter ? typeof adapter.webRTCReady === 'function' : false) { - adapter.webRTCReady(function () { - - var apiKey, room, defaultRoom, region; - var startDateTime, duration, credentials; - var roomServer = self._roomServer; - // NOTE: Should we get all the default values from the variables - // rather than setting it? - var enableIceTrickle = true; - var enableDataChannel = true; - var enableSTUNServer = true; - var enableTURNServer = true; - var TURNTransport = self.TURN_TRANSPORT.ANY; - var audioFallback = false; - var forceSSL = false; - var socketTimeout = 0; - var audioCodec = self.AUDIO_CODEC.AUTO; - var videoCodec = self.VIDEO_CODEC.AUTO; - - log.log('Provided init options:', options); - - if (typeof options === 'string') { - // set all the default api key, default room and room - apiKey = options; - defaultRoom = apiKey; - room = apiKey; - } else { - // set the api key - apiKey = options.apiKey; - // set the room server - roomServer = options.roomServer || roomServer; - // check room server if it ends with /. Remove the extra / - roomServer = (roomServer.lastIndexOf('/') === - (roomServer.length - 1)) ? roomServer.substring(0, - roomServer.length - 1) : roomServer; - // set the region - region = options.region || region; - // set the default room - defaultRoom = options.defaultRoom || apiKey; - // set the selected room - room = defaultRoom; - // set ice trickle option - enableIceTrickle = (typeof options.enableIceTrickle === 'boolean') ? - options.enableIceTrickle : enableIceTrickle; - // set data channel option - enableDataChannel = (typeof options.enableDataChannel === 'boolean') ? - options.enableDataChannel : enableDataChannel; - // set stun server option - enableSTUNServer = (typeof options.enableSTUNServer === 'boolean') ? - options.enableSTUNServer : enableSTUNServer; - // set turn server option - enableTURNServer = (typeof options.enableTURNServer === 'boolean') ? - options.enableTURNServer : enableTURNServer; - // set the force ssl always option - forceSSL = (typeof options.forceSSL === 'boolean') ? - options.forceSSL : forceSSL; - // set the socket timeout option - socketTimeout = (typeof options.socketTimeout === 'number') ? - options.socketTimeout : socketTimeout; - // set the socket timeout option to be above 5000 - socketTimeout = (socketTimeout < 5000) ? 5000 : socketTimeout; - // set the preferred audio codec - audioCodec = typeof options.audioCodec === 'string' ? - options.audioCodec : audioCodec; - // set the preferred video codec - videoCodec = typeof options.videoCodec === 'string' ? - options.videoCodec : videoCodec; - - // set turn transport option - if (typeof options.TURNServerTransport === 'string') { - // loop out for every transport option - for (var type in self.TURN_TRANSPORT) { - if (self.TURN_TRANSPORT.hasOwnProperty(type)) { - // do a check if the transport option is valid - if (self.TURN_TRANSPORT[type] === options.TURNServerTransport) { - TURNTransport = options.TURNServerTransport; - break; - } - } - } - } - // set audio fallback option - audioFallback = options.audioFallback || audioFallback; - // Custom default meeting timing and duration - // Fallback to default if no duration or startDateTime provided - if (options.credentials) { - // set start data time - startDateTime = options.credentials.startDateTime || - (new Date()).toISOString(); - // set the duration - duration = options.credentials.duration || 200; - // set the credentials - credentials = options.credentials.credentials; - } - } - // api key path options - self._apiKey = apiKey; - self._roomServer = roomServer; - self._defaultRoom = defaultRoom; - self._selectedRoom = room; - self._serverRegion = region; - self._path = roomServer + '/api/' + apiKey + '/' + room; - // set credentials if there is - if (credentials) { - self._roomStart = startDateTime; - self._roomDuration = duration; - self._roomCredentials = credentials; - self._path += (credentials) ? ('/' + startDateTime + '/' + - duration + '?&cred=' + credentials) : ''; - } - - self._path += ((credentials) ? '&' : '?') + 'rand=' + (new Date()).toISOString(); - - // check if there is a other query parameters or not - if (region) { - self._path += '&rg=' + region; - } - // skylink functionality options - self._enableIceTrickle = enableIceTrickle; - self._enableDataChannel = enableDataChannel; - self._enableSTUN = enableSTUNServer; - self._enableTURN = enableTURNServer; - self._TURNTransport = TURNTransport; - self._audioFallback = audioFallback; - self._forceSSL = forceSSL; - self._socketTimeout = socketTimeout; - self._selectedAudioCodec = audioCodec; - self._selectedVideoCodec = videoCodec; - - log.log('Init configuration:', { - serverUrl: self._path, - readyState: self._readyState, - apiKey: self._apiKey, - roomServer: self._roomServer, - defaultRoom: self._defaultRoom, - selectedRoom: self._selectedRoom, - serverRegion: self._serverRegion, - enableDataChannel: self._enableDataChannel, - enableIceTrickle: self._enableIceTrickle, - enableTURNServer: self._enableTURN, - enableSTUNServer: self._enableSTUN, - TURNTransport: self._TURNTransport, - audioFallback: self._audioFallback, - forceSSL: self._forceSSL, - socketTimeout: self._socketTimeout, - audioCodec: self._selectedAudioCodec, - videoCodec: self._selectedVideoCodec - }); - // trigger the readystate - self._readyState = 0; - self._trigger('readyStateChange', self.READY_STATE_CHANGE.INIT); - self._loadInfo(); - - if (typeof callback === 'function'){ - //Success callback fired if readyStateChange is completed - self.once('readyStateChange',function(readyState, error){ - log.log([null, 'Socket', null, 'Firing callback. ' + - 'Ready state change has met provided state ->'], readyState); - callback(null,{ - serverUrl: self._path, - readyState: self._readyState, - apiKey: self._apiKey, - roomServer: self._roomServer, - defaultRoom: self._defaultRoom, - selectedRoom: self._selectedRoom, - serverRegion: self._serverRegion, - enableDataChannel: self._enableDataChannel, - enableIceTrickle: self._enableIceTrickle, - enableTURNServer: self._enableTURN, - enableSTUNServer: self._enableSTUN, - TURNTransport: self._TURNTransport, - audioFallback: self._audioFallback, - forceSSL: self._forceSSL, - socketTimeout: self._socketTimeout, - audioCodec: self._selectedAudioCodec, - videoCodec: self._selectedVideoCodec - }); - }, - function(state){ - return state === self.READY_STATE_CHANGE.COMPLETED; - }, - false - ); - - //Error callback fired if readyStateChange is error - self.once('readyStateChange',function(readyState, error){ - log.log([null, 'Socket', null, 'Firing callback. ' + - 'Ready state change has met provided state ->'], readyState); - callback(error,null); - }, - function(state){ - return state === self.READY_STATE_CHANGE.ERROR; - }, - false - ); - } - }); - } else { - self._trigger('readyStateChange', self.READY_STATE_CHANGE.ERROR, { - status: null, - content: 'AdapterJS dependency is not loaded or incorrect AdapterJS dependency is used', - errorCode: self.READY_STATE_CHANGE_ERROR.ADAPTER_NO_LOADED - }); - - if (typeof callback === 'function'){ - callback(new Error('AdapterJS dependency is not loaded or incorrect AdapterJS dependency is used'),null); + self.init(initOptions, function (error, success) { + self._defaultRoom = defaultRoom; + if (error) { + callback(error); + } else { + callback(null); } - } + }); }; @@ -4166,13 +7634,12 @@ Skylink.prototype.LOG_LEVEL = { }; /** - * The log key + * Stores the log message starting header string. + * E.g. " - ". * @attribute _LOG_KEY * @type String - * @scoped true - * @readOnly * @private - * @component Log + * @scoped true * @for Skylink * @since 0.5.4 */ @@ -4180,100 +7647,83 @@ var _LOG_KEY = 'SkylinkJS'; /** - * The list of level levels based on index. + * Stores the list of available SDK log levels. * @attribute _LOG_LEVELS * @type Array - * @required - * @scoped true * @private - * @component Log + * @scoped true * @for Skylink * @since 0.5.5 */ var _LOG_LEVELS = ['error', 'warn', 'info', 'log', 'debug']; /** - * The log level of Skylink + * Stores the current SDK log level. + * Default is ERROR (0). * @attribute _logLevel * @type String - * @default Skylink.LOG_LEVEL.ERROR - * @required - * @scoped true + * @default 0 * @private - * @component Log + * @scoped true * @for Skylink * @since 0.5.4 */ var _logLevel = 0; /** - * The current state if debugging mode is enabled. + * Stores the flag if debugging mode is enabled. + * This manipulates the SkylinkLogs interface. * @attribute _enableDebugMode * @type Boolean * @default false * @private - * @required * @scoped true - * @component Log * @for Skylink * @since 0.5.4 */ var _enableDebugMode = false; /** - * The current state if debugging mode should store - * the logs in SkylinkLogs. + * Stores the flag if logs should be stored in SkylinkLogs interface. * @attribute _enableDebugStack * @type Boolean * @default false * @private - * @required * @scoped true - * @component Log * @for Skylink * @since 0.5.5 */ var _enableDebugStack = false; /** - * The current state if debugging mode should - * print the trace in every log information. + * Stores the flag if logs should trace if available. + * This uses the console.trace API. * @attribute _enableDebugTrace * @type Boolean * @default false * @private - * @required * @scoped true - * @component Log * @for Skylink * @since 0.5.5 */ var _enableDebugTrace = false; /** - * An internal array of logs. + * Stores the logs used for SkylinkLogs object. * @attribute _storedLogs * @type Array * @private - * @required * @scoped true - * @component Log * @for Skylink * @since 0.5.5 */ var _storedLogs = []; /** - * Gets the list of logs + * Function that gets the stored logs. * @method _getStoredLogsFn - * @param {Number} [logLevel] The log level that get() should return. - * If not provided, it get() will return all logs from all levels. - * [Rel: Skylink.LOG_LEVEL] - * @return {Array} The array of logs * @private - * @required * @scoped true - * @component Log * @for Skylink * @since 0.5.5 */ @@ -4291,16 +7741,10 @@ var _getStoredLogsFn = function (logLevel) { }; /** - * Gets the list of logs + * Function that clears the stored logs. * @method _clearAllStoredLogsFn - * @param {Number} [logLevel] The log level that get() should return. - * If not provided, it get() will return all logs from all levels. - * [Rel: Skylink.LOG_LEVEL] - * @return {Array} The array of logs * @private - * @required * @scoped true - * @component Log * @for Skylink * @since 0.5.5 */ @@ -4309,12 +7753,10 @@ var _clearAllStoredLogsFn = function () { }; /** - * Print out all the store logs in console. + * Function that prints in the Web Console interface the stored logs. * @method _printAllStoredLogsFn * @private - * @required * @scoped true - * @component Log * @for Skylink * @since 0.5.5 */ @@ -4335,51 +7777,71 @@ var _printAllStoredLogsFn = function () { }; /** - * Handles the list of Skylink logs. + * + * To utilise and enable the SkylinkLogs API functionalities, the + * setDebugMode() method + * options.storeLogs parameter has to be enabled. + * + * The object interface to manage the SDK + * Javascript Web Console logs. * @property SkylinkLogs * @type JSON - * @required * @global true - * @component Log * @for Skylink * @since 0.5.5 */ window.SkylinkLogs = { /** - * Gets the list of logs + * Function that gets the current stored SDK console logs. * @property SkylinkLogs.getLogs - * @param {Number} [logLevel] The log level that getLogs() should return. - * If not provided, it getLogs() will return all logs from all levels. + * @param {Number} [logLevel] The specific log level of logs to return. + * - When not provided or that the level does not exists, it will return all logs of all levels. * [Rel: Skylink.LOG_LEVEL] - * @return {Array} The array of logs + * @return {Array} The array of stored logs. + * <#index>{Array}The stored log item. + * 0{Date}The DateTime of when the log was stored. + * 1{String}The log level. [Rel: Skylink.LOG_LEVEL] + * 2{String}The log message. + * 3{Any}OptionalThe log message object. + * + * @example + * // Example 1: Get logs of specific level + * var debugLogs = SkylinkLogs.getLogs(skylinkDemo.LOG_LEVEL.DEBUG); + * + * // Example 2: Get all the logs + * var allLogs = SkylinkLogs.getLogs(); * @type Function - * @required * @global true - * @component Log + * @triggerForPropHackNone true * @for Skylink * @since 0.5.5 */ getLogs: _getStoredLogsFn, /** - * Clear all the stored logs. + * Function that clears all the current stored SDK console logs. * @property SkylinkLogs.clearAllLogs * @type Function - * @required + * @example + * // Example 1: Clear all the logs + * SkylinkLogs.clearAllLogs(); * @global true - * @component Log + * @triggerForPropHackNone true * @for Skylink * @since 0.5.5 */ clearAllLogs: _clearAllStoredLogsFn, /** - * Print out all the store logs in console. + * Function that prints all the current stored SDK console logs into the + * Javascript Web Console. * @property SkylinkLogs.printAllLogs * @type Function - * @required + * @example + * // Example 1: Print all the logs + * SkylinkLogs.printAllLogs(); * @global true - * @component Log + * @triggerForPropHackNone true * @for Skylink * @since 0.5.5 */ @@ -4387,19 +7849,11 @@ window.SkylinkLogs = { }; /** - * Logs all the console information. + * Function that handles the logs received and prints in the Web Console interface according to the log level set. * @method _logFn - * @param {String} logLevel The log level. - * @param {Array|String} message The console message. - * @param {String} message.0 The targetPeerId the message is targeted to. - * @param {String} message.1 The interface the message is targeted to. - * @param {String|Array} message.2 The events the message is targeted to. - * @param {String} message.3: The log message. - * @param {Object|String} [debugObject] The console parameter string or object. * @private * @required * @scoped true - * @component Log * @for Skylink * @since 0.5.5 */ @@ -4464,151 +7918,64 @@ var _logFn = function(logLevel, message, debugObject) { }; /** - * Logs all the console information. + * Stores the logging functions. * @attribute log + * @param {Function} debug The function that handles the DEBUG level logs. + * @param {Function} log The function that handles the LOG level logs. + * @param {Function} info The function that handles the INFO level logs. + * @param {Function} warn The function that handles the WARN level logs. + * @param {Function} error The function that handles the ERROR level logs. * @type JSON - * @param {Function} debug For debug mode. - * @param {Function} log For log mode. - * @param {Function} info For info mode. - * @param {Function} warn For warn mode. - * @param {Function} serror For error mode. * @private - * @required * @scoped true - * @component Log * @for Skylink * @since 0.5.4 */ var log = { - /** - * Outputs a debug log in the console. - * @property log.debug - * @type Function - * @param {Array|String} message or the message - * @param {String} message.0 The targetPeerId the log is targetted to - * @param {String} message.1 he interface the log is targetted to - * @param {String|Array} message.2 The related names, keys or events to the log - * @param {String} message.3 The log message. - * @param {String|Object} [object] The log object. - * @example - * // Logging for message - * log.debug('This is my message', object); - * @private - * @required - * @scoped true - * @component Log - * @for Skylink - * @since 0.5.4 - */ debug: function (message, object) { _logFn(4, message, object); }, - /** - * Outputs a normal log in the console. - * @property log.log - * @type Function - * @param {Array|String} message or the message - * @param {String} message.0 The targetPeerId the log is targetted to - * @param {String} message.1 he interface the log is targetted to - * @param {String|Array} message.2 The related names, keys or events to the log - * @param {String} message.3 The log message. - * @param {String|Object} [object] The log object. - * @example - * // Logging for message - * log.log('This is my message', object); - * @private - * @required - * @scoped true - * @component Log - * @for Skylink - * @since 0.5.4 - */ log: function (message, object) { _logFn(3, message, object); }, - /** - * Outputs an info log in the console. - * @property log.info - * @type Function - * @param {Array|String} message or the message - * @param {String} message.0 The targetPeerId the log is targetted to - * @param {String} message.1 he interface the log is targetted to - * @param {String|Array} message.2 The related names, keys or events to the log - * @param {String} message.3 The log message. - * @param {String|Object} [object] The log object. - * @example - * // Logging for message - * log.debug('This is my message', object); - * @private - * @required - * @scoped true - * @component Log - * @for Skylink - * @since 0.5.4 - */ info: function (message, object) { _logFn(2, message, object); }, - /** - * Outputs a warning log in the console. - * @property log.warn - * @type Function - * @param {Array|String} message or the message - * @param {String} message.0 The targetPeerId the log is targetted to - * @param {String} message.1 he interface the log is targetted to - * @param {String|Array} message.2 The related names, keys or events to the log - * @param {String} message.3 The log message. - * @param {String|Object} [object] The log object. - * @example - * // Logging for message - * log.debug('Here\'s a warning. Please do xxxxx to resolve this issue', object); - * @private - * @required - * @component Log - * @for Skylink - * @since 0.5.4 - */ warn: function (message, object) { _logFn(1, message, object); }, - /** - * Outputs an error log in the console. - * @property log.error - * @type Function - * @param {Array|String} message or the message - * @param {String} message.0 The targetPeerId the log is targetted to - * @param {String} message.1 he interface the log is targetted to - * @param {String|Array} message.2 The related names, keys or events to the log - * @param {String} message.3 The log message. - * @param {String|Object} [object] The log object. - * // Logging for external information - * log.error('There has been an error', object); - * @private - * @required - * @scoped true - * @component Log - * @for Skylink - * @since 0.5.4 - */ error: function (message, object) { _logFn(0, message, object); } }; /** - * Sets the debugging log level. A log level displays logs of his level and higher: - * ERROR > WARN > INFO > LOG > DEBUG. - * - The default log level is Skylink.LOG_LEVEL.WARN + * Function that configures the level of console API logs to be printed in the + * Javascript Web Console. * @method setLogLevel - * @param {Number} [logLevel] The log level.[Rel: Skylink.Data.LOG_LEVEL] + * @param {Number} [logLevel] The specific log level of logs to return. + * - When not provided or that the level does not exists, it will not overwrite the current log level. + * By default, the initial log level is ERROR. + * [Rel: Skylink.LOG_LEVEL] * @example - * //Display logs level: Error, warn, info, log and debug. - * SkylinkDemo.setLogLevel(SkylinkDemo.LOG_LEVEL.DEBUG); - * @component Log + * // Example 1: Print all of the console.debug, console.log, console.info, console.warn and console.error logs. + * skylinkDemo.setLogLevel(skylinkDemo.LOG_LEVEL.DEBUG); + * + * // Example 2: Print only the console.log, console.info, console.warn and console.error logs. + * skylinkDemo.setLogLevel(skylinkDemo.LOG_LEVEL.LOG); + * + * // Example 3: Print only the console.info, console.warn and console.error logs. + * skylinkDemo.setLogLevel(skylinkDemo.LOG_LEVEL.INFO); + * + * // Example 4: Print only the console.warn and console.error logs. + * skylinkDemo.setLogLevel(skylinkDemo.LOG_LEVEL.WARN); + * + * // Example 5: Print only the console.error logs. This is done by default. + * skylinkDemo.setLogLevel(skylinkDemo.LOG_LEVEL.ERROR); * @for Skylink * @since 0.5.5 */ @@ -4627,22 +7994,28 @@ Skylink.prototype.setLogLevel = function(logLevel) { }; /** - * Sets Skylink in debugging mode to display log stack trace. - * - By default, debugging mode is turned off. + * Function that configures the debugging mode of the SDK. * @method setDebugMode - * @param {Boolean|JSON} [options=false] Is debugging mode enabled. - * @param {Boolean} [options.trace=false] If console output should trace. - * @param {Boolean} [options.storeLogs=false] If SkylinkLogs should store - * the output logs. + * @param {Boolean|JSON} [options=false] The debugging options. + * - When provided as a boolean, this sets both options.trace + * and options.storeLogs to its boolean value. + * @param {Boolean} [options.trace=false] The flag if SDK console logs + * should output as console.trace() logs for tracing the Function call stack. + * Note that the console.trace() output logs is determined by the log level set + * setLogLevel() method. + * If console.trace() API is not supported, setDebugMode() + * will fallback to use console.log() API. + * @param {Boolean} [options.storeLogs=false] The flag if SDK should store the console logs. + * This is required to be enabled for SkylinkLogs API. * @example - * // Example 1: just to enable - * SkylinkDemo.setDebugMode(true); - * // or - * SkylinkDemo.setDebugMode(); + * // Example 1: Enable both options.storeLogs and options.trace + * skylinkDemo.setDebugMode(true); * - * // Example 2: just to disable - * SkylinkDemo.setDebugMode(false); - * @component Log + * // Example 2: Enable only options.storeLogs + * skylinkDemo.setDebugMode({ storeLogs: true }); + * + * // Example 3: Disable debugging mode + * skylinkDemo.setDebugMode(); * @for Skylink * @since 0.5.2 */ @@ -4670,577 +8043,700 @@ Skylink.prototype.setDebugMode = function(isDebugMode) { }; Skylink.prototype._EVENTS = { /** - * Event fired when the socket connection to the signaling - * server is open. + * Event triggered when socket connection to Signaling server has opened. * @event channelOpen - * @component Events * @for Skylink * @since 0.1.0 */ channelOpen: [], /** - * Event fired when the socket connection to the signaling - * server has closed. + * Event triggered when socket connection to Signaling server has closed. * @event channelClose - * @component Events * @for Skylink * @since 0.1.0 */ channelClose: [], /** - * Event fired when the socket connection received a message - * from the signaling server. + * + * Note that this is used only for SDK developer purposes. + * + * Event triggered when receiving socket message from the Signaling server. * @event channelMessage - * @param {JSON} message - * @component Events + * @param {JSON} message The socket message object. * @for Skylink * @since 0.1.0 */ channelMessage: [], /** - * Event fired when the socket connection has occurred an error. + * + * This may be caused by Javascript errors in the event listener when subscribing to events. + * It may be resolved by checking for code errors in your Web App in the event subscribing listener. + * skylinkDemo.on("eventName", function () { // Errors here }); + * + * Event triggered when socket connection encountered exception. * @event channelError - * @param {Object|String} error Error message or object thrown. - * @component Events + * @param {Error|String} error The error object. * @for Skylink * @since 0.1.0 */ channelError: [], /** - * Event fired when the socket re-tries to connection with fallback ports. + * Event triggered when attempting to establish socket connection to Signaling server when failed. * @event channelRetry - * @param {String} fallbackType The type of fallback [Rel: Skylink.SOCKET_FALLBACK] - * @param {Number} currentAttempt The current attempt of the fallback re-try attempt. - * @component Events + * @param {String} fallbackType The current fallback state. + * [Rel: Skylink.SOCKET_FALLBACK] + * @param {Number} currentAttempt The current reconnection attempt. * @for Skylink * @since 0.5.6 */ channelRetry: [], /** - * Event fired when the socket connection failed connecting. - * - The difference between this and channelError is that - * channelError triggers during the connection. This throws - * when connection failed to be established. + * Event triggered when attempt to establish socket connection to Signaling server has failed. * @event socketError - * @param {String} errorCode The error code. + * @param {String} errorCode The socket connection error code. * [Rel: Skylink.SOCKET_ERROR] - * @param {Number|String|Object} error The reconnection attempt or error object. - * @param {String} fallbackType The type of fallback [Rel: Skylink.SOCKET_FALLBACK] - * @component Events + * @param {Error|String|Number} error The error object. + * @param {String} type The fallback state of the socket connection attempt. + * [Rel: Skylink.SOCKET_FALLBACK] * @for Skylink * @since 0.5.5 */ socketError: [], /** - * Event fired whether the room is ready for use. + * Event triggered when init() method ready state changes. * @event readyStateChange - * @param {String} readyState [Rel: Skylink.READY_STATE_CHANGE] - * @param {JSON} error Error object thrown. - * @param {Number} error.status Http status when retrieving information. - * May be empty for other errors. - * @param {String} error.content Error message. - * @param {Number} error.errorCode Error code. + * @param {String} readyState The current init() ready state. + * [Rel: Skylink.READY_STATE_CHANGE] + * @param {JSON} [error] The error result. + * Defined only when state is ERROR. + * @param {Number} error.status The HTTP status code when failed. + * @param {Number} error.errorCode The ready state change failure code. * [Rel: Skylink.READY_STATE_CHANGE_ERROR] - * @component Events + * @param {Error} error.content The error object. + * @param {String} room The Room to The Room to retrieve session token for. * @for Skylink * @since 0.4.0 */ readyStateChange: [], /** - * Event fired when a peer's handshake progress has changed. + * Event triggered when a Peer connection establishment state has changed. * @event handshakeProgress - * @param {String} step The handshake progress step. + * @param {String} state The current Peer connection establishment state. * [Rel: Skylink.HANDSHAKE_PROGRESS] - * @param {String} peerId PeerId of the peer's handshake progress. - * @param {Object|String} error Error message or object thrown. - * @component Events + * @param {String} peerId The Peer ID. + * @param {Error|String} [error] The error object. + * Defined only when state is ERROR. * @for Skylink * @since 0.3.0 */ handshakeProgress: [], /** - * Event fired when an ICE gathering state has changed. + * Event triggered when a Peer connection ICE gathering state has changed. * @event candidateGenerationState - * @param {String} state The ice candidate generation state. + * @param {String} state The current Peer connection ICE gathering state. * [Rel: Skylink.CANDIDATE_GENERATION_STATE] - * @param {String} peerId PeerId of the peer that had an ice candidate - * generation state change. - * @component Events + * @param {String} peerId The Peer ID. * @for Skylink * @since 0.1.0 */ candidateGenerationState: [], /** - * Event fired when a peer Connection state has changed. + * Event triggered when a Peer connection session description exchanging state has changed. * @event peerConnectionState - * @param {String} state The peer connection state. + * @param {String} state The current Peer connection session description exchanging state. * [Rel: Skylink.PEER_CONNECTION_STATE] - * @param {String} peerId PeerId of the peer that had a peer connection state - * change. - * @component Events + * @param {String} peerId The Peer ID. * @for Skylink * @since 0.1.0 */ peerConnectionState: [], /** - * Event fired when an ICE connection state has changed. - * @iceConnectionState - * @param {String} state The ice connection state. + * Event triggered when a Peer connection ICE connection state has changed. + * @event iceConnectionState + * @param {String} state The current Peer connection ICE connection state. * [Rel: Skylink.ICE_CONNECTION_STATE] - * @param {String} peerId PeerId of the peer that had an ice connection state change. - * @component Events + * @param {String} peerId The Peer ID. * @for Skylink * @since 0.1.0 */ iceConnectionState: [], /** - * Event fired when webcam or microphone media access fails. + * Event triggered when retrieval of Stream failed. * @event mediaAccessError - * @param {Object|String} error Error object thrown. - * @component Events + * @param {Error|String} error The error object. + * @param {Boolean} isScreensharing The flag if event occurred during + * shareScreen() method and not + * getUserMedia() method. + * @param {Boolean} isAudioFallbackError The flag if event occurred during + * retrieval of audio tracks only when getUserMedia() method + * had failed to retrieve both audio and video tracks. * @for Skylink * @since 0.1.0 */ mediaAccessError: [], /** - * Event fired when webcam or microphone media acces passes. + * Event triggered when Stream retrieval fallback state has changed. + * @event mediaAccessFallback + * @param {JSON} error The error result. + * @param {Error|String} error.error The error object. + * @param {JSON} [error.diff=null] The list of excepted but received audio and video tracks in Stream. + * Defined only when state payload is FALLBACKED. + * @param {JSON} error.diff.video The expected and received video tracks. + * @param {Number} error.diff.video.expected The expected video tracks. + * @param {Number} error.diff.video.received The received video tracks. + * @param {JSON} error.diff.audio The expected and received audio tracks. + * @param {Number} error.diff.audio.expected The expected audio tracks. + * @param {Number} error.diff.audio.received The received audio tracks. + * @param {Number} state The fallback state. + * [Rel: Skylink.MEDIA_ACCESS_FALLBACK_STATE] + * @param {Boolean} isScreensharing The flag if event occurred during + * shareScreen() method and not + * getUserMedia() method. + * @param {Boolean} isAudioFallback The flag if event occurred during + * retrieval of audio tracks only when getUserMedia() method + * had failed to retrieve both audio and video tracks. + * @param {String} streamId The Stream ID. + * Defined only when state payload is FALLBACKED. + * @for Skylink + * @since 0.6.3 + */ + mediaAccessFallback: [], + + /** + * Event triggered when retrieval of Stream is successful. * @event mediaAccessSuccess - * @param {Object} stream MediaStream object. - * @component Events + * @param {MediaStream} stream The Stream object. + * To attach it to an element: attachMediaStream(videoElement, stream);. + * @param {Boolean} isScreensharing The flag if event occurred during + * shareScreen() method and not + * getUserMedia() method. + * @param {Boolean} isAudioFallback The flag if event occurred during + * retrieval of audio tracks only when getUserMedia() method + * had failed to retrieve both audio and video tracks. + * @param {String} streamId The Stream ID. * @for Skylink * @since 0.1.0 */ mediaAccessSuccess: [], /** - * Event fired when it's required to have audio or video access. + * Event triggered when retrieval of Stream is required to complete + * joinRoom() method request. * @event mediaAccessRequired - * @component Events * @for Skylink * @since 0.5.5 */ mediaAccessRequired: [], /** - * Event fired when media access to MediaStream has stopped. + * Event triggered when Stream has stopped streaming. * @event mediaAccessStopped - * @component Events + * @param {Boolean} isScreensharing The flag if event occurred during + * shareScreen() method and not + * getUserMedia() method. + * @param {Boolean} isAudioFallback The flag if event occurred during + * retrieval of audio tracks only when getUserMedia() method + * had failed to retrieve both audio and video tracks. + * @param {String} streamId The Stream ID. * @for Skylink * @since 0.5.6 */ mediaAccessStopped: [], /** - * Event fired when a peer joins the room. + * Event triggered when a Peer joins the room. * @event peerJoined - * @param {String} peerId PeerId of the peer that joined the room. - * @param {JSON} peerInfo Peer's information. - * @param {JSON} peerInfo.settings Peer's stream settings. - * @param {Boolean|JSON} [peerInfo.settings.audio=false] Peer's audio stream - * settings. - * @param {Boolean} [peerInfo.settings.audio.stereo=false] If peer has stereo - * enabled or not. - * @param {Boolean|JSON} [peerInfo.settings.video=false] Peer's video stream - * settings. - * @param {JSON} [peerInfo.settings.video.resolution] - * Peer's video stream resolution [Rel: Skylink.VIDEO_RESOLUTION] - * @param {Number} [peerInfo.settings.video.resolution.width] - * Peer's video stream resolution width. - * @param {Number} [peerInfo.settings.video.resolution.height] - * Peer's video stream resolution height. - * @param {Number} [peerInfo.settings.video.frameRate] - * Peer's video stream resolution minimum frame rate. - * @param {JSON} peerInfo.mediaStatus Peer stream status. - * @param {Boolean} [peerInfo.mediaStatus.audioMuted=true] If peer's audio - * stream is muted. - * @param {Boolean} [peerInfo.mediaStatus.videoMuted=true] If peer's video - * stream is muted. - * @param {JSON|String} peerInfo.userData Peer's custom user data. - * @param {JSON} peerInfo.agent Peer's browser agent. - * @param {String} peerInfo.agent.name Peer's browser agent name. - * @param {Number} peerInfo.agent.version Peer's browser agent version. - * @param {Boolean} isSelf Is the peer self. - * @component Events + * @param {String} peerId The Peer ID. + * @param {JSON} peerInfo The Peer session information. + * @param {JSON|String} peerInfo.userData The Peer current custom data. + * @param {JSON} peerInfo.settings The Peer sending Stream settings. + * @param {Boolean|JSON} peerInfo.settings.audio The Peer Stream audio settings. + * When defined as false, it means there is no audio being sent from Peer. + * @param {Boolean} peerInfo.settings.audio.stereo The flag if stereo band is configured + * when encoding audio codec is OPUS for receiving audio data. + * @param {Array} [peerInfo.settings.audio.optional] The Peer Stream navigator.getUserMedia() API + * audio: { optional [..] } property. + * @param {String} [peerInfo.settings.audio.deviceId] The Peer Stream audio track source ID of the device used. + * @param {Boolean} peerInfo.settings.audio.exactConstraints The flag if Peer Stream audio track is sending exact + * requested values of peerInfo.settings.audio.deviceId when provided. + * @param {Boolean|JSON} peerInfo.settings.video The Peer Stream video settings. + * When defined as false, it means there is no video being sent from Peer. + * @param {JSON} peerInfo.settings.video.resolution The Peer Stream video resolution. + * [Rel: Skylink.VIDEO_RESOLUTION] + * @param {Number} peerInfo.settings.video.resolution.width The Peer Stream video resolution width. + * @param {Number} peerInfo.settings.video.resolution.height The Peer Stream video resolution height. + * @param {Number} [peerInfo.settings.video.frameRate] The Peer Stream video + * frameRate per second (fps). + * @param {Boolean} peerInfo.settings.video.screenshare The flag if Peer Stream is a screensharing Stream. + * @param {Array} [peerInfo.settings.video.optional] The Peer Stream navigator.getUserMedia() API + * video: { optional [..] } property. + * @param {String} [peerInfo.settings.video.deviceId] The Peer Stream video track source ID of the device used. + * @param {Boolean} peerInfo.settings.video.exactConstraints The flag if Peer Stream video track is sending exact + * requested values of peerInfo.settings.video.resolution, + * peerInfo.settings.video.frameRate and peerInfo.settings.video.deviceId + * when provided. + * @param {JSON} peerInfo.settings.bandwidth The maximum streaming bandwidth sent from Peer. + * @param {Number} [peerInfo.settings.bandwidth.audio] The maximum audio streaming bandwidth sent from Peer. + * @param {Number} [peerInfo.settings.bandwidth.video] The maximum video streaming bandwidth sent from Peer. + * @param {Number} [peerInfo.settings.bandwidth.data] The maximum data streaming bandwidth sent from Peer. + * @param {JSON} peerInfo.mediaStatus The Peer Stream muted settings. + * @param {Boolean} peerInfo.mediaStatus.audioMuted The flag if Peer Stream audio tracks is muted or not. + * If Peer peerInfo.settings.audio is false, this will be defined as true. + * @param {Boolean} peerInfo.mediaStatus.videoMuted The flag if Peer Stream video tracks is muted or not. + * If Peer peerInfo.settings.video is false, this will be defined as true. + * @param {JSON} peerInfo.agent The Peer agent information. + * @param {String} peerInfo.agent.name The Peer agent name. + * Data may be accessing browser or non-Web SDK name. + * @param {Number} peerInfo.agent.version The Peer agent version. + * Data may be accessing browser or non-Web SDK version. + * @param {String} [peerInfo.agent.os] The Peer platform name. + * Data may be accessing OS platform version from Web SDK. + * @param {String} [peerInfo.agent.pluginVersion] The Peer Temasys Plugin version. + * Defined only when Peer is using the Temasys Plugin (IE / Safari). + * @param {String} peerInfo.room The Room Peer is from. + * @param {Boolean} isSelf The flag if Peer is User. * @for Skylink * @since 0.5.2 */ peerJoined: [], /** - * Event fired when a peer's connection is restarted. + * Event triggered when a Peer connection has been refreshed. * @event peerRestart - * @param {String} peerId PeerId of the peer that is being restarted. - * @param {JSON} peerInfo Peer's information. - * @param {JSON} peerInfo.settings Peer's stream settings. - * @param {Boolean|JSON} peerInfo.settings.audio Peer's audio stream - * settings. - * @param {Boolean} peerInfo.settings.audio.stereo If peer has stereo - * enabled or not. - * @param {Boolean|JSON} peerInfo.settings.video Peer's video stream - * settings. - * @param {JSON} peerInfo.settings.video.resolution - * Peer's video stream resolution [Rel: Skylink.VIDEO_RESOLUTION] - * @param {Number} peerInfo.settings.video.resolution.width - * Peer's video stream resolution width. - * @param {Number} peerInfo.settings.video.resolution.height - * Peer's video stream resolution height. - * @param {Number} peerInfo.settings.video.frameRate - * Peer's video stream resolution minimum frame rate. - * @param {JSON} peerInfo.mediaStatus Peer stream status. - * @param {Boolean} peerInfo.mediaStatus.audioMuted If peer's audio - * stream is muted. - * @param {Boolean} peerInfo.mediaStatus.videoMuted If peer's video - * stream is muted. - * @param {JSON|String} peerInfo.userData Peer's custom user data. - * @param {JSON} peerInfo.agent Peer's browser agent. - * @param {String} peerInfo.agent.name Peer's browser agent name. - * @param {Number} peerInfo.agent.version Peer's browser agent version. - * @param {Boolean} isSelfInitiateRestart Is it us who initiated the restart. - * @component Events + * @param {String} peerId The Peer ID. + * @param {JSON} peerInfo The Peer session information. + * Object signature matches the peerInfo parameter payload received in the + * peerJoined event. + * @param {Boolean} isSelfInitiateRestart The flag if User is initiating the Peer connection refresh. * @for Skylink * @since 0.5.5 */ peerRestart: [], /** - * Event fired when a peer information is updated. + * Event triggered when a Peer session information has been updated. * @event peerUpdated - * @param {String} peerId PeerId of the peer that had information updaed. - * @param {JSON} peerInfo Peer's information. - * @param {JSON} peerInfo.settings Peer's stream settings. - * @param {Boolean|JSON} [peerInfo.settings.audio=false] Peer's audio stream - * settings. - * @param {Boolean} [peerInfo.settings.audio.stereo=false] If peer has stereo - * enabled or not. - * @param {Boolean|JSON} [peerInfo.settings.video=false] Peer's video stream - * settings. - * @param {JSON} [peerInfo.settings.video.resolution] - * Peer's video stream resolution [Rel: Skylink.VIDEO_RESOLUTION] - * @param {Number} [peerInfo.settings.video.resolution.width] - * Peer's video stream resolution width. - * @param {Number} [peerInfo.settings.video.resolution.height] - * Peer's video stream resolution height. - * @param {Number} [peerInfo.settings.video.frameRate] - * Peer's video stream resolution minimum frame rate. - * @param {JSON} peerInfo.mediaStatus Peer stream status. - * @param {Boolean} [peerInfo.mediaStatus.audioMuted=true] If peer's audio - * stream is muted. - * @param {Boolean} [peerInfo.mediaStatus.videoMuted=true] If peer's video - * stream is muted. - * @param {JSON|String} peerInfo.userData Peer's custom user data. - * @param {JSON} peerInfo.agent Peer's browser agent. - * @param {String} peerInfo.agent.name Peer's browser agent name. - * @param {Number} peerInfo.agent.version Peer's browser agent version. - * @param {Boolean} isSelf Is the peer self. - * @component Events + * @param {String} peerId The Peer ID. + * @param {JSON} peerInfo The Peer session information. + * Object signature matches the peerInfo parameter payload received in the + * peerJoined event. + * @param {Boolean} isSelf The flag if Peer is User. * @for Skylink * @since 0.5.2 */ peerUpdated: [], /** - * Event fired when a peer leaves the room + * Event triggered when a Peer leaves the room. * @event peerLeft - * @param {String} peerId PeerId of the peer that left. - * @param {JSON} peerInfo Peer's information. - * @param {JSON} peerInfo.settings Peer's stream settings. - * @param {Boolean|JSON} [peerInfo.settings.audio=false] Peer's audio stream - * settings. - * @param {Boolean} [peerInfo.settings.audio.stereo=false] If peer has stereo - * enabled or not. - * @param {Boolean|JSON} [peerInfo.settings.video=false] Peer's video stream - * settings. - * @param {JSON} [peerInfo.settings.video.resolution] - * Peer's video stream resolution [Rel: Skylink.VIDEO_RESOLUTION] - * @param {Number} [peerInfo.settings.video.resolution.width] - * Peer's video stream resolution width. - * @param {Number} [peerInfo.settings.video.resolution.height] - * Peer's video stream resolution height. - * @param {Number} [peerInfo.settings.video.frameRate] - * Peer's video stream resolution minimum frame rate. - * @param {JSON} peerInfo.mediaStatus Peer stream status. - * @param {Boolean} [peerInfo.mediaStatus.audioMuted=true] If peer's audio - * stream is muted. - * @param {Boolean} [peerInfo.mediaStatus.videoMuted=true] If peer's video - * stream is muted. - * @param {JSON|String} peerInfo.userData Peer's custom user data. - * @param {JSON} peerInfo.agent Peer's browser agent. - * @param {String} peerInfo.agent.name Peer's browser agent name. - * @param {Number} peerInfo.agent.version Peer's browser agent version. - * @param {Boolean} isSelf Is the peer self. - * @component Events + * @param {String} peerId The Peer ID. + * @param {JSON} peerInfo The Peer session information. + * Object signature matches the peerInfo parameter payload received in the + * peerJoined event. + * @param {Boolean} isSelf The flag if Peer is User. * @for Skylink * @since 0.5.2 */ peerLeft: [], /** - * Event fired when a remote stream has become available. - * - This occurs after the user joins the room. - * - This is changed from addPeerStream event. - * - Note that addPeerStream is removed from the specs. - * - There has been a documentation error whereby the stream it is - * supposed to be (stream, peerId, isSelf), but instead is received - * as (peerId, stream, isSelf) in 0.5.0. + * Event triggered when Room session has ended abruptly due to network disconnections. + * @event sessionDisconnect + * @param {String} peerId The User's Room session Peer ID. + * @param {JSON} peerInfo The User's Room session information. + * Object signature matches the peerInfo parameter payload received in the + * peerJoined event. + * @for Skylink + * @since 0.6.10 + */ + sessionDisconnect: [], + + /** + * Event triggered when receiving Peer Stream. * @event incomingStream - * @param {String} peerId PeerId of the peer that is sending the stream. - * @param {Object} stream MediaStream object. - * @param {Boolean} isSelf Is the peer self. - * @param {JSON} peerInfo Peer's information. - * @component Events + * @param {String} peerId The Peer ID. + * @param {MediaStream} stream The Stream object. + * To attach it to an element: attachMediaStream(videoElement, stream);. + * @param {Boolean} isSelf The flag if Peer is User. + * @param {JSON} peerInfo The Peer session information. + * Object signature matches the peerInfo parameter payload received in the + * peerJoined event. * @for Skylink * @since 0.5.5 */ incomingStream: [], /** - * Event fired when a message being broadcasted is received. - * - This is changed from chatMessageReceived, - * privateMessage and publicMessage event. - * - Note that chatMessageReceived, privateMessage - * and publicMessage is removed from the specs. + * Event triggered when receiving message from Peer. * @event incomingMessage - * @param {JSON} message Message object that is received. - * @param {JSON|String} message.content Data that is broadcasted. - * @param {String} message.senderPeerId PeerId of the sender peer. - * @param {String} message.targetPeerId PeerId that is specifically - * targeted to receive the message. - * @param {Boolean} message.isPrivate Is data received a private message. - * @param {Boolean} message.isDataChannel Is data received from a - * data channel. - * @param {String} peerId PeerId of the sender peer. - * @param {JSON} peerInfo Peer's information. - * @param {JSON} peerInfo.settings Peer's stream settings. - * @param {Boolean|JSON} [peerInfo.settings.audio=false] Peer's audio stream - * settings. - * @param {Boolean} [peerInfo.settings.audio.stereo=false] If peer has stereo - * enabled or not. - * @param {Boolean|JSON} [peerInfo.settings.video=false] Peer's video stream - * settings. - * @param {JSON} [peerInfo.settings.video.resolution] - * Peer's video stream resolution [Rel: Skylink.VIDEO_RESOLUTION] - * @param {Number} [peerInfo.settings.video.resolution.width] - * Peer's video stream resolution width. - * @param {Number} [peerInfo.settings.video.resolution.height] - * Peer's video stream resolution height. - * @param {Number} [peerInfo.settings.video.frameRate] - * Peer's video stream resolution minimum frame rate. - * @param {JSON} peerInfo.mediaStatus Peer stream status. - * @param {Boolean} [peerInfo.mediaStatus.audioMuted=true] If peer's audio - * stream is muted. - * @param {Boolean} [peerInfo.mediaStatus.videoMuted=true] If peer's video - * stream is muted. - * @param {JSON|String} peerInfo.userData Peer's custom user data. - * @param {JSON} peerInfo.agent Peer's browser agent. - * @param {String} peerInfo.agent.name Peer's browser agent name. - * @param {Number} peerInfo.agent.version Peer's browser agent version. - * @param {Boolean} isSelf Is the peer self. - * @component Events + * @param {JSON} message The message result. + * @param {JSON|String} message.content The message object. + * @param {String} message.senderPeerId The sender Peer ID. + * @param {String|Array} [message.targetPeerId=null] The value of the targetPeerId + * defined in sendP2PMessage() method or + * sendMessage() method. + * @param {Boolean} message.isPrivate The flag if message is targeted or not, basing + * off the targetPeerId parameter being defined in + * sendP2PMessage() method or + * sendMessage() method. + * @param {Boolean} message.isDataChannel The flag if message is sent from + * sendP2PMessage() method. + * @param {JSON} peerInfo The Peer session information. + * Object signature matches the peerInfo parameter payload received in the + * peerJoined event. + * @param {Boolean} isSelf The flag if Peer is User. * @for Skylink * @since 0.5.2 */ incomingMessage: [], /** - * Event fired when connected to a room and the lock status has changed. + * Event triggered when receiving completed data transfer from Peer. + * @event incomingData + * @param {Blob|String} data The data. + * @param {String} transferId The data transfer ID. + * @param {String} peerId The Peer ID. + * @param {JSON} transferInfo The data transfer information. + * Object signature matches the transferInfo parameter payload received in the + * dataTransferState event. + * @param {Boolean} isSelf The flag if Peer is User. + * @for Skylink + * @since 0.6.1 + */ + incomingData: [], + + + /** + * Event triggered when receiving upload data transfer from Peer. + * @event incomingDataRequest + * @param {String} transferId The transfer ID. + * @param {String} peerId The Peer ID. + * @param {String} transferInfo The data transfer information. + * Object signature matches the transferInfo parameter payload received in the + * dataTransferState event. + * @param {Boolean} isSelf The flag if Peer is User. + * @for Skylink + * @since 0.6.1 + */ + incomingDataRequest: [], + + /** + * Event triggered when Room locked status has changed. * @event roomLock - * @param {Boolean} isLocked Is the room locked. - * @param {String} peerId PeerId of the peer that is locking/unlocking - * the room. - * @param {JSON} peerInfo Peer's information. - * @param {JSON} peerInfo.settings Peer's stream settings. - * @param {Boolean|JSON} [peerInfo.settings.audio=false] Peer's audio stream - * settings. - * @param {Boolean} [peerInfo.settings.audio.stereo=false] If peer has stereo - * enabled or not. - * @param {Boolean|JSON} [peerInfo.settings.video=false] Peer's video stream - * settings. - * @param {JSON} [peerInfo.settings.video.resolution] - * Peer's video stream resolution [Rel: Skylink.VIDEO_RESOLUTION] - * @param {Number} [peerInfo.settings.video.resolution.width] - * Peer's video stream resolution width. - * @param {Number} [peerInfo.settings.video.resolution.height] - * Peer's video stream resolution height. - * @param {Number} [peerInfo.settings.video.frameRate] - * Peer's video stream resolution minimum frame rate. - * @param {JSON} peerInfo.mediaStatus Peer stream status. - * @param {Boolean} [peerInfo.mediaStatus.audioMuted=true] If peer's audio - * stream is muted. - * @param {Boolean} [peerInfo.mediaStatus.videoMuted=true] If peer's video - * stream is muted. - * @param {JSON|String} peerInfo.userData Peer's custom user data. - * @param {JSON} peerInfo.agent Peer's browser agent. - * @param {String} peerInfo.agent.name Peer's browser agent name. - * @param {Number} peerInfo.agent.version Peer's browser agent version. - * @param {Boolean} isSelf Is the peer self. - * @component Events + * @param {Boolean} isLocked The flag if Room is locked. + * @param {JSON} peerInfo The Peer session information. + * Object signature matches the peerInfo parameter payload received in the + * peerJoined event. + * @param {Boolean} isSelf The flag if User changed the Room locked status. * @for Skylink * @since 0.5.2 */ roomLock: [], /** - * Event fired when a peer's datachannel state has changed. + * Event triggered when a Datachannel connection state has changed. * @event dataChannelState - * @param {String} state The datachannel state. + * @param {String} state The current Datachannel connection state. * [Rel: Skylink.DATA_CHANNEL_STATE] - * @param {String} peerId PeerId of peer that has a datachannel - * state change. - * @param {String} [error] Error message in case there is failure - * @component Events + * @param {String} peerId The Peer ID. + * @param {Error} [error] The error object. + * Defined only when state payload is ERROR. + * @param {String} channelName The DataChannel ID. + * @param {String} channelType The DataChannel type. + * [Rel: Skylink.DATA_CHANNEL_TYPE] * @for Skylink * @since 0.1.0 */ dataChannelState: [], /** - * Event fired when a data transfer state has changed. - * - Note that transferInfo.data sends the blob data, and - * no longer a blob url. + * Event triggered when a data transfer state has changed. * @event dataTransferState - * @param {String} state The data transfer state. + * @param {String} state The current data transfer state. * [Rel: Skylink.DATA_TRANSFER_STATE] - * @param {String} transferId TransferId of the data. - * @param {String} peerId PeerId of the peer that has a data - * transfer state change. - * @param {JSON} transferInfo Data transfer information. - * @param {JSON} transferInfo.percentage The percetange of data being - * uploaded / downloaded. - * @param {JSON} transferInfo.senderPeerId PeerId of the sender. - * @param {JSON} transferInfo.data The blob data. See the - * [createObjectURL](https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL) - * method on how you can convert the blob to a download link. - * @param {JSON} transferInfo.name Data name. - * @param {JSON} transferInfo.size Data size. - * @param {JSON} error The error object. - * @param {String} error.message Error message thrown. - * @param {String} error.transferType Is error from uploading or downloading. + * @param {String} transferId The data transfer ID. + * @param {String} peerId The Peer ID. + * @param {JSON} transferInfo The data transfer information. + * @param {Blob|String} [transferInfo.data] The data object. + * Defined only when state payload is UPLOAD_STARTED or + * DOWNLOAD_COMPLETED. + * @param {String} transferInfo.name The data transfer name. + * @param {Number} transferInfo.size The data transfer data object original size. + * @param {String} transferInfo.dataType The data transfer session type. + * [Rel: Skylink.DATA_TRANSFER_SESSION_TYPE] + * @param {Number} transferInfo.timeout The flag if message is targeted or not, basing + * off the targetPeerId parameter being defined in + * sendURLData() method or + * sendBlobData() method. + * @param {Boolean} transferInfo.isPrivate The flag if data transfer + * @param {JSON} [error] The error result. + * Defined only when state payload is ERROR or CANCEL. + * @param {Error|String} error.message The error object. + * @param {String} error.transferType The data transfer direction from where the error occurred. * [Rel: Skylink.DATA_TRANSFER_TYPE] - * @component Events * @for Skylink * @since 0.4.1 */ dataTransferState: [], /** - * Event fired when the signaling server warns the user. + * Event triggered when Signaling server reaction state has changed. * @event systemAction - * @param {String} action The action that is required for - * the user to follow. [Rel: Skylink.SYSTEM_ACTION] - * @param {String} message The reason for the action. - * @param {String} reason The reason why the action is given. + * @param {String} action The current Signaling server reaction state. + * [Rel: Skylink.SYSTEM_ACTION] + * @param {String} message The message. + * @param {String} reason The Signaling server reaction state reason of action code. * [Rel: Skylink.SYSTEM_ACTION_REASON] - * @component Events * @for Skylink * @since 0.5.1 */ - systemAction: [] + systemAction: [], + + /** + * Event triggered when a server Peer joins the room. + * @event serverPeerJoined + * @param {String} peerId The Peer ID. + * @param {String} serverPeerType The server Peer type + * [Rel: Skylink.SERVER_PEER_TYPE] + * @for Skylink + * @since 0.6.1 + */ + serverPeerJoined: [], + + /** + * Event triggered when a server Peer leaves the room. + * @event serverPeerLeft + * @param {String} peerId The Peer ID. + * @param {String} serverPeerType The server Peer type + * [Rel: Skylink.SERVER_PEER_TYPE] + * @for Skylink + * @since 0.6.1 + */ + serverPeerLeft: [], + + /** + * Event triggered when a server Peer connection has been refreshed. + * @event serverPeerRestart + * @param {String} peerId The Peer ID. + * @param {String} serverPeerType The server Peer type + * [Rel: Skylink.SERVER_PEER_TYPE] + * @for Skylink + * @since 0.6.1 + */ + serverPeerRestart: [], + + /** + * Event triggered when Peer Stream streaming has stopped. + * @event streamEnded + * @param {String} peerId The Peer ID. + * @param {JSON} peerInfo The Peer session information. + * Object signature matches the peerInfo parameter payload received in the + * peerJoined event. + * @param {Boolean} isSelf The flag if Peer is User. + * @param {Boolean} isScreensharing The flag if Peer Stream is a screensharing Stream. + * @param {String} streamId The Stream ID. + * @for Skylink + * @since 0.5.10 + */ + streamEnded: [], + + /** + * Event triggered when Peer Stream audio or video tracks has been muted / unmuted. + * @event streamMuted + * @param {String} peerId The Peer ID. + * @param {JSON} peerInfo The Peer session information. + * Object signature matches the peerInfo parameter payload received in the + * peerJoined event. + * @param {Boolean} isSelf The flag if Peer is User. + * @param {Boolean} isScreensharing The flag if Peer Stream is a screensharing Stream. + * @for Skylink + * @since 0.6.1 + */ + streamMuted: [], + + /** + * Event triggered when getPeers() method retrieval state changes. + * @event getPeersStateChange + * @param {String} state The current getPeers() retrieval state. + * [Rel: Skylink.GET_PEERS_STATE] + * @param {String} privilegedPeerId The User's privileged Peer ID. + * @param {JSON} peerList The list of Peer IDs Rooms within the same App space. + * @param {Array} peerList.#room The list of Peer IDs associated with the Room defined in #room property. + * @for Skylink + * @since 0.6.1 + */ + getPeersStateChange: [], + + /** + * Event triggered when introducePeer() method + * introduction request state changes. + * @event introduceStateChange + * @param {String} state The current introducePeer() introduction request state. + * [Rel: Skylink.INTRODUCE_STATE] + * @param {String} privilegedPeerId The User's privileged Peer ID. + * @param {String} sendingPeerId The Peer ID to be connected with receivingPeerId. + * @param {String} receivingPeerId The Peer ID to be connected with sendingPeerId. + * @param {String} [reason] The error object. + * Defined only when state payload is ERROR. + * @for Skylink + * @since 0.6.1 + */ + introduceStateChange: [], + + /** + * Event triggered when getConnectionStatus() method + * retrieval state changes. + * @event getConnectionStatusStateChange + * @param {Number} state The current getConnectionStatus() retrieval state. + * [Rel: Skylink.GET_CONNECTION_STATUS_STATE] + * @param {String} peerId The Peer ID. + * @param {JSON} [stats] The Peer connection current stats. + * Defined only when state payload is RETRIEVE_SUCCESS. + * @param {JSON} stats.raw The Peer connection raw stats before parsing. + * @param {JSON} stats.audio The Peer connection audio streaming stats. + * @param {JSON} stats.audio.sending The Peer connection sending audio streaming stats. + * @param {Number} stats.audio.sending.bytes The Peer connection sending audio streaming bytes. + * @param {Number} stats.audio.sending.packets The Peer connection sending audio streaming packets. + * @param {Number} stats.audio.sending.packetsLost The Peer connection sending audio streaming packets lost. + * @param {Number} stats.audio.sending.ssrc The Peer connection sending audio streaming RTP packets SSRC. + * @param {Number} stats.audio.sending.rtt The Peer connection sending audio streaming RTT (Round-trip delay time). + * Defined as 0 if it's not present in original raw stats before parsing. + * @param {JSON} stats.audio.receiving The Peer connection receiving audio streaming stats. + * @param {Number} stats.audio.receiving.bytes The Peer connection sending audio streaming bytes. + * @param {Number} stats.audio.receiving.packets The Peer connection receiving audio streaming packets. + * @param {Number} stats.audio.receiving.packetsLost The Peer connection receiving audio streaming packets lost. + * @param {Number} stats.audio.receiving.ssrc The Peer connection receiving audio streaming RTP packets SSRC. + * @param {JSON} stats.video The Peer connection video streaming stats. + * @param {JSON} stats.video.sending The Peer connection sending video streaming stats. + * @param {Number} stats.video.sending.bytes The Peer connection sending video streaming bytes. + * @param {Number} stats.video.sending.packets The Peer connection sending video streaming packets. + * @param {Number} stats.video.sending.packetsLost The Peer connection sending video streaming packets lost. + * @param {JSON} stats.video.sending.ssrc The Peer connection sending video streaming RTP packets SSRC. + * @param {Number} stats.video.sending.rtt The Peer connection sending video streaming RTT (Round-trip delay time). + * Defined as 0 if it's not present in original raw stats before parsing. + * @param {JSON} stats.video.receiving The Peer connection receiving video streaming stats. + * @param {Number} stats.video.receiving.bytes The Peer connection receiving video streaming bytes. + * @param {Number} stats.video.receiving.packets The Peer connection receiving video streaming packets. + * @param {Number} stats.video.receiving.packetsLost The Peer connection receiving video streaming packets lost. + * @param {Number} stats.video.receiving.ssrc The Peer connection receiving video streaming RTP packets SSRC. + * @param {JSON} stats.selectedCandidate The Peer connection selected ICE candidate pair stats. + * @param {JSON} stats.selectedCandidate.local The Peer connection selected local ICE candidate. + * @param {String} stats.selectedCandidate.local.ipAddress The Peer connection selected + * local ICE candidate IP address. + * @param {Number} stats.selectedCandidate.local.portNumber The Peer connection selected + * local ICE candidate port number. + * @param {String} stats.selectedCandidate.local.transport The Peer connection selected + * local ICE candidate IP transport type. + * @param {String} stats.selectedCandidate.local.candidateType The Peer connection selected + * local ICE candidate type. + * @param {JSON} stats.selectedCandidate.remote The Peer connection selected remote ICE candidate. + * @param {String} stats.selectedCandidate.remote.ipAddress The Peer connection selected + * remote ICE candidate IP address. + * @param {Number} stats.selectedCandidate.remote.portNumber The Peer connection selected + * remote ICE candidate port number. + * @param {String} stats.selectedCandidate.remote.transport The Peer connection selected + * remote ICE candidate IP transport type. + * @param {String} stats.selectedCandidate.remote.candidateType The Peer connection selected + * remote ICE candidate type. + * @param {JSON} stats.connection The Peer connection object stats. + * @param {String} stats.connection.iceConnectionState The Peer connection ICE connection state. + * @param {String} stats.connection.iceGatheringState The Peer connection ICE gathering state. + * @param {String} stats.connection.signalingState The Peer connection signaling state. + * @param {JSON} stats.connection.localDescription The Peer connection local session description. + * @param {String} stats.connection.localDescription.type The Peer connection local session description type. + * @param {String} stats.connection.localDescription.sdp The Peer connection local session description SDP. + * @param {JSON} stats.connection.remoteDescription The Peer connection remote session description. + * @param {String} stats.connection.remoteDescription.type The Peer connection remote session description type. + * @param {String} stats.connection.remoteDescription.sdp The Peer connection remote session description sdp. + * @param {JSON} stats.connection.candidates The Peer connection list of ICE candidates sent or received. + * @param {JSON} stats.connection.candidates.sending The Peer connection list of local ICE candidates sent. + * @param {Array} stats.connection.candidates.sending.host The Peer connection list of local + * "host" ICE candidates sent. + * @param {Array} stats.connection.candidates.sending.srflx The Peer connection list of local + * "srflx" ICE candidates sent. + * @param {Array} stats.connection.candidates.sending.relay The Peer connection list of local + * "relay" candidates sent. + * @param {JSON} stats.connection.candidates.receiving The Peer connection list of remote ICE candidates received. + * @param {Array} stats.connection.candidates.receiving.host The Peer connection list of remote + * "host" ICE candidates received. + * @param {Array} stats.connection.candidates.receiving.srflx The Peer connection list of remote + * "srflx" ICE candidates received. + * @param {Array} stats.connection.candidates.receiving.relay The Peer connection list of remote + * "relay" ICE candidates received. + * @param {Error} error The error object received. + * Defined only when state payload is RETRIEVE_ERROR. + * @for Skylink + * @since 0.6.14 + */ + getConnectionStatusStateChange: [], + + /** + * Event triggered when muteStream() method changes + * User Streams audio and video tracks muted status. + * @event localMediaMuted + * @param {JSON} mediaStatus The Streams muted settings. + * This indicates the muted settings for both + * getUserMedia() Stream and + * shareScreen() Stream. + * @param {Boolean} mediaStatus.audioMuted The flag if all Streams audio tracks is muted or not. + * If User's peerInfo.settings.audio is false, this will be defined as true. + * @param {Boolean} mediaStatus.videoMuted The flag if all Streams video tracks is muted or not. + * If User's peerInfo.settings.video is false, this will be defined as true. + * @for Skylink + * @since 0.6.15 + */ + localMediaMuted: [] }; /** - * Events with callbacks that would be fired only once once condition is met. + * Stores the list of once() event handlers. + * These events are only triggered once. * @attribute _onceEvents + * @param {Array} <#event> The list of event handlers associated with the event. + * @param {Array} <#event>.<#index> The array of event handler function and its condition function. * @type JSON * @private - * @required - * @component Events * @for Skylink * @since 0.5.4 */ Skylink.prototype._onceEvents = {}; /** - * The timestamp for throttle function to use. + * Stores the timestamps data used for throttling. * @attribute _timestamp * @type JSON * @private - * @required - * @component Events * @for Skylink * @since 0.5.8 */ Skylink.prototype._timestamp = { - now: Date.now() || function() { return +new Date(); } -}; - -/** - * Trigger all the callbacks associated with an event. - * - Note that extra arguments can be passed to the callback which - * extra argument can be expected by callback is documented by each event. - * @method _trigger - * @param {String} eventName The Skylink event. - * @for Skylink - * @private - * @component Events - * @for Skylink - * @since 0.1.0 - */ -Skylink.prototype._trigger = function(eventName) { - //convert the arguments into an array - var args = Array.prototype.slice.call(arguments); - var arr = this._EVENTS[eventName]; - var once = this._onceEvents[eventName] || null; - args.shift(); //Omit the first argument since it's the event name - if (arr) { - // for events subscribed forever - for (var i = 0; i < arr.length; i++) { - try { - log.log([null, 'Event', eventName, 'Event is fired']); - if(arr[i].apply(this, args) === false) { - break; - } - } catch(error) { - log.error([null, 'Event', eventName, 'Exception occurred in event:'], error); - throw error; - } - } - } - if (once){ - // for events subscribed on once - for (var j = 0; j < once.length; j++) { - if (once[j][1].apply(this, args) === true) { - log.log([null, 'Event', eventName, 'Condition is met. Firing event']); - if(once[j][0].apply(this, args) === false) { - break; - } - if (!once[j][2]) { - log.log([null, 'Event', eventName, 'Removing event after firing once']); - once.splice(j, 1); - //After removing current element, the next element should be element of the same index - j--; - } - } else { - log.log([null, 'Event', eventName, 'Condition is still not met. ' + - 'Holding event from being fired']); - } - } - } - - log.log([null, 'Event', eventName, 'Event is triggered']); + now: Date.now() || function() { return +new Date(); }, + screen: false }; /** - * To register a callback function to an event. + * Function that subscribes a listener to an event. * @method on - * @param {String} eventName The Skylink event. See the event list to see what you can register. - * @param {Function} callback The callback fired after the event is triggered. + * @param {String} eventName The event. + * @param {Function} callback The listener. + * This will be invoked when event is triggered. * @example - * SkylinkDemo.on('peerJoined', function (peerId, peerInfo) { - * alert(peerId + ' has joined the room'); + * // Example 1: Subscribing to "peerJoined" event + * skylinkDemo.on("peerJoined", function (peerId, peerInfo, isSelf) { + * console.info("peerJoined event has been triggered with:", peerId, peerInfo, isSelf); * }); - * @component Events * @for Skylink * @since 0.1.0 */ @@ -5255,23 +8751,38 @@ Skylink.prototype.on = function(eventName, callback) { }; /** - * To register a callback function to an event that is fired once a condition is met. + * Function that subscribes a listener to an event once. * @method once - * @param {String} eventName The Skylink event. See the event list to see what you can register. - * @param {Function} callback The callback fired after the event is triggered. - * @param {Function} [condition] - * The provided condition that would trigger this event. - * If not provided, it will return true when the event is triggered. - * Return a true to fire the callback. - * @param {Boolean} [fireAlways=false] The function does not get removed onced triggered, - * but triggers everytime the event is called. + * @param {String} eventName The event. + * @param {Function} callback The listener. + * This will be invoked once when event is triggered and conditional function is satisfied. + * @param {Function} [condition] The conditional function that will be invoked when event is triggered. + * Return true when invoked to satisfy condition. + * When not provided, the conditional function will always return true. + * @param {Boolean} [fireAlways=false] The flag that indicates if once() should act like + * on() but only invoke listener only when conditional function is satisfied. * @example - * SkylinkDemo.once('peerConnectionState', function (state, peerId) { - * alert('Peer has left'); - * }, function (state, peerId) { - * return state === SkylinkDemo.PEER_CONNECTION_STATE.CLOSED; + * // Example 1: Subscribing to "peerJoined" event that triggers without condition + * skylinkDemo.once("peerJoined", function (peerId, peerInfo, isSelf) { + * console.info("peerJoined event has been triggered once with:", peerId, peerInfo, isSelf); + * }); + * + * // Example 2: Subscribing to "incomingStream" event that triggers with condition + * skylinkDemo.once("incomingStream", function (peerId, stream, isSelf, peerInfo) { + * console.info("incomingStream event has been triggered with User stream:", stream); + * }, function (peerId, peerInfo, isSelf) { + * return isSelf; * }); - * @component Events + * + * // Example 3: Subscribing to "dataTransferState" event that triggers always only when condition is satisfied + * skylinkDemo.once("dataTransferState", function (state, transferId, peerId, transferInfo) { + * console.info("Received data transfer from Peer:", transferInfo.data); + * }, function (state, transferId, peerId) { + * if (state === skylinkDemo.DATA_TRANSFER_STATE.UPLOAD_REQUEST) { + * skylinkDemo.acceptDataTransfer(peerId, transferId); + * } + * return state === skylinkDemo.DATA_TRANSFER_STATE.DOWNLOAD_COMPLETED; + * }, true); * @for Skylink * @since 0.5.4 */ @@ -5298,14 +8809,21 @@ Skylink.prototype.once = function(eventName, callback, condition, fireAlways) { }; /** - * To unregister a callback function from an event. + * Function that unsubscribes listeners from an event. * @method off - * @param {String} eventName The Skylink event. See the event list to see what you can unregister. - * @param {Function} callback The callback fired after the event is triggered. - * Not providing any callback turns all callbacks tied to that event off. + * @param {String} eventName The event. + * @param {Function} [callback] The listener to unsubscribe. + * - When not provided, all listeners associated to the event will be unsubscribed. * @example - * SkylinkDemo.off('peerJoined', callback); - * @component Events + * // Example 1: Unsubscribe all "peerJoined" event + * skylinkDemo.off("peerJoined"); + * + * // Example 2: Unsubscribe only one listener from "peerJoined event" + * var pJListener = function (peerId, peerInfo, isSelf) { + * console.info("peerJoined event has been triggered with:", peerId, peerInfo, isSelf); + * }; + * + * skylinkDemo.off("peerJoined", pJListener); * @for Skylink * @since 0.5.5 */ @@ -5340,23 +8858,64 @@ Skylink.prototype.off = function(eventName, callback) { }; /** - * Does a check condition first to check if event is required to be subscribed. - * If check condition fails, it subscribes an event with - * {{#crossLink "Skylink/once:method"}}once(){{/crossLink}} method to wait for - * the condition to pass to fire the callback. + * Function that triggers an event. + * The rest of the parameters after the eventName parameter is considered as the event parameter payloads. + * @method _trigger + * @private + * @for Skylink + * @since 0.1.0 + */ +Skylink.prototype._trigger = function(eventName) { + //convert the arguments into an array + var args = Array.prototype.slice.call(arguments); + var arr = this._EVENTS[eventName]; + var once = this._onceEvents[eventName] || null; + args.shift(); //Omit the first argument since it's the event name + if (arr) { + // for events subscribed forever + for (var i = 0; i < arr.length; i++) { + try { + log.log([null, 'Event', eventName, 'Event is fired']); + if(arr[i].apply(this, args) === false) { + break; + } + } catch(error) { + log.error([null, 'Event', eventName, 'Exception occurred in event:'], error); + throw error; + } + } + } + if (once){ + // for events subscribed on once + for (var j = 0; j < once.length; j++) { + if (once[j][1].apply(this, args) === true) { + log.log([null, 'Event', eventName, 'Condition is met. Firing event']); + if(once[j][0].apply(this, args) === false) { + break; + } + if (!once[j][2]) { + log.log([null, 'Event', eventName, 'Removing event after firing once']); + once.splice(j, 1); + //After removing current element, the next element should be element of the same index + j--; + } + } else { + log.log([null, 'Event', eventName, 'Condition is still not met. ' + + 'Holding event from being fired']); + } + } + } + + log.log([null, 'Event', eventName, 'Event is triggered']); +}; + + + +/** + * Function that checks if the current state condition is met before subscribing + * event handler to wait for condition to be fulfilled. * @method _condition - * @param {String} eventName The Skylink event. - * @param {Function} callback The callback fired after the condition is met. - * @param {Function} checkFirst The condition to check that if pass, it would fire the callback, - * or it will just subscribe to an event and fire when condition is met. - * @param {Function} [condition] - * The provided condition that would trigger this event. - * If not provided, it will return true when the event is triggered. - * Return a true to fire the callback. - * @param {Boolean} [fireAlways=false] The function does not get removed onced triggered, - * but triggers everytime the event is called. - * @private - * @component Events + * @private * @for Skylink * @since 0.5.5 */ @@ -5379,14 +8938,9 @@ Skylink.prototype._condition = function(eventName, callback, checkFirst, conditi }; /** - * Sets an interval check. If condition is met, fires callback. + * Function that starts an interval check to wait for a condition to be resolved. * @method _wait - * @param {Function} callback The callback fired after the condition is met. - * @param {Function} condition The provided condition that would trigger this the callback. - * @param {Number} [intervalTime=50] The interval loop timeout. - * @for Skylink * @private - * @component Events * @for Skylink * @since 0.5.5 */ @@ -5422,13 +8976,10 @@ Skylink.prototype._wait = function(callback, condition, intervalTime, fireAlways }; /** - * Returns a wrapper of the original function, which only fires once during - * a specified amount of time. + * Function that throttles a method function to prevent multiple invokes over a specified amount of time. + * Returns a function to be invoked ._throttle(fn, 1000)() to make throttling functionality work. * @method _throttle - * @param {Function} func The function that should be throttled. - * @param {Number} wait The amount of time that function need to throttled (in ms) * @private - * @component Events * @for Skylink * @since 0.5.8 */ @@ -5456,162 +9007,149 @@ Skylink.prototype.SOCKET_ERROR = { }; /** - * The queue of messages to be sent to signaling server. + * The list of joinRoom() method socket connection reconnection states. + * @attribute SOCKET_FALLBACK + * @param {String} NON_FALLBACK Value "nonfallback" + * The value of the reconnection state when joinRoom() socket connection is at its initial state + * without transitioning to any new socket port or transports yet. + * @param {String} FALLBACK_PORT Value "fallbackPortNonSSL" + * The value of the reconnection state when joinRoom() socket connection is reconnecting with + * another new HTTP port using WebSocket transports to attempt to establish connection with Signaling server. + * @param {String} FALLBACK_PORT_SSL Value "fallbackPortSSL" + * The value of the reconnection state when joinRoom() socket connection is reconnecting with + * another new HTTPS port using WebSocket transports to attempt to establish connection with Signaling server. + * @param {String} LONG_POLLING Value "fallbackLongPollingNonSSL" + * The value of the reconnection state when joinRoom() socket connection is reconnecting with + * another new HTTP port using Polling transports to attempt to establish connection with Signaling server. + * @param {String} LONG_POLLING Value "fallbackLongPollingSSL" + * The value of the reconnection state when joinRoom() socket connection is reconnecting with + * another new HTTPS port using Polling transports to attempt to establish connection with Signaling server. + * @type JSON + * @readOnly + * @for Skylink + * @since 0.5.6 + */ +Skylink.prototype.SOCKET_FALLBACK = { + NON_FALLBACK: 'nonfallback', + FALLBACK_PORT: 'fallbackPortNonSSL', + FALLBACK_SSL_PORT: 'fallbackPortSSL', + LONG_POLLING: 'fallbackLongPollingNonSSL', + LONG_POLLING_SSL: 'fallbackLongPollingSSL' +}; + +/** + * Stores the current socket connection information. + * @attribute _socketSession + * @type JSON + * @private + * @for Skylink + * @since 0.6.13 + */ +Skylink.prototype._socketSession = {}; + +/** + * Stores the queued socket messages. + * This is to prevent too many sent over less than a second interval that might cause dropped messages + * or jams to the Signaling connection. * @attribute _socketMessageQueue * @type Array * @private - * @required - * @component Socket * @for Skylink * @since 0.5.8 */ Skylink.prototype._socketMessageQueue = []; /** - * The timeout used to send socket message queue. + * Stores the setTimeout to sent queued socket messages. * @attribute _socketMessageTimeout - * @type Function + * @type Object * @private - * @required - * @component Socket * @for Skylink * @since 0.5.8 */ Skylink.prototype._socketMessageTimeout = null; - /** - * The list of ports that SkylinkJS would use to attempt to connect to the signaling server with. + * Stores the list of socket ports to use to connect to the Signaling. + * These ports are defined by default which is commonly used currently by the Signaling. + * Should re-evaluate this sometime. * @attribute _socketPorts + * @param {Array} http: The list of HTTP socket ports. + * @param {Array} https: The list of HTTPS socket ports. * @type JSON - * @param {Array} http:// The list of HTTP ports. - * @param {Array} https:// The list of HTTPs ports. * @private - * @required - * @component Socket - * @for Skylink - * @since 0.5.8 - */ -Skylink.prototype._socketPorts = { - 'http:': [80, 3000], - 'https:': [443, 3443] -}; - -/** - * The list of channel connection fallback states. - * - The fallback states that would occur are: - * @attribute SOCKET_FALLBACK - * @type JSON - * @param {String} NON_FALLBACK Non-fallback state, - * @param {String} FALLBACK_PORT Fallback to non-ssl port for channel re-try. - * @param {String} FALLBACK_PORT_SSL Fallback to ssl port for channel re-try. - * @param {String} LONG_POLLING Fallback to non-ssl long-polling. - * @param {String} LONG_POLLING_SSL Fallback to ssl port for long-polling. - * @readOnly - * @component Socket * @for Skylink - * @since 0.5.6 + * @since 0.5.8 */ -Skylink.prototype.SOCKET_FALLBACK = { - NON_FALLBACK: 'nonfallback', - FALLBACK_PORT: 'fallbackPortNonSSL', - FALLBACK_SSL_PORT: 'fallbackPortSSL', - LONG_POLLING: 'fallbackLongPollingNonSSL', - LONG_POLLING_SSL: 'fallbackLongPollingSSL' +Skylink.prototype._socketPorts = { + 'http:': [80, 3000], + 'https:': [443, 3443] }; /** - * The current socket opened state. + * Stores the flag that indicates if socket connection to the Signaling has opened. * @attribute _channelOpen * @type Boolean * @private - * @required - * @component Socket * @for Skylink * @since 0.5.2 */ Skylink.prototype._channelOpen = false; /** - * The signaling server to connect to. + * Stores the Signaling server url. * @attribute _signalingServer * @type String * @private - * @component Socket * @for Skylink * @since 0.5.2 */ Skylink.prototype._signalingServer = null; /** - * The signaling server protocol to use. - * - * https: - * Default port is 443. - * Fallback port is 3443. - * - * http: - * Default port is 80. - * Fallback port is 3000. - * - * + * Stores the Signaling server protocol. * @attribute _signalingServerProtocol * @type String * @private - * @component Socket * @for Skylink * @since 0.5.4 */ Skylink.prototype._signalingServerProtocol = window.location.protocol; /** - * The signaling server port to connect to. + * Stores the Signaling server port. * @attribute _signalingServerPort * @type Number * @private - * @component Socket * @for Skylink * @since 0.5.4 */ Skylink.prototype._signalingServerPort = null; /** - * The actual socket object that handles the connection. + * Stores the Signaling socket connection object. * @attribute _socket - * @type Object - * @required + * @type io * @private - * @component Socket * @for Skylink * @since 0.1.0 */ Skylink.prototype._socket = null; /** - * The socket connection timeout - * - * 0 Uses the default timeout from socket.io - * 20000ms. - * >0 Uses the user set timeout - * + * Stores the socket connection timeout when establishing connection to the Signaling. * @attribute _socketTimeout * @type Number - * @default 0 - * @required * @private - * @component Socket * @for Skylink * @since 0.5.4 */ Skylink.prototype._socketTimeout = 0; /** - * The socket connection to use XDomainRequest. + * Stores the flag that indicates if XDomainRequest is used for IE 8/9. * @attribute _socketUseXDR * @type Boolean - * @default false - * @required - * @component Socket * @private * @for Skylink * @since 0.5.4 @@ -5619,14 +9157,9 @@ Skylink.prototype._socketTimeout = 0; Skylink.prototype._socketUseXDR = false; /** - * Sends a message to the signaling server. - * - Not to be confused with method - * {{#crossLink "Skylink/sendMessage:method"}}sendMessage(){{/crossLink}} - * that broadcasts messages. This is for sending socket messages. + * Function that sends a socket message over the socket connection to the Signaling. * @method _sendChannelMessage - * @param {JSON} message * @private - * @component Socket * @for Skylink * @since 0.5.8 */ @@ -5654,12 +9187,24 @@ Skylink.prototype._sendChannelMessage = function(message) { rid: self._room.id }); - self._socket.send({ - type: self._SIG_MESSAGE_TYPE.GROUP, - lists: self._socketMessageQueue.splice(0,self._socketMessageQueue.length), - mid: self._user.sid, - rid: self._room.id - }); + // fix for self._socket undefined errors in firefox + if (self._socket) { + self._socket.send({ + type: self._SIG_MESSAGE_TYPE.GROUP, + lists: self._socketMessageQueue.splice(0,self._socketMessageQueue.length), + mid: self._user.sid, + rid: self._room.id + }); + } else { + log.error([(message.target ? message.target : 'server'), null, null, + 'Dropping delayed message' + ((!message.target) ? 's' : '') + + ' as socket object is no longer defined ->'], { + type: self._SIG_MESSAGE_TYPE.GROUP, + lists: self._socketMessageQueue.slice(0,self._socketMessageQueue.length), + mid: self._user.sid, + rid: self._room.id + }); + } clearTimeout(self._socketMessageTimeout); self._socketMessageTimeout = null; @@ -5675,12 +9220,24 @@ Skylink.prototype._sendChannelMessage = function(message) { rid: self._room.id }); - self._socket.send({ - type: self._SIG_MESSAGE_TYPE.GROUP, - lists: self._socketMessageQueue.splice(0,throughput), - mid: self._user.sid, - rid: self._room.id - }); + // fix for self._socket undefined errors in firefox + if (self._socket) { + self._socket.send({ + type: self._SIG_MESSAGE_TYPE.GROUP, + lists: self._socketMessageQueue.splice(0,throughput), + mid: self._user.sid, + rid: self._room.id + }); + } else { + log.error([(message.target ? message.target : 'server'), null, null, + 'Dropping delayed message' + ((!message.target) ? 's' : '') + + ' as socket object is no longer defined ->'], { + type: self._SIG_MESSAGE_TYPE.GROUP, + lists: self._socketMessageQueue.slice(0,throughput), + mid: self._user.sid, + rid: self._room.id + }); + } clearTimeout(self._socketMessageTimeout); self._socketMessageTimeout = null; @@ -5721,15 +9278,9 @@ Skylink.prototype._sendChannelMessage = function(message) { }; /** - * Create the socket object to refresh connection. + * Function that creates and opens a socket connection to the Signaling. * @method _createSocket - * @param {String} type The type of socket.io connection to use. - * - * "WebSocket": Uses the WebSocket connection - * "Polling": Uses the long-polling connection - * * @private - * @component Socket * @for Skylink * @since 0.5.10 */ @@ -5756,8 +9307,6 @@ Skylink.prototype._createSocket = function (type) { // re-refresh to long-polling port if (type === 'WebSocket') { - console.log(type, self._signalingServerPort); - type = 'Polling'; self._signalingServerPort = ports[0]; @@ -5772,8 +9321,8 @@ Skylink.prototype._createSocket = function (type) { self._signalingServerPort = ports[ ports.indexOf(self._signalingServerPort) + 1 ]; } - var url = self._signalingServerProtocol + '//' + - self._signalingServer + ':' + self._signalingServerPort; + var url = self._signalingServerProtocol + '//' + self._signalingServer + ':' + self._signalingServerPort; + //'http://ec2-52-8-93-170.us-west-1.compute.amazonaws.com:6001'; if (type === 'WebSocket') { options.transports = ['websocket']; @@ -5804,6 +9353,12 @@ Skylink.prototype._createSocket = function (type) { options: options }); + self._socketSession = { + type: type, + options: options, + url: url + }; + self._socket = io.connect(url, options); if (connectionType === null) { @@ -5873,6 +9428,11 @@ Skylink.prototype._createSocket = function (type) { self._channelOpen = false; self._trigger('channelClose'); log.log([null, 'Socket', null, 'Channel closed']); + + if (self._inRoom) { + self.leaveRoom(false); + self._trigger('sessionDisconnect', self._user.sid, self.getPeerInfo()); + } }); self._socket.on('message', function(message) { @@ -5882,11 +9442,10 @@ Skylink.prototype._createSocket = function (type) { }; /** - * Initiate a socket signaling connection. + * Function that starts the socket connection to the Signaling. + * This starts creating the socket connection and called at first not when requiring to fallback. * @method _openChannel - * @trigger channelMessage, channelOpen, channelError, channelClose * @private - * @component Socket * @for Skylink * @since 0.5.5 */ @@ -5911,15 +9470,23 @@ Skylink.prototype._openChannel = function() { self._signalingServerProtocol = window.location.protocol; } + var socketType = 'WebSocket'; + + // For IE < 9 that doesn't support WebSocket + if (!window.WebSocket) { + socketType = 'Polling'; + } + + self._signalingServerPort = null; + // Begin with a websocket connection - self._createSocket('WebSocket'); + self._createSocket(socketType); }; /** - * Closes the socket signaling connection. + * Function that stops the socket connection to the Signaling. * @method _closeChannel * @private - * @component Socket * @for Skylink * @since 0.5.5 */ @@ -5946,36 +9513,12 @@ Skylink.prototype._closeChannel = function() { Skylink.prototype.SM_PROTOCOL_VERSION = '0.1.1'; /** - * The Message protocol list. The message object is an - * indicator of the expected parameters to be given and received. + * Stores the list of socket messaging protocol types. + * See confluence docs for the list based on the current SM_PROTOCOL_VERSION. * @attribute _SIG_MESSAGE_TYPE * @type JSON - * @param {String} JOIN_ROOM Send to initiate the connection to the Room. - * @param {String} ENTER Broadcasts to any Peers connected to the room to - * intiate a Peer connection. - * @param {String} WELCOME Send as a response to Peer's enter received. User starts creating - * offer to the Peer. - * @param {String} OFFER Send when createOffer is completed and generated. - * @param {String} ANSWER Send as a response to Peer's offer Message after createAnswer - * is called. - * @param {String} CANDIDATE Send when an ICE Candidate is generated. - * @param {String} BYE Received as a response from server that a Peer has left the Room. - * @param {String} REDIRECT Received as a warning from server when User is rejected or - * is jamming the server. - * @param {String} UPDATE_USER Broadcast when a User's information is updated to reflect the - * the changes on Peer's end. - * @param {String} ROOM_LOCK Broadcast to change the Room lock status. - * @param {String} MUTE_VIDEO Broadcast when User's video stream is muted or unmuted. - * @param {String} MUTE_AUDIO Broadcast when User's audio stream is muted or unmuted. - * @param {String} PUBLIC_MESSAGE Broadcasts a Message object to all Peers in the Room. - * @param {String} PRIVATE_MESSAGE Sends a Message object to a Peer in the Room. - * @param {String} RESTART Sends when a Peer connection is restarted. - * @param {String} STREAM Broadcast when a Stream has ended. This is temporal. - * @param {String} GROUP Messages are bundled together when messages are sent too fast to - * prevent server redirects over sending less than 1 second interval. * @readOnly * @private - * @component Message * @for Skylink * @since 0.5.6 */ @@ -5997,17 +9540,29 @@ Skylink.prototype._SIG_MESSAGE_TYPE = { PUBLIC_MESSAGE: 'public', PRIVATE_MESSAGE: 'private', STREAM: 'stream', - GROUP: 'group' + GROUP: 'group', + GET_PEERS: 'getPeers', + PEER_LIST: 'peerList', + INTRODUCE: 'introduce', + INTRODUCE_ERROR: 'introduceError', + APPROACH: 'approach' }; +/** + * Stores the flag if MCU environment is enabled. + * @attribute _hasMCU + * @type Boolean + * @private + * @for Skylink + * @since 0.5.4 + */ +Skylink.prototype._hasMCU = false; /** - * List of signaling message types that can be queued before sending to server. + * Stores the list of socket messaging protocol types to queue when sent less than a second interval. * @attribute _groupMessageList * @type Array * @private - * @required - * @component Message * @for Skylink * @since 0.5.10 */ @@ -6021,12 +9576,11 @@ Skylink.prototype._groupMessageList = [ ]; /** - * The flag that indicates if MCU is enabled. + * Stores the flag that indicates if MCU is available in the Room. + * If App Key enables MCU but this is false, this means likely there are problems connecting to the MCU server. * @attribute _hasMCU * @type Boolean - * @development true * @private - * @component Message * @for Skylink * @since 0.5.4 */ @@ -6034,26 +9588,135 @@ Skylink.prototype._hasMCU = false; /** - * Indicates whether the other peers should only receive stream - * from the current peer and not sending out any stream. - * Suitable for use cases such as streaming lecture/concert. + * Stores the flag that indicates if User should only receive Stream from Peer connections but + * do not send User's Stream to Peer connections. * @attribute _receiveOnly * @type Boolean * @private - * @required - * @component Message * @for Skylink * @since 0.5.10 */ - Skylink.prototype._receiveOnly = false; +Skylink.prototype._receiveOnly = false; + +/** + * Stores the list of Peer messages timestamp. + * @attribute _peerMessagesStamps + * @type JSON + * @private + * @for Skylink + * @since 0.6.15 + */ +Skylink.prototype._peerMessagesStamps = {}; + +/** + * + * Note that broadcasted events from muteStream() method, + * stopStream() method, + * stopScreen() method, + * sendMessage() method, + * unlockRoom() method and + * lockRoom() method may be queued when + * sent within less than an interval. + * + * Function that sends a message to Peers via the Signaling socket connection. + * @method sendMessage + * @param {String|JSON} message The message. + * @param {String|Array} [targetPeerId] The target Peer ID to send message to. + * - When provided as an Array, it will send the message to only Peers which IDs are in the list. + * - When not provided, it will broadcast the message to all connected Peers in the Room. + * @example + * // Example 1: Broadcasting to all Peers + * skylinkDemo.sendMessage("Hi all!"); + * + * // Example 2: Sending to specific Peers + * var peersInExclusiveParty = []; + * + * skylinkDemo.on("peerJoined", function (peerId, peerInfo, isSelf) { + * if (isSelf) return; + * if (peerInfo.userData.exclusive) { + * peersInExclusiveParty.push(peerId); + * } + * }); + * + * function updateExclusivePartyStatus (message) { + * skylinkDemo.sendMessage(message, peersInExclusiveParty); + * } + * @trigger + * Sends socket connection message to all targeted Peers via Signaling server. + * incomingMessage event triggers parameter payload + * message.isDataChannel value as false. + * @for Skylink + * @since 0.4.0 + */ +Skylink.prototype.sendMessage = function(message, targetPeerId) { + var params = { + cid: this._key, + data: message, + mid: this._user.sid, + rid: this._room.id, + type: this._SIG_MESSAGE_TYPE.PUBLIC_MESSAGE + }; + + var listOfPeers = Object.keys(this._peerConnections); + var isPrivate = false; + var i; + + if(Array.isArray(targetPeerId)) { + listOfPeers = targetPeerId; + isPrivate = true; + + } else if (typeof targetPeerId === 'string') { + listOfPeers = [targetPeerId]; + isPrivate = true; + } + + if (!isPrivate) { + log.log([null, 'Socket', null, 'Broadcasting message to peers']); + + this._sendChannelMessage({ + cid: this._key, + data: message, + mid: this._user.sid, + rid: this._room.id, + type: this._SIG_MESSAGE_TYPE.PUBLIC_MESSAGE + }); + } + for (i = 0; i < listOfPeers.length; i++) { + var peerId = listOfPeers[i]; + + // Ignore MCU peer + if (peerId === 'MCU') { + continue; + } + + if (isPrivate) { + log.log([peerId, 'Socket', null, 'Sending message to peer']); + + this._sendChannelMessage({ + cid: this._key, + data: message, + mid: this._user.sid, + rid: this._room.id, + target: peerId, + type: this._SIG_MESSAGE_TYPE.PRIVATE_MESSAGE + }); + } + } + + this._trigger('incomingMessage', { + content: message, + isPrivate: isPrivate, + targetPeerId: targetPeerId, + isDataChannel: false, + senderPeerId: this._user.sid + }, this._user.sid, this.getPeerInfo(), true); +}; /** - * Handles every incoming signaling message received. + * Function that process and parses the socket message string received from the Signaling. * @method _processSigMessage - * @param {String} messageString The message object stringified received. * @private - * @component Message * @for Skylink * @since 0.1.0 */ @@ -6070,11 +9733,9 @@ Skylink.prototype._processSigMessage = function(messageString) { }; /** - * Handles the single signaling message received. + * Function that handles and processes the socket message received. * @method _processingSingleMessage - * @param {JSON} message The message object received. * @private - * @component Message * @for Skylink * @since 0.1.0 */ @@ -6142,6 +9803,15 @@ Skylink.prototype._processSingleMessage = function(message) { case this._SIG_MESSAGE_TYPE.ROOM_LOCK: this._roomLockEventHandler(message); break; + case this._SIG_MESSAGE_TYPE.PEER_LIST: + this._peerListEventHandler(message); + break; + case this._SIG_MESSAGE_TYPE.INTRODUCE_ERROR: + this._introduceErrorEventHandler(message); + break; + case this._SIG_MESSAGE_TYPE.APPROACH: + this._approachEventHandler(message); + break; default: log.error([message.mid, null, null, 'Unsupported message ->'], message.type); break; @@ -6149,19 +9819,74 @@ Skylink.prototype._processSingleMessage = function(message) { }; /** - * Handles the REDIRECT Message event. + * Function that handles the "peerList" socket message received. + * See confluence docs for the "peerList" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. + * @method _peerListEventHandler + * @private + * @for Skylink + * @since 0.6.1 + */ +Skylink.prototype._peerListEventHandler = function(message){ + var self = this; + self._peerList = message.result; + log.log(['Server', null, message.type, 'Received list of peers'], self._peerList); + self._trigger('getPeersStateChange',self.GET_PEERS_STATE.RECEIVED, self._user.sid, self._peerList); +}; + +/** + * Function that handles the "introduceError" socket message received. + * See confluence docs for the "introduceError" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. + * @method _introduceErrorEventHandler + * @private + * @for Skylink + * @since 0.6.1 + */ +Skylink.prototype._introduceErrorEventHandler = function(message){ + var self = this; + log.log(['Server', null, message.type, 'Introduce failed. Reason: '+message.reason]); + self._trigger('introduceStateChange',self.INTRODUCE_STATE.ERROR, self._user.sid, + message.sendingPeerId, message.receivingPeerId, message.reason); +}; + +/** + * Function that handles the "approach" socket message received. + * See confluence docs for the "approach" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. + * @method _approachEventHandler + * @private + * @for Skylink + * @since 0.6.1 + */ +Skylink.prototype._approachEventHandler = function(message){ + var self = this; + log.log(['Server', null, message.type, 'Approaching peer'], message.target); + // self._room.connection.peerConfig = self._setIceServers(message.pc_config); + // self._inRoom = true; + self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ENTER, self._user.sid); + self._sendChannelMessage({ + type: self._SIG_MESSAGE_TYPE.ENTER, + mid: self._user.sid, + rid: self._room.id, + agent: window.webrtcDetectedBrowser, + version: window.webrtcDetectedVersion, + os: window.navigator.platform, + userInfo: self._getUserInfo(), + receiveOnly: self._receiveOnly, + sessionType: !!self._streams.screenshare ? 'screensharing' : 'stream', + target: message.target, + weight: self._peerPriorityWeight, + temasysPluginVersion: AdapterJS.WebRTCPlugin.plugin ? AdapterJS.WebRTCPlugin.plugin.VERSION : null + }); +}; + +/** + * Function that handles the "redirect" socket message received. + * See confluence docs for the "redirect" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. * @method _redirectHandler - * @param {JSON} message The Message object received. - * @param {String} message.rid The roomId of the connected Room. - * @param {String} message.info The server's message. - * @param {String} message.action The action that User has to take on. - * [Rel: Skylink.SYSTEM_ACTION] - * @param {String} message.reason The reason of why the action is worked upon. - * [Rel: Skylink.SYSTEM_ACTION_REASON] - * @param {String} message.type Protocol step: "redirect". - * @trigger systemAction - * @private - * @component Message + * @private * @for Skylink * @since 0.5.1 */ @@ -6179,20 +9904,21 @@ Skylink.prototype._redirectHandler = function(message) { } } } + + // Handle the differences provided in Signaling server + if (message.reason === 'toClose') { + message.reason = 'toclose'; + } + this._trigger('systemAction', message.action, message.info, message.reason); }; /** - * Handles the UPDATE_USER Message event. + * Function that handles the "updateUserEvent" socket message received. + * See confluence docs for the "updateUserEvent" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. * @method _updateUserEventHandler - * @param {JSON} message The Message object received. - * @param {String} message.rid The roomId of the connected Room. - * @param {String} message.mid The sender's peerId. - * @param {JSON|String} message.userData The updated User data. - * @param {String} message.type Protocol step: "updateUserEvent". - * @trigger peerUpdated * @private - * @component Message * @for Skylink * @since 0.2.0 */ @@ -6200,25 +9926,27 @@ Skylink.prototype._updateUserEventHandler = function(message) { var targetMid = message.mid; log.log([targetMid, null, message.type, 'Peer updated userData:'], message.userData); if (this._peerInformations[targetMid]) { + if (this._peerMessagesStamps[targetMid] && typeof message.stamp === 'number') { + if (message.stamp < this._peerMessagesStamps[targetMid].userData) { + log.warn([targetMid, null, message.type, 'Dropping outdated status ->'], message); + return; + } + this._peerMessagesStamps[targetMid].userData = message.stamp; + } this._peerInformations[targetMid].userData = message.userData || {}; this._trigger('peerUpdated', targetMid, - this._peerInformations[targetMid], false); + this.getPeerInfo(targetMid), false); } else { log.log([targetMid, null, message.type, 'Peer does not have any user information']); } }; /** - * Handles the ROOM_LOCK Message event. + * Function that handles the "roomLockEvent" socket message received. + * See confluence docs for the "roomLockEvent" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. * @method _roomLockEventHandler - * @param {JSON} message The Message object received. - * @param {String} message.rid The roomId of the connected Room. - * @param {String} message.mid The sender's peerId. - * @param {String} message.lock The flag to indicate if the Room is locked or not - * @param {String} message.type Protocol step: "roomLockEvent". - * @trigger roomLock * @private - * @component Message * @for Skylink * @since 0.2.0 */ @@ -6226,17 +9954,15 @@ Skylink.prototype._roomLockEventHandler = function(message) { var targetMid = message.mid; log.log([targetMid, message.type, 'Room lock status:'], message.lock); this._trigger('roomLock', message.lock, targetMid, - this._peerInformations[targetMid], false); + this.getPeerInfo(targetMid), false); }; /** - * Handles the MUTE_AUDIO Message event. + * Function that handles the "muteAudioEvent" socket message received. + * See confluence docs for the "muteAudioEvent" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. * @method _muteAudioEventHandler - * @param {JSON} message The Message object received. - * [Rel: Skylink._SIG_MESSAGE_TYPE.MUTE_AUDIO.message] - * @trigger peerUpdated * @private - * @component Message * @for Skylink * @since 0.2.0 */ @@ -6244,26 +9970,29 @@ Skylink.prototype._muteAudioEventHandler = function(message) { var targetMid = message.mid; log.log([targetMid, null, message.type, 'Peer\'s audio muted:'], message.muted); if (this._peerInformations[targetMid]) { + if (this._peerMessagesStamps[targetMid] && typeof message.stamp === 'number') { + if (message.stamp < this._peerMessagesStamps[targetMid].audioMuted) { + log.warn([targetMid, null, message.type, 'Dropping outdated status ->'], message); + return; + } + this._peerMessagesStamps[targetMid].audioMuted = message.stamp; + } this._peerInformations[targetMid].mediaStatus.audioMuted = message.muted; - this._trigger('peerUpdated', targetMid, - this._peerInformations[targetMid], false); + this._trigger('streamMuted', targetMid, this.getPeerInfo(targetMid), false, + this._peerInformations[targetMid].settings.video && + this._peerInformations[targetMid].settings.video.screenshare); + this._trigger('peerUpdated', targetMid, this.getPeerInfo(targetMid), false); } else { log.log([targetMid, message.type, 'Peer does not have any user information']); } }; /** - * Handles the MUTE_VIDEO Message event. + * Function that handles the "muteVideoEvent" socket message received. + * See confluence docs for the "muteVideoEvent" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. * @method _muteVideoEventHandler - * @param {JSON} message The Message object received. - * @param {String} message.rid The roomId of the connected Room. - * @param {String} message.mid The sender's peerId. - * @param {String} message.muted The flag to indicate if the User's video - * stream is muted or not. - * @param {String} message.type Protocol step: "muteVideoEvent". - * @trigger peerUpdated * @private - * @component Message * @for Skylink * @since 0.2.0 */ @@ -6271,28 +10000,30 @@ Skylink.prototype._muteVideoEventHandler = function(message) { var targetMid = message.mid; log.log([targetMid, null, message.type, 'Peer\'s video muted:'], message.muted); if (this._peerInformations[targetMid]) { + if (this._peerMessagesStamps[targetMid] && typeof message.stamp === 'number') { + if (message.stamp < this._peerMessagesStamps[targetMid].videoMuted) { + log.warn([targetMid, null, message.type, 'Dropping outdated status ->'], message); + return; + } + this._peerMessagesStamps[targetMid].videoMuted = message.stamp; + } this._peerInformations[targetMid].mediaStatus.videoMuted = message.muted; + this._trigger('streamMuted', targetMid, this.getPeerInfo(targetMid), false, + this._peerInformations[targetMid].settings.video && + this._peerInformations[targetMid].settings.video.screenshare); this._trigger('peerUpdated', targetMid, - this._peerInformations[targetMid], false); + this.getPeerInfo(targetMid), false); } else { log.log([targetMid, null, message.type, 'Peer does not have any user information']); } }; /** - * Handles the STREAM Message event. + * Function that handles the "stream" socket message received. + * See confluence docs for the "stream" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. * @method _streamEventHandler - * @param {JSON} message The Message object received. - * @param {String} message.rid The roomId of the connected Room. - * @param {String} message.mid The peerId of the sender. - * @param {String} message.status The MediaStream status. - * - * ended: MediaStream has ended - * - * @param {String} message.type Protocol step: "stream". - * @trigger peerUpdated - * @private - * @component Message + * @private * @for Skylink * @since 0.2.0 */ @@ -6303,47 +10034,84 @@ Skylink.prototype._streamEventHandler = function(message) { if (this._peerInformations[targetMid]) { if (message.status === 'ended') { - this._trigger('streamEnded', targetMid, this.getPeerInfo(targetMid), false); - this._peerConnections[targetMid].hasStream = false; + this._trigger('streamEnded', targetMid, this.getPeerInfo(targetMid), + false, message.sessionType === 'screensharing', message.streamId); + this._trigger('peerUpdated', targetMid, this.getPeerInfo(targetMid), false); + + if (this._peerConnections[targetMid]) { + this._peerConnections[targetMid].hasStream = false; + if (message.sessionType === 'screensharing') { + this._peerConnections[targetMid].hasScreen = false; + } + } else { + log.log([targetMid, null, message.type, 'Peer connection not found']); + } + } else if (message.status === 'check') { + if (!message.streamId) { + return; + } + + // Prevent restarts unless its stable + if (this._peerConnections[targetMid] && this._peerConnections[targetMid].signalingState === + this.PEER_CONNECTION_STATE.STABLE) { + var streams = this._peerConnections[targetMid].getRemoteStreams(); + if (streams.length > 0 && message.streamId !== (streams[0].id || streams[0].label)) { + this._sendChannelMessage({ + type: this._SIG_MESSAGE_TYPE.RESTART, + mid: this._user.sid, + rid: this._room.id, + agent: window.webrtcDetectedBrowser, + version: window.webrtcDetectedVersion, + os: window.navigator.platform, + userInfo: this._getUserInfo(), + target: targetMid, + weight: this._peerPriorityWeight, + enableIceTrickle: this._enableIceTrickle, + enableDataChannel: this._enableDataChannel, + receiveOnly: this._peerConnections[targetMid] && this._peerConnections[targetMid].receiveOnly, + sessionType: !!this._streams.screenshare ? 'screensharing' : 'stream', + // SkylinkJS parameters (copy the parameters from received message parameters) + isConnectionRestart: !!message.isConnectionRestart, + lastRestart: message.lastRestart, + explicit: !!message.explicit, + temasysPluginVersion: AdapterJS.WebRTCPlugin.plugin ? AdapterJS.WebRTCPlugin.plugin.VERSION : null + }); + } + } } } else { - log.log([targetMid, message.type, 'Peer does not have any user information']); + log.log([targetMid, null, message.type, 'Peer does not have any user information']); } }; /** - * Handles the BYTE Message event. + * Function that handles the "bye" socket message received. + * See confluence docs for the "bye" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. * @method _byeHandler - * @param {JSON} message The Message object received. - * @param {String} message.rid The roomId of the connected Room. - * @param {String} message.mid The peerId of the Peer that has left the Room. - * @param {String} message.type Protocol step: "bye". - * @trigger peerLeft * @private - * @component Message * @for Skylink * @since 0.1.0 */ Skylink.prototype._byeHandler = function(message) { var targetMid = message.mid; - log.log([targetMid, null, message.type, 'Peer has left the room']); - this._removePeer(targetMid); + var selfId = (this._user || {}).sid; + + if (selfId !== targetMid){ + log.log([targetMid, null, message.type, 'Peer has left the room']); + this._removePeer(targetMid); + } else { + log.log([targetMid, null, message.type, 'Self has left the room']); + } }; /** - * Handles the PRIVATE_MESSAGE Message event. + * Function that handles the "private" socket message received. + * See confluence docs for the "private" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. * @method _privateMessageHandler - * @param {JSON} message The Message object received. - * @param {JSON|String} message.data The Message object. - * @param {String} message.rid The roomId of the connected Room. - * @param {String} message.cid The credentialId of the connected Room. - * @param {String} message.mid The sender's peerId. - * @param {String} message.target The peerId of the targeted Peer. - * @param {String} message.type Protocol step: "private". - * @trigger privateMessage * @private - * @component Message * @for Skylink * @since 0.4.0 */ @@ -6357,21 +10125,15 @@ Skylink.prototype._privateMessageHandler = function(message) { targetPeerId: message.target, // is not null if there's user isDataChannel: false, senderPeerId: targetMid - }, targetMid, this._peerInformations[targetMid], false); + }, targetMid, this.getPeerInfo(targetMid), false); }; /** - * Handles the PUBLIC_MESSAGE Message event. + * Function that handles the "public" socket message received. + * See confluence docs for the "public" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. * @method _publicMessageHandler - * @param {JSON} message The Message object received. - * @param {String} message.rid The roomId of the connected Room. - * @param {String} message.mid The sender's peerId. - * @param {String} message.muted The flag to indicate if the User's audio - * stream is muted or not. - * @param {String} message.type Protocol step: "muteAudioEvent". - * @trigger publicMessage * @private - * @component Message * @for Skylink * @since 0.4.0 */ @@ -6385,21 +10147,15 @@ Skylink.prototype._publicMessageHandler = function(message) { targetPeerId: null, // is not null if there's user isDataChannel: false, senderPeerId: targetMid - }, targetMid, this._peerInformations[targetMid], false); + }, targetMid, this.getPeerInfo(targetMid), false); }; /** - * Handles the IN_ROOM Message event. + * Function that handles the "inRoom" socket message received. + * See confluence docs for the "inRoom" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. * @method _inRoomHandler - * @param {JSON} message The Message object received. - * @param {JSON} message Expected IN_ROOM data object format. - * @param {String} message.rid The roomId of the connected room. - * @param {String} message.sid The User's userId. - * @param {JSON} message.pc_config The Peer connection iceServers configuration. - * @param {String} message.type Protocol step: "inRoom". - * @trigger peerJoined * @private - * @component Message * @for Skylink * @since 0.1.0 */ @@ -6410,8 +10166,27 @@ Skylink.prototype._inRoomHandler = function(message) { self._room.connection.peerConfig = self._setIceServers(message.pc_config); self._inRoom = true; self._user.sid = message.sid; + self._peerPriorityWeight = (new Date()).getTime(); + self._trigger('peerJoined', self._user.sid, self.getPeerInfo(), true); self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ENTER, self._user.sid); + + if (typeof message.tieBreaker === 'number') { + self._peerPriorityWeight = message.tieBreaker; + } + + // Make Firefox the answerer always when connecting with other browsers + if (window.webrtcDetectedBrowser === 'firefox') { + log.warn('Decreasing weight for Firefox browser connection'); + + self._peerPriorityWeight -= 100000000000; + } + + if (self._streams.screenshare && self._streams.screenshare.stream) { + self._trigger('incomingStream', self._user.sid, self._streams.screenshare.stream, true, self.getPeerInfo()); + } else if (self._streams.userMedia && self._streams.userMedia.stream) { + self._trigger('incomingStream', self._user.sid, self._streams.userMedia.stream, true, self.getPeerInfo()); + } // NOTE ALEX: should we wait for local streams? // or just go with what we have (if no stream, then one way?) // do we hardcode the logic here, or give the flexibility? @@ -6424,101 +10199,64 @@ Skylink.prototype._inRoomHandler = function(message) { agent: window.webrtcDetectedBrowser, version: window.webrtcDetectedVersion, os: window.navigator.platform, - userInfo: self.getPeerInfo(), + userInfo: self._getUserInfo(), receiveOnly: self._receiveOnly, - sessionType: !!self._mediaScreen ? 'screensharing' : 'stream' + sessionType: !!self._streams.screenshare ? 'screensharing' : 'stream', + weight: self._peerPriorityWeight, + temasysPluginVersion: AdapterJS.WebRTCPlugin.plugin ? AdapterJS.WebRTCPlugin.plugin.VERSION : null }); }; /** - * Handles the ENTER Message event. + * Function that handles the "enter" socket message received. + * See confluence docs for the "enter" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. * @method _enterHandler - * @param {JSON} message The Message object received. - * @param {String} message.rid The roomId of the connected Room. - * @param {String} message.mid The sender's peerId / userId. - * @param {Boolean} [message.receiveOnly=false] The flag to prevent Peers from sending - * any Stream to the User but receive User's stream only. - * @param {String} message.agent The Peer's browser agent. - * @param {String} message.version The Peer's browser version. - * @param {String} message.userInfo The Peer's information. - * @param {JSON} message.userInfo.settings The stream settings - * @param {Boolean|JSON} [message.userInfo.settings.audio=false] - * The flag to indicate if audio is enabled in the connection or not. - * @param {Boolean} [message.userInfo.settings.audio.stereo=false] - * The flag to indiciate if stereo should be enabled in OPUS connection. - * @param {Boolean|JSON} [message.userInfo.settings.video=false] - * The flag to indicate if video is enabled in the connection or not. - * @param {JSON} [message.userInfo.settings.video.resolution] - * [Rel: Skylink.VIDEO_RESOLUTION] - * The video stream resolution. - * @param {Number} [message.userInfo.settings.video.resolution.width] - * The video stream resolution width. - * @param {Number} [message.userInfo.settings.video.resolution.height] - * The video stream resolution height. - * @param {Number} [message.userInfo.settings.video.frameRate] - * The video stream maximum frame rate. - * @param {JSON} message.userInfo.mediaStatus The Peer's Stream status. - * This is used to indicate if connected video or audio stream is muted. - * @param {Boolean} [message.userInfo.mediaStatus.audioMuted=true] - * The flag to indicate that the Peer's audio stream is muted or disabled. - * @param {Boolean} [message.userInfo.mediaStatus.videoMuted=true] - * The flag to indicate that the Peer's video stream is muted or disabled. - * @param {String|JSON} message.userInfo.userData - * The custom User data. - * @param {String} message.type Protocol step: "enter". - * @trigger handshakeProgress, peerJoined - * @private - * @component Message + * @private * @for Skylink * @since 0.5.1 */ Skylink.prototype._enterHandler = function(message) { var self = this; var targetMid = message.mid; - log.log([targetMid, null, message.type, 'Incoming peer have initiated ' + - 'handshake. Peer\'s information:'], message.userInfo); - // need to check entered user is new or not. - // peerInformations because it takes a sequence before creating the - // peerconnection object. peerInformations are stored at the start of the - // handshake, so user knows if there is a peer already. - if (self._peerInformations[targetMid]) { - // NOTE ALEX: and if we already have a connection when the peer enter, - // what should we do? what are the possible use case? - log.log([targetMid, null, message.type, 'Ignoring message as peer is already added']); - return; - } - // add peer - self._addPeer(targetMid, { - agent: message.agent, - version: message.version, - os: message.os - }, false, false, message.receiveOnly, message.sessionType === 'screensharing'); - self._peerInformations[targetMid] = message.userInfo || {}; - self._peerInformations[targetMid].agent = { - name: message.agent, - version: message.version - }; - if (targetMid !== 'MCU') { - self._trigger('peerJoined', targetMid, message.userInfo, false); - self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ENTER, targetMid); - self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.WELCOME, targetMid); + var isNewPeer = false; - // disable mcu for incoming peer sent by MCU - //if (message.agent === 'MCU') { - // this._enableDataChannel = false; + log.log([targetMid, null, message.type, 'Received Peer\'s presence ->'], message.userInfo); - /*if (window.webrtcDetectedBrowser === 'firefox') { - this._enableIceTrickle = false; - }*/ - //} - } else { - log.log([targetMid, null, message.type, 'MCU has joined'], message.userInfo); - this._hasMCU = true; - // this._enableDataChannel = false; + if (!self._peerInformations[targetMid]) { + isNewPeer = true; + self._addPeer(targetMid, { + agent: message.agent, + version: message.version, + os: message.os + }, false, false, message.receiveOnly, message.sessionType === 'screensharing'); + + self._peerInformations[targetMid] = message.userInfo || {}; + self._peerMessagesStamps[targetMid] = self._peerMessagesStamps[targetMid] || { + userData: 0, + audioMuted: 0, + videoMuted: 0 + }; + self._peerInformations[targetMid].agent = { + name: message.agent, + version: message.version, + os: message.os || '', + pluginVersion: message.temasysPluginVersion + }; + + if (targetMid !== 'MCU') { + self._trigger('peerJoined', targetMid, message.userInfo, false); + + } else { + log.info([targetMid, 'RTCPeerConnection', 'MCU', 'MCU feature has been enabled'], message); + log.log([targetMid, null, message.type, 'MCU has joined'], message.userInfo); + this._hasMCU = true; + this._trigger('serverPeerJoined', targetMid, this.SERVER_PEER_TYPE.MCU); + } + + self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ENTER, targetMid); } - var weight = (new Date()).valueOf(); - self._peerHSPriorities[targetMid] = weight; self._sendChannelMessage({ type: self._SIG_MESSAGE_TYPE.WELCOME, mid: self._user.sid, @@ -6530,57 +10268,24 @@ Skylink.prototype._enterHandler = function(message) { agent: window.webrtcDetectedBrowser, version: window.webrtcDetectedVersion, os: window.navigator.platform, - userInfo: self.getPeerInfo(), + userInfo: self._getUserInfo(), target: targetMid, - weight: weight, - sessionType: !!self._mediaScreen ? 'screensharing' : 'stream' + weight: self._peerPriorityWeight, + sessionType: !!self._streams.screenshare ? 'screensharing' : 'stream', + temasysPluginVersion: AdapterJS.WebRTCPlugin.plugin ? AdapterJS.WebRTCPlugin.plugin.VERSION : null }); + + if (isNewPeer) { + self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.WELCOME, targetMid); + } }; /** - * Handles the RESTART Message event. + * Function that handles the "restart" socket message received. + * See confluence docs for the "restart" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. * @method _restartHandler - * @param {JSON} message The Message object received. - * @param {String} message.rid The roomId of the connected Room. - * @param {String} message.mid The sender's peerId / userId. - * @param {Boolean} [message.receiveOnly=false] The flag to prevent Peers from sending - * any Stream to the User but receive User's stream only. - * @param {Boolean} [message.enableIceTrickle=false] - * The flag to forcefully enable or disable ICE Trickle for the Peer connection. - * @param {Boolean} [message.enableDataChannel=false] - * The flag to forcefully enable or disable ICE Trickle for the Peer connection. - * @param {String} message.agent The Peer's browser agent. - * @param {String} message.version The Peer's browser version. - * @param {String} message.userInfo The Peer's information. - * @param {JSON} message.userInfo.settings The stream settings - * @param {Boolean|JSON} [message.userInfo.settings.audio=false] - * The flag to indicate if audio is enabled in the connection or not. - * @param {Boolean} [message.userInfo.settings.audio.stereo=false] - * The flag to indiciate if stereo should be enabled in OPUS connection. - * @param {Boolean|JSON} [message.userInfo.settings.video=false] - * The flag to indicate if video is enabled in the connection or not. - * @param {JSON} [message.userInfo.settings.video.resolution] - * [Rel: Skylink.VIDEO_RESOLUTION] - * The video stream resolution. - * @param {Number} [message.userInfo.settings.video.resolution.width] - * The video stream resolution width. - * @param {Number} [message.userInfo.settings.video.resolution.height] - * The video stream resolution height. - * @param {Number} [message.userInfo.settings.video.frameRate] - * The video stream maximum frame rate. - * @param {JSON} message.userInfo.mediaStatus The Peer's Stream status. - * This is used to indicate if connected video or audio stream is muted. - * @param {Boolean} [message.userInfo.mediaStatus.audioMuted=true] - * The flag to indicate that the Peer's audio stream is muted or disabled. - * @param {Boolean} [message.userInfo.mediaStatus.videoMuted=true] - * The flag to indicate that the Peer's video stream is muted or disabled. - * @param {String|JSON} message.userInfo.userData - * The custom User data. - * @param {String} message.target The peerId of the peer to respond the enter message to. - * @param {String} message.type Protocol step: "restart". - * @trigger handshakeProgress, peerRestart - * @private - * @component Message + * @private * @for Skylink * @since 0.5.6 */ @@ -6588,9 +10293,15 @@ Skylink.prototype._restartHandler = function(message){ var self = this; var targetMid = message.mid; + if (!self._peerInformations[targetMid]) { + log.error([targetMid, null, null, 'Peer does not have an existing ' + + 'session. Ignoring restart process.']); + return; + } + + // NOTE: for now we ignore, but we should take-note to implement in the near future if (self._hasMCU) { - log.warn([peerId, 'PeerConnection', null, 'Restart functionality for peer\'s connection ' + - 'for MCU is not yet supported']); + self._trigger('peerRestart', targetMid, self.getPeerInfo(targetMid), false); return; } @@ -6602,171 +10313,126 @@ Skylink.prototype._restartHandler = function(message){ return; } - //Only consider peer's restart weight if self also sent a restart which cause a potential conflict - //Otherwise go ahead with peer's restart - if (self._peerRestartPriorities.hasOwnProperty(targetMid)){ - //Peer's restart message was older --> ignore - if (self._peerRestartPriorities[targetMid] > message.weight){ - log.log([targetMid, null, message.type, 'Peer\'s generated restart weight ' + - 'is lesser than user\'s. Ignoring message' - ], this._peerRestartPriorities[targetMid] + ' > ' + message.weight); - return; - } + // mcu has re-joined + // NOTE: logic trip since _hasMCU flags are ignored, this could result in failure perhaps? + if (targetMid === 'MCU') { + log.log([targetMid, null, message.type, 'MCU has restarted its connection']); + self._hasMCU = true; } + // Uncomment because we do not need this + //self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.WELCOME, targetMid); + + message.agent = (!message.agent) ? 'chrome' : message.agent; + /*self._enableIceTrickle = (typeof message.enableIceTrickle === 'boolean') ? + message.enableIceTrickle : self._enableIceTrickle; + self._enableDataChannel = (typeof message.enableDataChannel === 'boolean') ? + message.enableDataChannel : self._enableDataChannel;*/ + // re-add information self._peerInformations[targetMid] = message.userInfo || {}; + self._peerMessagesStamps[targetMid] = self._peerMessagesStamps[targetMid] || { + userData: 0, + audioMuted: 0, + videoMuted: 0 + }; self._peerInformations[targetMid].agent = { name: message.agent, - version: message.version + version: message.version, + os: message.os || '', + pluginVersion: message.temasysPluginVersion }; - // mcu has joined - if (targetMid === 'MCU') { - log.log([targetMid, null, message.type, 'MCU has restarted its connection']); - self._hasMCU = true; - } + var agent = (self.getPeerInfo(targetMid) || {}).agent || {}; - self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.WELCOME, targetMid); + // This variable is not used + //var peerConnectionStateStable = false; - message.agent = (!message.agent) ? 'chrome' : message.agent; - self._enableIceTrickle = (typeof message.enableIceTrickle === 'boolean') ? - message.enableIceTrickle : self._enableIceTrickle; - self._enableDataChannel = (typeof message.enableDataChannel === 'boolean') ? - message.enableDataChannel : self._enableDataChannel; + log.info([targetMid, 'RTCPeerConnection', null, 'Received restart request from peer'], message); + // we are no longer adding any peer + /*self._addPeer(targetMid, { + agent: message.agent, + version: message.version, + os: message.os + }, true, true, message.receiveOnly, message.sessionType === 'screensharing');*/ - var peerConnectionStateStable = false; + // Make peer with highest weight do the offer + if (self._peerPriorityWeight > message.weight) { + log.debug([targetMid, 'RTCPeerConnection', null, 'Restarting negotiation'], agent); + self._doOffer(targetMid, { + agent: agent.name, + version: agent.version, + os: agent.os + }, true); - self._restartPeerConnection(targetMid, false, false, function () { - log.info('Received message', message); - self._addPeer(targetMid, { - agent: message.agent, - version: message.version, - os: message.os || window.navigator.platform - }, true, true, message.receiveOnly, message.sessionType === 'screensharing'); + } else { + log.debug([targetMid, 'RTCPeerConnection', null, 'Waiting for peer to start re-negotiation'], agent); + self._sendChannelMessage({ + type: self._SIG_MESSAGE_TYPE.RESTART, + mid: self._user.sid, + rid: self._room.id, + agent: window.webrtcDetectedBrowser, + version: window.webrtcDetectedVersion, + os: window.navigator.platform, + userInfo: self._getUserInfo(), + target: targetMid, + weight: self._peerPriorityWeight, + enableIceTrickle: self._enableIceTrickle, + enableDataChannel: self._enableDataChannel, + receiveOnly: self._peerConnections[targetMid] && self._peerConnections[targetMid].receiveOnly, + sessionType: !!self._streams.screenshare ? 'screensharing' : 'stream', + // SkylinkJS parameters (copy the parameters from received message parameters) + isConnectionRestart: !!message.isConnectionRestart, + lastRestart: message.lastRestart, + explicit: !!message.explicit, + temasysPluginVersion: AdapterJS.WebRTCPlugin.plugin ? AdapterJS.WebRTCPlugin.plugin.VERSION : null + }); + } - self._trigger('peerRestart', targetMid, self._peerInformations[targetMid] || {}, false); + self._trigger('peerRestart', targetMid, self.getPeerInfo(targetMid), false); - // do a peer connection health check - self._startPeerConnectionHealthCheck(targetMid); - }, message.explicit); + // following the previous logic to do checker always + self._startPeerConnectionHealthCheck(targetMid, false); }; /** - * Handles the WELCOME Message event. + * Function that handles the "welcome" socket message received. + * See confluence docs for the "welcome" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. * @method _welcomeHandler - * @param {JSON} message The Message object received. - * @param {String} message.rid The roomId of the connected Room. - * @param {String} message.mid The sender's peerId / userId. - * @param {Boolean} [message.receiveOnly=false] The flag to prevent Peers from sending - * any Stream to the User but receive User's stream only. - * @param {Boolean} [message.enableIceTrickle=false] - * The flag to forcefully enable or disable ICE Trickle for the Peer connection. - * @param {Boolean} [message.enableDataChannel=false] - * The flag to forcefully enable or disable ICE Trickle for the Peer connection. - * @param {String} message.agent The Peer's browser agent. - * @param {String} message.version The Peer's browser version. - * @param {String} message.userInfo The Peer's information. - * @param {JSON} message.userInfo.settings The stream settings - * @param {Boolean|JSON} [message.userInfo.settings.audio=false] - * The flag to indicate if audio is enabled in the connection or not. - * @param {Boolean} [message.userInfo.settings.audio.stereo=false] - * The flag to indiciate if stereo should be enabled in OPUS connection. - * @param {Boolean|JSON} [message.userInfo.settings.video=false] - * The flag to indicate if video is enabled in the connection or not. - * @param {JSON} [message.userInfo.settings.video.resolution] - * [Rel: Skylink.VIDEO_RESOLUTION] - * The video stream resolution. - * @param {Number} [message.userInfo.settings.video.resolution.width] - * The video stream resolution width. - * @param {Number} [message.userInfo.settings.video.resolution.height] - * The video stream resolution height. - * @param {Number} [message.userInfo.settings.video.frameRate] - * The video stream maximum frame rate. - * @param {JSON} message.userInfo.mediaStatus The Peer's Stream status. - * This is used to indicate if connected video or audio stream is muted. - * @param {Boolean} [message.userInfo.mediaStatus.audioMuted=true] - * The flag to indicate that the Peer's audio stream is muted or disabled. - * @param {Boolean} [message.userInfo.mediaStatus.videoMuted=true] - * The flag to indicate that the Peer's video stream is muted or disabled. - * @param {String|JSON} message.userInfo.userData - * The custom User data. - * @param {String} message.target The peerId of the peer to respond the enter message to. - * @param {Number} message.weight The priority weight of the message. This is required - * when two Peers receives each other's welcome message, hence disrupting the handshaking to - * be incorrect. With a generated weight usually done by invoking Date.UTC(), this - * would check against the received weight and generated weight for the Peer to prioritize who - * should create or receive the offer. - * - * >=0 An ongoing weight priority check is going on.Weight priority message. - * -1 Enforce create offer to happen without any priority weight check. - * -2 Enforce create offer and re-creating of Peer connection to happen without - * any priority weight check. - * - * @param {String} message.type Protocol step: "welcome". - * @trigger handshakeProgress, peerJoined - * @private - * @component Message + * @private * @for Skylink * @since 0.5.4 */ Skylink.prototype._welcomeHandler = function(message) { var targetMid = message.mid; var restartConn = false; + var beOfferer = this._peerPriorityWeight > message.weight; + var isNewPeer = false; - log.log([targetMid, null, message.type, 'Received peer\'s response ' + - 'to handshake initiation. Peer\'s information:'], message.userInfo); - - if (this._peerConnections[targetMid]) { - if (!this._peerConnections[targetMid].setOffer || message.weight < 0) { - if (message.weight < 0) { - log.log([targetMid, null, message.type, 'Peer\'s weight is lower ' + - 'than 0. Proceeding with offer'], message.weight); - restartConn = true; - - // -2: hard restart of connection - if (message.weight === -2) { - this._restartHandler(message); - return; - } + log.log([targetMid, null, message.type, 'Received Peer\'s presence ->'], message.userInfo); - } else if (this._peerHSPriorities[targetMid] > message.weight) { - log.log([targetMid, null, message.type, 'Peer\'s generated weight ' + - 'is lesser than user\'s. Ignoring message' - ], this._peerHSPriorities[targetMid] + ' > ' + message.weight); - return; + // We shouldn't assume as chrome + message.agent = (!message.agent) ? 'unknown' : message.agent; - } else { - log.log([targetMid, null, message.type, 'Peer\'s generated weight ' + - 'is higher than user\'s. Proceeding with offer' - ], this._peerHSPriorities[targetMid] + ' < ' + message.weight); - restartConn = true; - } - } else { - log.warn([targetMid, null, message.type, - 'Ignoring message as peer is already added']); - return; - } - } - message.agent = (!message.agent) ? 'chrome' : message.agent; - this._enableIceTrickle = (typeof message.enableIceTrickle === 'boolean') ? - message.enableIceTrickle : this._enableIceTrickle; - this._enableDataChannel = (typeof message.enableDataChannel === 'boolean') ? - message.enableDataChannel : this._enableDataChannel; + var agent = { + agent: message.agent, + version: message.version, + os: message.os + }; - // mcu has joined - if (targetMid === 'MCU') { - log.log([targetMid, null, message.type, 'MCU has ' + - ((message.weight > -1) ? 'joined and ' : '') + ' responded']); - this._hasMCU = true; - // disable mcu for incoming MCU peer - // this._enableDataChannel = false; - } if (!this._peerInformations[targetMid]) { this._peerInformations[targetMid] = message.userInfo || {}; + this._peerMessagesStamps[targetMid] = this._peerMessagesStamps[targetMid] || { + userData: 0, + audioMuted: 0, + videoMuted: 0 + }; this._peerInformations[targetMid].agent = { name: message.agent, - version: message.version + version: message.version, + os: message.os || '', + pluginVersion: message.temasysPluginVersion }; // disable mcu for incoming peer sent by MCU /*if (message.agent === 'MCU') { @@ -6776,28 +10442,65 @@ Skylink.prototype._welcomeHandler = function(message) { // user is not mcu if (targetMid !== 'MCU') { this._trigger('peerJoined', targetMid, message.userInfo, false); - this._trigger('handshakeProgress', this.HANDSHAKE_PROGRESS.WELCOME, targetMid); + + } else { + log.info([targetMid, 'RTCPeerConnection', 'MCU', 'MCU feature is currently enabled'], message); + log.log([targetMid, null, message.type, 'MCU has ' + + ((message.weight > -1) ? 'joined and ' : '') + ' responded']); + this._hasMCU = true; + this._trigger('serverPeerJoined', targetMid, this.SERVER_PEER_TYPE.MCU); + log.log([targetMid, null, message.type, 'Always setting as offerer because peer is MCU']); + beOfferer = true; + } + + if (!this._peerConnections[targetMid]) { + this._addPeer(targetMid, agent, false, restartConn, message.receiveOnly, message.sessionType === 'screensharing'); } + + this._trigger('handshakeProgress', this.HANDSHAKE_PROGRESS.WELCOME, targetMid); } - this._addPeer(targetMid, { - agent: message.agent, - version: message.version, - os: message.os - }, true, restartConn, message.receiveOnly, message.sessionType === 'screensharing'); + if (this._hasMCU) { + log.log([targetMid, null, message.type, 'Always setting as offerer because MCU is present']); + beOfferer = true; + } + + /*this._enableIceTrickle = (typeof message.enableIceTrickle === 'boolean') ? + message.enableIceTrickle : this._enableIceTrickle; + this._enableDataChannel = (typeof message.enableDataChannel === 'boolean') ? + message.enableDataChannel : this._enableDataChannel;*/ + + log.debug([targetMid, 'RTCPeerConnection', null, 'Peer should start connection ->'], beOfferer); + + if (beOfferer) { + log.debug([targetMid, 'RTCPeerConnection', null, 'Starting negotiation'], agent); + this._doOffer(targetMid, agent); + + } else { + log.debug([targetMid, 'RTCPeerConnection', null, 'Peer has to start the connection. Resending message'], beOfferer); + + this._sendChannelMessage({ + type: this._SIG_MESSAGE_TYPE.WELCOME, + mid: this._user.sid, + rid: this._room.id, + agent: window.webrtcDetectedBrowser, + version: window.webrtcDetectedVersion, + os: window.navigator.platform, + userInfo: this._getUserInfo(), + target: targetMid, + weight: this._peerPriorityWeight, + sessionType: !!this._streams.screenshare ? 'screensharing' : 'stream', + temasysPluginVersion: AdapterJS.WebRTCPlugin.plugin ? AdapterJS.WebRTCPlugin.plugin.VERSION : null + }); + } }; /** - * Handles the OFFER Message event. + * Function that handles the "offer" socket message received. + * See confluence docs for the "offer" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. * @method _offerHandler - * @param {JSON} message The Message object received. - * @param {String} message.rid The roomId of the connected room. - * @param {String} message.mid The sender's peerId. - * @param {String} message.sdp The generated offer session description. - * @param {String} message.type Protocol step: "offer". - * @trigger handshakeProgress * @private - * @component Message * @for Skylink * @since 0.5.1 */ @@ -6812,44 +10515,85 @@ Skylink.prototype._offerHandler = function(message) { return; } - if (pc.localDescription ? !!pc.localDescription.sdp : false) { + /*if (pc.localDescription ? !!pc.localDescription.sdp : false) { log.warn([targetMid, null, message.type, 'Peer has an existing connection'], pc.localDescription); return; + }*/ + + // Add-on by Web SDK fixes + if (message.userInfo && typeof message.userInfo === 'object') { + self._peerInformations[targetMid].settings = message.userInfo.settings; + self._peerInformations[targetMid].mediaStatus = message.userInfo.mediaStatus; + self._peerInformations[targetMid].userData = message.userInfo.userData; } log.log([targetMid, null, message.type, 'Received offer from peer. ' + 'Session description:'], message.sdp); - self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.OFFER, targetMid); - var offer = new window.RTCSessionDescription(message); + var offer = new window.RTCSessionDescription({ + type: message.type, + sdp: message.sdp + }); log.log([targetMid, 'RTCSessionDescription', message.type, 'Session description object created'], offer); - pc.setRemoteDescription(new window.RTCSessionDescription(offer), function() { + // Configure it to force TURN connections by removing non-"relay" candidates + if (self._forceTURN && !self._enableIceTrickle) { + if (!self._hasMCU) { + log.warn([targetMid, 'RTCICECandidate', null, 'Removing non-"relay" candidates from offer ' + + ' as TURN connections is forced']); + + offer.sdp = offer.sdp.replace(/a=candidate:(?!.*relay.*).*\r\n/g, ''); + + } else { + log.warn([targetMid, 'RTCICECandidate', null, 'Not removing non-"relay"' + + '" candidates although TURN connections is forced as MCU is present']); + } + } + + // This is always the initial state. or even after negotiation is successful + if (pc.signalingState !== self.PEER_CONNECTION_STATE.STABLE) { + log.warn([targetMid, null, message.type, 'Peer connection state is not in ' + + '"stable" state for re-negotiation. Dropping message.'], { + signalingState: pc.signalingState, + isRestart: !!message.resend + }); + return; + } + + // Added checks if there is a current remote sessionDescription being processing before processing this one + if (pc.processingRemoteSDP) { + log.warn([targetMid, 'RTCSessionDescription', 'offer', + 'Dropping of setting local offer as there is another ' + + 'sessionDescription being processed ->'], offer); + return; + } + + pc.processingRemoteSDP = true; + + pc.setRemoteDescription(offer, function() { log.debug([targetMid, 'RTCSessionDescription', message.type, 'Remote description set']); pc.setOffer = 'remote'; + pc.processingRemoteSDP = false; + self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.OFFER, targetMid); self._addIceCandidateFromQueue(targetMid); self._doAnswer(targetMid); }, function(error) { self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ERROR, targetMid, error); + + pc.processingRemoteSDP = false; + log.error([targetMid, null, message.type, 'Failed setting remote description:'], error); }); }; + /** - * Handles the CANDIDATE Message event. + * Function that handles the "candidate" socket message received. + * See confluence docs for the "candidate" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. * @method _candidateHandler - * @param {JSON} message The Message object received. - * @param {String} message.rid The roomId of the connected room. - * @param {String} message.mid The sender's peerId. - * @param {String} message.sdp The ICE Candidate's session description. - * @param {String} message.target The peerId of the targeted Peer. - * @param {String} message.id The ICE Candidate's id. - * @param {String} message.candidate The ICE Candidate's candidate object. - * @param {String} message.label The ICE Candidate's label. - * @param {String} message.type Protocol step: "candidate". - * @private - * @component Message + * @private * @for Skylink * @since 0.5.1 */ @@ -6876,10 +10620,22 @@ Skylink.prototype._candidateHandler = function(message) { sdpMid: message.id //label: index }); + + if (this._forceTURN && canType !== 'relay') { + if (!this._hasMCU) { + log.warn([targetMid, 'RTCICECandidate', null, 'Ignoring adding of "' + canType + + '" candidate as TURN connections is forced'], candidate); + return; + } + + log.warn([targetMid, 'RTCICECandidate', null, 'Not ignoring adding of "' + canType + + '" candidate although TURN connections is forced as MCU is present'], candidate); + } + if (pc) { if (pc.signalingState === this.PEER_CONNECTION_STATE.CLOSED) { log.warn([targetMid, null, message.type, 'Peer connection state ' + - 'is closed. Not adding candidate']); + 'is closed. Not adding candidate'], candidate); return; } /*if (pc.iceConnectionState === this.ICE_CONNECTION_STATE.CONNECTED) { @@ -6904,24 +10660,32 @@ Skylink.prototype._candidateHandler = function(message) { } else { // Added ice candidate to queue because it may be received before sending the offer log.debug([targetMid, 'RTCIceCandidate', message.type, - 'Not adding candidate as peer connection not present']); + 'Not adding candidate as peer connection not present'], candidate); // NOTE ALEX: if the offer was slow, this can happen // we might keep a buffer of candidates to replay after receiving an offer. this._addIceCandidateToQueue(targetMid, candidate); } + + if (!this._gatheredCandidates[targetMid]) { + this._gatheredCandidates[targetMid] = { + sending: { host: [], srflx: [], relay: [] }, + receiving: { host: [], srflx: [], relay: [] } + }; + } + + this._gatheredCandidates[targetMid].receiving[canType].push({ + sdpMid: candidate.sdpMid, + sdpMLineIndex: candidate.sdpMLineIndex, + candidate: candidate.candidate + }); }; /** - * Handles the ANSWER Message event. + * Function that handles the "answer" socket message received. + * See confluence docs for the "answer" expected properties to be received + * based on the current SM_PROTOCOL_VERSION. * @method _answerHandler - * @param {JSON} message The Message object received. - * @param {String} message.rid The roomId of the connected room. - * @param {String} message.sdp The generated answer session description. - * @param {String} message.mid The sender's peerId. - * @param {String} message.type Protocol step: "answer". - * @trigger handshakeProgress * @private - * @component Message * @for Skylink * @since 0.5.1 */ @@ -6932,1639 +10696,1991 @@ Skylink.prototype._answerHandler = function(message) { log.log([targetMid, null, message.type, 'Received answer from peer. Session description:'], message.sdp); - self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ANSWER, targetMid); - var answer = new window.RTCSessionDescription(message); - - log.log([targetMid, 'RTCSessionDescription', message.type, - 'Session description object created'], answer); - var pc = self._peerConnections[targetMid]; if (!pc) { log.error([targetMid, null, message.type, 'Peer connection object ' + - 'not found. Unable to setRemoteDescription for offer']); + 'not found. Unable to setRemoteDescription for answer']); return; } - if (pc.remoteDescription ? !!pc.remoteDescription.sdp : false) { + // Add-on by Web SDK fixes + if (message.userInfo && typeof message.userInfo === 'object') { + self._peerInformations[targetMid].settings = message.userInfo.settings; + self._peerInformations[targetMid].mediaStatus = message.userInfo.mediaStatus; + self._peerInformations[targetMid].userData = message.userInfo.userData; + } + + var answer = new window.RTCSessionDescription({ + type: message.type, + sdp: message.sdp + }); + + log.log([targetMid, 'RTCSessionDescription', message.type, + 'Session description object created'], answer); + + /*if (pc.remoteDescription ? !!pc.remoteDescription.sdp : false) { log.warn([targetMid, null, message.type, 'Peer has an existing connection'], pc.remoteDescription); return; } + if (pc.signalingState === self.PEER_CONNECTION_STATE.STABLE) { + log.error([targetMid, null, message.type, 'Unable to set peer connection ' + + 'at signalingState "stable". Ignoring remote answer'], pc.signalingState); + return; + }*/ + // if firefox and peer is mcu, replace the sdp to suit mcu needs if (window.webrtcDetectedType === 'moz' && targetMid === 'MCU') { - message.sdp = message.sdp.replace(/ generation 0/g, ''); - message.sdp = message.sdp.replace(/ udp /g, ' UDP '); + answer.sdp = answer.sdp.replace(/ generation 0/g, ''); + answer.sdp = answer.sdp.replace(/ udp /g, ' UDP '); + } + + // Configure it to force TURN connections by removing non-"relay" candidates + if (self._forceTURN && !self._enableIceTrickle) { + if (!self._hasMCU) { + log.warn([targetMid, 'RTCICECandidate', null, 'Removing non-"relay" candidates from answer ' + + ' as TURN connections is forced']); + + answer.sdp = answer.sdp.replace(/a=candidate:(?!.*relay.*).*\r\n/g, ''); + + } else { + log.warn([targetMid, 'RTCICECandidate', null, 'Not removing non-"relay"' + + '" candidates although TURN connections is forced as MCU is present']); + } + } + + // This should be the state after offer is received. or even after negotiation is successful + if (pc.signalingState !== self.PEER_CONNECTION_STATE.HAVE_LOCAL_OFFER) { + log.warn([targetMid, null, message.type, 'Peer connection state is not in ' + + '"have-local-offer" state for re-negotiation. Dropping message.'], { + signalingState: pc.signalingState, + isRestart: !!message.restart + }); + return; } - pc.setRemoteDescription(new window.RTCSessionDescription(answer), function() { + + // Added checks if there is a current remote sessionDescription being processing before processing this one + if (pc.processingRemoteSDP) { + log.warn([targetMid, 'RTCSessionDescription', 'answer', + 'Dropping of setting local answer as there is another ' + + 'sessionDescription being processed ->'], answer); + return; + } + + pc.processingRemoteSDP = true; + + pc.setRemoteDescription(answer, function() { log.debug([targetMid, null, message.type, 'Remote description set']); pc.setAnswer = 'remote'; + pc.processingRemoteSDP = false; + self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ANSWER, targetMid); self._addIceCandidateFromQueue(targetMid); + }, function(error) { self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ERROR, targetMid, error); - log.error([targetMid, null, message.type, 'Failed setting remote description:'], error); - }); -}; -/** - * Sends Message object to either a targeted Peer or Broadcasts to all Peers connected in the Room. - * - Message is sent using the socket connection to the signaling server and relayed to - * the recipient(s). For direct messaging to a recipient refer to - * {{#crossLink "Skylink/sendP2PMessage:method"}}sendP2PMessage(){{/crossLink}}. - * @method sendMessage - * @param {String|JSON} message The message data to send. - * @param {String} [targetPeerId] PeerId of the peer to send a private - * message data to. If not specified then send to all peers. - * @example - * // Example 1: Send to all peers - * SkylinkDemo.sendMessage('Hi there!'); - * - * // Example 2: Send to a targeted peer - * SkylinkDemo.sendMessage('Hi there peer!', targetPeerId); - * @trigger incomingMessage - * @component Message - * @for Skylink - * @since 0.4.0 - */ -Skylink.prototype.sendMessage = function(message, targetPeerId) { - var params = { - cid: this._key, - data: message, - mid: this._user.sid, - rid: this._room.id, - type: this._SIG_MESSAGE_TYPE.PUBLIC_MESSAGE - }; - if (targetPeerId) { - params.target = targetPeerId; - params.type = this._SIG_MESSAGE_TYPE.PRIVATE_MESSAGE; - } - log.log([targetPeerId, null, null, - 'Sending message to peer' + ((targetPeerId) ? 's' : '')]); - this._sendChannelMessage(params); - this._trigger('incomingMessage', { - content: message, - isPrivate: (targetPeerId) ? true: false, - targetPeerId: targetPeerId || null, - isDataChannel: false, - senderPeerId: this._user.sid - }, this._user.sid, this.getPeerInfo(), true); + pc.processingRemoteSDP = false; + + log.error([targetMid, null, message.type, 'Failed setting remote description:'], { + error: error, + state: pc.signalingState + }); + }); }; Skylink.prototype.VIDEO_CODEC = { AUTO: 'auto', VP8: 'VP8', H264: 'H264' + //H264UC: 'H264UC' }; /** - * The list of available Audio Codecs. - * - Note that setting this audio codec does not mean that it will be - * the primary codec used for the call as it may vary based on the offerer's - * codec set. - * - The available audio codecs are: + * + * Note that if the audio codec is not supported, the SDK will not configure the local "offer" or + * "answer" session description to prefer the codec. + * + * The list of available audio codecs to set as the preferred audio codec to use to encode + * sending audio data when available encoded audio codec for Peer connections + * configured in the init() method. * @attribute AUDIO_CODEC - * @param {String} AUTO The default option. This means to use any audio codec given by generated sdp. - * @param {String} OPUS Use the OPUS audio codec. - * This is the common and mandantory audio codec used. This codec supports stereo. - * @param {String} ISAC Use the ISAC audio codec. - * This only works if the browser supports ISAC. This codec is mono based. + * @param {String} AUTO Value "auto" + * The value of the option to not prefer any audio codec but rather use the created + * local "offer" / "answer" session description audio codec preference. + * @param {String} OPUS Value "opus" + * The value of the option to prefer the OPUS audio codec. + * @param {String} ISAC Value "ISAC" + * The value of the option to prefer the ISAC audio codec. * @type JSON * @readOnly - * @component Stream * @for Skylink * @since 0.5.10 */ Skylink.prototype.AUDIO_CODEC = { AUTO: 'auto', ISAC: 'ISAC', - OPUS: 'opus' + OPUS: 'opus', + //ILBC: 'ILBC', + //G711: 'G711', + //G722: 'G722', + //SILK: 'SILK' }; /** - * Stores the preferred audio codec. - * @attribute _selectedAudioCodec - * @type String - * @default Skylink.AUDIO_CODEC.OPUS + * + * Note that currently getUserMedia() method only configures + * the maximum resolution of the Stream due to browser interopability and support. + * + * The list of + * video resolutions sets configured in the getUserMedia() method. + * @attribute VIDEO_RESOLUTION + * @param {JSON} QQVGA Value { width: 160, height: 120 } + * The value of the option to configure QQVGA resolution. + * Aspect ratio: 4:3 + * Note that configurating this resolution may not be supported depending on browser and device supports. + * @param {JSON} HQVGA Value { width: 240, height: 160 } + * The value of the option to configure HQVGA resolution. + * Aspect ratio: 3:2 + * Note that configurating this resolution may not be supported depending on browser and device supports. + * @param {JSON} QVGA Value { width: 320, height: 240 } + * The value of the option to configure QVGA resolution. + * Aspect ratio: 4:3 + * @param {JSON} WQVGA Value { width: 384, height: 240 } + * The value of the option to configure WQVGA resolution. + * Aspect ratio: 16:10 + * Note that configurating this resolution may not be supported depending on browser and device supports. + * @param {JSON} HVGA Value { width: 480, height: 320 } + * The value of the option to configure HVGA resolution. + * Aspect ratio: 3:2 + * Note that configurating this resolution may not be supported depending on browser and device supports. + * @param {JSON} VGA Value { width: 640, height: 480 } + * The value of the option to configure VGA resolution. + * Aspect ratio: 4:3 + * @param {JSON} WVGA Value { width: 768, height: 480 } + * The value of the option to configure WVGA resolution. + * Aspect ratio: 16:10 + * Note that configurating this resolution may not be supported depending on browser and device supports. + * @param {JSON} FWVGA Value { width: 854, height: 480 } + * The value of the option to configure FWVGA resolution. + * Aspect ratio: 16:9 + * Note that configurating this resolution may not be supported depending on browser and device supports. + * @param {JSON} SVGA Value { width: 800, height: 600 } + * The value of the option to configure SVGA resolution. + * Aspect ratio: 4:3 + * Note that configurating this resolution may not be supported depending on browser and device supports. + * @param {JSON} DVGA Value { width: 960, height: 640 } + * The value of the option to configure DVGA resolution. + * Aspect ratio: 3:2 + * Note that configurating this resolution may not be supported depending on browser and device supports. + * @param {JSON} WSVGA Value { width: 1024, height: 576 } + * The value of the option to configure WSVGA resolution. + * Aspect ratio: 16:9 + * @param {JSON} HD Value { width: 1280, height: 720 } + * The value of the option to configure HD resolution. + * Aspect ratio: 16:9 + * Note that configurating this resolution may not be supported depending on device supports. + * @param {JSON} HDPLUS Value { width: 1600, height: 900 } + * The value of the option to configure HDPLUS resolution. + * Aspect ratio: 16:9 + * Note that configurating this resolution may not be supported depending on browser and device supports. + * @param {JSON} FHD Value { width: 1920, height: 1080 } + * The value of the option to configure FHD resolution. + * Aspect ratio: 16:9 + * Note that configurating this resolution may not be supported depending on device supports. + * @param {JSON} QHD Value { width: 2560, height: 1440 } + * The value of the option to configure QHD resolution. + * Aspect ratio: 16:9 + * Note that configurating this resolution may not be supported depending on browser and device supports. + * @param {JSON} WQXGAPLUS Value { width: 3200, height: 1800 } + * The value of the option to configure WQXGAPLUS resolution. + * Aspect ratio: 16:9 + * Note that configurating this resolution may not be supported depending on browser and device supports. + * @param {JSON} UHD Value { width: 3840, height: 2160 } + * The value of the option to configure UHD resolution. + * Aspect ratio: 16:9 + * Note that configurating this resolution may not be supported depending on browser and device supports. + * @param {JSON} UHDPLUS Value { width: 5120, height: 2880 } + * The value of the option to configure UHDPLUS resolution. + * Aspect ratio: 16:9 + * Note that configurating this resolution may not be supported depending on browser and device supports. + * @param {JSON} FUHD Value { width: 7680, height: 4320 } + * The value of the option to configure FUHD resolution. + * Aspect ratio: 16:9 + * Note that configurating this resolution may not be supported depending on browser and device supports. + * @param {JSON} QUHD Value { width: 15360, height: 8640 } + * The value of the option to configure QUHD resolution. + * Aspect ratio: 16:9 + * Note that configurating this resolution may not be supported depending on browser and device supports. + * @type JSON + * @readOnly + * @for Skylink + * @since 0.5.6 + */ +Skylink.prototype.VIDEO_RESOLUTION = { + QQVGA: { width: 160, height: 120 /*, aspectRatio: '4:3'*/ }, + HQVGA: { width: 240, height: 160 /*, aspectRatio: '3:2'*/ }, + QVGA: { width: 320, height: 240 /*, aspectRatio: '4:3'*/ }, + WQVGA: { width: 384, height: 240 /*, aspectRatio: '16:10'*/ }, + HVGA: { width: 480, height: 320 /*, aspectRatio: '3:2'*/ }, + VGA: { width: 640, height: 480 /*, aspectRatio: '4:3'*/ }, + WVGA: { width: 768, height: 480 /*, aspectRatio: '16:10'*/ }, + FWVGA: { width: 854, height: 480 /*, aspectRatio: '16:9'*/ }, + SVGA: { width: 800, height: 600 /*, aspectRatio: '4:3'*/ }, + DVGA: { width: 960, height: 640 /*, aspectRatio: '3:2'*/ }, + WSVGA: { width: 1024, height: 576 /*, aspectRatio: '16:9'*/ }, + HD: { width: 1280, height: 720 /*, aspectRatio: '16:9'*/ }, + HDPLUS: { width: 1600, height: 900 /*, aspectRatio: '16:9'*/ }, + FHD: { width: 1920, height: 1080 /*, aspectRatio: '16:9'*/ }, + QHD: { width: 2560, height: 1440 /*, aspectRatio: '16:9'*/ }, + WQXGAPLUS: { width: 3200, height: 1800 /*, aspectRatio: '16:9'*/ }, + UHD: { width: 3840, height: 2160 /*, aspectRatio: '16:9'*/ }, + UHDPLUS: { width: 5120, height: 2880 /*, aspectRatio: '16:9'*/ }, + FUHD: { width: 7680, height: 4320 /*, aspectRatio: '16:9'*/ }, + QUHD: { width: 15360, height: 8640 /*, aspectRatio: '16:9'*/ } +}; + +/** + * The list of getUserMedia() method or + * shareScreen() method Stream fallback states. + * @attribute MEDIA_ACCESS_FALLBACK_STATE + * @param {JSON} FALLBACKING Value 0 + * The value of the state when getUserMedia() will retrieve audio track only + * when retrieving audio and video tracks failed. + * This can be configured by init() method + * audioFallback option. + * @param {JSON} FALLBACKED Value 1 + * The value of the state when getUserMedia() or shareScreen() + * retrieves camera / screensharing Stream successfully but with missing originally required audio or video tracks. + * @param {JSON} ERROR Value -1 + * The value of the state when getUserMedia() failed to retrieve audio track only + * after retrieving audio and video tracks failed. + * @readOnly + * @for Skylink + * @since 0.6.14 + */ +Skylink.prototype.MEDIA_ACCESS_FALLBACK_STATE = { + FALLBACKING: 0, + FALLBACKED: 1, + ERROR: -1 +}; + +/** + * Stores the flag that indicates if getUserMedia() should fallback to retrieve + * audio only Stream after retrieval of audio and video Stream had failed. + * @attribute _audioFallback + * @type Boolean + * @default false * @private - * @component Stream * @for Skylink - * @since 0.5.10 + * @since 0.5.4 */ -Skylink.prototype._selectedAudioCodec = 'auto'; +Skylink.prototype._audioFallback = false; /** - * Stores the preferred video codec. - * @attribute _selectedVideoCodec - * @type String - * @default Skylink.VIDEO_CODEC.VP8 + * Stores the Streams. + * @attribute _streams + * @type JSON * @private - * @component Stream * @for Skylink - * @since 0.5.10 + * @since 0.6.15 */ -Skylink.prototype._selectedVideoCodec = 'auto'; +Skylink.prototype._streams = { + userMedia: null, + screenshare: null +}; + +/** + * Stores the default camera Stream settings. + * @attribute _streamsDefaultSettings + * @type JSON + * @private + * @for Skylink + * @since 0.6.15 + */ +Skylink.prototype._streamsDefaultSettings = { + userMedia: { + audio: { + stereo: false + }, + video: { + resolution: { + width: 640, + height: 480 + }, + frameRate: 50 + } + }, + screenshare: { + video: true + } +}; + +/** + * Stores all the Stream required muted settings. + * @attribute _streamsMutedSettings + * @type JSON + * @private + * @for Skylink + * @since 0.6.15 + */ +Skylink.prototype._streamsMutedSettings = { + audioMuted: false, + videoMuted: false +}; + +/** + * Stores all the Stream sending maximum bandwidth settings. + * @attribute _streamsBandwidthSettings + * @type JSON + * @private + * @for Skylink + * @since 0.6.15 + */ +Skylink.prototype._streamsBandwidthSettings = {}; + +/** + * Stores all the Stream stopped callbacks. + * @attribute _streamsStoppedCbs + * @type JSON + * @private + * @for Skylink + * @since 0.6.15 + */ +Skylink.prototype._streamsStoppedCbs = {}; + +/** + * Function that retrieves camera Stream. + * @method getUserMedia + * @param {JSON} [options] The camera Stream configuration options. + * - When not provided, the value is set to { audio: true, video: true }. + * To fallback to retrieve audio track only when retrieving of audio and video tracks failed, + * enable the audioFallback flag in the init() method. + * @param {Boolean} [options.useExactConstraints=false] + * Note that by enabling this flag, exact values will be requested when retrieving camera Stream, + * but it does not prevent constraints related errors. By default when not enabled, + * expected mandatory maximum values (or optional values for source ID) will requested to prevent constraints related + * errors, with an exception for options.video.frameRate option in Safari and IE (plugin-enabled) browsers, + * where the expected maximum value will not be requested due to the lack of support. + * The flag if getUserMedia() should request for camera Stream to match exact requested values of + * options.audio.deviceId and options.video.deviceId, options.video.resolution + * and options.video.frameRate when provided. + * @param {Boolean|JSON} [options.audio=false] The audio configuration options. + * @param {Boolean} [options.audio.stereo=false] The flag if stereo band should be configured + * when encoding audio codec is OPUS for sending audio data. + * @param {Boolean} [options.audio.mute=false] The flag if audio tracks should be muted upon receiving them. + * Providing the value as false does nothing to peerInfo.mediaStatus.audioMuted, + * but when provided as true, this sets the peerInfo.mediaStatus.audioMuted value to + * true and mutes any existing + * shareScreen() Stream audio tracks as well. + * @param {Array} [options.audio.optional] + * Note that this may result in constraints related error when options.useExactConstraints value is + * true. If you are looking to set the requested source ID of the audio track, + * use options.audio.deviceId instead. + * The navigator.getUserMedia() API audio: { optional [..] } property. + * @param {String} [options.audio.deviceId] + * Note this is currently not supported in Firefox browsers. + * The audio track source ID of the device to use. + * The list of available audio source ID can be retrieved by the navigator.mediaDevices.enumerateDevices + * API. + * @param {Boolean|JSON} [options.video=false] The video configuration options. + * @param {Boolean} [options.video.mute=false] The flag if video tracks should be muted upon receiving them. + * Providing the value as false does nothing to peerInfo.mediaStatus.videoMuted, + * but when provided as true, this sets the peerInfo.mediaStatus.videoMuted value to + * true and mutes any existing + * shareScreen() Stream video tracks as well. + * @param {JSON} [options.video.resolution] The video resolution. + * By default, VGA resolution option + * is selected when not provided. + * [Rel: Skylink.VIDEO_RESOLUTION] + * @param {Number} [options.video.resolution.width] The video resolution width. + * @param {Number} [options.video.resolution.height] The video resolution height. + * @param {Number} [options.video.frameRate] The video + * frameRate per second (fps). + * @param {Array} [options.video.optional] + * Note that this may result in constraints related error when options.useExactConstraints value is + * true. If you are looking to set the requested source ID of the video track, + * use options.video.deviceId instead. + * The navigator.getUserMedia() API video: { optional [..] } property. + * @param {String} [options.video.deviceId] + * Note this is currently not supported in Firefox browsers. + * The video track source ID of the device to use. + * The list of available video source ID can be retrieved by the navigator.mediaDevices.enumerateDevices + * API. + * @param {Function} [callback] The callback function fired when request has completed. + * Function parameters signature is function (error, success) + * Function request completion is determined by the + * mediaAccessSuccess event triggering isScreensharing parameter + * payload value as false for request success. + * @param {Error|String} callback.error The error result in request. + * Defined as null when there are no errors in request + * Object signature is the getUserMedia() error when retrieving camera Stream. + * @param {MediaStream} callback.success The success result in request. + * Defined as null when there are errors in request + * Object signature is the camera Stream object. + * @example + * // Example 1: Get both audio and video. + * skylinkDemo.getUserMedia(function (error, success) { + * if (error) return; + * attachMediaStream(document.getElementById("my-video"), success); + * }); + * + * // Example 2: Get only audio. + * skylinkDemo.getUserMedia({ + * audio: true + * }, function (error, success) { + * if (error) return; + * attachMediaStream(document.getElementById("my-audio"), success); + * }); + * + * // Example 3: Configure resolution for video + * skylinkDemo.getUserMedia({ + * audio: true, + * video: { + * resolution: skylinkDemo.VIDEO_RESOLUTION.HD + * } + * }, function (error, success) { + * if (error) return; + * attachMediaStream(document.getElementById("my-video"), success); + * }); + * + * // Example 4: Configure stereo flag for OPUS codec audio (OPUS is always used by default) + * skylinkDemo.init({ + * appKey: "xxxxxx", + * audioCodec: skylinkDemo.AUDIO_CODEC.OPUS + * }, function (initErr, initSuccess) { + * skylinkDemo.getUserMedia({ + * audio: { + * stereo: true + * }, + * video: true + * }, function (error, success) { + * if (error) return; + * attachMediaStream(document.getElementById("my-video"), success); + * }); + * }); + * + * // Example 5: Configure frameRate for video + * skylinkDemo.getUserMedia({ + * audio: true, + * video: { + * frameRate: 50 + * } + * }, function (error, success) { + * if (error) return; + * attachMediaStream(document.getElementById("my-video"), success); + * }); + * + * // Example 6: Configure video and audio based on selected sources. Does not work for Firefox currently. + * var sources = { audio: [], video: [] }; + * + * function selectStream (audioSourceId, videoSourceId) { + * if (window.webrtcDetectedBrowser === 'firefox') { + * console.warn("Currently this feature is not supported by Firefox browsers!"); + * return; + * } + * skylinkDemo.getUserMedia({ + * audio: { + * optional: [{ sourceId: audioSourceId }] + * }, + * video: { + * optional: [{ sourceId: videoSourceId }] + * } + * }, function (error, success) { + * if (error) return; + * attachMediaStream(document.getElementById("my-video"), success); + * }); + * } + * + * navigator.mediaDevices.enumerateDevices().then(function(devices) { + * var selectedAudioSourceId = ""; + * var selectedVideoSourceId = ""; + * devices.forEach(function(device) { + * console.log(device.kind + ": " + device.label + " source ID = " + device.deviceId); + * if (device.kind === "audio") { + * selectedAudioSourceId = device.deviceId; + * } else { + * selectedVideoSourceId = device.deviceId; + * } + * }); + * selectStream(selectedAudioSourceId, selectedVideoSourceId); + * }).catch(function (error) { + * console.error("Failed", error); + * }); + * @trigger + * If options.audio value is false and options.video + * value is false: ABORT and return error. + * Retrieve camera Stream. If retrieval was succesful: + * If there is any previous getUserMedia() Stream: + * Invokes stopStream() method. + * If there are missing audio or video tracks requested: + * mediaAccessFallback event triggers parameter payload + * state as FALLBACKED, isScreensharing value as false and + * isAudioFallback value as false. + * Mutes / Unmutes audio and video tracks based on current muted settings in peerInfo.mediaStatus. + * This can be retrieved with getPeerInfo() method. + * mediaAccessSuccess event triggers parameter payload + * isScreensharing value as false and isAudioFallback + * value as false.Else: + * If options.audioFallback is enabled in the init() method, + * options.audio value is true and options.video value is true: + * mediaAccessFallback event event triggers + * parameter payload state as FALLBACKING, isScreensharing + * value as false and isAudioFallback value as true. + * Retrieve camera Stream with audio tracks only. If retrieval was successful: + * If there is any previous getUserMedia() Stream: + * Invokes stopStream() method. + * mediaAccessFallback event event triggers + * parameter payload state as FALLBACKED, isScreensharing + * value as false and isAudioFallback value as true. + * Mutes / Unmutes audio and video tracks based on current muted settings in peerInfo.mediaStatus. + * This can be retrieved with getPeerInfo() method. + * mediaAccessSuccess event triggers + * parameter payload isScreensharing value as false and + * isAudioFallback value as true.Else: + * mediaAccessError event triggers + * parameter payload isScreensharing value as false and + * isAudioFallbackError value as true. + * mediaAccessFallback event event triggers + * parameter payload state as ERROR, isScreensharing value as + * false and isAudioFallback value as true. + * ABORT and return error.Else: + * mediaAccessError event triggers parameter payload + * isScreensharing value as false and isAudioFallbackError value as + * false.ABORT and return error. + * @for Skylink + * @since 0.5.6 + */ +Skylink.prototype.getUserMedia = function(options,callback) { + var self = this; + + if (typeof options === 'function'){ + callback = options; + options = { + audio: true, + video: true + }; + + } else if (typeof options !== 'object' || options === null) { + if (typeof options === 'undefined') { + options = { + audio: true, + video: true + }; + + } else { + var invalidOptionsError = 'Please provide a valid options'; + log.error(invalidOptionsError, options); + if (typeof callback === 'function') { + callback(new Error(invalidOptionsError), null); + } + return; + } + + } else if (!options.audio && !options.video) { + var noConstraintOptionsSelectedError = 'Please select audio or video'; + log.error(noConstraintOptionsSelectedError, options); + if (typeof callback === 'function') { + callback(new Error(noConstraintOptionsSelectedError), null); + } + return; + } + + /*if (window.location.protocol !== 'https:' && window.webrtcDetectedBrowser === 'chrome' && + window.webrtcDetectedVersion > 46) { + errorMsg = 'getUserMedia() has to be called in https:// application'; + log.error(errorMsg, options); + if (typeof callback === 'function') { + callback(new Error(errorMsg), null); + } + return; + }*/ + if (typeof callback === 'function') { + var mediaAccessSuccessFn = function (stream) { + self.off('mediaAccessError', mediaAccessErrorFn); + callback(null, stream); + }; + var mediaAccessErrorFn = function (error) { + self.off('mediaAccessSuccess', mediaAccessSuccessFn); + callback(error, null); + }; -/** - * The list of recommended video resolutions. - * - Note that the higher the resolution, the connectivity speed might - * be affected. - * - The available video resolutions type are: (See - * {{#crossLink "Skylink/joinRoom:method"}}joinRoom(){{/crossLink}} - * for more information) - * @param {JSON} QQVGA QQVGA resolution. - * @param {Number} QQVGA.width 160 - * @param {Number} QQVGA.height 120 - * @param {String} QQVGA.aspectRatio 4:3 - * @param {JSON} HQVGA HQVGA resolution. - * @param {Number} HQVGA.width 240 - * @param {Number} HQVGA.height 160 - * @param {String} HQVGA.aspectRatio 3:2 - * @param {JSON} QVGA QVGA resolution. - * @param {Number} QVGA.width 320 - * @param {Number} QVGA.height 240 - * @param {String} QVGA.aspectRatio 4:3 - * @param {JSON} WQVGA WQVGA resolution. - * @param {Number} WQVGA.width 384 - * @param {Number} WQVGA.height 240 - * @param {String} WQVGA.aspectRatio 16:10 - * @param {JSON} HVGA HVGA resolution. - * @param {Number} HVGA.width 480 - * @param {Number} HVGA.height 320 - * @param {String} HVGA.aspectRatio 3:2 - * @param {JSON} VGA VGA resolution. - * @param {Number} VGA.width 640 - * @param {Number} VGA.height 480 - * @param {String} VGA.aspectRatio 4:3 - * @param {JSON} WVGA WVGA resolution. - * @param {Number} WVGA.width 768 - * @param {Number} WVGA.height 480 - * @param {String} WVGA.aspectRatio 16:10 - * @param {JSON} FWVGA FWVGA resolution. - * @param {Number} FWVGA.width 854 - * @param {Number} FWVGA.height 480 - * @param {String} FWVGA.aspectRatio 16:9 - * @param {JSON} SVGA SVGA resolution. - * @param {Number} SVGA.width 800 - * @param {Number} SVGA.height 600 - * @param {String} SVGA.aspectRatio 4:3 - * @param {JSON} DVGA DVGA resolution. - * @param {Number} DVGA.width 960 - * @param {Number} DVGA.height 640 - * @param {String} DVGA.aspectRatio 3:2 - * @param {JSON} WSVGA WSVGA resolution. - * @param {Number} WSVGA.width 1024 - * @param {Number} WSVGA.height 576 - * @param {String} WSVGA.aspectRatio 16:9 - * @param {JSON} HD HD resolution. - * @param {Number} HD.width 1280 - * @param {Number} HD.height 720 - * @param {String} HD.aspectRatio 16:9 - * @param {JSON} HDPLUS HDPLUS resolution. - * @param {Number} HDPLUS.width 1600 - * @param {Number} HDPLUS.height 900 - * @param {String} HDPLUS.aspectRatio 16:9 - * @param {JSON} FHD FHD resolution. - * @param {Number} FHD.width 1920 - * @param {Number} FHD.height 1080 - * @param {String} FHD.aspectRatio 16:9 - * @param {JSON} QHD QHD resolution. - * @param {Number} QHD.width 2560 - * @param {Number} QHD.height 1440 - * @param {String} QHD.aspectRatio 16:9 - * @param {JSON} WQXGAPLUS WQXGAPLUS resolution. - * @param {Number} WQXGAPLUS.width 3200 - * @param {Number} WQXGAPLUS.height 1800 - * @param {String} WQXGAPLUS.aspectRatio 16:9 - * @param {JSON} UHD UHD resolution. - * @param {Number} UHD.width 3840 - * @param {Number} UHD.height 2160 - * @param {String} UHD.aspectRatio 16:9 - * @param {JSON} UHDPLUS UHDPLUS resolution. - * @param {Number} UHDPLUS.width 5120 - * @param {Number} UHDPLUS.height 2880 - * @param {String} UHDPLUS.aspectRatio 16:9 - * @param {JSON} FUHD FUHD resolution. - * @param {Number} FUHD.width 7680 - * @param {Number} FUHD.height 4320 - * @param {String} FUHD.aspectRatio 16:9 - * @param {JSON} QUHD resolution. - * @param {Number} QUHD.width 15360 - * @param {Number} QUHD.height 8640 - * @param {String} QUHD.aspectRatio 16:9 - * @attribute VIDEO_RESOLUTION - * @type JSON - * @readOnly - * @component Stream - * @for Skylink - * @since 0.5.6 - */ -Skylink.prototype.VIDEO_RESOLUTION = { - QQVGA: { width: 160, height: 120, aspectRatio: '4:3' }, - HQVGA: { width: 240, height: 160, aspectRatio: '3:2' }, - QVGA: { width: 320, height: 240, aspectRatio: '4:3' }, - WQVGA: { width: 384, height: 240, aspectRatio: '16:10' }, - HVGA: { width: 480, height: 320, aspectRatio: '3:2' }, - VGA: { width: 640, height: 480, aspectRatio: '4:3' }, - WVGA: { width: 768, height: 480, aspectRatio: '16:10' }, - FWVGA: { width: 854, height: 480, aspectRatio: '16:9' }, - SVGA: { width: 800, height: 600, aspectRatio: '4:3' }, - DVGA: { width: 960, height: 640, aspectRatio: '3:2' }, - WSVGA: { width: 1024, height: 576, aspectRatio: '16:9' }, - HD: { width: 1280, height: 720, aspectRatio: '16:9' }, - HDPLUS: { width: 1600, height: 900, aspectRatio: '16:9' }, - FHD: { width: 1920, height: 1080, aspectRatio: '16:9' }, - QHD: { width: 2560, height: 1440, aspectRatio: '16:9' }, - WQXGAPLUS: { width: 3200, height: 1800, aspectRatio: '16:9' }, - UHD: { width: 3840, height: 2160, aspectRatio: '16:9' }, - UHDPLUS: { width: 5120, height: 2880, aspectRatio: '16:9' }, - FUHD: { width: 7680, height: 4320, aspectRatio: '16:9' }, - QUHD: { width: 15360, height: 8640, aspectRatio: '16:9' } -}; - -/** - * The list of local media streams. - * @attribute _mediaStream - * @type Object - * @private - * @component Stream - * @for Skylink - * @since 0.5.6 - */ -Skylink.prototype._mediaStream = null; + self.once('mediaAccessSuccess', mediaAccessSuccessFn, function (stream, isScreensharing) { + return !isScreensharing; + }); -/** - * Stores the local MediaStream for screensharing. - * @attribute _mediaScreen - * @type Object - * @private - * @component Stream - * @for Skylink - * @since 0.5.11 - */ -Skylink.prototype._mediaScreen = null; + self.once('mediaAccessError', mediaAccessErrorFn, function (error, isScreensharing) { + return !isScreensharing; + }); + } -/** - * Stores the local MediaStream clone for audio screensharing. - * @attribute _mediaScreenClone - * @type Object - * @private - * @component Stream - * @for Skylink - * @since 0.5.11 - */ -Skylink.prototype._mediaScreenClone = null; + // Parse stream settings + var settings = self._parseStreamSettings(options); -/** - * The user stream settings. - * @attribute _defaultStreamSettings - * @type JSON - * @param {Boolean|JSON} [audio] If user enables audio, this is the default setting. - * @param {Boolean} [audio.stereo] Enabled stereo or not - * @param {Boolean|JSON} [video] If user enables video, this is the default setting. - * @param {JSON} [video.resolution] [Rel: Skylink.VIDEO_RESOLUTION] - * @param {Number} [video.resolution.width] Video width - * @param {Number} [video.resolution.height] Video height - * @param {Number} [video.frameRate] Maximum frameRate of Video - * @param {String} bandwidth Bandwidth settings. - * @param {String} bandwidth.audio Audio default Bandwidth - * @param {String} bandwidth.video Video default Bandwidth - * @param {String} bandwidth.data Data default Bandwidth. - * @private - * @component Stream - * @for Skylink - * @since 0.5.7 - */ -Skylink.prototype._defaultStreamSettings = { - audio: { - stereo: false - }, - video: { - resolution: { - width: 640, - height: 480 - }, - frameRate: 50 - }, - bandwidth: { - audio: 50, - video: 256, - data: 1638400 - } -}; + navigator.getUserMedia(settings.getUserMediaSettings, function (stream) { + if (settings.mutedSettings.shouldAudioMuted) { + self._streamsMutedSettings.audioMuted = true; + } -/** - * The user stream settings. - * @attribute _streamSettings - * @type JSON - * @param {Boolean|JSON} [audio=false] This call requires audio - * @param {Boolean} [audio.stereo] Enabled stereo or not - * @param {Boolean|JSON} [video=false] This call requires video - * @param {JSON} [video.resolution] [Rel: Skylink.VIDEO_RESOLUTION] - * @param {Number} [video.resolution.width] Video width - * @param {Number} [video.resolution.height] Video height - * @param {Number} [video.frameRate] Maximum frameRate of Video - * @param {String} [bandwidth] Bandwidth settings - * @param {String} [bandwidth.audio] Audio Bandwidth - * @param {String} [bandwidth.video] Video Bandwidth - * @param {String} [bandwidth.data] Data Bandwidth. - * @private - * @component Stream - * @for Skylink - * @since 0.5.6 - */ -Skylink.prototype._streamSettings = {}; + if (settings.mutedSettings.shouldVideoMuted) { + self._streamsMutedSettings.videoMuted = true; + } -/** - * The getUserMedia settings parsed from - * {{#crossLink "Skylink/_streamSettings:attr"}}_streamSettings{{/crossLink}}. - * @attribute _getUserMediaSettings - * @type JSON - * @param {Boolean|JSON} [audio=false] This call requires audio. - * @param {Boolean|JSON} [video=false] This call requires video. - * @param {Number} [video.mandatory.maxHeight] Video maximum width. - * @param {Number} [video.mandatory.maxWidth] Video maximum height. - * @param {Number} [video.mandatory.maxFrameRate] Maximum frameRate of Video. - * @param {Array} [video.optional] The getUserMedia options. - * @private - * @component Stream - * @for Skylink - * @since 0.5.6 - */ -Skylink.prototype._getUserMediaSettings = {}; + self._onStreamAccessSuccess(stream, settings, false, false); -/** - * The user MediaStream(s) status. - * @attribute _mediaStreamsStatus - * @type JSON - * @param {Boolean} [audioMuted=true] Is user's audio muted. - * @param {Boolean} [videoMuted=true] Is user's vide muted. - * @private - * @component Stream - * @for Skylink - * @since 0.5.6 - */ -Skylink.prototype._mediaStreamsStatus = {}; + }, function (error) { + self._onStreamAccessError(error, settings, false, false); + }); +}; /** - * Fallback to audio call if audio and video is required. - * @attribute _audioFallback - * @type Boolean - * @default false - * @private - * @required - * @component Stream + * + * Note that if shareScreen() Stream is available despite having + * getUserMedia() Stream available, the + * shareScreen() Stream is sent instead of the + * getUserMedia() Stream to Peers. + * + * Function that sends a new getUserMedia() Stream + * to all connected Peers in the Room. + * @method sendStream + * @param {JSON|MediaStream} options The getUserMedia() + * method options parameter settings. + * - When provided as a MediaStream object, this configures the options.audio and + * options.video based on the tracks available in the MediaStream object, + * and configures the options.audio.mute and options.video.mute based on the tracks + * .enabled flags in the tracks provided in the MediaStream object without + * invoking getUserMedia() method. + * Object signature matches the options parameter in the + * getUserMedia() method. + * @param {Function} [callback] The callback function fired when request has completed. + * Function parameters signature is function (error, success) + * Function request completion is determined by the + * mediaAccessSuccess event triggering isScreensharing parameter payload value + * as false for request success when User is in Room without Peers, + * or by the peerRestart event triggering + * isSelfInitiateRestart parameter payload value as true for all connected Peers + * for request success when User is in Room with Peers. + * @param {Error|String} callback.error The error result in request. + * Defined as null when there are no errors in request + * Object signature is the getUserMedia() method error or + * when invalid options is provided. + * @param {MediaStream} callback.success The success result in request. + * Defined as null when there are errors in request + * Object signature is the getUserMedia() method + * Stream object. + * @example + * // Example 1: Send MediaStream object + * function retrieveStreamBySourceForFirefox (sourceId) { + * navigator.mediaDevices.getUserMedia({ + * audio: true, + * video: { + * sourceId: { exact: sourceId } + * } + * }).then(function (stream) { + * skylinkDemo.sendStream(stream, function (error, success) { + * if (err) return; + * if (stream === success) { + * console.info("Same MediaStream has been sent"); + * } + * console.log("Stream is now being sent to Peers"); + * attachMediaStream(document.getElementById("my-video"), success); + * }); + * }); + * } + * + * // Example 2: Send video later + * var inRoom = false; + * + * function sendVideo () { + * if (!inRoom) return; + * skylinkDemo.sendStream({ + * audio: true, + * video: true + * }, function (error, success) { + * if (error) return; + * console.log("getUserMedia() Stream with video is now being sent to Peers"); + * attachMediaStream(document.getElementById("my-video"), success); + * }); + * } + * + * skylinkDemo.joinRoom({ + * audio: true + * }, function (jRError, jRSuccess) { + * if (jRError) return; + * inRoom = true; + * }); + * @trigger + * If User is not in Room: ABORT and return error. + * Checks options provided. If provided parameter options is not valid: + * ABORT and return error. + * Else if provided parameter options is a Stream object: + * Checks if there is any audio or video tracks. If there is no tracks: + * ABORT and return error.Else: + * Set options.audio value as true if Stream has audio tracks. + * Set options.video value as false if Stream has video tracks. + * Mutes / Unmutes audio and video tracks based on current muted settings in + * peerInfo.mediaStatus. This can be retrieved with + * getPeerInfo() method. + * If there is any previous getUserMedia() Stream: + * Invokes stopStream() method to stop previous Stream. + * mediaAccessSuccess event triggers + * parameter payload isScreensharing value as false and isAudioFallback + * value as false.Else: + * Invoke getUserMedia() method with + * options provided in sendStream(). If request has errors: + * ABORT and return error. + * If there is currently no shareScreen() Stream: + * incomingStream event triggers parameter payload + * isSelf value as true and stream as + * getUserMedia() Stream. + * peerUpdated event triggers parameter payload + * isSelf value as true. + * Checks if MCU is enabled for App Key provided in init() method. + * If MCU is enabled: Invoke refreshConnection() + * method. If request has errors: ABORT and return error. + * Else: If there are connected Peers in the Room: + * Invoke refreshConnection() method. + * If request has errors: ABORT and return error. + * * @for Skylink - * @since 0.5.4 + * @since 0.5.6 */ -Skylink.prototype._audioFallback = false; -/** - * Access to user's MediaStream is successful. - * @method _onUserMediaSuccess - * @param {MediaStream} stream MediaStream object. - * @param {Boolean} [isScreenSharing=false] The flag that indicates - * if stream is a screensharing stream. - * @trigger mediaAccessSuccess - * @private - * @component Stream - * @for Skylink - * @since 0.3.0 - */ -Skylink.prototype._onUserMediaSuccess = function(stream, isScreenSharing) { +Skylink.prototype.sendStream = function(options, callback) { var self = this; - log.log([null, 'MediaStream', stream.id, - 'User has granted access to local media'], stream); - self._trigger('mediaAccessSuccess', stream, !!isScreenSharing); - - var streamEnded = function () { - self._sendChannelMessage({ - type: self._SIG_MESSAGE_TYPE.STREAM, - mid: self._user.sid, - rid: self._room.id, - cid: self._key, - status: 'ended' - }); - self._trigger('streamEnded', self._user.sid, self.getPeerInfo(), true); - }; - stream.onended = streamEnded; - // Workaround for local stream.onended because firefox has not yet implemented it - if (window.webrtcDetectedBrowser === 'firefox') { - stream.onended = setInterval(function () { - if (typeof stream.recordedTime === 'undefined') { - stream.recordedTime = 0; + var restartFn = function (stream) { + if (self._inRoom) { + if (!self._streams.screenshare) { + self._trigger('incomingStream', self._user.sid, stream, true, self.getPeerInfo()); + self._trigger('peerUpdated', self._user.sid, self.getPeerInfo(), true); } - if (stream.recordedTime === stream.currentTime) { - clearInterval(stream.onended); - // trigger that it has ended - streamEnded(); - - } else { - stream.recordedTime = stream.currentTime; + if (Object.keys(self._peerConnections).length > 0 || self._hasMCU) { + self._refreshPeerConnection(Object.keys(self._peerConnections), false, function (err, success) { + if (err) { + log.error('Failed refreshing connections for sendStream() ->', err); + if (typeof callback === 'function') { + callback(new Error('Failed refreshing connections.'), null); + } + return; + } + if (typeof callback === 'function') { + callback(null, stream); + } + }); + } else if (typeof callback === 'function') { + callback(null, stream); + } + } else { + var notInRoomAgainError = 'Unable to send stream as user is not in the Room.'; + log.error(notInRoomAgainError, stream); + if (typeof callback === 'function') { + callback(new Error(notInRoomAgainError), null); } + } + }; - }, 1000); + if (typeof options !== 'object' || options === null) { + var invalidOptionsError = 'Provided stream settings is invalid'; + log.error(invalidOptionsError, options); + if (typeof callback === 'function'){ + callback(new Error(invalidOptionsError),null); + } + return; } - // check if readyStateChange is done - self._condition('readyStateChange', function () { - if (!isScreenSharing) { - self._mediaStream = stream; - } else { - self._mediaScreen = stream; + if (!self._inRoom) { + var notInRoomError = 'Unable to send stream as user is not in the Room.'; + log.error(notInRoomError, options); + if (typeof callback === 'function'){ + callback(new Error(notInRoomError),null); } + return; + } - self._muteLocalMediaStreams(); + if (typeof options.getAudioTracks === 'function' || typeof options.getVideoTracks === 'function') { + var checkActiveTracksFn = function (tracks) { + for (var t = 0; t < tracks.length; t++) { + if (!(tracks[t].ended || (typeof tracks[t].readyState === 'string' ? + tracks[t].readyState !== 'live' : false))) { + return true; + } + } + return false; + }; - // check if users is in the room already - self._condition('peerJoined', function () { - self._trigger('incomingStream', self._user.sid, stream, true, - self.getPeerInfo(), !!isScreenSharing); - }, function () { - return self._inRoom; - }, function (peerId, peerInfo, isSelf) { - return isSelf; + if (!checkActiveTracksFn( options.getAudioTracks() ) && !checkActiveTracksFn( options.getVideoTracks() )) { + var invalidStreamError = 'Provided stream object does not have audio or video tracks.'; + log.error(invalidStreamError, options); + if (typeof callback === 'function'){ + callback(new Error(invalidStreamError),null); + } + return; + } + + self._onStreamAccessSuccess(options, { + settings: { + audio: true, + video: true + }, + getUserMediaSettings: { + audio: true, + video: true + } + }, false, false); + + restartFn(options); + + } else { + self.getUserMedia(options, function (err, stream) { + if (err) { + if (typeof callback === 'function') { + callback(err, null); + } + return; + } + restartFn(stream); }); - }, function () { - return self._readyState === self.READY_STATE_CHANGE.COMPLETED; - }, function (state) { - return state === self.READY_STATE_CHANGE.COMPLETED; - }); + } }; /** - * Access to user's MediaStream failed. - * @method _onUserMediaError - * @param {Object} error Error object that was thrown. - * @param {Boolean} [isScreenSharing=false] The flag that indicates - * if stream is a screensharing stream. - * @trigger mediaAccessError - * @private - * @component Stream + * + * Note that broadcasted events from muteStream() method, + * stopStream() method, + * stopScreen() method, + * sendMessage() method, + * unlockRoom() method and + * lockRoom() method may be queued when + * sent within less than an interval. + * + * Function that stops getUserMedia() Stream. + * @method stopStream + * @example + * function stopStream () { + * skylinkDemo.stopStream(); + * } + * + * skylinkDemo.getUserMedia(); + * @trigger + * Checks if there is getUserMedia() Stream. + * If there is getUserMedia() Stream: + * Stop getUserMedia() Stream Stream. + * mediaAccessStopped event triggers + * parameter payload isScreensharing value as false.If User is in Room: + * streamEnded event triggers parameter + * payload isSelf value as true and isScreensharing value asfalse + * .peerUpdated event triggers parameter payload + * isSelf value as true. * @for Skylink - * @since 0.5.4 + * @since 0.5.6 */ -Skylink.prototype._onUserMediaError = function(error, isScreenSharing) { - var self = this; - if (self._audioFallback && self._streamSettings.video && !isScreenSharing) { - // redefined the settings for video as false - self._streamSettings.video = false; - - log.debug([null, 'MediaStream', null, 'Falling back to audio stream call']); - window.getUserMedia({ - audio: true - }, function(stream) { - self._onUserMediaSuccess(stream); - }, function(error) { - log.error([null, 'MediaStream', null, - 'Failed retrieving audio in audio fallback:'], error); - self._trigger('mediaAccessError', error); +Skylink.prototype.stopStream = function () { + if (this._streams.userMedia) { + this._stopStreams({ + userMedia: true }); - this.getUserMedia({ audio: true }); - } else { - log.error([null, 'MediaStream', null, 'Failed retrieving stream:'], error); - self._trigger('mediaAccessError', error, !!isScreenSharing); } }; /** - * The remote peer advertised streams, that we are forwarding to the app. This is part - * of the peerConnection's addRemoteDescription() API's callback. - * @method _onRemoteStreamAdded - * @param {String} targetMid PeerId of the peer that has remote stream to send. - * @param {Event} event This is provided directly by the peerconnection API. - * @param {Boolean} [isScreenSharing=false] The flag that indicates - * if stream is a screensharing stream. - * @trigger incomingStream - * @private - * @component Stream + * + * Note that broadcasted events from muteStream() method, + * stopStream() method, + * stopScreen() method, + * sendMessage() method, + * unlockRoom() method and + * lockRoom() method may be queued when + * sent within less than an interval. + * + * Function that mutes both getUserMedia() Stream and + * shareScreen() Stream audio or video tracks. + * @method muteStream + * @param {JSON} options The Streams muting options. + * @param {Boolean} [options.audioMuted=true] The flag if all Streams audio + * tracks should be muted or not. + * @param {Boolean} [options.videoMuted=true] The flag if all Streams video + * tracks should be muted or not. + * @example + * // Example 1: Mute both audio and video tracks in all Streams + * skylinkDemo.muteStream({ + * audioMuted: true, + * videoMuted: true + * }); + * + * // Example 2: Mute only audio tracks in all Streams + * skylinkDemo.muteStream({ + * audioMuted: true, + * videoMuted: false + * }); + * + * // Example 3: Mute only video tracks in all Streams + * skylinkDemo.muteStream({ + * audioMuted: false, + * videoMuted: true + * }); + * @trigger + * If provided parameter options is invalid: ABORT and return error. + * Checks if there is any available Streams: If there is no available Streams: + * ABORT and return error.If User is in Room: + * Checks if there is audio tracks to mute / unmute: If there is audio tracks to mute / unmute: + * If options.audioMuted value is not the same as the current + * peerInfo.mediaStatus.audioMuted: This can be retrieved with + * getPeerInfo() method. + * For Peer only peerUpdated event + * triggers with parameter payload isSelf value as false. + * For Peer only streamMuted event + * triggers with parameter payload isSelf value as false. + * Checks if there is video tracks to mute / unmute: If there is video tracks to mute / unmute: + * If options.videoMuted value is not the same as the current + * peerInfo.mediaStatus.videoMuted: This can be retrieved with + * getPeerInfo() method. + * For Peer only peerUpdated event + * triggers with parameter payload isSelf value as false. + * For Peer only streamMuted event triggers with + * parameter payload isSelf value as false. + * If options.audioMuted value is not the same as the current + * peerInfo.mediaStatus.audioMuted or options.videoMuted value is not + * the same as the current peerInfo.mediaStatus.videoMuted: + * localMediaMuted event triggers. + * If User is in Room: streamMuted event + * triggers with parameter payload isSelf value as true. + * peerUpdated event triggers with + * parameter payload isSelf value as true. * @for Skylink - * @since 0.5.2 + * @since 0.5.7 */ -Skylink.prototype._onRemoteStreamAdded = function(targetMid, event, isScreenSharing) { +Skylink.prototype.muteStream = function(options) { var self = this; - if(targetMid !== 'MCU') { - if (!self._peerInformations[targetMid]) { - log.error([targetMid, 'MediaStream', event.stream.id, - 'Received remote stream when peer is not connected. ' + - 'Ignoring stream ->'], event.stream); - return; - } + if (typeof options !== 'object') { + log.error('Provided settings is not an object'); + return; + } - if (!self._peerInformations[targetMid].settings.audio && - !self._peerInformations[targetMid].settings.video && !isScreenSharing) { - log.log([targetMid, 'MediaStream', event.stream.id, - 'Receive remote stream but ignoring stream as it is empty ->' - ], event.stream); - return; + if (!(self._streams.userMedia && self._streams.userMedia.stream) && + !(self._streams.screenshare && self._streams.screenshare.stream)) { + log.warn('No streams are available to mute / unmute!'); + return; + } + + var audioMuted = typeof options.audioMuted === 'boolean' ? options.audioMuted : true; + var videoMuted = typeof options.videoMuted === 'boolean' ? options.videoMuted : true; + var hasToggledAudio = false; + var hasToggledVideo = false; + + if (self._streamsMutedSettings.audioMuted !== audioMuted) { + self._streamsMutedSettings.audioMuted = audioMuted; + hasToggledAudio = true; + } + + if (self._streamsMutedSettings.videoMuted !== videoMuted) { + self._streamsMutedSettings.videoMuted = videoMuted; + hasToggledVideo = true; + } + + if (hasToggledVideo || hasToggledAudio) { + var streamTracksAvailability = self._muteStreams(); + + if (hasToggledVideo && self._inRoom) { + self._sendChannelMessage({ + type: self._SIG_MESSAGE_TYPE.MUTE_VIDEO, + mid: self._user.sid, + rid: self._room.id, + muted: self._streamsMutedSettings.videoMuted, + stamp: (new Date()).getTime() + }); } - log.log([targetMid, 'MediaStream', event.stream.id, - 'Received remote stream ->'], event.stream); - if (isScreenSharing) { - log.log([targetMid, 'MediaStream', event.stream.id, - 'Peer is having a screensharing session with user']); + if (hasToggledAudio && self._inRoom) { + setTimeout(function () { + self._sendChannelMessage({ + type: self._SIG_MESSAGE_TYPE.MUTE_AUDIO, + mid: self._user.sid, + rid: self._room.id, + muted: self._streamsMutedSettings.audioMuted, + stamp: (new Date()).getTime() + }); + }, hasToggledVideo ? 1050 : 0); } - self._trigger('incomingStream', targetMid, event.stream, - false, self._peerInformations[targetMid], !!isScreenSharing); - } else { - log.log([targetMid, null, null, 'MCU is listening']); + if ((streamTracksAvailability.hasVideo && hasToggledVideo) || + (streamTracksAvailability.hasAudio && hasToggledAudio)) { + + self._trigger('localMediaMuted', { + audioMuted: streamTracksAvailability.hasAudio ? self._streamsMutedSettings.audioMuted : true, + videoMuted: streamTracksAvailability.hasVideo ? self._streamsMutedSettings.videoMuted : true + }); + + if (self._inRoom) { + self._trigger('streamMuted', self._user.sid, self.getPeerInfo(), true, + self._streams.screenshare && self._streams.screenshare.stream); + self._trigger('peerUpdated', self._user.sid, self.getPeerInfo(), true); + } + } } }; /** - * Parse stream settings - * @method _parseAudioStreamSettings - * @param {Boolean|JSON} [options=false] This call requires audio - * @param {Boolean} [options.stereo] Enabled stereo or not. - * @return {JSON} The parsed audio options. - * - settings: User set audio options - * - userMedia: getUserMedia options - * @private - * @component Stream + * Deprecation Warning! + * This method has been deprecated. Use muteStream() method instead. + * + * Function that unmutes both getUserMedia() Stream and + * shareScreen() Stream audio tracks. + * @method enableAudio + * @deprecated true + * @example + * function unmuteAudio () { + * skylinkDemo.enableAudio(); + * } + * @trigger + * Invokes muteStream() method with + * options.audioMuted value as false and + * options.videoMuted value with current peerInfo.mediaStatus.videoMuted value. + * See getPeerInfo() method for more information. * @for Skylink * @since 0.5.5 */ -Skylink.prototype._parseAudioStreamSettings = function (audioOptions) { - audioOptions = (typeof audioOptions === 'object') ? - audioOptions : !!audioOptions; - - // Cleaning of unwanted keys - if (audioOptions !== false) { - audioOptions = (typeof audioOptions === 'boolean') ? {} : audioOptions; - var tempAudioOptions = {}; - tempAudioOptions.stereo = !!audioOptions.stereo; - audioOptions = tempAudioOptions; - } - - var userMedia = (typeof audioOptions === 'object') ? - true : audioOptions; - - return { - settings: audioOptions, - userMedia: userMedia - }; +Skylink.prototype.enableAudio = function() { + this.muteStream({ + audioMuted: false, + videoMuted: this._streamsMutedSettings.videoMuted + }); }; /** - * Parse stream settings - * @method _parseAudioStreamSettings - * @param {Boolean|JSON} [options=false] This call requires video - * @param {JSON} [options.resolution] [Rel: Skylink.VIDEO_RESOLUTION] - * @param {Number} [options.resolution.width] Video width - * @param {Number} [options.resolution.height] Video height - * @param {Number} [options.frameRate] Maximum frameRate of Video - * @return {JSON} The parsed video options. - * - settings: User set video options - * - userMedia: getUserMedia options - * @private - * @component Stream + * Deprecation Warning! + * This method has been deprecated. Use muteStream() method instead. + * + * Function that mutes both getUserMedia() Stream and + * shareScreen() Stream audio tracks. + * @method disableAudio + * @deprecated true + * @example + * function muteAudio () { + * skylinkDemo.disableAudio(); + * } + * @trigger + * Invokes muteStream() method with + * options.audioMuted value as true and + * options.videoMuted value with current peerInfo.mediaStatus.videoMuted value. + * See getPeerInfo() method for more information. * @for Skylink - * @since 0.5.8 + * @since 0.5.5 */ -Skylink.prototype._parseVideoStreamSettings = function (videoOptions) { - videoOptions = (typeof videoOptions === 'object') ? - videoOptions : !!videoOptions; - - var userMedia = false; - - // Cleaning of unwanted keys - if (videoOptions !== false) { - videoOptions = (typeof videoOptions === 'boolean') ? - { resolution: {} } : videoOptions; - var tempVideoOptions = {}; - // set the resolution parsing - videoOptions.resolution = videoOptions.resolution || {}; - tempVideoOptions.resolution = tempVideoOptions.resolution || {}; - // set resolution - tempVideoOptions.resolution.width = videoOptions.resolution.width || - this._defaultStreamSettings.video.resolution.width; - tempVideoOptions.resolution.height = videoOptions.resolution.height || - this._defaultStreamSettings.video.resolution.height; - // set the framerate - tempVideoOptions.frameRate = videoOptions.frameRate || - this._defaultStreamSettings.video.frameRate; - videoOptions = tempVideoOptions; - - userMedia = { - mandatory: { - //minWidth: videoOptions.resolution.width, - //minHeight: videoOptions.resolution.height, - maxWidth: videoOptions.resolution.width, - maxHeight: videoOptions.resolution.height, - //minFrameRate: videoOptions.frameRate, - maxFrameRate: videoOptions.frameRate - }, - optional: [] - }; - - //Remove maxFrameRate for AdapterJS to work with Safari - if (window.webrtcDetectedType === 'plugin') { - delete userMedia.mandatory.maxFrameRate; - } - } - - return { - settings: videoOptions, - userMedia: userMedia - }; +Skylink.prototype.disableAudio = function() { + this.muteStream({ + audioMuted: true, + videoMuted: this._streamsMutedSettings.videoMuted + }); }; /** - * Parse and set bandwidth settings. - * @method _parseBandwidthSettings - * @param {String} [options] Bandwidth settings - * @param {String} [options.audio=50] Audio Bandwidth - * @param {String} [options.video=256] Video Bandwidth - * @param {String} [options.data=1638400] Data Bandwidth - * @private - * @component Stream + * Deprecation Warning! + * This method has been deprecated. Use muteStream() method instead. + * + * Function that unmutes both getUserMedia() Stream and + * shareScreen() Stream video tracks. + * @method enableVideo + * @deprecated true + * @example + * function unmuteVideo () { + * skylinkDemo.enableVideo(); + * } + * @trigger + * Invokes muteStream() method with + * options.videoMuted value as false and + * options.audioMuted value with current peerInfo.mediaStatus.audioMuted value. + * See getPeerInfo() method for more information. * @for Skylink * @since 0.5.5 */ -Skylink.prototype._parseBandwidthSettings = function (bwOptions) { - bwOptions = (typeof bwOptions === 'object') ? - bwOptions : {}; - - // set audio bandwidth - bwOptions.audio = (typeof bwOptions.audio === 'number') ? - bwOptions.audio : 50; - // set video bandwidth - bwOptions.video = (typeof bwOptions.video === 'number') ? - bwOptions.video : 256; - // set data bandwidth - bwOptions.data = (typeof bwOptions.data === 'number') ? - bwOptions.data : 1638400; - - // set the settings - this._streamSettings.bandwidth = bwOptions; -}; - -/** - * Parse stream settings - * @method _parseMutedSettings - * @param {JSON} options Media Constraints. - * @param {Boolean|JSON} [options.audio=false] This call requires audio - * @param {Boolean} [options.audio.stereo] Enabled stereo or not. - * @param {Boolean} [options.audio.mute=false] If audio stream should be muted. - * @param {Boolean|JSON} [options.video=false] This call requires video - * @param {JSON} [options.video.resolution] [Rel: VIDEO_RESOLUTION] - * @param {Number} [options.video.resolution.width] Video width - * @param {Number} [options.video.resolution.height] Video height - * @param {Number} [options.video.frameRate] Maximum frameRate of video. - * @param {Boolean} [options.video.mute=false] If video stream should be muted. - * @return {JSON} The parsed muted options. - * @private - * @component Stream +Skylink.prototype.enableVideo = function() { + this.muteStream({ + videoMuted: false, + audioMuted: this._streamsMutedSettings.audioMuted + }); +}; + +/** + * Deprecation Warning! + * This method has been deprecated. Use muteStream() method instead. + * + * Function that mutes both getUserMedia() Stream and + * shareScreen() Stream video tracks. + * @method disableVideo + * @deprecated true + * @example + * function muteVideo () { + * skylinkDemo.disableVideo(); + * } + * @trigger + * Invokes muteStream() method with + * options.videoMuted value as true and + * options.audioMuted value with current peerInfo.mediaStatus.audioMuted value. + * See getPeerInfo() method for more information. * @for Skylink * @since 0.5.5 */ -Skylink.prototype._parseMutedSettings = function (options) { - // the stream options - options = (typeof options === 'object') ? - options : { audio: false, video: false }; - - var updateAudioMuted = (typeof options.audio === 'object') ? - !!options.audio.mute : !options.audio; - var updateVideoMuted = (typeof options.video === 'object') ? - !!options.video.mute : !options.video; - - return { - audioMuted: updateAudioMuted, - videoMuted: updateVideoMuted - }; +Skylink.prototype.disableVideo = function() { + this.muteStream({ + videoMuted: true, + audioMuted: this._streamsMutedSettings.audioMuted + }); }; -/** - * Parse stream default settings - * @method _parseDefaultMediaStreamSettings - * @param {JSON} options Media default Constraints. - * @param {Boolean|JSON} [options.maxWidth=640] Video default width. - * @param {Boolean} [options.maxHeight=480] Video default height. - * @private - * @component Stream - * @for Skylink - * @since 0.5.7 - */ -Skylink.prototype._parseDefaultMediaStreamSettings = function(options) { - var hasMediaChanged = false; +/** + * Function that retrieves screensharing Stream. + * @method shareScreen + * @param {JSON} [enableAudio=false] The flag if audio tracks should be retrieved. + * @param {Function} [callback] The callback function fired when request has completed. + * Function parameters signature is function (error, success) + * Function request completion is determined by the + * mediaAccessSuccess event triggering isScreensharing parameter payload value + * as true for request success when User is not in the Room or is in Room without Peers, + * or by the peerRestart event triggering + * isSelfInitiateRestart parameter payload value as true for all connected Peers + * for request success when User is in Room with Peers. + * @param {Error|String} callback.error The error result in request. + * Defined as null when there are no errors in request + * Object signature is the shareScreen() error when retrieving screensharing Stream. + * @param {MediaStream} callback.success The success result in request. + * Defined as null when there are errors in request + * Object signature is the screensharing Stream object. + * @example + * // Example 1: Share screen with audio + * skylinkDemo.shareScreen(function (error, success) { + * if (error) return; + * attachMediaStream(document.getElementById("my-screen"), success); + * }); + * + * // Example 2: Share screen without audio + * skylinkDemo.shareScreen(false, function (error, success) { + * if (error) return; + * attachMediaStream(document.getElementById("my-screen"), success); + * }); + * @trigger + * Retrieves screensharing Stream. If retrieval was successful: If browser is Firefox: + * If there are missing audio or video tracks requested: + * If there is any previous shareScreen() Stream: + * Invokes stopScreen() method. + * mediaAccessFallback event + * triggers parameter payload state as FALLBACKED, isScreensharing + * value as true and isAudioFallback value as false. + * mediaAccessSuccess event triggers + * parameter payload isScreensharing value as true and isAudioFallback + * value as false.Else: + * If audio is requested: Chrome, Safari and IE currently doesn't support retrieval of + * audio track together with screensharing video track. Retrieves audio Stream: + * If retrieval was successful: Attempts to attach screensharing Stream video track to audio Stream. + * If attachment was successful: + * mediaAccessSuccess event triggers parameter payload isScreensharing + * value as true and isAudioFallback value as false.Else: + * If there is any previous shareScreen() Stream: + * Invokes stopScreen() method. + * mediaAccessFallback event triggers parameter payload + * state as FALLBACKED, isScreensharing value as true and + * isAudioFallback value as false. + * mediaAccessSuccess event triggers + * parameter payload isScreensharing value as true and isAudioFallback + * value as false.Else: + * If there is any previous shareScreen() Stream: + * Invokes stopScreen() method. + * mediaAccessFallback event + * triggers parameter payload state as FALLBACKED, isScreensharing + * value as true and isAudioFallback value as false. + * mediaAccessSuccess event triggers + * parameter payload isScreensharing value as true and isAudioFallback + * value as false.Else: + * mediaAccessSuccess event + * triggers parameter payload isScreensharing value as true + * and isAudioFallback value as false.Else: + * mediaAccessError event triggers parameter payload + * isScreensharing value as true and isAudioFallback value as + * false.ABORT and return error.If User is in Room: + * incomingStream event triggers parameter payload + * isSelf value as true and stream as shareScreen() Stream. + * peerUpdated event triggers parameter payload + * isSelf value as true. + * Checks if MCU is enabled for App Key provided in init() method. + * If MCU is enabled: Invoke refreshConnection() method. + * If request has errors: ABORT and return error.Else: + * If there are connected Peers in the Room: Invoke + * refreshConnection() method. If request has errors: ABORT and return error. + * + * @for Skylink + * @since 0.6.0 + */ +Skylink.prototype.shareScreen = function (enableAudio, callback) { + var self = this; + + if (typeof enableAudio === 'function') { + callback = enableAudio; + enableAudio = true; + } - // prevent undefined error - options = options || {}; + if (typeof enableAudio !== 'boolean') { + enableAudio = true; + } - log.debug('Parsing stream settings. Default stream options:', options); + var throttleFn = function (fn, wait) { + if (!self._timestamp.func){ + //First time run, need to force timestamp to skip condition + self._timestamp.func = self._timestamp.now - wait; + } + var now = Date.now(); - options.maxWidth = (typeof options.maxWidth === 'number') ? options.maxWidth : - 640; - options.maxHeight = (typeof options.maxHeight === 'number') ? options.maxHeight : - 480; + if (!self._timestamp.screen) { + if (now - self._timestamp.func < wait) { + return; + } + } + fn(); + self._timestamp.screen = false; + self._timestamp.func = now; + }; - // parse video resolution. that's for now - this._defaultStreamSettings.video.resolution.width = options.maxWidth; - this._defaultStreamSettings.video.resolution.height = options.maxHeight; + throttleFn(function () { + var settings = { + settings: { + audio: enableAudio, + video: { + screenshare: true + } + }, + getUserMediaSettings: { + video: { + mediaSource: 'window' + } + } + }; - log.debug('Parsed default media stream settings', this._defaultStreamSettings); -}; + var mediaAccessSuccessFn = function (stream) { + self.off('mediaAccessError', mediaAccessErrorFn); -/** - * Parse stream settings - * @method _parseMediaStreamSettings - * @param {JSON} options Media Constraints. - * @param {Boolean|JSON} [options.audio=false] This call requires audio - * @param {Boolean} [options.audio.stereo] Enabled stereo or not. - * @param {Boolean} [options.audio.mute=false] If audio stream should be muted. - * @param {Boolean|JSON} [options.video=false] This call requires video - * @param {JSON} [options.video.resolution] [Rel: VIDEO_RESOLUTION] - * @param {Number} [options.video.resolution.width] Video width - * @param {Number} [options.video.resolution.height] Video height - * @param {Number} [options.video.frameRate] Maximum frameRate of video. - * @param {Boolean} [options.video.mute=false] If video stream should be muted. - * @private - * @component Stream - * @for Skylink - * @since 0.5.6 - */ -Skylink.prototype._parseMediaStreamSettings = function(options) { - var hasMediaChanged = false; + if (self._inRoom) { + self._trigger('incomingStream', self._user.sid, stream, true, self.getPeerInfo()); + self._trigger('peerUpdated', self._user.sid, self.getPeerInfo(), true); - options = options || {}; + if (Object.keys(self._peerConnections).length > 0 || self._hasMCU) { + self._refreshPeerConnection(Object.keys(self._peerConnections), false, function (err, success) { + if (err) { + log.error('Failed refreshing connections for shareScreen() ->', err); + if (typeof callback === 'function') { + callback(new Error('Failed refreshing connections.'), null); + } + return; + } + if (typeof callback === 'function') { + callback(null, stream); + } + }); + } else if (typeof callback === 'function') { + callback(null, stream); + } + } else if (typeof callback === 'function') { + callback(null, stream); + } + }; - log.debug('Parsing stream settings. Stream options:', options); + var mediaAccessErrorFn = function (error) { + self.off('mediaAccessSuccess', mediaAccessSuccessFn); - // Set audio settings - var audioSettings = this._parseAudioStreamSettings(options.audio); - // check for change - this._streamSettings.audio = audioSettings.settings; - this._getUserMediaSettings.audio = audioSettings.userMedia; + if (typeof callback === 'function') { + callback(error, null); + } + }; - // Set video settings - var videoSettings = this._parseVideoStreamSettings(options.video); - // check for change - this._streamSettings.video = videoSettings.settings; - this._getUserMediaSettings.video = videoSettings.userMedia; + self.once('mediaAccessSuccess', mediaAccessSuccessFn, function (stream, isScreensharing) { + return isScreensharing; + }); - // Set user media status options - var mutedSettings = this._parseMutedSettings(options); + self.once('mediaAccessError', mediaAccessErrorFn, function (error, isScreensharing) { + return isScreensharing; + }); - this._mediaStreamsStatus = mutedSettings; + try { + if (enableAudio && window.webrtcDetectedBrowser === 'firefox') { + settings.getUserMediaSettings.audio = true; + } - log.debug('Parsed user media stream settings', this._streamSettings); + navigator.getUserMedia(settings.getUserMediaSettings, function (stream) { + if (window.webrtcDetectedBrowser === 'firefox' || !enableAudio) { + self._onStreamAccessSuccess(stream, settings, true, false); + return; + } - log.debug('User media status:', this._mediaStreamsStatus); -}; + navigator.getUserMedia({ + audio: true -/** - * Sends our Local MediaStreams to other Peers. - * By default, it sends all it's other stream - * @method _addLocalMediaStreams - * @param {String} peerId The peerId of the peer to send local stream to. - * @private - * @component Stream - * @for Skylink - * @since 0.5.2 - */ -Skylink.prototype._addLocalMediaStreams = function(peerId) { - // NOTE ALEX: here we could do something smarter - // a mediastream is mainly a container, most of the info - // are attached to the tracks. We should iterates over track and print - try { - log.log([peerId, null, null, 'Adding local stream']); + }, function (audioStream) { + try { + audioStream.addTrack(stream.getVideoTracks()[0]); - var pc = this._peerConnections[peerId]; + self.once('mediaAccessSuccess', function () { + self._streams.screenshare.streamClone = stream; + }, function (stream, isScreensharing) { + return isScreensharing; + }); - if (pc) { - if (pc.signalingState !== this.PEER_CONNECTION_STATE.CLOSED) { - if (this._mediaScreen && this._mediaScreen !== null) { - pc.addStream(this._mediaScreen); - log.debug([peerId, 'MediaStream', this._mediaStream, 'Sending screen']); + self._onStreamAccessSuccess(audioStream, settings, true, false); - } else if (this._mediaStream && this._mediaStream !== null) { - pc.addStream(this._mediaStream); - log.debug([peerId, 'MediaStream', this._mediaStream, 'Sending stream']); + } catch (error) { + log.error('Failed retrieving audio stream for screensharing stream', error); + self._onStreamAccessSuccess(stream, settings, true, false); + } + }, function (error) { + log.error('Failed retrieving audio stream for screensharing stream', error); + self._onStreamAccessSuccess(stream, settings, true, false); + }); - } else { - log.warn([peerId, null, null, 'No media to send. Will be only receiving']); - } + }, function (error) { + self._onStreamAccessError(error, settings, true, false); + }); - } else { - log.warn([peerId, 'MediaStream', this._mediaStream, - 'Not adding stream as signalingState is closed']); - } - } else { - log.warn([peerId, 'MediaStream', this._mediaStream, - 'Not adding stream as peerconnection object does not exists']); + } catch (error) { + self._onStreamAccessError(error, settings, true, false); } - } catch (error) { - // Fix errors thrown like NS_ERROR_UNEXPECTED - log.error([peerId, null, null, 'Failed adding local stream'], error); - } + + }, 10000); }; /** - * Stops current MediaStream playback and streaming. - * @method stopStream - * @for Skylink - * @since 0.5.6 + * + * Note that broadcasted events from muteStream() method, + * stopStream() method, + * stopScreen() method, + * sendMessage() method, + * unlockRoom() method and + * lockRoom() method may be queued when + * sent within less than an interval. + * + * Function that stops shareScreen() Stream. + * @method stopScreen + * @example + * function stopScreen () { + * skylinkDemo.stopScreen(); + * } + * + * skylinkDemo.shareScreen(); + * @trigger + * Checks if there is shareScreen() Stream. + * If there is shareScreen() Stream: + * Stop shareScreen() Stream Stream. + * mediaAccessStopped event + * triggers parameter payload isScreensharing value as true and + * isAudioFallback value as false.If User is in Room: + * streamEnded event triggers parameter payload + * isSelf value as true and isScreensharing value as true. + * peerUpdated event triggers parameter payload + * isSelf value as true. + * If User is in Room: SKIP this step if stopScreen() + * was invoked from shareScreen() method. + * If there is getUserMedia()Stream Stream: + * incomingStream event triggers parameter payload + * isSelf value as true and stream as + * getUserMedia() Stream. + * peerUpdated event triggers parameter payload + * isSelf value as true. + * Invoke refreshConnection() method. + * + * @for Skylink + * @since 0.6.0 */ -Skylink.prototype.stopStream = function () { - if (this._mediaStream && this._mediaStream !== null) { - this._mediaStream.stop(); - } +Skylink.prototype.stopScreen = function () { + if (this._streams.screenshare) { + this._stopStreams({ + screenshare: true + }); - // if previous line break, recheck again to trigger event - if (this._mediaStream && this._mediaStream !== null) { - this._trigger('mediaAccessStopped', false); + if (this._inRoom) { + if (this._streams.userMedia && this._streams.userMedia.stream) { + this._trigger('incomingStream', this._user.sid, this._streams.userMedia.stream, true, this.getPeerInfo()); + this._trigger('peerUpdated', this._user.sid, this.getPeerInfo(), true); + } + this._refreshPeerConnection(Object.keys(this._peerConnections), false); + } } - - this._mediaStream = null; }; /** - * Handles the muting of audio and video streams. - * @method _muteLocalMediaStreams - * @return options If MediaStream(s) has specified tracks. - * @return options.hasAudioTracks If MediaStream(s) has audio tracks. - * @return options.hasVideoTracks If MediaStream(s) has video tracks. + * Function that handles the muting of Stream audio and video tracks. + * @method _muteStreams * @private * @for Skylink - * @since 0.5.6 + * @since 0.6.15 */ -Skylink.prototype._muteLocalMediaStreams = function () { - var hasAudioTracks = false; - var hasVideoTracks = false; - - var audioTracks; - var videoTracks; - var a, v; - - // Loop and enable tracks accordingly (mediaStream) - if (this._mediaStream && this._mediaStream !== null) { - audioTracks = this._mediaStream.getAudioTracks(); - videoTracks = this._mediaStream.getVideoTracks(); +Skylink.prototype._muteStreams = function () { + var self = this; + var hasVideo = false; + var hasAudio = false; - hasAudioTracks = audioTracks.length > 0 || hasAudioTracks; - hasVideoTracks = videoTracks.length > 0 || hasVideoTracks; + var muteFn = function (stream) { + var audioTracks = stream.getAudioTracks(); + var videoTracks = stream.getVideoTracks(); - // loop audio tracks - for (a = 0; a < audioTracks.length; a++) { - audioTracks[a].enabled = this._mediaStreamsStatus.audioMuted !== true; + for (var a = 0; a < audioTracks.length; a++) { + audioTracks[a].enabled = !self._streamsMutedSettings.audioMuted; + hasAudio = true; } - // loop video tracks - for (v = 0; v < videoTracks.length; v++) { - videoTracks[v].enabled = this._mediaStreamsStatus.videoMuted !== true; - } - } - // Loop and enable tracks accordingly (mediaScreen) - if (this._mediaScreen && this._mediaScreen !== null) { - audioTracks = this._mediaScreen.getAudioTracks(); - videoTracks = this._mediaScreen.getVideoTracks(); - - hasAudioTracks = hasAudioTracks || audioTracks.length > 0; - hasVideoTracks = hasVideoTracks || videoTracks.length > 0; - - // loop audio tracks - for (a = 0; a < audioTracks.length; a++) { - audioTracks[a].enabled = this._mediaStreamsStatus.audioMuted !== true; + for (var v = 0; v < videoTracks.length; v++) { + videoTracks[v].enabled = !self._streamsMutedSettings.videoMuted; + hasVideo = true; } - // loop video tracks - for (v = 0; v < videoTracks.length; v++) { - videoTracks[v].enabled = this._mediaStreamsStatus.videoMuted !== true; - } - } - - // Loop and enable tracks accordingly (mediaScreenClone) - if (this._mediaScreenClone && this._mediaScreenClone !== null) { - videoTracks = this._mediaScreen.getVideoTracks(); - - hasVideoTracks = hasVideoTracks || videoTracks.length > 0; + }; - // loop video tracks - for (v = 0; v < videoTracks.length; v++) { - videoTracks[v].enabled = this._mediaStreamsStatus.videoMuted !== true; - } + if (self._streams.userMedia && self._streams.userMedia.stream) { + muteFn(self._streams.userMedia.stream); } - // update accordingly if failed - if (!hasAudioTracks) { - //this._mediaStreamsStatus.audioMuted = true; - this._streamSettings.audio = false; + if (self._streams.screenshare && self._streams.screenshare.stream) { + muteFn(self._streams.screenshare.stream); } - if (!hasVideoTracks) { - //this._mediaStreamsStatus.videoMuted = true; - this._streamSettings.video = false; + + if (self._streams.screenshare && self._streams.screenshare.streamClone) { + muteFn(self._streams.screenshare.streamClone); } - log.log('Update to isAudioMuted status ->', this._mediaStreamsStatus.audioMuted); - log.log('Update to isVideoMuted status ->', this._mediaStreamsStatus.videoMuted); + log.debug('Updated Streams muted status ->', self._streamsMutedSettings); return { - hasAudioTracks: hasAudioTracks, - hasVideoTracks: hasVideoTracks + hasVideo: hasVideo, + hasAudio: hasAudio }; }; /** - * Waits for MediaStream. - * - Once the stream is loaded, callback is called - * - If there's not a need for stream, callback is called - * @method _waitForLocalMediaStream - * @param {Function} callback Callback after requested constraints are loaded. - * @param {JSON} [options] Media Constraints. - * @param {JSON} [options.userData] User custom data. - * @param {Boolean|JSON} [options.audio=false] This call requires audio - * @param {Boolean} [options.audio.stereo] Enabled stereo or not - * @param {Boolean} [options.audio.mute=false] If audio stream should be muted. - * @param {Boolean|JSON} [options.video=false] This call requires video - * @param {JSON} [options.video.resolution] [Rel: VIDEO_RESOLUTION] - * @param {Number} [options.video.resolution.width] Video width - * @param {Number} [options.video.resolution.height] Video height - * @param {Number} [options.video.frameRate] Maximum frameRate of Video - * @param {Boolean} [options.video.mute=false] If video stream should be muted. - * @param {String} [options.bandwidth] Bandwidth settings - * @param {String} [options.bandwidth.audio] Audio Bandwidth - * @param {String} [options.bandwidth.video] Video Bandwidth - * @param {String} [options.bandwidth.data] Data Bandwidth - * @trigger mediaAccessRequired - * @private - * @component Stream + * Function that handles stopping the Stream streaming. + * @method _stopStreams + * @private * @for Skylink - * @since 0.5.6 + * @since 0.6.15 */ -Skylink.prototype._waitForLocalMediaStream = function(callback, options) { +Skylink.prototype._stopStreams = function (options) { var self = this; - options = options || {}; + var stopFn = function (stream) { + var streamId = stream.id || stream.label; + log.debug([null, 'MediaStream', streamId, 'Stopping Stream ->'], stream); - // get the stream - if (options.manualGetUserMedia === true) { - self._trigger('mediaAccessRequired'); - } - // If options video or audio false, do the opposite to throw a true. - var requireAudio = !!options.audio; - var requireVideo = !!options.video; + try { + var audioTracks = stream.getAudioTracks(); + var videoTracks = stream.getVideoTracks(); + + for (var a = 0; a < audioTracks.length; a++) { + audioTracks[a].stop(); + } + + for (var v = 0; v < videoTracks.length; v++) { + videoTracks[v].stop(); + } - log.log('Requested audio:', requireAudio); - log.log('Requested video:', requireVideo); + } catch (error) { + stream.stop(); + } - // check if it requires audio or video - if (!requireAudio && !requireVideo && !options.manualGetUserMedia) { - // set to default - if (options.audio === false && options.video === false) { - self._parseMediaStreamSettings(options); + if (self._streamsStoppedCbs[streamId]) { + self._streamsStoppedCbs[streamId](); } + }; - callback(); - return; - } + var stopUserMedia = false; + var stopScreenshare = false; + var hasStoppedMedia = false; - // get the user media - if (!options.manualGetUserMedia && (options.audio || options.video)) { - self.getUserMedia({ - audio: options.audio, - video: options.video - }); + if (typeof options === 'object') { + stopUserMedia = options.userMedia === true; + stopScreenshare = options.screenshare === true; } - // clear previous mediastreams - self.stopStream(); - - var current50Block = 0; - var mediaAccessRequiredFailure = false; + if (stopUserMedia && self._streams.userMedia) { + if (self._streams.userMedia.stream) { + stopFn(self._streams.userMedia.stream); + } - // wait for available audio or video stream - self._wait(function () { - if (mediaAccessRequiredFailure === true) { - self._onUserMediaError('Waiting for stream timeout'); + self._streams.userMedia = null; + hasStoppedMedia = true; + } - } else { - callback(); + if (stopScreenshare && self._streams.screenshare) { + if (self._streams.screenshare.streamClone) { + stopFn(self._streams.screenshare.streamClone); } - }, function () { - var hasAudio = !requireAudio; - var hasVideo = !requireVideo; + if (self._streams.screenshare.stream) { + stopFn(self._streams.screenshare.stream); + } - // for now we require one MediaStream with both audio and video - // due to firefox non-supported audio or video - if (self._mediaStream && self._mediaStream !== null) { - if (self._mediaStream && options.manualGetUserMedia) { - return true; - } + self._streams.screenshare = null; + hasStoppedMedia = true; + } - // do the check - if (requireAudio) { - hasAudio = self._mediaStream.getAudioTracks().length > 0; - } - if (requireVideo) { - hasVideo = self._mediaStream.getVideoTracks().length > 0; - } - if (hasAudio && hasVideo) { - return true; - } - } + if (self._inRoom && hasStoppedMedia) { + self._trigger('peerUpdated', self._user.sid, self.getPeerInfo(), true); + } - if (options.manualGetUserMedia === true) { - current50Block += 1; - if (current50Block === 600) { - mediaAccessRequiredFailure = true; - return true; - } - } - }, 50); + log.log('Stopping Streams with settings ->', options); }; /** - * Gets the default video source and microphone source. - * - This is an implemented function for Skylink. - * - Constraints are not the same as the [MediaStreamConstraints](http://dev.w3. - * org/2011/webrtc/editor/archives/20140817/getusermedia.html#dictionary - * -mediastreamconstraints-members) specified in the w3c specs. - * - Calling getUserMedia while having streams being sent to another peer may - * actually cause problems, because currently getUserMedia refreshes all streams. - * @method getUserMedia - * @param {JSON} [options] MediaStream constraints. - * @param {JSON|Boolean} [options.audio=true] Option to allow audio stream. - * @param {Boolean} [options.audio.stereo] Option to enable stereo - * during call. - * @param {Boolean} [options.audio.mute=false] If audio stream should be muted. - * @param {JSON|Boolean} [options.video=true] Option to allow video stream. - * @param {JSON} [options.video.resolution] The resolution of video stream. - * [Rel: Skylink.VIDEO_RESOLUTION] - * @param {Number} [options.video.resolution.width] - * The video stream resolution width (in px). - * @param {Number} [options.video.resolution.height] - * The video stream resolution height (in px). - * @param {Number} [options.video.frameRate] - * The video stream maximum frameRate. - * @param {Boolean} [options.video.mute=false] If video stream should be muted. - * @param {Function} [callback] The callback fired after media was successfully accessed. - * Default signature: function(error object, success object) - * @example - * // Default is to get both audio and video - * // Example 1: Get both audio and video by default. - * SkylinkDemo.getUserMedia(); - * - * // Example 2: Get the audio stream only - * SkylinkDemo.getUserMedia({ - * 'video' : false, - * 'audio' : true - * }); - * - * // Example 3: Set the stream settings for the audio and video - * SkylinkDemo.getUserMedia({ - * 'video' : { - * 'resolution': SkylinkDemo.VIDEO_RESOLUTION.HD, - * 'frameRate': 50 - * }, - * 'audio' : { - * 'stereo': true - * } - * }); - * - * // Example 4: Get user media with callback - * SkylinkDemo.getUserMedia({ - * 'video' : false, - * 'audio' : true - * },function(error,success){ - * if (error){ - * console.log(error); - * } - * else{ - * console.log(success); - * } - * }); - * @trigger mediaAccessSuccess, mediaAccessError, streamEnded - * @component Stream + * Function that parses the getUserMedia() settings provided. + * @method _parseStreamSettings + * @private * @for Skylink - * @since 0.5.6 + * @since 0.6.15 */ -Skylink.prototype.getUserMedia = function(options,callback) { - var self = this; +Skylink.prototype._parseStreamSettings = function(options) { + var settings = { + settings: { audio: false, video: false }, + mutedSettings: { shouldAudioMuted: false, shouldVideoMuted: false }, + getUserMediaSettings: { audio: false, video: false } + }; - if (!options){ - options = { - audio: true, - video: true - }; - } - else if (typeof options === 'function'){ - callback = options; - options = { - audio: true, - video: true + if (options.audio) { + settings.settings.audio = { + stereo: false, + exactConstraints: !!options.useExactConstraints }; - } + settings.getUserMediaSettings.audio = {}; - // parse stream settings - self._parseMediaStreamSettings(options); + if (typeof options.audio.stereo === 'boolean') { + settings.settings.audio.stereo = options.audio.stereo; + } - // if audio and video is false, do not call getUserMedia - if (!(options.audio === false && options.video === false)) { - // clear previous mediastreams - self.stopStream(); - try { - window.getUserMedia(self._getUserMediaSettings, function (stream) { - self._onUserMediaSuccess(stream); - if (typeof callback === 'function'){ - callback(null,stream); - } - }, function (error) { - self._onUserMediaError(error); - if (typeof callback === 'function'){ - callback(error,null); + if (typeof options.audio.mute === 'boolean') { + settings.mutedSettings.shouldAudioMuted = options.audio.mute; + } + + if (Array.isArray(options.audio.optional)) { + settings.settings.audio.optional = clone(options.audio.optional); + settings.getUserMediaSettings.audio.optional = clone(options.audio.optional); + } + + if (options.audio.deviceId && typeof options.audio.deviceId === 'string' && + window.webrtcDetectedBrowser !== 'firefox') { + settings.settings.audio.deviceId = options.audio.deviceId; + + if (options.useExactConstraints) { + settings.getUserMediaSettings.audio.deviceId = { exact: options.audio.deviceId }; + + } else { + if (!Array.isArray(settings.getUserMediaSettings.audio.optional)) { + settings.getUserMediaSettings.audio.optional = []; } - }); - } catch (error) { - self._onUserMediaError(error); - if (typeof callback === 'function'){ - callback(error,null); + + settings.getUserMediaSettings.audio.optional.push({ + sourceId: options.audio.deviceId + }); } } - } else { - log.warn([null, 'MediaStream', null, 'Not retrieving stream']); - } -}; -/** - * Resends a Local MediaStreams. This overrides all previous MediaStreams sent. - * Provided MediaStream would be automatically detected as unmuted by default. - * @method sendStream - * @param {Object|JSON} stream The stream object or options. - * @param {Boolean} [stream.audio=false] If send a new stream with audio. - * @param {Boolean} [stream.audio.stereo] Option to enable stereo - * during call. - * @param {Boolean} [stream.audio.mute=false] If send a new stream with audio muted. - * @param {JSON|Boolean} [stream.video=false] Option to allow video stream. - * @param {JSON} [stream.video.resolution] The resolution of video stream. - * [Rel: Skylink.VIDEO_RESOLUTION] - * @param {Number} [stream.video.resolution.width] - * The video stream resolution width (in px). - * @param {Number} [stream.video.resolution.height] - * The video stream resolution height (in px). - * @param {Number} [stream.video.frameRate] - * The video stream maximum frameRate. - * @param {Boolean} [stream.video.mute=false] If send a new stream with video muted. - * @param {Function} [callback] The callback fired after stream was sent. - * Default signature: function(error object, success object) - * @example - * // Example 1: Send a stream object instead - * SkylinkDemo.on('mediaAccessSuccess', function (stream) { - * SkylinkDemo.sendStream(stream); - * }); - * - * // Example 2: Send stream with getUserMedia automatically called for you - * SkylinkDemo.sendStream({ - * audio: true, - * video: false - * }); - * - * // Example 3: Send stream with getUserMedia automatically called for you - * // and audio is muted - * SkylinkDemo.sendStream({ - * audio: { mute: true }, - * video: false - * }); - * - * // Example 4: Send stream with callback - * SkylinkDemo.sendStream({ - * audio: true, - * video: true - * },function(error,success){ - * if (error){ - * console.log('Error occurred. Stream was not sent: '+error) - * } - * else{ - * console.log('Stream successfully sent: '+success); - * } - * }); - * - * @trigger peerRestart, incomingStream - * @component Stream - * @for Skylink - * @since 0.5.6 - */ + // For Edge to work since they do not support the advanced constraints yet + if (window.webrtcDetectedBrowser === 'edge') { + settings.getUserMediaSettings.audio = true; + } + } -Skylink.prototype.sendStream = function(stream, callback) { - var self = this; - var restartCount = 0; - var peerCount = Object.keys(self._peerConnections).length; + if (options.video) { + settings.settings.video = { + resolution: clone(this.VIDEO_RESOLUTION.VGA), + screenshare: false, + exactConstraints: !!options.useExactConstraints + }; + settings.getUserMediaSettings.video = {}; - if (typeof stream !== 'object') { - var error = 'Provided stream settings is not an object'; - log.error(error); - if (typeof callback === 'function'){ - callback(error,null); + if (typeof options.video.mute === 'boolean') { + settings.mutedSettings.shouldVideoMuted = options.video.mute; } - return; - } - // Stream object - // getAudioTracks or getVideoTracks first because adapterjs - // has not implemeneted MediaStream as an interface - // interopability with firefox and chrome - //MediaStream = MediaStream || webkitMediaStream; - // NOTE: eventually we should do instanceof - if (typeof stream.getAudioTracks === 'function' || - typeof stream.getVideoTracks === 'function') { - // stop playback - self.stopStream(); - // send the stream - if (self._mediaStream !== stream) { - self._onUserMediaSuccess(stream); + if (Array.isArray(options.video.optional)) { + settings.settings.video.optional = clone(options.video.optional); + settings.getUserMediaSettings.video.optional = clone(options.video.optional); } - self._mediaStreamsStatus.audioMuted = false; - self._mediaStreamsStatus.videoMuted = false; + if (options.video.deviceId && typeof options.video.deviceId === 'string' && + window.webrtcDetectedBrowser !== 'firefox') { + settings.settings.video.deviceId = options.video.deviceId; - self._streamSettings.audio = stream.getAudioTracks().length > 0; - self._streamSettings.video = stream.getVideoTracks().length > 0; + if (options.useExactConstraints) { + settings.getUserMediaSettings.video.deviceId = { exact: options.video.deviceId }; - if (typeof callback === 'function'){ - self.once('peerRestart',function(peerId, peerInfo, isSelfInitiatedRestart){ - log.log([null, 'MediaStream', stream.id, - 'Stream was sent. Firing callback'], stream); - callback(null,stream); - restartCount = 0; //reset counter - },function(peerId, peerInfo, isSelfInitiatedRestart){ - if (isSelfInitiatedRestart){ - restartCount++; - if (restartCount === peerCount){ - return true; - } + } else { + if (!Array.isArray(settings.getUserMediaSettings.video.optional)) { + settings.getUserMediaSettings.video.optional = []; } - return false; - },false); - } - for (var peer in self._peerConnections) { - if (self._peerConnections.hasOwnProperty(peer)) { - self._restartPeerConnection(peer, true, false, null, true); + settings.getUserMediaSettings.video.optional.push({ + sourceId: options.video.deviceId + }); } } - self._trigger('peerUpdated', self._user.sid, self.getPeerInfo(), true); + if (options.video.resolution && typeof options.video.resolution === 'object') { + if (typeof options.video.resolution.width === 'number') { + settings.settings.video.resolution.width = options.video.resolution.width; + } + if (typeof options.video.resolution.height === 'number') { + settings.settings.video.resolution.height = options.video.resolution.height; + } + } - // Options object - } else { + if (options.useExactConstraints) { + settings.getUserMediaSettings.video.width = { exact: settings.settings.video.resolution.width }; + settings.getUserMediaSettings.video.height = { exact: settings.settings.video.resolution.height }; - if (typeof callback === 'function'){ - self.once('peerRestart',function(peerId, peerInfo, isSelfInitiatedRestart){ - log.log([null, 'MediaStream', stream.id, - 'Stream was sent. Firing callback'], stream); - callback(null,stream); - restartCount = 0; //reset counter - },function(peerId, peerInfo, isSelfInitiatedRestart){ - if (isSelfInitiatedRestart){ - restartCount++; - if (restartCount === peerCount){ - return true; - } - } - return false; - },false); + if (typeof options.video.frameRate === 'number') { + settings.settings.video.frameRate = options.video.frameRate; + settings.getUserMediaSettings.video.frameRate = { exact: options.video.frameRate }; } - // get the mediastream and then wait for it to be retrieved before sending - self._waitForLocalMediaStream(function () { - // mute unwanted streams - for (var peer in self._peerConnections) { - if (self._peerConnections.hasOwnProperty(peer)) { - self._restartPeerConnection(peer, true, false, null, true); - } + } else { + settings.getUserMediaSettings.video.mandatory = { + maxWidth: settings.settings.video.resolution.width, + maxHeight: settings.settings.video.resolution.height + }; + + if (typeof options.video.frameRate === 'number' && ['IE', 'safari'].indexOf(window.webrtcDetectedBrowser) === -1) { + settings.settings.video.frameRate = options.video.frameRate; + settings.getUserMediaSettings.video.mandatory.maxFrameRate = options.video.frameRate; } + } - self._trigger('peerUpdated', self._user.sid, self.getPeerInfo(), true); - }, stream); + // For Edge to work since they do not support the advanced constraints yet + if (window.webrtcDetectedBrowser === 'edge') { + settings.getUserMediaSettings.video = true; + } } + + return settings; }; /** - * Mutes a Local MediaStreams. - * @method muteStream - * @param {Object|JSON} options The muted options. - * @param {Boolean} [options.audioMuted=true] If send a new stream with audio muted. - * @param {Boolean} [options.videoMuted=true] If send a new stream with video muted. - * @example - * SkylinkDemo.muteStream({ - * audioMuted: true, - * videoMuted: false - * }); - * @trigger peerRestart, peerUpdated, incomingStream - * @component Stream + * Function that handles the native navigator.getUserMedia() API success callback result. + * @method _onStreamAccessSuccess + * @private * @for Skylink - * @since 0.5.7 + * @since 0.3.0 */ -Skylink.prototype.muteStream = function(options) { +Skylink.prototype._onStreamAccessSuccess = function(stream, settings, isScreenSharing, isAudioFallback) { var self = this; + var streamId = stream.id || stream.label; - if (typeof options !== 'object') { - log.error('Provided settings is not an object'); - return; + log.log([null, 'MediaStream', streamId, 'Has access to stream ->'], stream); + + // Stop previous stream + if (!isScreenSharing && self._streams.userMedia) { + self._stopStreams({ + userMedia: true, + screenshare: false + }); + + } else if (isScreenSharing && self._streams.screenshare) { + self._stopStreams({ + userMedia: false, + screenshare: true + }); } - if (!self._mediaStream || self._mediaStream === null) { - log.warn('No streams are available to mute / unmute!'); - return; - } + self._streamsStoppedCbs[streamId] = function () { + log.log([null, 'MediaStream', streamId, 'Stream has ended']); + + self._trigger('mediaAccessStopped', !!isScreenSharing, !!isAudioFallback, streamId); + + if (self._inRoom) { + log.debug([null, 'MediaStream', streamId, 'Sending Stream ended status to Peers']); + + self._sendChannelMessage({ + type: self._SIG_MESSAGE_TYPE.STREAM, + mid: self._user.sid, + rid: self._room.id, + cid: self._key, + sessionType: !!isScreenSharing ? 'screensharing' : 'stream', + streamId: streamId, + status: 'ended' + }); + + self._trigger('streamEnded', self._user.sid, self.getPeerInfo(), true, !!isScreenSharing, streamId); + + if (isScreenSharing && self._streams.screenshare && self._streams.screenshare.stream && + (self._streams.screenshare.stream.id || self._streams.screenshare.stream.label) === streamId) { + self._streams.screenshare = null; + + } else if (!isScreenSharing && self._streams.userMedia && self._streams.userMedia.stream && + (self._streams.userMedia.stream.id || self._streams.userMedia.stream.label) === streamId) { + self._streams.userMedia = null; + } + } + }; + + // Handle event for Chrome / Opera + if (['chrome', 'opera'].indexOf(window.webrtcDetectedBrowser) > -1) { + stream.oninactive = function () { + if (self._streamsStoppedCbs[streamId]) { + self._streamsStoppedCbs[streamId](); + } + }; - // set the muted status - if (typeof options.audioMuted === 'boolean') { - self._mediaStreamsStatus.audioMuted = !!options.audioMuted; - } - if (typeof options.videoMuted === 'boolean') { - self._mediaStreamsStatus.videoMuted = !!options.videoMuted; - } + // Handle event for Firefox (use an interval) + } else if (window.webrtcDetectedBrowser === 'firefox') { + stream.endedInterval = setInterval(function () { + if (typeof stream.recordedTime === 'undefined') { + stream.recordedTime = 0; + } + if (stream.recordedTime === stream.currentTime) { + clearInterval(stream.endedInterval); - var hasTracksOption = self._muteLocalMediaStreams(); - var refetchAudio = false; - var refetchVideo = false; + if (self._streamsStoppedCbs[streamId]) { + self._streamsStoppedCbs[streamId](); + } - // update to mute status of audio tracks - if (!hasTracksOption.hasAudioTracks) { - // do a refetch - refetchAudio = options.audioMuted === false; - } + } else { + stream.recordedTime = stream.currentTime; + } + }, 1000); - // update to mute status of video tracks - if (!hasTracksOption.hasVideoTracks) { - // do a refetch - refetchVideo = options.videoMuted === false; + } else { + stream.onended = function () { + if (self._streamsStoppedCbs[streamId]) { + self._streamsStoppedCbs[streamId](); + } + }; } - // do a refetch - if (refetchAudio || refetchVideo) { - // set the settings - self._parseMediaStreamSettings({ - audio: options.audioMuted === false || self._streamSettings.audio, - video: options.videoMuted === false || self._streamSettings.video - }); + if ((settings.settings.audio && stream.getAudioTracks().length === 0) || + (settings.settings.video && stream.getVideoTracks().length === 0)) { - self.getUserMedia(self._streamSettings); + var tracksNotSameError = 'Expected audio tracks length with ' + + (settings.settings.audio ? '1' : '0') + ' and video tracks length with ' + + (settings.settings.video ? '1' : '0') + ' but received audio tracks length ' + + 'with ' + stream.getAudioTracks().length + ' and video ' + + 'tracks length with ' + stream.getVideoTracks().length; - self.once('mediaAccessSuccess', function (stream) { - // mute unwanted streams - for (var peer in self._peerConnections) { - if (self._peerConnections.hasOwnProperty(peer)) { - self._restartPeerConnection(peer, true, false, null, true); - } - } - self._trigger('peerUpdated', self._user.sid, self.getPeerInfo(), true); - }); - // get the mediastream and then wait for it to be retrieved before sending - /*self._waitForLocalMediaStream(function () { + log.warn([null, 'MediaStream', streamId, tracksNotSameError]); - }, stream);*/ + var requireAudio = !!settings.settings.audio; + var requireVideo = !!settings.settings.video; - } else { - // update to mute status of video tracks - if (hasTracksOption.hasVideoTracks) { - // send message - self._sendChannelMessage({ - type: self._SIG_MESSAGE_TYPE.MUTE_VIDEO, - mid: self._user.sid, - rid: self._room.id, - muted: self._mediaStreamsStatus.videoMuted - }); + if (settings.settings.audio && stream.getAudioTracks().length === 0) { + settings.settings.audio = false; } - // update to mute status of audio tracks - if (hasTracksOption.hasAudioTracks) { - // send message - // set timeout to do a wait interval of 1s - setTimeout(function () { - self._sendChannelMessage({ - type: self._SIG_MESSAGE_TYPE.MUTE_AUDIO, - mid: self._user.sid, - rid: self._room.id, - muted: self._mediaStreamsStatus.audioMuted - }); - }, 1050); + + if (settings.settings.video && stream.getVideoTracks().length === 0) { + settings.settings.video = false; } - self._trigger('peerUpdated', self._user.sid, self.getPeerInfo(), true); + + self._trigger('mediaAccessFallback', { + error: new Error(tracksNotSameError), + diff: { + video: { expected: requireVideo ? 1 : 0, received: stream.getVideoTracks().length }, + audio: { expected: requireAudio ? 1 : 0, received: stream.getAudioTracks().length } + } + }, self.MEDIA_ACCESS_FALLBACK_STATE.FALLBACKED, !!isScreenSharing, !!isAudioFallback, streamId); } -}; -/** - * Enable microphone. - * - Try to start the audio source. - * - If no audio source was initialy set, this function has no effect. - * - If you want to activate your audio but haven't initially enabled it you would need to - * reinitiate your connection with - * {{#crossLink "Skylink/joinRoom:method"}}joinRoom(){{/crossLink}} - * process and set the audio parameter to true. - * @method enableAudio - * @trigger peerUpdated, peerRestart - * @deprecated - * @example - * SkylinkDemo.enableAudio(); - * @component Stream - * @for Skylink - * @since 0.5.5 - */ -Skylink.prototype.enableAudio = function() { - this.muteStream({ - audioMuted: false - }); + self._streams[ isScreenSharing ? 'screenshare' : 'userMedia' ] = { + stream: stream, + settings: settings.settings, + constraints: settings.getUserMediaSettings + }; + self._muteStreams(); + self._trigger('mediaAccessSuccess', stream, !!isScreenSharing, !!isAudioFallback, streamId); }; /** - * Disable microphone. - * - Try to disable the microphone. - * - If no microphone was initially set, this function has no effect. - * @method disableAudio - * @example - * SkylinkDemo.disableAudio(); - * @trigger peerUpdated, peerRestart - * @deprecated - * @component Stream + * Function that handles the native navigator.getUserMedia() API failure callback result. + * @method _onStreamAccessError + * @private * @for Skylink - * @since 0.5.5 + * @since 0.6.15 */ -Skylink.prototype.disableAudio = function() { - this.muteStream({ - audioMuted: true - }); -}; +Skylink.prototype._onStreamAccessError = function(error, settings, isScreenSharing) { + var self = this; -/** - * Enable webcam video. - * - Try to start the video source. - * - If no video source was initialy set, this function has no effect. - * - If you want to activate your video but haven't initially enabled it you would need to - * reinitiate your connection with - * {{#crossLink "Skylink/joinRoom:method"}}joinRoom(){{/crossLink}} - * process and set the video parameter to true. - * @method enableVideo - * @example - * SkylinkDemo.enableVideo(); - * @trigger peerUpdated, peerRestart - * @deprecated - * @component Stream - * @for Skylink - * @since 0.5.5 - */ -Skylink.prototype.enableVideo = function() { - this.muteStream({ - videoMuted: false - }); + if (!isScreenSharing && settings.settings.audio && settings.settings.video && self._audioFallback) { + log.debug('Fallbacking to retrieve audio only Stream'); + + self._trigger('mediaAccessFallback', { + error: error, + diff: null + }, self.MEDIA_ACCESS_FALLBACK_STATE.FALLBACKING, false, true); + + navigator.getUserMedia({ + audio: true + }, function (stream) { + self._onStreamAccessSuccess(stream, settings, false, true); + + }, function (error) { + log.error('Failed fallbacking to retrieve audio only Stream ->', error); + + self._trigger('mediaAccessError', error, false, true); + self._trigger('mediaAccessFallback', { + error: error, + diff: null + }, self.MEDIA_ACCESS_FALLBACK_STATE.ERROR, false, true); + }); + return; + } + + log.error('Failed retrieving ' + (isScreenSharing ? 'screensharing' : 'camera') + ' Stream ->', error); + + self._trigger('mediaAccessError', error, !!isScreenSharing, false); }; /** - * Disable video source. - * - Try to disable the video source. - * - If no video source was initially set, this function has no effect. - * @method disableVideo - * @example - * SkylinkDemo.disableVideo(); - * @trigger peerUpdated, peerRestart - * @deprecated - * @component Stream + * Function that handles the RTCPeerConnection.onaddstream remote MediaStream received. + * @method _onRemoteStreamAdded + * @private * @for Skylink - * @since 0.5.5 + * @since 0.5.2 */ -Skylink.prototype.disableVideo = function() { - this.muteStream({ - videoMuted: true - }); +Skylink.prototype._onRemoteStreamAdded = function(targetMid, stream, isScreenSharing) { + var self = this; + + if (!self._peerInformations[targetMid]) { + log.warn([targetMid, 'MediaStream', stream.id, + 'Received remote stream when peer is not connected. ' + + 'Ignoring stream ->'], stream); + return; + } + + /*if (!self._peerInformations[targetMid].settings.audio && + !self._peerInformations[targetMid].settings.video && !isScreenSharing) { + log.log([targetMid, 'MediaStream', stream.id, + 'Receive remote stream but ignoring stream as it is empty ->' + ], stream); + return; + }*/ + log.log([targetMid, 'MediaStream', stream.id, 'Received remote stream ->'], stream); + + if (isScreenSharing) { + log.log([targetMid, 'MediaStream', stream.id, 'Peer is having a screensharing session with user']); + } + + self._trigger('incomingStream', targetMid, stream, false, self.getPeerInfo(targetMid)); + self._trigger('peerUpdated', targetMid, self.getPeerInfo(targetMid), false); }; /** - * Shares the current screen with users. - * - You will require our own Temasys Skylink extension to do screensharing. - * Currently, opera does not support this feature. - * @method shareScreen - * @param {Function} [callback] The callback fired after media was successfully accessed. - * Default signature: function(error object, success object) - * @example - * // Example 1: Share the screen - * SkylinkDemo.shareScreen(); - * - * // Example 2: Share screen with callback when screen is ready and shared - * SkylinkDemo.shareScreen(function(error,success){ - * if (error){ - * console.log(error); - * } - * else{ - * console.log(success); - * } - * }); - * @trigger mediaAccessSuccess, mediaAccessError, incomingStream - * @component Stream + * Function that sets User's Stream to send to Peer connection. + * Priority for shareScreen() Stream over getUserMedia() Stream. + * @method _addLocalMediaStreams + * @private * @for Skylink - * @since 0.5.11 + * @since 0.5.2 */ -Skylink.prototype.shareScreen = function (callback) { +Skylink.prototype._addLocalMediaStreams = function(peerId) { var self = this; - var constraints = { - video: { - mediaSource: 'window' - }, - audio: false - }; - - if (window.webrtcDetectedBrowser === 'firefox') { - constraints.audio = true; - } - + // NOTE ALEX: here we could do something smarter + // a mediastream is mainly a container, most of the info + // are attached to the tracks. We should iterates over track and print try { - window.getUserMedia(constraints, function (stream) { + log.log([peerId, null, null, 'Adding local stream']); - if (window.webrtcDetectedBrowser !== 'firefox') { - window.getUserMedia({ - audio: true - }, function (audioStream) { - try { - audioStream.addTrack(stream.getVideoTracks()[0]); - self._mediaScreenClone = stream; - self._onUserMediaSuccess(audioStream, true); + var pc = self._peerConnections[peerId]; - } catch (error) { - log.warn('This screensharing session will not support audio streaming', error); - self._onUserMediaSuccess(stream, true); + if (pc) { + if (pc.signalingState !== self.PEER_CONNECTION_STATE.CLOSED) { + // Updates the streams accordingly + var updateStreamFn = function (updatedStream) { + var hasStream = false; + + // remove streams + var streams = pc.getLocalStreams(); + for (var i = 0; i < streams.length; i++) { + if (updatedStream !== null && streams[i].id === updatedStream.id) { + hasStream = true; + continue; + } + // try removeStream + pc.removeStream(streams[i]); } - }, function (error) { - log.warn('This screensharing session will not support audio streaming', error); + if (updatedStream !== null && !hasStream) { + pc.addStream(updatedStream); + } + }; - self._onUserMediaSuccess(stream, true); - }); - } else { - self._onUserMediaSuccess(stream, true); - } + if (self._streams.screenshare && self._streams.screenshare.stream) { + log.debug([peerId, 'MediaStream', null, 'Sending screen'], self._streams.screenshare.stream); + + updateStreamFn(self._streams.screenshare.stream); + + } else if (self._streams.userMedia && self._streams.userMedia.stream) { + log.debug([peerId, 'MediaStream', null, 'Sending stream'], self._streams.userMedia.stream); + + updateStreamFn(self._streams.userMedia.stream); - self._wait(function () { - if (self._inRoom) { - for (var peer in self._peerConnections) { - if (self._peerConnections.hasOwnProperty(peer)) { - self._restartPeerConnection(peer, true, false, null, true); - } - } } else { - if (typeof callback === 'function') { - callback(null, stream); - } - } - }, function () { - return self._mediaScreen && self._mediaScreen !== null; - }); + log.warn([peerId, 'MediaStream', null, 'No media to send. Will be only receiving']); - }, function (error) { - self._onUserMediaError(error, true); + updateStreamFn(null); + } - if (typeof callback === 'function') { - callback(error, null); + } else { + log.warn([peerId, 'MediaStream', null, + 'Not adding any stream as signalingState is closed']); } - }); - + } else { + log.warn([peerId, 'MediaStream', self._mediaStream, + 'Not adding stream as peerconnection object does not exists']); + } } catch (error) { - self._onUserMediaError(error, true); - - if (typeof callback === 'function') { - callback(error, null); + if ((error.message || '').indexOf('already added') > -1) { + log.warn([peerId, null, null, 'Not re-adding stream as LocalMediaStream is already added'], error); + } else { + // Fix errors thrown like NS_ERROR_UNEXPECTED + log.error([peerId, null, null, 'Failed adding local stream'], error); } } + + setTimeout(function () { + var streamId = null; + + if (self._streams.screenshare && self._streams.screenshare.stream) { + streamId = self._streams.screenshare.stream.id || self._streams.screenshare.stream.label; + } else if (self._streams.userMedia && self._streams.userMedia.stream) { + streamId = self._streams.userMedia.stream.id || self._streams.userMedia.stream.label; + } + + if (self._inRoom) { + self._sendChannelMessage({ + type: self._SIG_MESSAGE_TYPE.STREAM, + mid: self._user.sid, + rid: self._room.id, + cid: self._key, + sessionType: self._streams.screenshare && self._streams.screenshare.stream ? 'screensharing' : 'stream', + streamId: streamId, + status: 'check' + }); + } + }, 3500); }; +Skylink.prototype._selectedAudioCodec = 'auto'; /** - * Stops screensharing playback and streaming. - * @method stopScreen + * Stores the preferred sending Peer connection streaming video codec. + * @attribute _selectedVideoCodec + * @type String + * @default "auto" + * @private * @for Skylink - * @since 0.5.11 + * @since 0.5.10 */ -Skylink.prototype.stopScreen = function () { - var endSession = false; - - if (this._mediaScreen && this._mediaScreen !== null) { - endSession = !!this._mediaScreen.endSession; - this._mediaScreen.stop(); - } - - if (this._mediaScreenClone && this._mediaScreenClone !== null) { - this._mediaScreenClone.stop(); - } - - if (this._mediaScreen && this._mediaScreen !== null) { - this._trigger('mediaAccessStopped', true); - this._mediaScreen = null; - this._mediaScreenClone = null; - - if (!endSession) { - this._trigger('incomingStream', this._user.sid, this._mediaStream, true, - this.getPeerInfo(), false); +Skylink.prototype._selectedVideoCodec = 'auto'; - for (var peer in this._peerConnections) { - if (this._peerConnections.hasOwnProperty(peer)) { - this._restartPeerConnection(peer, true, false, null, true); - } - } - } - } -}; +/** + * Function that modifies the SessionDescription string to enable OPUS stereo. + * @method _addSDPStereo + * @private + * @for Skylink + * @since 0.5.10 + */ Skylink.prototype._addSDPStereo = function(sdpLines) { var opusRtmpLineIndex = 0; var opusLineFound = false; @@ -8613,20 +12729,16 @@ Skylink.prototype._addSDPStereo = function(sdpLines) { return sdpLines; }; - /** - * Sets the video resolution by modifying the SDP. - * - This is broken. + * Function that modifies the SessionDescription string to set the video resolution. + * This is not even supported in the specs, and we should re-evalute it to be removed. * @method _setSDPVideoResolution - * @param {Array} sdpLines Sdp received. - * @return {Array} Updated version with custom Resolution settings * @private - * @component SDP * @for Skylink * @since 0.5.10 */ Skylink.prototype._setSDPVideoResolution = function(sdpLines){ - var video = this._streamSettings.video; + var video = this._streams.userMedia && this._streams.userMedia.settings.video; var frameRate = video.frameRate || 50; var resolution = { width: 320, @@ -8703,76 +12815,96 @@ Skylink.prototype._setSDPVideoResolution = function(sdpLines){ }; /** - * Set the audio, video and data streamming bandwidth by modifying the SDP. - * It sets the bandwidth when the connection is good. In low bandwidth environment, - * the bandwidth is managed by the browser. + * Function that modifies the SessionDescription string to set the sending bandwidth. + * Setting this may not necessarily work in Firefox. * @method _setSDPBitrate - * @param {Array} sdpLines The session description received. - * @return {Array} Updated session description. * @private - * @component SDP * @for Skylink * @since 0.5.10 */ Skylink.prototype._setSDPBitrate = function(sdpLines, settings) { // Find if user has audioStream - var bandwidth = this._streamSettings.bandwidth; - var hasAudio = !!(settings || {}).audio; - var hasVideo = !!(settings || {}).video; - - var i, j, k; + var bandwidth = this._streamsBandwidthSettings; - var audioIndex = 0; - var videoIndex = 0; - var dataIndex = 0; + // Prevent setting of bandwidth audio if not configured + if (typeof bandwidth.audio === 'number' && bandwidth.audio > 0) { + var hasSetAudio = false; - var audioLineFound = false; - var videoLineFound = false; - var dataLineFound = false; - - for (i = 0; i < sdpLines.length; i += 1) { - // set the audio bandwidth - if (sdpLines[i].indexOf('a=audio') === 0 || sdpLines[i].indexOf('m=audio') === 0) { + for (var i = 0; i < sdpLines.length; i += 1) { + // set the audio bandwidth + if (sdpLines[i].indexOf('m=audio') === 0) { + //if (sdpLines[i].indexOf('a=audio') === 0 || sdpLines[i].indexOf('m=audio') === 0) { + sdpLines.splice(i + 1, 0, window.webrtcDetectedBrowser === 'firefox' ? + 'b=TIAS:' + (bandwidth.audio * 1024) : 'b=AS:' + bandwidth.audio); - sdpLines.splice(i + 1, 0, 'b=AS:' + bandwidth.audio); + log.info([null, 'SDP', null, 'Setting maximum sending audio bandwidth bitrate @(index:' + i + ') -> '], bandwidth.audio); + hasSetAudio = true; + break; + } + } - log.debug([null, 'SDP', null, 'Setting audio bitrate (' + - bandwidth.audio + ')'], i); - break; + if (!hasSetAudio) { + log.warn([null, 'SDP', null, 'Not setting maximum sending audio bandwidth bitrate as m=audio line is not found']); } + } else { + log.warn([null, 'SDP', null, 'Not setting maximum sending audio bandwidth bitrate and leaving to browser\'s defaults']); } - for (j = 0; j < sdpLines.length; j += 1) { - // set the video bandwidth - if (sdpLines[j].indexOf('a=video') === 0 || sdpLines[j].indexOf('m=video') === 0) { - sdpLines.splice(j + 1, 0, 'b=AS:' + bandwidth.video); + // Prevent setting of bandwidth video if not configured + if (typeof bandwidth.video === 'number' && bandwidth.video > 0) { + var hasSetVideo = false; - log.debug([null, 'SDP', null, 'Setting video bitrate (' + - bandwidth.video + ')'], j); - break; + for (var j = 0; j < sdpLines.length; j += 1) { + // set the video bandwidth + if (sdpLines[j].indexOf('m=video') === 0) { + //if (sdpLines[j].indexOf('a=video') === 0 || sdpLines[j].indexOf('m=video') === 0) { + sdpLines.splice(j + 1, 0, window.webrtcDetectedBrowser === 'firefox' ? + 'b=TIAS:' + (bandwidth.video * 1024) : 'b=AS:' + bandwidth.video); + + log.info([null, 'SDP', null, 'Setting maximum sending video bandwidth bitrate @(index:' + j + ') -> '], bandwidth.video); + hasSetVideo = true; + break; + } } + + if (!hasSetVideo) { + log.warn([null, 'SDP', null, 'Not setting maximum sending video bandwidth bitrate as m=video line is not found']); + } + } else { + log.warn([null, 'SDP', null, 'Not setting maximum sending video bandwidth bitrate and leaving to browser\'s defaults']); } - for (k = 0; k < sdpLines.length; k += 1) { - // set the data bandwidth - if (sdpLines[k].indexOf('a=application') === 0 || sdpLines[k].indexOf('m=application') === 0) { - sdpLines.splice(k + 1, 0, 'b=AS:' + bandwidth.data); + // Prevent setting of bandwidth data if not configured + if (typeof bandwidth.data === 'number' && bandwidth.data > 0) { + var hasSetData = false; - log.debug([null, 'SDP', null, 'Setting data bitrate (' + - bandwidth.data + ')'], k); - break; + for (var k = 0; k < sdpLines.length; k += 1) { + // set the data bandwidth + if (sdpLines[k].indexOf('m=application') === 0) { + //if (sdpLines[k].indexOf('a=application') === 0 || sdpLines[k].indexOf('m=application') === 0) { + sdpLines.splice(k + 1, 0, window.webrtcDetectedBrowser === 'firefox' ? + 'b=TIAS:' + (bandwidth.data * 1024) : 'b=AS:' + bandwidth.data); + + log.info([null, 'SDP', null, 'Setting maximum sending data bandwidth bitrate @(index:' + k + ') -> '], bandwidth.data); + hasSetData = true; + break; + } + } + + if (!hasSetData) { + log.warn([null, 'SDP', null, 'Not setting maximum sending data bandwidth bitrate as m=application line is not found']); } + } else { + log.warn([null, 'SDP', null, 'Not setting maximum sending data bandwidth bitrate and leaving to browser\'s defaults']); } + return sdpLines; }; /** - * Sets the audio codec for the connection, + * Function that modifies the SessionDescription string to set the preferred sending video codec. * @method _setSDPVideoCodec - * @param {Array} sdpLines The session description received. - * @return {Array} Updated session description. * @private - * @component SDP * @for Skylink * @since 0.5.2 */ @@ -8823,12 +12955,9 @@ Skylink.prototype._setSDPVideoCodec = function(sdpLines) { }; /** - * Sets the audio codec for the connection, + * Function that modifies the SessionDescription string to set the preferred sending audio codec. * @method _setSDPAudioCodec - * @param {Array} sdpLines The session description received. - * @return {Array} Updated session description. * @private - * @component SDP * @for Skylink * @since 0.5.2 */ @@ -8878,13 +13007,11 @@ Skylink.prototype._setSDPAudioCodec = function(sdpLines) { }; /** - * Removes Firefox 32 H262 preference in the SDP to prevent breaking connection in - * unsupported browsers. + * Function that modifies the SessionDescription string to remove the experimental H264 Firefox flag + * that is breaking connections. + * To evaluate removal of this change once we roll out H264 codec interop. * @method _removeSDPFirefoxH264Pref - * @param {Array} sdpLines The session description received. - * @return {Array} Updated session description. * @private - * @component SDP * @for Skylink * @since 0.5.2 */ @@ -8897,5 +13024,87 @@ Skylink.prototype._removeSDPFirefoxH264Pref = function(sdpLines) { } return sdpLines; }; -window.Skyway = window.Skylink = Skylink; -}).call(this); + +/** + * Function that modifies the SessionDescription string to set with the correct MediaStream ID and + * MediaStreamTrack IDs that is not provided from Firefox connection to Chromium connection. + * @method _addSDPSsrcFirefoxAnswer + * @private + * @for Skylink + * @since 0.6.6 + */ +Skylink.prototype._addSDPSsrcFirefoxAnswer = function (targetMid, sdp) { + var self = this; + var agent = self.getPeerInfo(targetMid).agent; + + var pc = self._peerConnections[targetMid]; + + if (!pc) { + log.error([targetMid, 'RTCSessionDesription', 'answer', 'Peer connection object ' + + 'not found. Unable to parse answer session description for peer']); + return; + } + + var updatedSdp = sdp; + + // for this case, this is because firefox uses Unified Plan and Chrome uses + // Plan B. we have to remodify this a bit to let the non-ff detect as new mediastream + // as chrome/opera/safari detects it as default due to missing ssrc specified as used in plan B. + if (window.webrtcDetectedBrowser === 'firefox' && agent.name !== 'firefox' && + //pc.remoteDescription.sdp.indexOf('a=msid-semantic: WMS *') === -1 && + updatedSdp.indexOf('a=msid-semantic:WMS *') > 0) { + // start parsing + var sdpLines = updatedSdp.split('\r\n'); + var streamId = ''; + var replaceSSRCSemantic = -1; + var i; + var trackId = ''; + + var parseTracksSSRC = function (track) { + for (i = 0, trackId = ''; i < sdpLines.length; i++) { + if (!!trackId) { + if (sdpLines[i].indexOf('a=ssrc:') === 0) { + var ssrcId = sdpLines[i].split(':')[1].split(' ')[0]; + sdpLines.splice(i+1, 0, 'a=ssrc:' + ssrcId + ' msid:' + streamId + ' ' + trackId, + 'a=ssrc:' + ssrcId + ' mslabel:default', + 'a=ssrc:' + ssrcId + ' label:' + trackId); + break; + } else if (sdpLines[i].indexOf('a=mid:') === 0) { + break; + } + } else if (sdpLines[i].indexOf('a=msid:') === 0) { + if (i > 0 && sdpLines[i-1].indexOf('a=mid:' + track) === 0) { + var parts = sdpLines[i].split(':')[1].split(' '); + + streamId = parts[0]; + trackId = parts[1]; + replaceSSRCSemantic = true; + } + } + } + }; + + parseTracksSSRC('video'); + parseTracksSSRC('audio'); + + /*if (replaceSSRCSemantic) { + for (i = 0; i < sdpLines.length; i++) { + if (sdpLines[i].indexOf('a=msid-semantic:WMS ') === 0) { + var parts = sdpLines[i].split(' '); + parts[parts.length - 1] = streamId; + sdpLines[i] = parts.join(' '); + break; + } + } + + }*/ + updatedSdp = sdpLines.join('\r\n'); + + log.debug([targetMid, 'RTCSessionDesription', 'answer', 'Parsed remote description from firefox'], sdpLines); + } + + return updatedSdp; +}; +this.Skylink = Skylink; +window.Skylink = Skylink; +}).call(this); \ No newline at end of file diff --git a/source/jsx/loader.jsx b/source/jsx/loader.jsx index e993033..e06ce42 100644 --- a/source/jsx/loader.jsx +++ b/source/jsx/loader.jsx @@ -10,8 +10,8 @@ require.config({ paths: { socketio: '//cdn.temasys.com.sg/libraries/socket.io-client/1.4.4/socket.io', - adapter: '//cdn.temasys.com.sg/adapterjs/0.13.3/adapter.screenshare', - skylink: '//cdn.temasys.com.sg/skylink/skylinkjs/0.6.14/skylink.debug', + adapter: '//cdn.temasys.com.sg/adapterjs/0.13.4/adapter.screenshare', + skylink: '//cdn.temasys.com.sg/skylink/skylinkjs/0.6.15/skylink.debug', // facebook: '//connect.facebook.net/en_US/all', // twitter: '//platform.twitter.com/widgets', fastclick: '//cdnjs.cloudflare.com/ajax/libs/fastclick/0.6.11/fastclick.min',
init()
joinRoom()
"messaging"
sendP2PMessage()
peerConnectionState
state
CLOSED
"data"
sendURLData()
sendBlobData()
dataTransferState
DOWNLOAD_COMPLETED
UPLOAD_COMPLETED
REJECTED
CANCEL
ERROR
"main"
49152
16384
ARRAY_BUFFER
BLOB
1
data
ackN
DATA
"upload"
"download"
"blob"
"dataURL"
method_sendBlobData()
"request"
acceptDataTransfer()
"uploadStarted"
cancelDataTransfer()
"downloadStarted"
"rejected"
"uploading"
"downloading"
"uploadCompleted"
"downloadCompleted"
"cancel"
"error"
-1
0
setTimeout
false
+ * Note that Android and iOS SDKs do not support simultaneous data transfers. + *
function (error, success)
null
+ * Deprecation Warning! This property has been deprecated. + * Please use callback.error.transferInfo.transferId instead. + *
callback.error.transferInfo.transferId
+ * Deprecation Warning! This property has been deprecated. + * Please use dataTransferState + * event instead.
+ * Deprecation Warning! This property has been deprecated. + * Please use callback.error.listOfPeers instead. + *
callback.error.listOfPeers
+ * Deprecation Warning! This property has been deprecated. + * Please use callback.error.transferInfo.isPrivate instead. + *
callback.error.transferInfo.isPrivate
targetPeerId
+ * Deprecation Warning! This property has been deprecated. + * Please use callback.error.transferErrors instead. + *
callback.error.transferErrors
#peerId
"self"
transferInfo
+ * Deprecation Warning! This property has been deprecated. + * Please use callback.success.transferInfo.transferId instead. + *
callback.success.transferInfo.transferId
+ * Deprecation Warning! This property has been deprecated. + * Please use callback.success.listOfPeers instead. + *
callback.success.listOfPeers
+ * Deprecation Warning! This property has been deprecated. + * Please use callback.success.transferInfo.isPrivate instead. + *
callback.success.transferInfo.isPrivate
dataChannelState
CONNECTING
channelType
OPEN
MESSAGING
UPLOAD_REQUEST
incomingDataRequest
accept
true
UPLOAD_STARTED
DOWNLOAD_STARTED
UPLOADING
DOWNLOADING
incomingData
"WRQ"
+ * Deprecation Warning! This method has been deprecated, please use + * acceptDataTransfer() method instead. + *
>0
"ACK"
"MESSAGE"
+ * Deprecation Warning! This method has been deprecated, please use + * method_cancelDataTransfer() method instead. + *
method_cancelDataTransfer()
"ERROR"
"CANCEL"
incomingMessage
message.isDataChannel
isSelf
+ * Currently, the Android and iOS SDKs do not support this type of data transfer. + *
16Kb
+ * Note that configuring the protocol may not necessarily result in the desired network transports protocol + * used in the actual TURN network traffic as it depends which protocol the browser selects and connects with. + * This simply configures the TURN ICE server urls query option when constructing + * the Peer connection. When all protocols are selected, the ICE servers urls are duplicated with all protocols. + *
query option when constructing + * the Peer connection. When all protocols are selected, the ICE servers urls are duplicated with all protocols. + *
.urls
"turn:server.com?transport=tcp"
"turn:server1.com:3478"
"turn:server.com?transport=udp"
"tcp"
"turn:server1.com:3478?transport=tcp"
"udp"
"turn:server1.com:3478?transport=udp"
"any"
"none"
"turn:server.com"
?transport=(protocol)
"all"
?transport=protocol
getConnectionStatus()
+ * As there are more features getting implemented, there will be eventually more different types of + * server Peers. + *
"mcu"
refreshConnection
+ * For MCU enabled Peer connections, the restart functionality may differ, you may learn more about how to workaround + * it in this article here. + * For restarts with Peers connecting from Android or iOS SDKs, restarts might not work as written in + * in this article here. + * Note that this functionality should be used when Peer connection stream freezes during a connection, + * and is throttled when invoked many times in less than 3 seconds interval. + *
+ * Note that this is ignored if MCU is enabled for the App Key provided in + * init() method. refreshConnection() will "refresh" + * all Peer connections. See the Event Sequence for more information.
refreshConnection()
peerRestart
isSelfInitiateRestart
serverPeerRestart
getConnectionStatusStateChange
RETRIEVE_SUCCESS
stats
RETRIEVE_ERROR
RETRIEVING
_restartPeerConnection
""
peerUpdated
peerInfo
peerJoined
+ * Note that this feature requires "isPrivileged" flag to be enabled and + * "autoIntroduce" flag to be disabled for the App Key provided in the + * init() method, as only Users connecting using + * the App Key with this flag enabled (which we call privileged Users / Peers) can retrieve the list of + * Peer IDs from Rooms within the same App space. + * + * Read more about privileged App Key feature here. + *
"isPrivileged"
"autoIntroduce"
introducePeer
"enquired"
getPeers()
+ * Note that this feature requires "isPrivileged" flag to be enabled for the App Key + * provided in the init() method, as only Users connecting using + * the App Key with this flag enabled (which we call privileged Users / Peers) can retrieve the list of + * Peer IDs from Rooms within the same App space. + * + * Read more about privileged App Key feature here. + *
getPeersStateChange
RECEIVED
peerList
ENQUIRED
receivingPeerId
sendingPeerId
introduceStateChange
INTRODUCING
"oldTimeStamp"
REJECT
"credentialError"
"duplicatedLogin"
"notStart"
"expired"
"locked"
"fastmsg"
sendMessage()
setUserData()
muteStream()
enableAudio()
enableVideo()
disableAudio()
disableVideo()
WARNING
"toClose"
ROOM_CLOSED
"roomclose"
"serverError"
"keyFailed"
options.defaultRoom
getUserMedia()
options.useExactConstraints
options.audio
options.video
Note that this is currently not supported + * with Firefox browsers versions 48 and below as noted in an existing + * bugzilla ticket here.
50
200
256
500
1024
2048
4096
mediaAccessRequired
shareScreen()
leaveRoom()
stopMediaOptions
socketError
errorCode
CONNECTION_FAILED
channelRetry
RECONNECTION_ATTEMPT
RECONNECTION_FAILED
RECONNECTION_ABORTED
CONNECTION_ABORTED
channelOpen
options.manualGetUserMedia
serverPeerJoined
serverPeerType
MCU
incomingStream
stream
systemAction
action
stopMediaOptions.userMedia
stopMediaOptions.screenshare
stopStream()
stopScreen()
peerLeft
serverPeerLeft
channelClose
+ * Note that broadcasted events from muteStream() method, + * stopStream() method, + * stopScreen() method, + * sendMessage() method, + * unlockRoom() method and + * lockRoom() method may be queued when + * sent within less than an interval. + *
unlockRoom()
lockRoom()
roomLock
isLocked
4001
4002
"domainName"
4003
"corsurl"
4004
options.credentials.credentials
4005
options.credentials.startDateTime
options.credentials.duration
"secret"
"secret
4006
4010
options.credentials
4020
4021
5005
roomServer
2
3
4
7
Deprecation Warning! + * This constant has been deprecated.Automatic nearest regional server has been implemented + * on the platform. + *
"sg"
"us2"
options.appKey
room
defaultRoom
Deprecation Warning! + * This option has been deprecated.Automatic nearest regional server has been implemented + * on the platform.
options.enableTURNServer
options.enableSTUNServer
"relay"
?transport=
ANY
To generate the credentials: + * Concatenate a string that consists of the Room name you provide in the options.defaultRoom, + * the options.credentials.duration and the options.credentials.startDateTime. + * Example: var concatStr = defaultRoom + "_" + duration + "_" + startDateTime; + * Hash the concatenated string with the App Key "secret" property using + * SHA-1. + * Example: var hash = CryptoJS.HmacSHA1(concatStr, appKeySecret); + * See the CryptoJS.HmacSHA1 library. + * Encode the hashed string using base64 + * Example: var b64Str = hash.toString(CryptoJS.enc.Base64); + * See the CryptoJS.enc.Base64 library. + * Encode the base64 encoded string to replace special characters using UTF-8 encoding. + * Example: var credentials = encodeURIComponent(base64String); + * See encodeURIComponent() API.
var concatStr = defaultRoom + "_" + duration + "_" + startDateTime;
var hash = CryptoJS.HmacSHA1(concatStr, appKeySecret);
CryptoJS.HmacSHA1
var b64Str = hash.toString(CryptoJS.enc.Base64);
var credentials = encodeURIComponent(base64String);
window.location.protocol
"http:"
"https:"
+ * Note that if the audio codec is not supported, the SDK will not configure the local "offer" or + * "answer" session description to prefer the codec.
"offer"
"answer"
AUTO
+ * Note that if the video codec is not supported, the SDK will not configure the local "offer" or + * "answer" session description to prefer the codec.
5000
+ * Note that currently Firefox does not support the TURNS protocol, and that if TURNS is required, + * TURN ICE servers using port 443 will be used instead.
443
readyStateChange
COMPLETED
error.errorCode
error.content
error.status
options.roomServer
options.enableIceTrickle
options.enableDataChannel
options.TURNServerTransport
options.region
options.audioFallback
options.forceSSL
options.audioCodec
options.videoCodec
options.socketTimeout
options.forceTURNSSL
options.forceTURN
options.usePublicSTUN
options
ADAPTER_NO_LOADED
NO_SOCKET_IO
NO_XMLHTTPREQUEST_SUPPORT
NO_WEBRTC_SUPPORT
LOADING
0.5
1.4
console.trace
+ * To utilise and enable the SkylinkLogs API functionalities, the + * setDebugMode() method + * options.storeLogs parameter has to be enabled. + *
SkylinkLogs
setDebugMode()
options.storeLogs
console
<#index>
The stored log item.
The DateTime of when the log was stored.
The log level. [Rel: Skylink.LOG_LEVEL]
The log message.
The log message object. + *
options.trace
console.trace()
Function
setLogLevel()
console.log()
+ * Note that this is used only for SDK developer purposes. + *
+ * This may be caused by Javascript errors in the event listener when subscribing to events. + * It may be resolved by checking for code errors in your Web App in the event subscribing listener. + * skylinkDemo.on("eventName", function () { // Errors here }); + *
skylinkDemo.on("eventName", function () { // Errors here });
FALLBACKED
attachMediaStream(videoElement, stream);
OPUS
navigator.getUserMedia()
audio: { optional [..] }
peerInfo.settings.audio.deviceId
video: { optional [..] }
peerInfo.settings.video.resolution
peerInfo.settings.video.frameRate
peerInfo.settings.video.deviceId
peerInfo.settings.audio
peerInfo.settings.video
#room
introducePeer()
"host"
"srflx"
once()
on()
eventName
._throttle(fn, 1000)()
"nonfallback"
"fallbackPortNonSSL"
"fallbackPortSSL"
"fallbackLongPollingNonSSL"
"fallbackLongPollingSSL"
https:
3443
http:
80
3000
20000
"WebSocket"
"Polling"
message
SM_PROTOCOL_VERSION
createOffer
createAnswer
"redirect"
"updateUserEvent"
"roomLockEvent"
"muteVideoEvent"
ended
"stream"
"bye"
"private"
"muteAudioEvent"
"inRoom"
"enter"
"restart"
Date.UTC()
>=0
-2
"welcome"
"candidate"
+ * Note that if the audio codec is not supported, the SDK will not configure the local "offer" or + * "answer" session description to prefer the codec. + *
"auto"
"opus"
"ISAC"
+ * Note that currently getUserMedia() method only configures + * the maximum resolution of the Stream due to browser interopability and support. + *
{ width: 160, height: 120 }
4:3
{ width: 240, height: 160 }
3:2
{ width: 320, height: 240 }
{ width: 384, height: 240 }
16:10
{ width: 480, height: 320 }
{ width: 640, height: 480 }
{ width: 768, height: 480 }
{ width: 854, height: 480 }
16:9
{ width: 800, height: 600 }
{ width: 960, height: 640 }
{ width: 1024, height: 576 }
{ width: 1280, height: 720 }
{ width: 1600, height: 900 }
{ width: 1920, height: 1080 }
{ width: 2560, height: 1440 }
{ width: 3200, height: 1800 }
{ width: 3840, height: 2160 }
{ width: 5120, height: 2880 }
{ width: 7680, height: 4320 }
{ width: 15360, height: 8640 }
audioFallback
{ audio: true, video: true }
+ * Note that by enabling this flag, exact values will be requested when retrieving camera Stream, + * but it does not prevent constraints related errors. By default when not enabled, + * expected mandatory maximum values (or optional values for source ID) will requested to prevent constraints related + * errors, with an exception for options.video.frameRate option in Safari and IE (plugin-enabled) browsers, + * where the expected maximum value will not be requested due to the lack of support.
options.video.frameRate
options.audio.deviceId
options.video.deviceId
options.video.resolution
peerInfo.mediaStatus.audioMuted
+ * Note that this may result in constraints related error when options.useExactConstraints value is + * true. If you are looking to set the requested source ID of the audio track, + * use options.audio.deviceId instead.
+ * Note this is currently not supported in Firefox browsers. + *
navigator.mediaDevices.enumerateDevices
peerInfo.mediaStatus.videoMuted
VGA
+ * Note that this may result in constraints related error when options.useExactConstraints value is + * true. If you are looking to set the requested source ID of the video track, + * use options.video.deviceId instead.
mediaAccessSuccess
isScreensharing
mediaAccessFallback
isAudioFallback
peerInfo.mediaStatus
getPeerInfo()
FALLBACKING
mediaAccessError
isAudioFallbackError
+ * Note that if shareScreen() Stream is available despite having + * getUserMedia() Stream available, the + * shareScreen() Stream is sent instead of the + * getUserMedia() Stream to Peers. + *
MediaStream
options.audio.mute
options.video.mute
.enabled
sendStream()
mediaAccessStopped
streamEnded
options.audioMuted
streamMuted
options.videoMuted
localMediaMuted
Deprecation Warning! + * This method has been deprecated. Use muteStream() method instead. + *
RTCPeerConnection.onaddstream