mirror of https://github.com/jitsi/jitsi-meet
parent
fcf785f32c
commit
faaf24d3c4
@ -0,0 +1,32 @@ |
||||
/** |
||||
* |
||||
* @constructor |
||||
*/ |
||||
function SimulcastLogger(name, lvl) { |
||||
this.name = name; |
||||
this.lvl = lvl; |
||||
} |
||||
|
||||
SimulcastLogger.prototype.log = function (text) { |
||||
if (this.lvl) { |
||||
console.log(text); |
||||
} |
||||
}; |
||||
|
||||
SimulcastLogger.prototype.info = function (text) { |
||||
if (this.lvl > 1) { |
||||
console.info(text); |
||||
} |
||||
}; |
||||
|
||||
SimulcastLogger.prototype.fine = function (text) { |
||||
if (this.lvl > 2) { |
||||
console.log(text); |
||||
} |
||||
}; |
||||
|
||||
SimulcastLogger.prototype.error = function (text) { |
||||
console.error(text); |
||||
}; |
||||
|
||||
module.exports = SimulcastLogger; |
@ -0,0 +1,302 @@ |
||||
var SimulcastLogger = require("./SimulcastLogger"); |
||||
var SimulcastUtils = require("./SimulcastUtils"); |
||||
|
||||
function SimulcastReceiver() { |
||||
this.simulcastUtils = new SimulcastUtils(); |
||||
this.logger = new SimulcastLogger('SimulcastReceiver', 1); |
||||
} |
||||
|
||||
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 (!this.simulcastUtils.isValidDescription(desc)) { |
||||
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]; |
||||
}; |
||||
|
||||
/** |
||||
* 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) { |
||||
|
||||
if (desc && desc.sdp) { |
||||
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); |
||||
|
||||
if (desc.sdp.indexOf('a=ssrc-group:SIM') !== -1) { |
||||
// We don't need the goog conference flag if we're not doing
|
||||
// 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; |
||||
}; |
||||
|
||||
module.exports = SimulcastReceiver; |
@ -0,0 +1,519 @@ |
||||
var SimulcastLogger = require("./SimulcastLogger"); |
||||
var SimulcastUtils = require("./SimulcastUtils"); |
||||
|
||||
function SimulcastSender() { |
||||
this.simulcastUtils = new SimulcastUtils(); |
||||
this.logger = new SimulcastLogger('SimulcastSender', 1); |
||||
} |
||||
|
||||
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(); |
||||
}; |
||||
}()); |
||||
|
||||
// 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.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 = {}; |
||||
NativeSimulcastSender.prototype._isUsingScreenStream = false; |
||||
NativeSimulcastSender.prototype._localVideoSourceCache = ''; |
||||
|
||||
NativeSimulcastSender.prototype.reset = function () { |
||||
this._localExplosionMap = {}; |
||||
this._isUsingScreenStream = isUsingScreenStream; |
||||
}; |
||||
|
||||
NativeSimulcastSender.prototype._cacheLocalVideoSources = function (lines) { |
||||
this._localVideoSourceCache = this.simulcastUtils._getVideoSources(lines); |
||||
}; |
||||
|
||||
NativeSimulcastSender.prototype._restoreLocalVideoSources = function (lines) { |
||||
this.simulcastUtils._replaceVideoSources(lines, this._localVideoSourceCache); |
||||
}; |
||||
|
||||
NativeSimulcastSender.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.
|
||||
NativeSimulcastSender.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); |
||||
} |
||||
}; |
||||
|
||||
/** |
||||
* 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.
|
||||
navigator.webkitGetUserMedia(constraints, function (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 (!this.simulcastUtils.isValidDescription(desc) || this._isUsingScreenStream) { |
||||
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) { |
||||
|
||||
if (!this.simulcastUtils.isValidDescription(desc) || this._isUsingScreenStream) { |
||||
return 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; |
||||
}; |
||||
|
||||
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.localStream = null; |
||||
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 (!this.simulcastUtils.isValidDescription(desc)) { |
||||
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) { |
||||
navigator.webkitGetUserMedia(constraints, function (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} |
||||
*/ |
||||
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; |
||||
|
||||
module.exports = { |
||||
"native": NativeSimulcastSender, |
||||
"no": NoSimulcastSender |
||||
} |
@ -0,0 +1,233 @@ |
||||
var SimulcastLogger = require("./SimulcastLogger"); |
||||
|
||||
/** |
||||
* |
||||
* @constructor |
||||
*/ |
||||
function SimulcastUtils() { |
||||
this.logger = new SimulcastLogger("SimulcastUtils", 1); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @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.isValidDescription = function (desc) |
||||
{ |
||||
return desc && desc != null |
||||
&& desc.type && desc.type != '' |
||||
&& desc.sdp && desc.sdp != ''; |
||||
}; |
||||
|
||||
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 = true; |
||||
|
||||
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); |
||||
} |
||||
} |
||||
|
||||
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; |
||||
}; |
||||
|
||||
module.exports = SimulcastUtils; |
@ -0,0 +1,203 @@ |
||||
/*jslint plusplus: true */ |
||||
/*jslint nomen: true*/ |
||||
|
||||
var SimulcastSender = require("./SimulcastSender"); |
||||
var NoSimulcastSender = SimulcastSender["no"]; |
||||
var NativeSimulcastSender = SimulcastSender["native"]; |
||||
var SimulcastReceiver = require("./SimulcastReceiver"); |
||||
var SimulcastUtils = require("./SimulcastUtils"); |
||||
|
||||
|
||||
/** |
||||
* |
||||
* @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); |
||||
}; |
||||
|
||||
SimulcastManager.prototype.resetSender = function() { |
||||
if (typeof this.simulcastSender.reset === 'function'){ |
||||
this.simulcastSender.reset(); |
||||
} |
||||
}; |
||||
|
||||
$(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); |
||||
}); |
||||
|
||||
|
||||
var simulcast = new SimulcastManager(); |
||||
|
||||
module.exports = simulcast; |
Loading…
Reference in new issue