From e2077261485c980887dd434856eecd298e04d107 Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Mon, 27 Jul 2015 14:46:08 +0200 Subject: [PATCH] Updated WebRTC adapter to latest version. --- static/js/controllers/uicontroller.js | 10 +- static/js/libs/webrtc.adapter.js | 502 ++++++++++++++++++++------ static/js/mediastream/usermedia.js | 13 - static/js/services/constraints.js | 27 +- static/js/services/mediastream.js | 4 +- 5 files changed, 423 insertions(+), 133 deletions(-) diff --git a/static/js/controllers/uicontroller.js b/static/js/controllers/uicontroller.js index 9693366e..07fd3af1 100644 --- a/static/js/controllers/uicontroller.js +++ b/static/js/controllers/uicontroller.js @@ -727,15 +727,15 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web }); _.defer(function() { - if (!Modernizr.websockets) { - alertify.dialog.alert(translation._("Your browser is not supported. Please upgrade to a current version.")); - $scope.setStatus("unsupported"); - return; - } if (!$window.webrtcDetectedVersion) { alertify.dialog.custom("webrtcUnsupported"); return; } + if (!Modernizr.websockets || $window.webrtcDetectedVersion < $window.webrtcMinimumVersion) { + alertify.dialog.alert(translation._("Your browser is not supported. Please upgrade to a current version.")); + $scope.setStatus("unsupported"); + return; + } if (mediaStream.config.Renegotiation && $window.webrtcDetectedBrowser === "firefox" && $window.webrtcDetectedVersion < 38) { // See https://bugzilla.mozilla.org/show_bug.cgi?id=1017888 // and https://bugzilla.mozilla.org/show_bug.cgi?id=840728 diff --git a/static/js/libs/webrtc.adapter.js b/static/js/libs/webrtc.adapter.js index 8cf4a8f5..4ab95c73 100644 --- a/static/js/libs/webrtc.adapter.js +++ b/static/js/libs/webrtc.adapter.js @@ -30,30 +30,62 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var RTCPeerConnection = null; 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')) { + console.log.apply(console, arguments); + } + } +}; -if (navigator.mozGetUserMedia) { - console.log('This appears to be Firefox'); +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 = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10); + // the minimum firefox version still supported by adapter. + webrtcMinimumVersion = 31; + // The RTCPeerConnection object. window.RTCPeerConnection = function(pcConfig, pcConstraints) { - // .urls is not supported in FF yet. - if (pcConfig && pcConfig.iceServers) { - for (var i = 0; i < pcConfig.iceServers.length; i++) { - if (pcConfig.iceServers[i].hasOwnProperty('urls')) { - pcConfig.iceServers[i].url = pcConfig.iceServers[i].urls; - delete pcConfig.iceServers[i].urls; + 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); @@ -65,139 +97,407 @@ if (navigator.mozGetUserMedia) { // The RTCIceCandidate object. window.RTCIceCandidate = mozRTCIceCandidate; - // getUserMedia shim (only difference is the prefix). - // Code from Adam Barth. - getUserMedia = navigator.mozGetUserMedia.bind(navigator); - navigator.getUserMedia = getUserMedia; - - // Creates ICE server from the URL for FF. - window.createIceServer = function(url, username, password) { - var iceServer = null; - var urlParts = url.split(':'); - if (urlParts[0].indexOf('stun') === 0) { - // Create ICE server with STUN URL. - iceServer = { - 'url': url - }; - } else if (urlParts[0].indexOf('turn') === 0) { - if (webrtcDetectedVersion < 27) { - // Create iceServer with turn url. - // Ignore the transport parameter from TURN url for FF version <=27. - var turnUrlParts = url.split('?'); - // Return null for createIceServer if transport=tcp. - if (turnUrlParts.length === 1 || - turnUrlParts[1].indexOf('transport=udp') === 0) { - iceServer = { - 'url': turnUrlParts[0], - 'credential': password, - 'username': username - }; + // 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; } - } else { - // FF 27 and above supports transport parameters in TURN url, - // So passing in the full url to create iceServer. - iceServer = { - 'url': url, - 'credential': password, - 'username': username - }; + 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 iceServer; + return navigator.mozGetUserMedia(constraints, onSuccess, onError); }; - window.createIceServers = function(urls, username, password) { - var iceServers = []; - // Use .url for FireFox. - for (var i = 0; i < urls.length; i++) { - var iceServer = - window.createIceServer(urls[i], username, password); - if (iceServer !== null) { - iceServers.push(iceServer); - } - } - return iceServers; + 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().catch(function(e) { + if (e.name === 'NotFoundError') { + return []; + } + throw e; + }); + }; + } // Attach a media stream to an element. attachMediaStream = function(element, stream) { - console.log('Attaching media stream'); element.mozSrcObject = stream; }; reattachMediaStream = function(to, from) { - console.log('Reattaching media stream'); to.mozSrcObject = from.mozSrcObject; }; } else if (navigator.webkitGetUserMedia) { - console.log('This appears to be Chrome'); + webrtcUtils.log('This appears to be Chrome'); webrtcDetectedBrowser = 'chrome'; - // Temporary fix until crbug/374263 is fixed. - // Setting Chrome version to 999, if version is unavailable. - var result = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); - if (result !== null) { - webrtcDetectedVersion = parseInt(result[2], 10); - } else { - webrtcDetectedVersion = 999; - } - // Creates iceServer from the url for Chrome M33 and earlier. - window.createIceServer = function(url, username, password) { - var iceServer = null; - var urlParts = url.split(':'); - if (urlParts[0].indexOf('stun') === 0) { - // Create iceServer with stun url. - iceServer = { - 'url': url + // the detected chrome version. + webrtcDetectedVersion = + parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10); + + // the minimum chrome version still supported by adapter. + webrtcMinimumVersion = 38; + + // The RTCPeerConnection object. + window.RTCPeerConnection = function(pcConfig, pcConstraints) { + var pc = new webkitRTCPeerConnection(pcConfig, pcConstraints); + 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; }; - } else if (urlParts[0].indexOf('turn') === 0) { - // Chrome M28 & above uses below TURN format. - iceServer = { - 'url': url, - 'credential': password, - 'username': username + + 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) { + origGetStats.apply(self, [resolve, reject]); + }); + }; + + return pc; + }; + + // 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 iceServer; + return cc; }; - // Creates an ICEServer object from multiple URLs. - window.createIceServers = function(urls, username, password) { - return [{ - 'urls': urls, - 'credential': password, - 'username': username - }]; + 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; - // The RTCPeerConnection object. - RTCPeerConnection = function(pcConfig, pcConstraints) { - return new webkitRTCPeerConnection(pcConfig, pcConstraints); - }; + 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: ''}; + })); + }); + }); + }}; + } - // Get UserMedia (only difference is the prefix). - // Code from Adam Barth. - getUserMedia = navigator.webkitGetUserMedia.bind(navigator); - navigator.getUserMedia = getUserMedia; + // 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 (typeof element.srcObject !== 'undefined') { element.srcObject = stream; - } else if (typeof element.mozSrcObject !== 'undefined') { - element.mozSrcObject = stream; } else if (typeof element.src !== 'undefined') { element.src = URL.createObjectURL(stream); } else { - console.log('Error attaching stream to element.'); + webrtcUtils.log('Error attaching stream to element.'); } }; reattachMediaStream = function(to, from) { to.src = from.src; }; + +} else if (navigator.mediaDevices && navigator.userAgent.match( + /Edge\/(\d+).(\d+)$/)) { + webrtcUtils.log('This appears to be Edge'); + webrtcDetectedBrowser = 'edge'; + + webrtcDetectedVersion = + parseInt(navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)[2], 10); + + // the minimum version still supported by adapter. + webrtcMinimumVersion = 12; + + getUserMedia = navigator.getUserMedia; + + attachMediaStream = function(element, stream) { + element.srcObject = stream; + }; + reattachMediaStream = function(to, from) { + to.srcObject = from.srcObject; + }; } else { - console.log('Browser does not appear to be WebRTC-capable'); + 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 = {}; +Object.defineProperty(webrtcTesting, 'version', { + set: function(version) { + webrtcDetectedVersion = version; + } +}); + +if (typeof module !== 'undefined') { + var RTCPeerConnection; + if (typeof window !== 'undefined') { + RTCPeerConnection = window.RTCPeerConnection; + } + module.exports = { + RTCPeerConnection: RTCPeerConnection, + getUserMedia: getUserMedia, + attachMediaStream: attachMediaStream, + reattachMediaStream: reattachMediaStream, + webrtcDetectedBrowser: webrtcDetectedBrowser, + webrtcDetectedVersion: webrtcDetectedVersion, + webrtcMinimumVersion: webrtcMinimumVersion, + webrtcTesting: webrtcTesting + //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, + getUserMedia: getUserMedia, + attachMediaStream: attachMediaStream, + reattachMediaStream: reattachMediaStream, + webrtcDetectedBrowser: webrtcDetectedBrowser, + webrtcDetectedVersion: webrtcDetectedVersion, + webrtcMinimumVersion: webrtcMinimumVersion, + webrtcTesting: webrtcTesting + //requestUserMedia: not exposed on purpose. + //trace: not exposed on purpose. + }; + }); +} \ No newline at end of file diff --git a/static/js/mediastream/usermedia.js b/static/js/mediastream/usermedia.js index 2c9b1795..b6bf2cf0 100644 --- a/static/js/mediastream/usermedia.js +++ b/static/js/mediastream/usermedia.js @@ -83,19 +83,6 @@ define(['jquery', 'underscore', 'audiocontext', 'mediastream/dummystream', 'webr } } }); - if (window.webrtcDetectedBrowser === "firefox" && window.webrtcDetectedVersion < 38) { - // Firefox < 38 needs a extra require field. - var r = []; - if (c.height) { - r.push("height"); - } - if (c.width) { - r.push("width"); - } - if (r.length) { - c.require = r; - } - } return c; }; // Adapter to support navigator.mediaDevices API. diff --git a/static/js/services/constraints.js b/static/js/services/constraints.js index 29c3b02e..87fe4693 100644 --- a/static/js/services/constraints.js +++ b/static/js/services/constraints.js @@ -128,23 +128,24 @@ }; service.iceServers = function(constraints) { - + var createIceServers = function(urls, username, password) { + var s = { + urls: urls + } + if (username) { + s.username = username; + s.credential = password; + } + return s; + }; var iceServers = []; - var iceServer; if (service.stun && service.stun.length) { - iceServer = $window.createIceServers(service.stun); - if (iceServer.length) { - iceServers.push.apply(iceServers, iceServer) - } + iceServers.push(createIceServers(service.stun)); } if (service.turn && service.turn.urls && service.turn.urls.length) { - iceServer = $window.createIceServers(service.turn.urls, service.turn.username, service.turn.password); - if (iceServer.length) { - iceServers.push.apply(iceServers, iceServer) - } + iceServers.push(createIceServers(service.turn.urls, service.turn.username, service.turn.password)); } webrtc.settings.pcConfig.iceServers = iceServers; - }; // Some default constraints. @@ -191,6 +192,7 @@ supported: (function() { var isChrome = $window.webrtcDetectedBrowser === "chrome"; var isFirefox = $window.webrtcDetectedBrowser === "firefox"; + var isEdge = $window.webrtcDetectedBrowser === "edge"; var version = $window.webrtcDetectedVersion; // Constraints support table. return { @@ -201,7 +203,8 @@ // Chrome supports this on Windows only. renderToAssociatedSink: isChrome && $window.navigator.platform.indexOf("Win") === 0, chrome: isChrome, - firefox: isFirefox + firefox: isFirefox, + edge: isEdge }; })() }; diff --git a/static/js/services/mediastream.js b/static/js/services/mediastream.js index 27879784..a65426a5 100644 --- a/static/js/services/mediastream.js +++ b/static/js/services/mediastream.js @@ -46,8 +46,8 @@ define([ // Apply configuration details. webrtc.settings.renegotiation = context.Cfg.Renegotiation && true; - if (webrtc.settings.renegotiation && $window.webrtcDetectedBrowser === "firefox") { - console.warn("Disable renegotiation in Firefox for now."); + if (webrtc.settings.renegotiation && $window.webrtcDetectedBrowser !== "chrome") { + console.warn("Disable renegotiation in anything but Chrome for now."); webrtc.settings.renegotiation = false; }