/*jslint plusplus: true */ /*jslint nomen: true*/ /** * * @constructor */ function SimulcastUtils() { this.logger = new SimulcastLogger("SimulcastUtils"); } /** * * @type {{}} * @private */ SimulcastUtils.prototype._emptyCompoundIndex = {}; /** * * @param lines * @param videoSources * @private */ SimulcastUtils.prototype._replaceVideoSources = function (lines, videoSources) { var i, inVideo = false, index = -1, howMany = 0; 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; } 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:' || lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:')) { if (index === -1) { index = i; } howMany++; } } // efficiency baby ;) lines.splice.apply(lines, [index, howMany].concat(videoSources)); }; 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; } 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]); } } return sb; }; SimulcastUtils.prototype.parseMedia = function (lines, mediatypes) { var i, res = [], type, cur_media, idx, ssrcs, cur_ssrc, ssrc, ssrc_attribute, group, semantics, skip; this.logger.info('Parsing media sources...'); for (i = 0; i < lines.length; i++) { if (lines[i].substring(0, 'm='.length) === 'm=') { type = lines[i] .substr('m='.length, lines[i].indexOf(' ') - 'm='.length); skip = mediatypes !== undefined && mediatypes.indexOf(type) === -1; if (!skip) { cur_media = { 'type': type, 'sources': {}, 'groups': [] }; res.push(cur_media); } } else if (!skip && lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:') { 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 }; 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); } } return res; }; /** * 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; }; SimulcastUtils.prototype._removeSimulcastGroup = function (lines) { var i; for (i = lines.length - 1; i >= 0; i--) { if (lines[i].indexOf('a=ssrc-group:SIM') !== -1) { lines.splice(i, 1); } } }; SimulcastUtils.prototype._compileVideoSources = function (videoSources) { var sb = [], ssrc, addedSSRCs = []; 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() { this.simulcastUtils = new SimulcastUtils(); this.logger = new SimulcastLogger('SimulcastReceiver'); } SimulcastReceiver.prototype._remoteVideoSourceCache = ''; SimulcastReceiver.prototype._remoteMaps = { msid2Quality: {}, ssrc2Msid: {}, msid2ssrc: {}, 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) { // TODO(gp) do that for the audio as well as suggested by fippo. // 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 = {}; this._remoteMaps.msid2ssrc = {}; 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; self._remoteMaps.msid2ssrc[videoSource.msid] = videoSource.ssrc; }); } }); } }; SimulcastReceiver.prototype._setReceivingVideoStream = function (resource, ssrc) { this._remoteMaps.receivingVideoStreams[resource] = 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 (resource) { var ssrc = self._remoteMaps.receivingVideoStreams[resource]; 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; }; SimulcastReceiver.prototype.getReceivingSSRC = function (jid) { var resource = Strophe.getResourceFromJid(jid); var ssrc = this._remoteMaps.receivingVideoStreams[resource]; // If we haven't receiving a "changed" event yet, then we must be receiving // low quality (that the sender always streams). if (!ssrc && connection.jingle) { var session; var i, j, k; var keys = Object.keys(connection.jingle.sessions); for (i = 0; i < keys.length; i++) { var sid = keys[i]; if (ssrc) { // stream found, stop. break; } session = connection.jingle.sessions[sid]; if (session.remoteStreams) { for (j = 0; j < session.remoteStreams.length; j++) { var remoteStream = session.remoteStreams[j]; if (ssrc) { // stream found, stop. break; } var tracks = remoteStream.getVideoTracks(); if (tracks) { for (k = 0; k < tracks.length; k++) { var track = tracks[k]; var msid = [remoteStream.id, track.id].join(' '); var _ssrc = this._remoteMaps.msid2ssrc[msid]; var _jid = ssrc2jid[_ssrc]; var quality = this._remoteMaps.msid2Quality[msid]; if (jid == _jid && quality == 0) { ssrc = _ssrc; // stream found, stop. break; } } } } } } } return ssrc; }; SimulcastReceiver.prototype.getReceivingVideoStreamBySSRC = function (ssrc) { var session, electedStream; var i, j, k; if (connection.jingle) { var keys = Object.keys(connection.jingle.sessions); for (i = 0; i < keys.length; i++) { var sid = keys[i]; if (electedStream) { // stream found, stop. break; } session = connection.jingle.sessions[sid]; if (session.remoteStreams) { for (j = 0; j < session.remoteStreams.length; j++) { var remoteStream = session.remoteStreams[j]; if (electedStream) { // stream found, stop. break; } var tracks = remoteStream.getVideoTracks(); if (tracks) { for (k = 0; k < tracks.length; k++) { var track = tracks[k]; var msid = [remoteStream.id, track.id].join(' '); var tmp = this._remoteMaps.msid2ssrc[msid]; if (tmp == ssrc) { electedStream = new webkitMediaStream([track]); // stream found, stop. break; } } } } } } } return { session: session, stream: electedStream }; }; /** * 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 = {}; /** * Produces a single stream with multiple tracks for local video sources. * * @param lines * @private */ NativeSimulcastSender.prototype._explodeSimulcastSenderSources = function (lines) { var sb, msid, sid, tid, videoSources, self; this.logger.info('Exploding local video sources...'); videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0]; self = this; if (videoSources.groups && videoSources.groups.length !== 0) { videoSources.groups.forEach(function (group) { if (group.semantics === 'SIM') { group.ssrcs.forEach(function (ssrc) { // Get the msid for this ssrc.. if (self._localExplosionMap[ssrc]) { // .. either from the explosion map.. msid = self._localExplosionMap[ssrc]; } else { // .. or generate a new one (msid). sid = videoSources.sources[ssrc].msid .substring(0, videoSources.sources[ssrc].msid.indexOf(' ')); tid = self._generateGuid(); msid = [sid, tid].join(' '); self._localExplosionMap[ssrc] = msid; } // Assign it to the source object. videoSources.sources[ssrc].msid = msid; // TODO(gp) Change the msid of associated sources. }); } }); } sb = this.simulcastUtils._compileVideoSources(videoSources); this.simulcastUtils._replaceVideoSources(lines, sb); }; /** * GUM for simulcast. * * @param constraints * @param success * @param err */ NativeSimulcastSender.prototype.getUserMedia = function (constraints, success, err) { // There's nothing special to do for native simulcast, so just do a normal GUM. var self = this; navigator.webkitGetUserMedia(constraints, function (hqStream) { self.localStream = hqStream; success(hqStream); }, err); }; /** * Prepares the local description for public usage (i.e. to be signaled * through Jingle to the focus). * * @param desc * @returns {RTCSessionDescription} */ NativeSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) { var sb; if (!desc || desc == null) { return desc; } sb = desc.sdp.split('\r\n'); this._explodeSimulcastSenderSources(sb); desc = new RTCSessionDescription({ type: desc.type, sdp: sb.join('\r\n') }); this.logger.fine(['Exploded local video sources', desc.sdp].join(' ')); return desc; }; /** * Ensures that the simulcast group is present in the answer, _if_ native * simulcast is enabled, * * @param desc * @returns {*} */ NativeSimulcastSender.prototype.transformAnswer = function (desc) { var sb = desc.sdp.split('\r\n'); // Even if we have enabled native simulcasting previously // (with a call to SLD with an appropriate SDP, for example), // createAnswer seems to consistently generate incomplete SDP // with missing SSRCS. // // So, subsequent calls to SLD will have missing SSRCS and presence // won't have the complete list of SRCs. this._ensureSimulcastGroup(sb); desc = new RTCSessionDescription({ type: desc.type, sdp: sb.join('\r\n') }); this.logger.fine(['Transformed answer', desc.sdp].join(' ')); return desc; }; /** * * * @param desc * @returns {*} */ NativeSimulcastSender.prototype.transformLocalDescription = function (desc) { 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 {*} */ SimulcastReceiver.prototype.transformRemoteDescription = function (desc) { var sb = desc.sdp.split('\r\n'); this._updateRemoteMaps(sb); this._cacheRemoteVideoSources(sb); // 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({ type: desc.type, sdp: sb.join('\r\n') }); this.logger.fine(['Transformed remote description', desc.sdp].join(' ')); return desc; }; NativeSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) { // Nothing to do here, native simulcast does that auto-magically. }; NativeSimulcastSender.prototype.constructor = NativeSimulcastSender; function SimpleSimulcastSender() { SimulcastSender.call(this); } SimpleSimulcastSender.prototype = Object.create(SimulcastSender.prototype); SimpleSimulcastSender.prototype._localMaps = { msids: [], msid2ssrc: {} }; /** * Groups local video sources together in the ssrc-group:SIM group. * * @param lines * @private */ SimpleSimulcastSender.prototype._groupLocalVideoSources = function (lines) { var sb, videoSources, ssrcs = [], ssrc; this.logger.info('Grouping local video sources...'); videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0]; for (ssrc in videoSources.sources) { // jitsi-meet destroys/creates streams at various places causing // the original local stream ids to change. The only thing that // remains unchanged is the trackid. this._localMaps.msid2ssrc[videoSources.sources[ssrc].msid.split(' ')[1]] = ssrc; } var self = this; // TODO(gp) add only "free" sources. this._localMaps.msids.forEach(function (msid) { ssrcs.push(self._localMaps.msid2ssrc[msid]); }); if (!videoSources.groups) { videoSources.groups = []; } videoSources.groups.push({ 'semantics': 'SIM', 'ssrcs': ssrcs }); sb = this.simulcastUtils._compileVideoSources(videoSources); this.simulcastUtils._replaceVideoSources(lines, sb); }; /** * GUM for simulcast. * * @param constraints * @param success * @param err */ SimpleSimulcastSender.prototype.getUserMedia = function (constraints, success, err) { // TODO(gp) what if we request a resolution not supported by the hardware? // TODO(gp) make the lq stream configurable; although this wouldn't work with native simulcast var lqConstraints = { audio: false, video: { mandatory: { maxWidth: 320, maxHeight: 180, maxFrameRate: 15 } } }; this.logger.info('HQ constraints: ', constraints); this.logger.info('LQ constraints: ', lqConstraints); // NOTE(gp) if we request the lq stream first webkitGetUserMedia // fails randomly. Tested with Chrome 37. As fippo suggested, the // reason appears to be that Chrome only acquires the cam once and // then downscales the picture (https://code.google.com/p/chromium/issues/detail?id=346616#c11) var self = this; navigator.webkitGetUserMedia(constraints, function (hqStream) { self.localStream = hqStream; // reset local maps. self._localMaps.msids = []; self._localMaps.msid2ssrc = {}; // add hq trackid to local map self._localMaps.msids.push(hqStream.getVideoTracks()[0].id); navigator.webkitGetUserMedia(lqConstraints, function (lqStream) { self.displayedLocalVideoStream = lqStream; // NOTE(gp) The specification says Array.forEach() will visit // the array elements in numeric order, and that it doesn't // visit elements that don't exist. // add lq trackid to local map self._localMaps.msids.splice(0, 0, lqStream.getVideoTracks()[0].id); self.localStream.addTrack(lqStream.getVideoTracks()[0]); success(self.localStream); }, err); }, err); }; /** * Prepares the local description for public usage (i.e. to be signaled * through Jingle to the focus). * * @param desc * @returns {RTCSessionDescription} */ SimpleSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) { var sb; if (!desc || desc == null) { return desc; } sb = desc.sdp.split('\r\n'); this._groupLocalVideoSources(sb); desc = new RTCSessionDescription({ type: desc.type, sdp: sb.join('\r\n') }); this.logger.fine('Grouped local video sources'); this.logger.fine(desc.sdp); return desc; }; /** * Ensures that the simulcast group is present in the answer, _if_ native * simulcast is enabled, * * @param desc * @returns {*} */ SimpleSimulcastSender.prototype.transformAnswer = function (desc) { return desc; }; /** * * * @param desc * @returns {*} */ SimpleSimulcastSender.prototype.transformLocalDescription = function (desc) { var sb = desc.sdp.split('\r\n'); this.simulcastUtils._removeSimulcastGroup(sb); desc = new RTCSessionDescription({ type: desc.type, sdp: sb.join('\r\n') }); this.logger.fine('Transformed local description'); this.logger.fine(desc.sdp); return desc; }; SimpleSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) { var trackid; var self = this; 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) { trackid = tid; return true; } }) && self.localStream.getVideoTracks().some(function (track) { // Start/stop the track that corresponds to the track id if (track.id === trackid) { track.enabled = enabled; return true; } })) { this.logger.log([trackid, enabled ? 'enabled' : 'disabled'].join(' ')); $(document).trigger(enabled ? 'simulcastlayerstarted' : 'simulcastlayerstopped'); } else { this.logger.error("I don't have a local stream with SSRC " + ssrc); } }; SimpleSimulcastSender.prototype.constructor = SimpleSimulcastSender; function NoSimulcastSender() { SimulcastSender.call(this); } NoSimulcastSender.prototype = Object.create(SimulcastSender.prototype); /** * GUM for simulcast. * * @param constraints * @param success * @param err */ NoSimulcastSender.prototype.getUserMedia = function (constraints, success, err) { var self = this; navigator.webkitGetUserMedia(constraints, function (hqStream) { self.localStream = hqStream; success(self.localStream); }, err); }; /** * Prepares the local description for public usage (i.e. to be signaled * through Jingle to the focus). * * @param desc * @returns {RTCSessionDescription} */ NoSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) { return desc; }; /** * Ensures that the simulcast group is present in the answer, _if_ native * simulcast is enabled, * * @param desc * @returns {*} */ NoSimulcastSender.prototype.transformAnswer = function (desc) { return desc; }; /** * * * @param desc * @returns {*} */ 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(); // Initialize local simulcast. // TODO(gp) move into SimulcastManager.prototype.getUserMedia and take into // account constraints. if (!config.enableSimulcast) { this.simulcastSender = new NoSimulcastSender(); } else { var isChromium = window.chrome, vendorName = window.navigator.vendor; if(isChromium !== null && isChromium !== undefined /* skip opera */ && vendorName === "Google Inc." /* skip Chromium as suggested by fippo */ && !window.navigator.appVersion.match(/Chromium\//) ) { 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(); } } } /** * 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 * is already present, it fails to set the remote description. * * @param desc * @returns {*} */ SimulcastManager.prototype.transformRemoteDescription = function (desc) { return this.simulcastReceiver.transformRemoteDescription(desc); }; /** * 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); }; /** * 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); }; /** * * * @param desc * @returns {*} */ SimulcastManager.prototype.transformLocalDescription = function (desc) { return this.simulcastSender.transformLocalDescription(desc); }; /** * * @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) { 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); }; SimulcastManager.prototype.getReceivingSSRC = function (jid) { return this.simulcastReceiver.getReceivingSSRC(jid); }; SimulcastManager.prototype.getReceivingVideoStreamBySSRC = function (msid) { return this.simulcastReceiver.getReceivingVideoStreamBySSRC(msid); }; /** * * @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(resource, ssrc) { this.simulcastReceiver._setReceivingVideoStream(resource, 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; simulcast._setReceivingVideoStream(esl.endpoint, ssrc); }); }); $(document).bind('startsimulcastlayer', function (event, simulcastLayer) { var ssrc = simulcastLayer.primarySSRC; simulcast._setLocalVideoStreamEnabled(ssrc, true); }); $(document).bind('stopsimulcastlayer', function (event, simulcastLayer) { var ssrc = simulcastLayer.primarySSRC; simulcast._setLocalVideoStreamEnabled(ssrc, false); });