From 76394c507e52e1ab7e48eb6de4f8b909820877e9 Mon Sep 17 00:00:00 2001 From: George Politis Date: Mon, 20 Oct 2014 10:24:13 +0200 Subject: [PATCH] Splits the simulcast management in two parts, a receiver and a sender. Splits the simulcast management in two classes, a SimulcastReceiver and a SimulcastSender. A SimulcastManager class manages the receiver and the sender. The SimulcastSender class specializes into the NativeSimulcastSender, SimpleSimulcastSender and NoSimulcastSender classes. --- index.html | 2 +- simulcast.js | 1167 +++++++++++++++++++++++++++----------------------- 2 files changed, 632 insertions(+), 537 deletions(-) diff --git a/index.html b/index.html index cd2d7d8f1..8171f3f3e 100644 --- a/index.html +++ b/index.html @@ -11,7 +11,7 @@ - + diff --git a/simulcast.js b/simulcast.js index 7e8aec041..8990a0cfe 100644 --- a/simulcast.js +++ b/simulcast.js @@ -2,495 +2,486 @@ /*jslint nomen: true*/ /** - * Created by gp on 11/08/14. * - * TODO(gp) split into SimulcastReceiver and SimulcastSender + * @constructor */ -function Simulcast() { - "use strict"; - - // TODO(gp) we need a logging framework for javascript à la log4j or the - // java logging framework that allows for selective log display - this.debugLvl = 0; +function SimulcastUtils() { + this.logger = new SimulcastLogger("SimulcastUtils"); } -Simulcast.prototype = { +/** + * + * @type {{}} + * @private + */ +SimulcastUtils.prototype._emptyCompoundIndex = {}; - // global state for all transformers. - _localVideoSourceCache: '', - _remoteVideoSourceCache: '', - _remoteMaps: { - msid2Quality: {}, - ssrc2Msid: {}, - receivingVideoStreams: {} - }, - _emptyCompoundIndex: {}, +/** + * + * @param lines + * @param videoSources + * @private + */ +SimulcastUtils.prototype._replaceVideoSources = function (lines, videoSources) { + var i, inVideo = false, index = -1, howMany = 0; - _generateGuid: (function () { - function s4() { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); + this.logger.info('Replacing video sources...'); + + for (i = 0; i < lines.length; i++) { + if (inVideo && lines[i].substring(0, 'm='.length) === 'm=') { + // Out of video. + break; } - return function () { - return s4() + s4() + '-' + s4() + '-' + s4() + '-' + - s4() + '-' + s4() + s4() + s4(); - }; - }()), - - _cacheLocalVideoSources: function (lines) { - this._localVideoSourceCache = this._getVideoSources(lines); - }, - - _restoreLocalVideoSources: function (lines) { - this._replaceVideoSources(lines, this._localVideoSourceCache); - }, - - _cacheRemoteVideoSources: function (lines) { - this._remoteVideoSourceCache = this._getVideoSources(lines); - }, - - _restoreRemoteVideoSources: function (lines) { - this._replaceVideoSources(lines, this._remoteVideoSourceCache); - }, - - _replaceVideoSources: function (lines, videoSources) { - - var i, inVideo = false, index = -1, howMany = 0; - - if (this.debugLvl) { - console.info('Replacing video sources...'); + if (!inVideo && lines[i].substring(0, 'm=video '.length) === 'm=video ') { + // In video. + inVideo = true; } - for (i = 0; i < lines.length; i++) { - if (inVideo && lines[i].substring(0, 'm='.length) === 'm=') { - // Out of video. - break; + if (inVideo && (lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:' + || lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:')) { + + if (index === -1) { + index = i; } - if (!inVideo && lines[i].substring(0, 'm=video '.length) === 'm=video ') { - // In video. - inVideo = true; - } + howMany++; + } + } - if (inVideo && (lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:' - || lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:')) { + // efficiency baby ;) + lines.splice.apply(lines, + [index, howMany].concat(videoSources)); - if (index === -1) { - index = i; - } +}; - howMany++; - } +SimulcastUtils.prototype._getVideoSources = function (lines) { + var i, inVideo = false, sb = []; + + this.logger.info('Getting video sources...'); + + for (i = 0; i < lines.length; i++) { + if (inVideo && lines[i].substring(0, 'm='.length) === 'm=') { + // Out of video. + break; } - // efficiency baby ;) - lines.splice.apply(lines, - [index, howMany].concat(videoSources)); - - }, - - _getVideoSources: function (lines) { - var i, inVideo = false, sb = []; - - if (this.debugLvl) { - console.info('Getting video sources...'); + if (!inVideo && lines[i].substring(0, 'm=video '.length) === 'm=video ') { + // In video. + inVideo = true; } - for (i = 0; i < lines.length; i++) { - if (inVideo && lines[i].substring(0, 'm='.length) === 'm=') { - // Out of video. - break; - } - - if (!inVideo && lines[i].substring(0, 'm=video '.length) === 'm=video ') { - // In video. - inVideo = true; - } - - if (inVideo && lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:') { - // In SSRC. - sb.push(lines[i]); - } - - if (inVideo && lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:') { - sb.push(lines[i]); - } + if (inVideo && lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:') { + // In SSRC. + sb.push(lines[i]); } - return sb; - }, - - _parseMedia: function (lines, mediatypes) { - var i, res = [], type, cur_media, idx, ssrcs, cur_ssrc, ssrc, - ssrc_attribute, group, semantics, skip; - - if (this.debugLvl) { - console.info('Parsing media sources...'); + if (inVideo && lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:') { + sb.push(lines[i]); } + } - for (i = 0; i < lines.length; i++) { - if (lines[i].substring(0, 'm='.length) === 'm=') { + return sb; +}; - type = lines[i] - .substr('m='.length, lines[i].indexOf(' ') - 'm='.length); - skip = mediatypes !== undefined && mediatypes.indexOf(type) === -1; +SimulcastUtils.prototype.parseMedia = function (lines, mediatypes) { + var i, res = [], type, cur_media, idx, ssrcs, cur_ssrc, ssrc, + ssrc_attribute, group, semantics, skip; - if (!skip) { - cur_media = { - 'type': type, - 'sources': {}, - 'groups': [] - }; + this.logger.info('Parsing media sources...'); - res.push(cur_media); - } + for (i = 0; i < lines.length; i++) { + if (lines[i].substring(0, 'm='.length) === 'm=') { - } else if (!skip && lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:') { + type = lines[i] + .substr('m='.length, lines[i].indexOf(' ') - 'm='.length); + skip = mediatypes !== undefined && mediatypes.indexOf(type) === -1; - idx = lines[i].indexOf(' '); - ssrc = lines[i].substring('a=ssrc:'.length, idx); - if (cur_media.sources[ssrc] === undefined) { - cur_ssrc = {'ssrc': ssrc}; - cur_media.sources[ssrc] = cur_ssrc; - } - - ssrc_attribute = lines[i].substr(idx + 1).split(':', 2)[0]; - cur_ssrc[ssrc_attribute] = lines[i].substr(idx + 1).split(':', 2)[1]; - - if (cur_media.base === undefined) { - cur_media.base = cur_ssrc; - } - - } else if (!skip && lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:') { - idx = lines[i].indexOf(' '); - semantics = lines[i].substr(0, idx).substr('a=ssrc-group:'.length); - ssrcs = lines[i].substr(idx).trim().split(' '); - group = { - 'semantics': semantics, - 'ssrcs': ssrcs + if (!skip) { + cur_media = { + 'type': type, + 'sources': {}, + 'groups': [] }; - cur_media.groups.push(group); - } else if (!skip && (lines[i].substring(0, 'a=sendrecv'.length) === 'a=sendrecv' || - lines[i].substring(0, 'a=recvonly'.length) === 'a=recvonly' || - lines[i].substring(0, 'a=sendonly'.length) === 'a=sendonly' || - lines[i].substring(0, 'a=inactive'.length) === 'a=inactive')) { - cur_media.direction = lines[i].substring('a='.length, 8); + res.push(cur_media); } - } - return res; - }, + } else if (!skip && lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:') { - // Returns a random integer between min (included) and max (excluded) - // Using Math.round() gives a non-uniform distribution! - _generateRandomSSRC: function () { - var min = 0, max = 0xffffffff; - return Math.floor(Math.random() * (max - min)) + min; - }, - - - /** - * The _indexOfArray() method returns the first a CompoundIndex at which a - * given element can be found in the array, or _emptyCompoundIndex if it is - * not present. - * - * Example: - * - * _indexOfArray('3', [ 'this is line 1', 'this is line 2', 'this is line 3' ]) - * - * returns {row: 2, column: 14} - * - * @param needle - * @param haystack - * @param start - * @returns {} - * @private - */ - _indexOfArray: function (needle, haystack, start) { - var length = haystack.length, idx, i; - - if (!start) { - start = 0; - } - - for (i = start; i < length; i++) { - idx = haystack[i].indexOf(needle); - if (idx !== -1) { - return {row: i, column: idx}; + idx = lines[i].indexOf(' '); + ssrc = lines[i].substring('a=ssrc:'.length, idx); + if (cur_media.sources[ssrc] === undefined) { + cur_ssrc = {'ssrc': ssrc}; + cur_media.sources[ssrc] = cur_ssrc; } - } - return this._emptyCompoundIndex; - }, - _removeSimulcastGroup: function (lines) { - var i; + ssrc_attribute = lines[i].substr(idx + 1).split(':', 2)[0]; + cur_ssrc[ssrc_attribute] = lines[i].substr(idx + 1).split(':', 2)[1]; - for (i = lines.length - 1; i >= 0; i--) { - if (lines[i].indexOf('a=ssrc-group:SIM') !== -1) { - lines.splice(i, 1); + if (cur_media.base === undefined) { + cur_media.base = cur_ssrc; } + + } else if (!skip && lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:') { + idx = lines[i].indexOf(' '); + semantics = lines[i].substr(0, idx).substr('a=ssrc-group:'.length); + ssrcs = lines[i].substr(idx).trim().split(' '); + group = { + 'semantics': semantics, + 'ssrcs': ssrcs + }; + cur_media.groups.push(group); + } else if (!skip && (lines[i].substring(0, 'a=sendrecv'.length) === 'a=sendrecv' || + lines[i].substring(0, 'a=recvonly'.length) === 'a=recvonly' || + lines[i].substring(0, 'a=sendonly'.length) === 'a=sendonly' || + lines[i].substring(0, 'a=inactive'.length) === 'a=inactive')) { + + cur_media.direction = lines[i].substring('a='.length, 8); } - }, + } - _appendSimulcastGroup: function (lines) { - var videoSources, ssrcGroup, simSSRC, numOfSubs = 2, i, sb, msid; + return res; +}; - if (this.debugLvl) { - console.info('Appending simulcast group...'); +/** + * The _indexOfArray() method returns the first a CompoundIndex at which a + * given element can be found in the array, or _emptyCompoundIndex if it is + * not present. + * + * Example: + * + * _indexOfArray('3', [ 'this is line 1', 'this is line 2', 'this is line 3' ]) + * + * returns {row: 2, column: 14} + * + * @param needle + * @param haystack + * @param start + * @returns {} + * @private + */ +SimulcastUtils.prototype._indexOfArray = function (needle, haystack, start) { + var length = haystack.length, idx, i; + + if (!start) { + start = 0; + } + + for (i = start; i < length; i++) { + idx = haystack[i].indexOf(needle); + if (idx !== -1) { + return {row: i, column: idx}; } + } + return this._emptyCompoundIndex; +}; - // Get the primary SSRC information. - videoSources = this._parseMedia(lines, ['video'])[0]; +SimulcastUtils.prototype._removeSimulcastGroup = function (lines) { + var i; - // Start building the SIM SSRC group. - ssrcGroup = ['a=ssrc-group:SIM']; - - // The video source buffer. - sb = []; - - // Create the simulcast sub-streams. - for (i = 0; i < numOfSubs; i++) { - // TODO(gp) prevent SSRC collision. - simSSRC = this._generateRandomSSRC(); - ssrcGroup.push(simSSRC); - - sb.splice.apply(sb, [sb.length, 0].concat( - [["a=ssrc:", simSSRC, " cname:", videoSources.base.cname].join(''), - ["a=ssrc:", simSSRC, " msid:", videoSources.base.msid].join('')] - )); - - if (this.debugLvl) { - console.info(['Generated substream ', i, ' with SSRC ', simSSRC, '.'].join('')); - } + for (i = lines.length - 1; i >= 0; i--) { + if (lines[i].indexOf('a=ssrc-group:SIM') !== -1) { + lines.splice(i, 1); } - - // Add the group sim layers. - sb.splice(0, 0, ssrcGroup.join(' ')) - - this._replaceVideoSources(lines, sb); - }, - - // Does the actual patching. - _ensureSimulcastGroup: function (lines) { - if (this.debugLvl) { - console.info('Ensuring simulcast group...'); - } - - if (this._indexOfArray('a=ssrc-group:SIM', lines) === this._emptyCompoundIndex) { - this._appendSimulcastGroup(lines); - this._cacheLocalVideoSources(lines); - } else { - // verify that the ssrcs participating in the SIM group are present - // in the SDP (needed for presence). - this._restoreLocalVideoSources(lines); - } - }, - - _ensureGoogConference: function (lines) { - var sb; - if (this.debugLvl) { - console.info('Ensuring x-google-conference flag...') - } - - if (this._indexOfArray('a=x-google-flag:conference', lines) === this._emptyCompoundIndex) { - // Add the google conference flag - sb = this._getVideoSources(lines); - sb = ['a=x-google-flag:conference'].concat(sb); - this._replaceVideoSources(lines, sb); - } - }, - - _compileVideoSources: function (videoSources) { - var sb = [], ssrc, addedSSRCs = []; - - if (this.debugLvl) { - console.info('Compiling video sources...'); - } - - // Add the groups - if (videoSources.groups && videoSources.groups.length !== 0) { - videoSources.groups.forEach(function (group) { - if (group.ssrcs && group.ssrcs.length !== 0) { - sb.push([['a=ssrc-group:', group.semantics].join(''), group.ssrcs.join(' ')].join(' ')); - - // if (group.semantics !== 'SIM') { - group.ssrcs.forEach(function (ssrc) { - addedSSRCs.push(ssrc); - sb.splice.apply(sb, [sb.length, 0].concat([ - ["a=ssrc:", ssrc, " cname:", videoSources.sources[ssrc].cname].join(''), - ["a=ssrc:", ssrc, " msid:", videoSources.sources[ssrc].msid].join('')])); - }); - //} - } - }); - } - - // Then add any free sources. - if (videoSources.sources) { - for (ssrc in videoSources.sources) { - if (addedSSRCs.indexOf(ssrc) === -1) { - sb.splice.apply(sb, [sb.length, 0].concat([ - ["a=ssrc:", ssrc, " cname:", videoSources.sources[ssrc].cname].join(''), - ["a=ssrc:", ssrc, " msid:", videoSources.sources[ssrc].msid].join('')])); - } - } - } - - return sb; - }, - - - _restoreSimulcastGroups: function (sb) { - this._restoreRemoteVideoSources(sb); - }, - - /** - * Restores the simulcast groups of the remote description. In - * transformRemoteDescription we remove those in order for the set remote - * description to succeed. The focus needs the signal the groups to new - * participants. - * - * @param desc - * @returns {*} - */ - reverseTransformRemoteDescription: function (desc) { - var sb; - - if (!desc || desc == null) { - return desc; - } - - if (config.enableSimulcast) { - sb = desc.sdp.split('\r\n'); - - this._restoreSimulcastGroups(sb); - - desc = new RTCSessionDescription({ - type: desc.type, - sdp: sb.join('\r\n') - }); - } - - return desc; - }, - - - _ensureOrder: function (lines) { - var videoSources, sb; - - videoSources = this._parseMedia(lines, ['video'])[0]; - sb = this._compileVideoSources(videoSources); - - this._replaceVideoSources(lines, sb); - }, - - _updateRemoteMaps: function (lines) { - var remoteVideoSources = this._parseMedia(lines, ['video'])[0], - videoSource, quality; - - // (re) initialize the remote maps. - this._remoteMaps.msid2Quality = {}; - this._remoteMaps.ssrc2Msid = {}; - - var self = this; - if (remoteVideoSources.groups && remoteVideoSources.groups.length !== 0) { - remoteVideoSources.groups.forEach(function (group) { - if (group.semantics === 'SIM' && group.ssrcs && group.ssrcs.length !== 0) { - quality = 0; - group.ssrcs.forEach(function (ssrc) { - videoSource = remoteVideoSources.sources[ssrc]; - self._remoteMaps.msid2Quality[videoSource.msid] = quality++; - self._remoteMaps.ssrc2Msid[videoSource.ssrc] = videoSource.msid; - }); - } - }); - } - }, - - _setReceivingVideoStream: function (endpoint, ssrc) { - this._remoteMaps.receivingVideoStreams[endpoint] = ssrc; - }, - - /** - * Returns a stream with single video track, the one currently being - * received by this endpoint. - * - * @param stream the remote simulcast stream. - * @returns {webkitMediaStream} - */ - getReceivingVideoStream: function (stream) { - var tracks, i, electedTrack, msid, quality = 0, receivingTrackId; - - var self = this; - if (config.enableSimulcast) { - - stream.getVideoTracks().some(function (track) { - return Object.keys(self._remoteMaps.receivingVideoStreams).some(function (endpoint) { - var ssrc = self._remoteMaps.receivingVideoStreams[endpoint]; - var msid = self._remoteMaps.ssrc2Msid[ssrc]; - if (msid == [stream.id, track.id].join(' ')) { - electedTrack = track; - return true; - } - }); - }); - - if (!electedTrack) { - // we don't have an elected track, choose by initial quality. - tracks = stream.getVideoTracks(); - for (i = 0; i < tracks.length; i++) { - msid = [stream.id, tracks[i].id].join(' '); - if (this._remoteMaps.msid2Quality[msid] === quality) { - electedTrack = tracks[i]; - break; - } - } - - // TODO(gp) if the initialQuality could not be satisfied, lower - // the requirement and try again. - } - } - - return (electedTrack) - ? new webkitMediaStream([electedTrack]) - : stream; - }, - - localStream: null, displayedLocalVideoStream: null, - - /** - * Gets the fully qualified msid (stream.id + track.id) associated to the - * SSRC. - * - * @param ssrc - * @returns {*} - */ - getRemoteVideoStreamIdBySSRC: function (ssrc) { - return this._remoteMaps.ssrc2Msid[ssrc]; - }, - - parseMedia: function (desc, mediatypes) { - var lines = desc.sdp.split('\r\n'); - return this._parseMedia(lines, mediatypes); - }, - - getLocalVideoStream: function () { - return (this.displayedLocalVideoStream != null) - ? this.displayedLocalVideoStream - // in case we have no simulcast at all, i.e. we didn't perform the GUM - : connection.jingle.localVideo; } }; +SimulcastUtils.prototype._compileVideoSources = function (videoSources) { + var sb = [], ssrc, addedSSRCs = []; -function NativeSimulcastSender() { - Simulcast.call(this); // call the super constructor. + this.logger.info('Compiling video sources...'); + + // Add the groups + if (videoSources.groups && videoSources.groups.length !== 0) { + videoSources.groups.forEach(function (group) { + if (group.ssrcs && group.ssrcs.length !== 0) { + sb.push([['a=ssrc-group:', group.semantics].join(''), group.ssrcs.join(' ')].join(' ')); + + // if (group.semantics !== 'SIM') { + group.ssrcs.forEach(function (ssrc) { + addedSSRCs.push(ssrc); + sb.splice.apply(sb, [sb.length, 0].concat([ + ["a=ssrc:", ssrc, " cname:", videoSources.sources[ssrc].cname].join(''), + ["a=ssrc:", ssrc, " msid:", videoSources.sources[ssrc].msid].join('')])); + }); + //} + } + }); + } + + // Then add any free sources. + if (videoSources.sources) { + for (ssrc in videoSources.sources) { + if (addedSSRCs.indexOf(ssrc) === -1) { + sb.splice.apply(sb, [sb.length, 0].concat([ + ["a=ssrc:", ssrc, " cname:", videoSources.sources[ssrc].cname].join(''), + ["a=ssrc:", ssrc, " msid:", videoSources.sources[ssrc].msid].join('')])); + } + } + } + + return sb; +}; + +function SimulcastReceiver(simulcastUtils) { + this.simulcastUtils = simulcastUtils; + this.logger = new SimulcastLogger('SimulcastReceiver'); } -NativeSimulcastSender.prototype = Object.create(Simulcast.prototype); +SimulcastReceiver.prototype._remoteVideoSourceCache = ''; +SimulcastReceiver.prototype._remoteMaps = { + msid2Quality: {}, + ssrc2Msid: {}, + receivingVideoStreams: {} +}; + +SimulcastReceiver.prototype._cacheRemoteVideoSources = function (lines) { + this._remoteVideoSourceCache = this.simulcastUtils._getVideoSources(lines); +}; + +SimulcastReceiver.prototype._restoreRemoteVideoSources = function (lines) { + this.simulcastUtils._replaceVideoSources(lines, this._remoteVideoSourceCache); +}; + +SimulcastReceiver.prototype._ensureGoogConference = function (lines) { + var sb; + + this.logger.info('Ensuring x-google-conference flag...') + + if (this.simulcastUtils._indexOfArray('a=x-google-flag:conference', lines) === this.simulcastUtils._emptyCompoundIndex) { + // Add the google conference flag + sb = this.simulcastUtils._getVideoSources(lines); + sb = ['a=x-google-flag:conference'].concat(sb); + this.simulcastUtils._replaceVideoSources(lines, sb); + } +}; + +SimulcastReceiver.prototype._restoreSimulcastGroups = function (sb) { + this._restoreRemoteVideoSources(sb); +}; + +/** + * Restores the simulcast groups of the remote description. In + * transformRemoteDescription we remove those in order for the set remote + * description to succeed. The focus needs the signal the groups to new + * participants. + * + * @param desc + * @returns {*} + */ +SimulcastReceiver.prototype.reverseTransformRemoteDescription = function (desc) { + var sb; + + if (!desc || desc == null) { + return desc; + } + + if (config.enableSimulcast) { + sb = desc.sdp.split('\r\n'); + + this._restoreSimulcastGroups(sb); + + desc = new RTCSessionDescription({ + type: desc.type, + sdp: sb.join('\r\n') + }); + } + + return desc; +}; + +SimulcastUtils.prototype._ensureOrder = function (lines) { + var videoSources, sb; + + videoSources = this.parseMedia(lines, ['video'])[0]; + sb = this._compileVideoSources(videoSources); + + this._replaceVideoSources(lines, sb); +}; + +SimulcastReceiver.prototype._updateRemoteMaps = function (lines) { + var remoteVideoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0], + videoSource, quality; + + // (re) initialize the remote maps. + this._remoteMaps.msid2Quality = {}; + this._remoteMaps.ssrc2Msid = {}; + + var self = this; + if (remoteVideoSources.groups && remoteVideoSources.groups.length !== 0) { + remoteVideoSources.groups.forEach(function (group) { + if (group.semantics === 'SIM' && group.ssrcs && group.ssrcs.length !== 0) { + quality = 0; + group.ssrcs.forEach(function (ssrc) { + videoSource = remoteVideoSources.sources[ssrc]; + self._remoteMaps.msid2Quality[videoSource.msid] = quality++; + self._remoteMaps.ssrc2Msid[videoSource.ssrc] = videoSource.msid; + }); + } + }); + } +}; + +SimulcastReceiver.prototype._setReceivingVideoStream = function (endpoint, ssrc) { + this._remoteMaps.receivingVideoStreams[endpoint] = ssrc; +}; + +/** + * Returns a stream with single video track, the one currently being + * received by this endpoint. + * + * @param stream the remote simulcast stream. + * @returns {webkitMediaStream} + */ +SimulcastReceiver.prototype.getReceivingVideoStream = function (stream) { + var tracks, i, electedTrack, msid, quality = 0, receivingTrackId; + + var self = this; + if (config.enableSimulcast) { + + stream.getVideoTracks().some(function (track) { + return Object.keys(self._remoteMaps.receivingVideoStreams).some(function (endpoint) { + var ssrc = self._remoteMaps.receivingVideoStreams[endpoint]; + var msid = self._remoteMaps.ssrc2Msid[ssrc]; + if (msid == [stream.id, track.id].join(' ')) { + electedTrack = track; + return true; + } + }); + }); + + if (!electedTrack) { + // we don't have an elected track, choose by initial quality. + tracks = stream.getVideoTracks(); + for (i = 0; i < tracks.length; i++) { + msid = [stream.id, tracks[i].id].join(' '); + if (this._remoteMaps.msid2Quality[msid] === quality) { + electedTrack = tracks[i]; + break; + } + } + + // TODO(gp) if the initialQuality could not be satisfied, lower + // the requirement and try again. + } + } + + return (electedTrack) + ? new webkitMediaStream([electedTrack]) + : stream; +}; + +/** + * Gets the fully qualified msid (stream.id + track.id) associated to the + * SSRC. + * + * @param ssrc + * @returns {*} + */ +SimulcastReceiver.prototype.getRemoteVideoStreamIdBySSRC = function (ssrc) { + return this._remoteMaps.ssrc2Msid[ssrc]; +}; + +function SimulcastSender() { + this.simulcastUtils = new SimulcastUtils(); + this.logger = new SimulcastLogger('SimulcastSender'); +} + +SimulcastSender.prototype._localVideoSourceCache = ''; +SimulcastSender.prototype.localStream = null; +SimulcastSender.prototype.displayedLocalVideoStream = null; + +SimulcastSender.prototype._generateGuid = (function () { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + + return function () { + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + + s4() + '-' + s4() + s4() + s4(); + }; +}()); + +SimulcastSender.prototype._cacheLocalVideoSources = function (lines) { + this._localVideoSourceCache = this.simulcastUtils._getVideoSources(lines); +}; + +SimulcastSender.prototype._restoreLocalVideoSources = function (lines) { + this.simulcastUtils._replaceVideoSources(lines, this._localVideoSourceCache); +}; + +// Returns a random integer between min (included) and max (excluded) +// Using Math.round() gives a non-uniform distribution! +SimulcastSender.prototype._generateRandomSSRC = function () { + var min = 0, max = 0xffffffff; + return Math.floor(Math.random() * (max - min)) + min; +}; + +SimulcastSender.prototype._appendSimulcastGroup = function (lines) { + var videoSources, ssrcGroup, simSSRC, numOfSubs = 2, i, sb, msid; + + this.logger.info('Appending simulcast group...'); + + // Get the primary SSRC information. + videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0]; + + // Start building the SIM SSRC group. + ssrcGroup = ['a=ssrc-group:SIM']; + + // The video source buffer. + sb = []; + + // Create the simulcast sub-streams. + for (i = 0; i < numOfSubs; i++) { + // TODO(gp) prevent SSRC collision. + simSSRC = this._generateRandomSSRC(); + ssrcGroup.push(simSSRC); + + sb.splice.apply(sb, [sb.length, 0].concat( + [["a=ssrc:", simSSRC, " cname:", videoSources.base.cname].join(''), + ["a=ssrc:", simSSRC, " msid:", videoSources.base.msid].join('')] + )); + + this.logger.info(['Generated substream ', i, ' with SSRC ', simSSRC, '.'].join('')); + + } + + // Add the group sim layers. + sb.splice(0, 0, ssrcGroup.join(' ')) + + this.simulcastUtils._replaceVideoSources(lines, sb); +}; + +// Does the actual patching. +SimulcastSender.prototype._ensureSimulcastGroup = function (lines) { + + this.logger.info('Ensuring simulcast group...'); + + if (this.simulcastUtils._indexOfArray('a=ssrc-group:SIM', lines) === this.simulcastUtils._emptyCompoundIndex) { + this._appendSimulcastGroup(lines); + this._cacheLocalVideoSources(lines); + } else { + // verify that the ssrcs participating in the SIM group are present + // in the SDP (needed for presence). + this._restoreLocalVideoSources(lines); + } +}; + +SimulcastSender.prototype.getLocalVideoStream = function () { + return (this.displayedLocalVideoStream != null) + ? this.displayedLocalVideoStream + // in case we have no simulcast at all, i.e. we didn't perform the GUM + : connection.jingle.localVideo; +}; + +function NativeSimulcastSender() { + SimulcastSender.call(this); // call the super constructor. +} + +NativeSimulcastSender.prototype = Object.create(SimulcastSender.prototype); NativeSimulcastSender.prototype._localExplosionMap = {}; @@ -500,14 +491,12 @@ NativeSimulcastSender.prototype._localExplosionMap = {}; * @param lines * @private */ -NativeSimulcastSender.prototype._explodeLocalSimulcastSources = function (lines) { +NativeSimulcastSender.prototype._explodeSimulcastSenderSources = function (lines) { var sb, msid, sid, tid, videoSources, self; - if (this.debugLvl) { - console.info('Exploding local video sources...'); - } + this.logger.info('Exploding local video sources...'); - videoSources = this._parseMedia(lines, ['video'])[0]; + videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0]; self = this; if (videoSources.groups && videoSources.groups.length !== 0) { @@ -538,9 +527,9 @@ NativeSimulcastSender.prototype._explodeLocalSimulcastSources = function (lines) }); } - sb = this._compileVideoSources(videoSources); + sb = this.simulcastUtils._compileVideoSources(videoSources); - this._replaceVideoSources(lines, sb); + this.simulcastUtils._replaceVideoSources(lines, sb); }; /** @@ -578,17 +567,14 @@ NativeSimulcastSender.prototype.reverseTransformLocalDescription = function (des sb = desc.sdp.split('\r\n'); - this._explodeLocalSimulcastSources(sb); + this._explodeSimulcastSenderSources(sb); desc = new RTCSessionDescription({ type: desc.type, sdp: sb.join('\r\n') }); - if (this.debugLvl && this.debugLvl > 1) { - console.info('Exploded local video sources'); - console.info(desc.sdp); - } + this.logger.fine(['Exploded local video sources', desc.sdp].join(' ')); return desc; }; @@ -618,10 +604,7 @@ NativeSimulcastSender.prototype.transformAnswer = function (desc) { sdp: sb.join('\r\n') }); - if (this.debugLvl && this.debugLvl > 1) { - console.info('Transformed answer'); - console.info(desc.sdp); - } + this.logger.fine(['Transformed answer', desc.sdp].join(' ')); return desc; }; @@ -645,15 +628,19 @@ NativeSimulcastSender.prototype.transformLocalDescription = function (desc) { * @param desc * @returns {*} */ -NativeSimulcastSender.prototype.transformRemoteDescription = function (desc) { +SimulcastReceiver.prototype.transformRemoteDescription = function (desc) { var sb = desc.sdp.split('\r\n'); this._updateRemoteMaps(sb); this._cacheRemoteVideoSources(sb); - this._removeSimulcastGroup(sb); // NOTE(gp) this needs to be called after updateRemoteMaps because we need the simulcast group in the _updateRemoteMaps() method. - // We don't need the goog conference flag if we're not doing - // native simulcast. + + // NOTE(gp) this needs to be called after updateRemoteMaps because we need the simulcast group in the _updateRemoteMaps() method. + this.simulcastUtils._removeSimulcastGroup(sb); + + // We don't need the goog conference flag if we're not doing native + // simulcast, but at the receiver, we have no idea if the sender is + // doing native or not-native simulcast. this._ensureGoogConference(sb); desc = new RTCSessionDescription({ @@ -661,10 +648,7 @@ NativeSimulcastSender.prototype.transformRemoteDescription = function (desc) { sdp: sb.join('\r\n') }); - if (this.debugLvl && this.debugLvl > 1) { - console.info('Transformed remote description'); - console.info(desc.sdp); - } + this.logger.fine(['Transformed remote description', desc.sdp].join(' ')); return desc; }; @@ -676,10 +660,10 @@ NativeSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, en NativeSimulcastSender.prototype.constructor = NativeSimulcastSender; function SimpleSimulcastSender() { - Simulcast.call(this); + SimulcastSender.call(this); } -SimpleSimulcastSender.prototype = Object.create(Simulcast.prototype); +SimpleSimulcastSender.prototype = Object.create(SimulcastSender.prototype); SimpleSimulcastSender.prototype._localMaps = { msids: [], @@ -695,11 +679,9 @@ SimpleSimulcastSender.prototype._localMaps = { SimpleSimulcastSender.prototype._groupLocalVideoSources = function (lines) { var sb, videoSources, ssrcs = [], ssrc; - if (this.debugLvl) { - console.info('Grouping local video sources...'); - } + this.logger.info('Grouping local video sources...'); - videoSources = this._parseMedia(lines, ['video'])[0]; + videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0]; for (ssrc in videoSources.sources) { // jitsi-meet destroys/creates streams at various places causing @@ -723,9 +705,9 @@ SimpleSimulcastSender.prototype._groupLocalVideoSources = function (lines) { 'ssrcs': ssrcs }); - sb = this._compileVideoSources(videoSources); + sb = this.simulcastUtils._compileVideoSources(videoSources); - this._replaceVideoSources(lines, sb); + this.simulcastUtils._replaceVideoSources(lines, sb); }; /** @@ -750,8 +732,8 @@ SimpleSimulcastSender.prototype.getUserMedia = function (constraints, success, e } }; - console.log('HQ constraints: ', constraints); - console.log('LQ constraints: ', lqConstraints); + this.logger.info('HQ constraints: ', constraints); + this.logger.info('LQ constraints: ', lqConstraints); // NOTE(gp) if we request the lq stream first webkitGetUserMedia @@ -811,10 +793,8 @@ SimpleSimulcastSender.prototype.reverseTransformLocalDescription = function (des sdp: sb.join('\r\n') }); - if (this.debugLvl && this.debugLvl > 1) { - console.info('Grouped local video sources'); - console.info(desc.sdp); - } + this.logger.fine('Grouped local video sources'); + this.logger.fine(desc.sdp); return desc; }; @@ -841,46 +821,15 @@ SimpleSimulcastSender.prototype.transformLocalDescription = function (desc) { var sb = desc.sdp.split('\r\n'); - this._removeSimulcastGroup(sb); + this.simulcastUtils._removeSimulcastGroup(sb); desc = new RTCSessionDescription({ type: desc.type, sdp: sb.join('\r\n') }); - if (this.debugLvl && this.debugLvl > 1) { - console.info('Transformed local description'); - console.info(desc.sdp); - } - - return desc; -}; - -/** - * Removes the ssrc-group:SIM from the remote description bacause Chrome - * either gets confused and thinks this is an FID group or, if an FID group - * is already present, it fails to set the remote description. - * - * @param desc - * @returns {*} - */ -SimpleSimulcastSender.prototype.transformRemoteDescription = function (desc) { - - var sb = desc.sdp.split('\r\n'); - - this._updateRemoteMaps(sb); - this._cacheRemoteVideoSources(sb); - this._removeSimulcastGroup(sb); // NOTE(gp) this needs to be called after updateRemoteMaps because we need the simulcast group in the _updateRemoteMaps() method. - - desc = new RTCSessionDescription({ - type: desc.type, - sdp: sb.join('\r\n') - }); - - if (this.debugLvl && this.debugLvl > 1) { - console.info('Transformed remote description'); - console.info(desc.sdp); - } + this.logger.fine('Transformed local description'); + this.logger.fine(desc.sdp); return desc; }; @@ -889,7 +838,7 @@ SimpleSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, en var trackid; var self = this; - console.log(['Requested to', enabled ? 'enable' : 'disable', ssrc].join(' ')); + this.logger.log(['Requested to', enabled ? 'enable' : 'disable', ssrc].join(' ')); if (Object.keys(this._localMaps.msid2ssrc).some(function (tid) { // Search for the track id that corresponds to the ssrc if (self._localMaps.msid2ssrc[tid] == ssrc) { @@ -903,22 +852,22 @@ SimpleSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, en return true; } })) { - console.log([trackid, enabled ? 'enabled' : 'disabled'].join(' ')); + this.logger.log([trackid, enabled ? 'enabled' : 'disabled'].join(' ')); $(document).trigger(enabled ? 'simulcastlayerstarted' : 'simulcastlayerstopped'); } else { - console.error("I don't have a local stream with SSRC " + ssrc); + this.logger.error("I don't have a local stream with SSRC " + ssrc); } }; SimpleSimulcastSender.prototype.constructor = SimpleSimulcastSender; function NoSimulcastSender() { - Simulcast.call(this); + SimulcastSender.call(this); } -NoSimulcastSender.prototype = Object.create(Simulcast.prototype); +NoSimulcastSender.prototype = Object.create(SimulcastSender.prototype); /** * GUM for simulcast. @@ -968,6 +917,38 @@ NoSimulcastSender.prototype.transformLocalDescription = function (desc) { return desc; }; +NoSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) { + +}; + +NoSimulcastSender.prototype.constructor = NoSimulcastSender; + +/** + * + * @constructor + */ +function SimulcastManager() { + + // Create the simulcast utilities. + this.simulcastUtils = new SimulcastUtils(); + + // Create remote simulcast. + this.simulcastReceiver = new SimulcastReceiver(this.simulcastUtils); +} + +/** + * Restores the simulcast groups of the remote description. In + * transformRemoteDescription we remove those in order for the set remote + * description to succeed. The focus needs the signal the groups to new + * participants. + * + * @param desc + * @returns {*} + */ +SimulcastManager.prototype.reverseTransformRemoteDescription = function (desc) { + return this.simulcastReceiver.reverseTransformRemoteDescription(desc); +}; + /** * Removes the ssrc-group:SIM from the remote description bacause Chrome * either gets confused and thinks this is an FID group or, if an FID group @@ -976,37 +957,151 @@ NoSimulcastSender.prototype.transformLocalDescription = function (desc) { * @param desc * @returns {*} */ -NoSimulcastSender.prototype.transformRemoteDescription = function (desc) { - return desc; +SimulcastManager.prototype.transformRemoteDescription = function (desc) { + return this.simulcastReceiver.transformRemoteDescription(desc); }; -NoSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) { - +/** + * Gets the fully qualified msid (stream.id + track.id) associated to the + * SSRC. + * + * @param ssrc + * @returns {*} + */ +SimulcastManager.prototype.getRemoteVideoStreamIdBySSRC = function (ssrc) { + return this.simulcastReceiver.getRemoteVideoStreamIdBySSRC(ssrc); }; -NoSimulcastSender.prototype.constructor = NoSimulcastSender; +/** + * Returns a stream with single video track, the one currently being + * received by this endpoint. + * + * @param stream the remote simulcast stream. + * @returns {webkitMediaStream} + */ +SimulcastManager.prototype.getReceivingVideoStream = function (stream) { + return this.simulcastReceiver.getReceivingVideoStream(stream); +}; -// Initialize simulcast. -var simulcast; -if (!config.enableSimulcast) { - simulcast = new NoSimulcastSender(); -} else { +/** + * + * + * @param desc + * @returns {*} + */ +SimulcastManager.prototype.transformLocalDescription = function (desc) { + return this.simulcastSender.transformLocalDescription(desc); +}; - var isChromium = window.chrome, - vendorName = window.navigator.vendor; - if(isChromium !== null && isChromium !== undefined && vendorName === "Google Inc.") { - var ver = parseInt(window.navigator.appVersion.match(/Chrome\/(\d+)\./)[1], 10); - if (ver > 37) { - simulcast = new NativeSimulcastSender(); - } else { - simulcast = new NoSimulcastSender(); - } +/** + * + * @returns {*} + */ +SimulcastManager.prototype.getLocalVideoStream = function() { + return this.simulcastSender.getLocalVideoStream(); +}; + +/** + * GUM for simulcast. + * + * @param constraints + * @param success + * @param err + */ +SimulcastManager.prototype.getUserMedia = function (constraints, success, err) { + + // Initialize local simulcast. + + // TODO(gp) take into account constraints. + if (!config.enableSimulcast) { + this.simulcastSender = new NoSimulcastSender(); } else { - simulcast = new NoSimulcastSender(); + + var isChromium = window.chrome, + vendorName = window.navigator.vendor; + if(isChromium !== null && isChromium !== undefined && vendorName === "Google Inc.") { + var ver = parseInt(window.navigator.appVersion.match(/Chrome\/(\d+)\./)[1], 10); + if (ver > 37) { + this.simulcastSender = new NativeSimulcastSender(); + } else { + this.simulcastSender = new NoSimulcastSender(); + } + } else { + this.simulcastSender = new NoSimulcastSender(); + } + } + this.simulcastSender.getUserMedia(constraints, success, err); +}; + +/** + * Prepares the local description for public usage (i.e. to be signaled + * through Jingle to the focus). + * + * @param desc + * @returns {RTCSessionDescription} + */ +SimulcastManager.prototype.reverseTransformLocalDescription = function (desc) { + return this.simulcastSender.reverseTransformLocalDescription(desc); +}; + +/** + * Ensures that the simulcast group is present in the answer, _if_ native + * simulcast is enabled, + * + * @param desc + * @returns {*} + */ +SimulcastManager.prototype.transformAnswer = function (desc) { + return this.simulcastSender.transformAnswer(desc); +}; + +/** + * + * @param lines + * @param mediatypes + * @returns {*} + */ +SimulcastManager.prototype.parseMedia = function(lines, mediatypes) { + var sb = lines.sdp.split('\r\n'); + return this.simulcastUtils.parseMedia(sb, mediatypes); +}; + +SimulcastManager.prototype._setReceivingVideoStream = function(endpoint, ssrc) { + this.simulcastReceiver._setReceivingVideoStream(endpoint, ssrc); +}; + +SimulcastManager.prototype._setLocalVideoStreamEnabled = function(ssrc, enabled) { + this.simulcastSender._setLocalVideoStreamEnabled(ssrc, enabled); +}; + +/** + * + * @constructor + */ +function SimulcastLogger(name) { + this.name = name; } +SimulcastLogger.prototype.log = function (text) { + console.log(text); +}; + +SimulcastLogger.prototype.info = function (text) { + console.info(text); +}; + +SimulcastLogger.prototype.fine = function (text) { + console.log(text); +}; + +SimulcastLogger.prototype.error = function (text) { + console.error(text); +}; + +var simulcast = new SimulcastManager(); + $(document).bind('simulcastlayerschanged', function (event, endpointSimulcastLayers) { endpointSimulcastLayers.forEach(function (esl) { var ssrc = esl.simulcastLayer.primarySSRC;