Refactors simulcast support.
This commit is contained in:
parent
b038d276c9
commit
23f1dc174e
1
app.js
1
app.js
|
@ -9,7 +9,6 @@ var APP =
|
|||
this.connectionquality = require("./modules/connectionquality/connectionquality");
|
||||
this.statistics = require("./modules/statistics/statistics");
|
||||
this.RTC = require("./modules/RTC/RTC");
|
||||
this.simulcast = require("./modules/simulcast/simulcast");
|
||||
this.desktopsharing = require("./modules/desktopsharing/desktopsharing");
|
||||
this.xmpp = require("./modules/xmpp/xmpp");
|
||||
this.keyboardshortcut = require("./modules/keyboardshortcut/keyboardshortcut");
|
||||
|
|
|
@ -112,24 +112,6 @@ var DataChannels =
|
|||
eventEmitter.emit(RTCEvents.LASTN_ENDPOINT_CHANGED,
|
||||
lastNEndpoints, endpointsEnteringLastN, obj);
|
||||
}
|
||||
else if ("SimulcastLayersChangedEvent" === colibriClass)
|
||||
{
|
||||
eventEmitter.emit(RTCEvents.SIMULCAST_LAYER_CHANGED,
|
||||
obj.endpointSimulcastLayers);
|
||||
}
|
||||
else if ("SimulcastLayersChangingEvent" === colibriClass)
|
||||
{
|
||||
eventEmitter.emit(RTCEvents.SIMULCAST_LAYER_CHANGING,
|
||||
obj.endpointSimulcastLayers);
|
||||
}
|
||||
else if ("StartSimulcastLayerEvent" === colibriClass)
|
||||
{
|
||||
eventEmitter.emit(RTCEvents.SIMULCAST_START, obj.simulcastLayer);
|
||||
}
|
||||
else if ("StopSimulcastLayerEvent" === colibriClass)
|
||||
{
|
||||
eventEmitter.emit(RTCEvents.SIMULCAST_STOP, obj.simulcastLayer);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.debug("Data channel JSON-formatted message: ", obj);
|
||||
|
|
|
@ -131,7 +131,7 @@ function RTCUtils(RTCService)
|
|||
console.log('This appears to be Firefox');
|
||||
var version = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
|
||||
if (version >= 40
|
||||
&& !config.enableSimulcast && config.useBundle && config.useRtcpMux) {
|
||||
&& config.useBundle && config.useRtcpMux) {
|
||||
this.peerconnection = mozRTCPeerConnection;
|
||||
this.browser = RTCBrowserType.RTC_BROWSER_FIREFOX;
|
||||
this.getUserMedia = navigator.mozGetUserMedia.bind(navigator);
|
||||
|
@ -237,44 +237,20 @@ RTCUtils.prototype.getUserMediaWithConstraints = function(
|
|||
var self = this;
|
||||
|
||||
try {
|
||||
if (config.enableSimulcast
|
||||
&& constraints.video
|
||||
&& constraints.video.chromeMediaSource !== 'screen'
|
||||
&& constraints.video.chromeMediaSource !== 'desktop'
|
||||
&& !isAndroid
|
||||
|
||||
// We currently do not support FF, as it doesn't have multistream support.
|
||||
&& !isFF) {
|
||||
APP.simulcast.getUserMedia(constraints, function (stream) {
|
||||
console.log('onUserMediaSuccess');
|
||||
self.setAvailableDevices(um, true);
|
||||
success_callback(stream);
|
||||
},
|
||||
function (error) {
|
||||
console.warn('Failed to get access to local media. Error ', error);
|
||||
self.setAvailableDevices(um, false);
|
||||
if (failure_callback) {
|
||||
failure_callback(error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
this.getUserMedia(constraints,
|
||||
function (stream) {
|
||||
console.log('onUserMediaSuccess');
|
||||
self.setAvailableDevices(um, true);
|
||||
success_callback(stream);
|
||||
},
|
||||
function (error) {
|
||||
self.setAvailableDevices(um, false);
|
||||
console.warn('Failed to get access to local media. Error ',
|
||||
error, constraints);
|
||||
if (failure_callback) {
|
||||
failure_callback(error);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
this.getUserMedia(constraints,
|
||||
function (stream) {
|
||||
console.log('onUserMediaSuccess');
|
||||
self.setAvailableDevices(um, true);
|
||||
success_callback(stream);
|
||||
},
|
||||
function (error) {
|
||||
self.setAvailableDevices(um, false);
|
||||
console.warn('Failed to get access to local media. Error ',
|
||||
error, constraints);
|
||||
if (failure_callback) {
|
||||
failure_callback(error);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('GUM failed: ', e);
|
||||
if(failure_callback) {
|
||||
|
|
|
@ -122,14 +122,6 @@ function registerListeners() {
|
|||
VideoLayout.onLastNEndpointsChanged(lastNEndpoints,
|
||||
endpointsEnteringLastN, stream);
|
||||
});
|
||||
APP.RTC.addListener(RTCEvents.SIMULCAST_LAYER_CHANGED,
|
||||
function (endpointSimulcastLayers) {
|
||||
VideoLayout.onSimulcastLayersChanged(endpointSimulcastLayers);
|
||||
});
|
||||
APP.RTC.addListener(RTCEvents.SIMULCAST_LAYER_CHANGING,
|
||||
function (endpointSimulcastLayers) {
|
||||
VideoLayout.onSimulcastLayersChanging(endpointSimulcastLayers);
|
||||
});
|
||||
APP.RTC.addListener(RTCEvents.AVAILABLE_DEVICES_CHANGED,
|
||||
function (devices) {
|
||||
VideoLayout.setDeviceAvailabilityIcons(null, devices);
|
||||
|
|
|
@ -96,17 +96,9 @@ ConnectionIndicator.prototype.generateText = function () {
|
|||
if(this.resolution && this.jid != null)
|
||||
{
|
||||
var keys = Object.keys(this.resolution);
|
||||
if(keys.length == 1)
|
||||
for(var ssrc in this.resolution)
|
||||
{
|
||||
for(var ssrc in this.resolution)
|
||||
{
|
||||
resolutionValue = this.resolution[ssrc];
|
||||
}
|
||||
}
|
||||
else if(keys.length > 1)
|
||||
{
|
||||
var displayedSsrc = APP.simulcast.getReceivingSSRC(this.jid);
|
||||
resolutionValue = this.resolution[displayedSsrc];
|
||||
resolutionValue = this.resolution[ssrc];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -93,8 +93,7 @@ function waitForRemoteVideo(selector, ssrc, stream, jid) {
|
|||
if (stream.id === 'mixedmslabel') return;
|
||||
|
||||
if (selector[0].currentTime > 0) {
|
||||
var videoStream = APP.simulcast.getReceivingVideoStream(stream);
|
||||
APP.RTC.attachMediaStream(selector, videoStream); // FIXME: why do i have to do this for FF?
|
||||
APP.RTC.attachMediaStream(selector, stream); // FIXME: why do i have to do this for FF?
|
||||
videoactive(selector);
|
||||
} else {
|
||||
setTimeout(function () {
|
||||
|
@ -629,8 +628,7 @@ var VideoLayout = (function (my) {
|
|||
}
|
||||
|
||||
// Attach WebRTC stream
|
||||
var videoStream = APP.simulcast.getLocalVideoStream();
|
||||
APP.RTC.attachMediaStream(localVideoSelector, videoStream);
|
||||
APP.RTC.attachMediaStream(localVideoSelector, stream.getOriginalStream());
|
||||
|
||||
// Add stream ended handler
|
||||
stream.getOriginalStream().onended = function () {
|
||||
|
@ -824,113 +822,67 @@ var VideoLayout = (function (my) {
|
|||
// Screen stream is already rotated
|
||||
largeVideoState.flipX = (newSrc === localVideoSrc) && flipXLocalVideo;
|
||||
|
||||
var userChanged = false;
|
||||
if (largeVideoState.oldResourceJid !== largeVideoState.userResourceJid) {
|
||||
userChanged = true;
|
||||
// we want the notification to trigger even if userJid is undefined,
|
||||
// or null.
|
||||
eventEmitter.emit(UIEvents.SELECTED_ENDPOINT,
|
||||
largeVideoState.userResourceJid);
|
||||
}
|
||||
|
||||
if (!largeVideoState.updateInProgress) {
|
||||
largeVideoState.updateInProgress = true;
|
||||
$('#largeVideo').fadeOut(300, function () {
|
||||
Avatar.updateActiveSpeakerAvatarSrc(
|
||||
APP.xmpp.findJidFromResource(
|
||||
largeVideoState.userResourceJid));
|
||||
|
||||
var doUpdate = function () {
|
||||
Avatar.updateActiveSpeakerAvatarSrc(
|
||||
APP.xmpp.findJidFromResource(
|
||||
largeVideoState.userResourceJid));
|
||||
APP.RTC.setVideoSrc($('#largeVideo')[0], largeVideoState.newSrc);
|
||||
|
||||
if (!userChanged && largeVideoState.preload &&
|
||||
largeVideoState.preload !== null &&
|
||||
APP.RTC.getVideoSrc($(largeVideoState.preload)[0]) === newSrc)
|
||||
{
|
||||
var videoTransform = document.getElementById('largeVideo')
|
||||
.style.webkitTransform;
|
||||
|
||||
console.info('Switching to preloaded video');
|
||||
var attributes = $('#largeVideo').prop("attributes");
|
||||
|
||||
// loop through largeVideo attributes and apply them on
|
||||
// preload.
|
||||
$.each(attributes, function () {
|
||||
if (this.name !== 'id' && this.name !== 'src') {
|
||||
largeVideoState.preload.attr(this.name, this.value);
|
||||
}
|
||||
});
|
||||
|
||||
largeVideoState.preload.appendTo($('#largeVideoContainer'));
|
||||
$('#largeVideo').attr('id', 'previousLargeVideo');
|
||||
largeVideoState.preload.attr('id', 'largeVideo');
|
||||
$('#previousLargeVideo').remove();
|
||||
|
||||
largeVideoState.preload.on('loadedmetadata', function (e) {
|
||||
currentVideoWidth = this.videoWidth;
|
||||
currentVideoHeight = this.videoHeight;
|
||||
VideoLayout.positionLarge(currentVideoWidth, currentVideoHeight);
|
||||
});
|
||||
largeVideoState.preload = null;
|
||||
largeVideoState.preload_ssrc = 0;
|
||||
} else {
|
||||
APP.RTC.setVideoSrc($('#largeVideo')[0], largeVideoState.newSrc);
|
||||
}
|
||||
|
||||
var videoTransform = document.getElementById('largeVideo')
|
||||
.style.webkitTransform;
|
||||
|
||||
if (largeVideoState.flipX && videoTransform !== 'scaleX(-1)') {
|
||||
document.getElementById('largeVideo').style.webkitTransform
|
||||
= "scaleX(-1)";
|
||||
}
|
||||
else if (!largeVideoState.flipX && videoTransform === 'scaleX(-1)') {
|
||||
document.getElementById('largeVideo').style.webkitTransform
|
||||
= "none";
|
||||
}
|
||||
|
||||
// Change the way we'll be measuring and positioning large video
|
||||
|
||||
VideoLayout.getVideoSize = largeVideoState.isDesktop
|
||||
? getDesktopVideoSize
|
||||
: getCameraVideoSize;
|
||||
VideoLayout.getVideoPosition = largeVideoState.isDesktop
|
||||
? getDesktopVideoPosition
|
||||
: getCameraVideoPosition;
|
||||
|
||||
|
||||
// Only if the large video is currently visible.
|
||||
// Disable previous dominant speaker video.
|
||||
if (largeVideoState.oldResourceJid) {
|
||||
VideoLayout.enableDominantSpeaker(
|
||||
largeVideoState.oldResourceJid,
|
||||
false);
|
||||
}
|
||||
|
||||
// Enable new dominant speaker in the remote videos section.
|
||||
if (largeVideoState.userResourceJid) {
|
||||
VideoLayout.enableDominantSpeaker(
|
||||
largeVideoState.userResourceJid,
|
||||
true);
|
||||
}
|
||||
|
||||
if (userChanged && largeVideoState.isVisible) {
|
||||
// using "this" should be ok because we're called
|
||||
// from within the fadeOut event.
|
||||
$(this).fadeIn(300);
|
||||
}
|
||||
|
||||
if(userChanged) {
|
||||
Avatar.showUserAvatar(
|
||||
APP.xmpp.findJidFromResource(
|
||||
largeVideoState.oldResourceJid));
|
||||
}
|
||||
|
||||
largeVideoState.updateInProgress = false;
|
||||
};
|
||||
|
||||
if (userChanged) {
|
||||
$('#largeVideo').fadeOut(300, doUpdate);
|
||||
} else {
|
||||
doUpdate();
|
||||
if (largeVideoState.flipX && videoTransform !== 'scaleX(-1)') {
|
||||
document.getElementById('largeVideo').style.webkitTransform
|
||||
= "scaleX(-1)";
|
||||
}
|
||||
}
|
||||
else if (!largeVideoState.flipX && videoTransform === 'scaleX(-1)') {
|
||||
document.getElementById('largeVideo').style.webkitTransform
|
||||
= "none";
|
||||
}
|
||||
|
||||
// Change the way we'll be measuring and positioning large video
|
||||
|
||||
VideoLayout.getVideoSize = largeVideoState.isDesktop
|
||||
? getDesktopVideoSize
|
||||
: getCameraVideoSize;
|
||||
VideoLayout.getVideoPosition = largeVideoState.isDesktop
|
||||
? getDesktopVideoPosition
|
||||
: getCameraVideoPosition;
|
||||
|
||||
|
||||
// Only if the large video is currently visible.
|
||||
// Disable previous dominant speaker video.
|
||||
if (largeVideoState.oldResourceJid) {
|
||||
VideoLayout.enableDominantSpeaker(
|
||||
largeVideoState.oldResourceJid,
|
||||
false);
|
||||
}
|
||||
|
||||
// Enable new dominant speaker in the remote videos section.
|
||||
if (largeVideoState.userResourceJid) {
|
||||
VideoLayout.enableDominantSpeaker(
|
||||
largeVideoState.userResourceJid,
|
||||
true);
|
||||
}
|
||||
|
||||
if (largeVideoState.isVisible) {
|
||||
// using "this" should be ok because we're called
|
||||
// from within the fadeOut event.
|
||||
$(this).fadeIn(300);
|
||||
}
|
||||
|
||||
Avatar.showUserAvatar(
|
||||
APP.xmpp.findJidFromResource(
|
||||
largeVideoState.oldResourceJid));
|
||||
});
|
||||
} else {
|
||||
Avatar.showUserAvatar(
|
||||
APP.xmpp.findJidFromResource(
|
||||
|
@ -1188,8 +1140,7 @@ var VideoLayout = (function (my) {
|
|||
// If the container is currently visible we attach the stream.
|
||||
if (!isVideo
|
||||
|| (container.offsetParent !== null && isVideo)) {
|
||||
var videoStream = APP.simulcast.getReceivingVideoStream(stream);
|
||||
APP.RTC.attachMediaStream(sel, videoStream);
|
||||
APP.RTC.attachMediaStream(sel, stream);
|
||||
|
||||
if (isVideo)
|
||||
waitForRemoteVideo(sel, thessrc, stream, peerJid);
|
||||
|
@ -2025,9 +1976,7 @@ var VideoLayout = (function (my) {
|
|||
var mediaStream = APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
|
||||
var sel = $('#participant_' + resourceJid + '>video');
|
||||
|
||||
var videoStream = APP.simulcast.getReceivingVideoStream(
|
||||
mediaStream.stream);
|
||||
APP.RTC.attachMediaStream(sel, videoStream);
|
||||
APP.RTC.attachMediaStream(sel, mediaStream.stream);
|
||||
if (lastNPickupJid == mediaStream.peerjid) {
|
||||
// Clean up the lastN pickup jid.
|
||||
lastNPickupJid = null;
|
||||
|
@ -2078,146 +2027,6 @@ var VideoLayout = (function (my) {
|
|||
}
|
||||
};
|
||||
|
||||
my.onSimulcastLayersChanging = function (endpointSimulcastLayers) {
|
||||
endpointSimulcastLayers.forEach(function (esl) {
|
||||
|
||||
var resource = esl.endpoint;
|
||||
|
||||
// if lastN is enabled *and* the endpoint is *not* in the lastN set,
|
||||
// then ignore the event (= do not preload anything).
|
||||
//
|
||||
// The bridge could probably stop sending this message if it's for
|
||||
// an endpoint that's not in lastN.
|
||||
|
||||
if (lastNCount != -1
|
||||
&& (lastNCount < 1 || lastNEndpointsCache.indexOf(resource) === -1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var primarySSRC = esl.simulcastLayer.primarySSRC;
|
||||
|
||||
// Get session and stream from primary ssrc.
|
||||
var res = APP.simulcast.getReceivingVideoStreamBySSRC(primarySSRC);
|
||||
var sid = res.sid;
|
||||
var electedStream = res.stream;
|
||||
|
||||
if (sid && electedStream) {
|
||||
var msid = APP.simulcast.getRemoteVideoStreamIdBySSRC(primarySSRC);
|
||||
|
||||
console.info([esl, primarySSRC, msid, sid, electedStream]);
|
||||
|
||||
var preload = (Strophe.getResourceFromJid(APP.xmpp.getJidFromSSRC(primarySSRC)) == largeVideoState.userResourceJid);
|
||||
|
||||
if (preload) {
|
||||
if (largeVideoState.preload)
|
||||
{
|
||||
$(largeVideoState.preload).remove();
|
||||
}
|
||||
console.info('Preloading remote video');
|
||||
largeVideoState.preload = $('<video autoplay></video>');
|
||||
// ssrcs are unique in an rtp session
|
||||
largeVideoState.preload_ssrc = primarySSRC;
|
||||
|
||||
APP.RTC.attachMediaStream(largeVideoState.preload, electedStream)
|
||||
}
|
||||
|
||||
} else {
|
||||
console.error('Could not find a stream or a session.', sid, electedStream);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* On simulcast layers changed event.
|
||||
*/
|
||||
my.onSimulcastLayersChanged = function (endpointSimulcastLayers) {
|
||||
endpointSimulcastLayers.forEach(function (esl) {
|
||||
|
||||
var resource = esl.endpoint;
|
||||
|
||||
// if lastN is enabled *and* the endpoint is *not* in the lastN set,
|
||||
// then ignore the event (= do not change large video/thumbnail
|
||||
// SRCs).
|
||||
//
|
||||
// Note that even if we ignore the "changed" event in this event
|
||||
// handler, the bridge must continue sending these events because
|
||||
// the simulcast code in simulcast.js uses it to know what's going
|
||||
// to be streamed by the bridge when/if the endpoint gets back into
|
||||
// the lastN set.
|
||||
|
||||
if (lastNCount != -1
|
||||
&& (lastNCount < 1 || lastNEndpointsCache.indexOf(resource) === -1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var primarySSRC = esl.simulcastLayer.primarySSRC;
|
||||
|
||||
// Get session and stream from primary ssrc.
|
||||
var res = APP.simulcast.getReceivingVideoStreamBySSRC(primarySSRC);
|
||||
var sid = res.sid;
|
||||
var electedStream = res.stream;
|
||||
|
||||
if (sid && electedStream) {
|
||||
var msid = APP.simulcast.getRemoteVideoStreamIdBySSRC(primarySSRC);
|
||||
|
||||
console.info('Switching simulcast substream.');
|
||||
console.info([esl, primarySSRC, msid, sid, electedStream]);
|
||||
|
||||
var msidParts = msid.split(' ');
|
||||
var selRemoteVideo = $(['#', 'remoteVideo_', sid, '_', msidParts[0]].join(''));
|
||||
|
||||
var updateLargeVideo = (Strophe.getResourceFromJid(APP.xmpp.getJidFromSSRC(primarySSRC))
|
||||
== largeVideoState.userResourceJid);
|
||||
var updateFocusedVideoSrc = (focusedVideoInfo && focusedVideoInfo.src && focusedVideoInfo.src != '' &&
|
||||
(APP.RTC.getVideoSrc(selRemoteVideo[0]) == focusedVideoInfo.src));
|
||||
|
||||
var electedStreamUrl;
|
||||
if (largeVideoState.preload_ssrc == primarySSRC)
|
||||
{
|
||||
APP.RTC.setVideoSrc(selRemoteVideo[0], APP.RTC.getVideoSrc(largeVideoState.preload[0]));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (largeVideoState.preload
|
||||
&& largeVideoState.preload != null) {
|
||||
$(largeVideoState.preload).remove();
|
||||
}
|
||||
|
||||
largeVideoState.preload_ssrc = 0;
|
||||
|
||||
APP.RTC.attachMediaStream(selRemoteVideo, electedStream);
|
||||
}
|
||||
|
||||
var jid = APP.xmpp.getJidFromSSRC(primarySSRC);
|
||||
|
||||
if (updateLargeVideo) {
|
||||
VideoLayout.updateLargeVideo(APP.RTC.getVideoSrc(selRemoteVideo[0]), null,
|
||||
Strophe.getResourceFromJid(jid));
|
||||
}
|
||||
|
||||
if (updateFocusedVideoSrc) {
|
||||
focusedVideoInfo.src = APP.RTC.getVideoSrc(selRemoteVideo[0]);
|
||||
}
|
||||
|
||||
var videoId;
|
||||
if(resource == APP.xmpp.myResource())
|
||||
{
|
||||
videoId = "localVideoContainer";
|
||||
}
|
||||
else
|
||||
{
|
||||
videoId = "participant_" + resource;
|
||||
}
|
||||
var connectionIndicator = VideoLayout.connectionIndicators[videoId];
|
||||
if(connectionIndicator)
|
||||
connectionIndicator.updatePopoverData();
|
||||
|
||||
} else {
|
||||
console.error('Could not find a stream or a sid.', sid, electedStream);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates local stats
|
||||
* @param percent
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
/**
|
||||
*
|
||||
* @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;
|
|
@ -1,268 +0,0 @@
|
|||
var SimulcastLogger = require("./SimulcastLogger");
|
||||
var SimulcastUtils = require("./SimulcastUtils");
|
||||
var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
|
||||
|
||||
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)
|
||||
{
|
||||
var remoteStreamObject = APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
|
||||
var remoteStream = remoteStreamObject.getOriginalStream();
|
||||
var tracks = remoteStream.getVideoTracks();
|
||||
if (tracks) {
|
||||
for (var k = 0; k < tracks.length; k++) {
|
||||
var track = tracks[k];
|
||||
var msid = [remoteStream.id, track.id].join(' ');
|
||||
var _ssrc = this._remoteMaps.msid2ssrc[msid];
|
||||
var quality = this._remoteMaps.msid2Quality[msid];
|
||||
if (quality == 0) {
|
||||
ssrc = _ssrc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ssrc;
|
||||
};
|
||||
|
||||
SimulcastReceiver.prototype.getReceivingVideoStreamBySSRC = function (ssrc)
|
||||
{
|
||||
var sid, electedStream;
|
||||
var i, j, k;
|
||||
var jid = APP.xmpp.getJidFromSSRC(ssrc);
|
||||
if(jid && APP.RTC.remoteStreams[jid])
|
||||
{
|
||||
var remoteStreamObject = APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
|
||||
var remoteStream = remoteStreamObject.getOriginalStream();
|
||||
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]);
|
||||
sid = remoteStreamObject.sid;
|
||||
// stream found, stop.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
console.debug(APP.RTC.remoteStreams, jid, ssrc);
|
||||
}
|
||||
|
||||
return {
|
||||
sid: sid,
|
||||
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;
|
|
@ -1,521 +0,0 @@
|
|||
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
|
||||
: APP.RTC.localVideo.getOriginalStream();
|
||||
};
|
||||
|
||||
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 = APP.desktopsharing.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);
|
||||
|
||||
if (videoSources.base) {
|
||||
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
|
||||
}
|
|
@ -1,233 +0,0 @@
|
|||
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;
|
|
@ -1,202 +0,0 @@
|
|||
/*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");
|
||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
|
||||
}
|
||||
APP.RTC.addListener(RTCEvents.SIMULCAST_LAYER_CHANGED,
|
||||
function (endpointSimulcastLayers) {
|
||||
endpointSimulcastLayers.forEach(function (esl) {
|
||||
var ssrc = esl.simulcastLayer.primarySSRC;
|
||||
simulcast._setReceivingVideoStream(esl.endpoint, ssrc);
|
||||
});
|
||||
});
|
||||
APP.RTC.addListener(RTCEvents.SIMULCAST_START, function (simulcastLayer) {
|
||||
var ssrc = simulcastLayer.primarySSRC;
|
||||
simulcast._setLocalVideoStreamEnabled(ssrc, true);
|
||||
});
|
||||
APP.RTC.addListener(RTCEvents.SIMULCAST_STOP, function (simulcastLayer) {
|
||||
var ssrc = simulcastLayer.primarySSRC;
|
||||
simulcast._setLocalVideoStreamEnabled(ssrc, false);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
};
|
||||
|
||||
var simulcast = new SimulcastManager();
|
||||
|
||||
module.exports = simulcast;
|
|
@ -1,97 +0,0 @@
|
|||
var transform = require('sdp-transform');
|
||||
|
||||
exports.write = function(session, opts) {
|
||||
|
||||
if (typeof session !== 'undefined' &&
|
||||
typeof session.media !== 'undefined' &&
|
||||
Array.isArray(session.media)) {
|
||||
|
||||
session.media.forEach(function (mLine) {
|
||||
// expand sources to ssrcs
|
||||
if (typeof mLine.sources !== 'undefined' &&
|
||||
Object.keys(mLine.sources).length !== 0) {
|
||||
mLine.ssrcs = [];
|
||||
Object.keys(mLine.sources).forEach(function (ssrc) {
|
||||
var source = mLine.sources[ssrc];
|
||||
Object.keys(source).forEach(function (attribute) {
|
||||
mLine.ssrcs.push({
|
||||
id: ssrc,
|
||||
attribute: attribute,
|
||||
value: source[attribute]
|
||||
});
|
||||
});
|
||||
});
|
||||
delete mLine.sources;
|
||||
}
|
||||
|
||||
// join ssrcs in ssrc groups
|
||||
if (typeof mLine.ssrcGroups !== 'undefined' &&
|
||||
Array.isArray(mLine.ssrcGroups)) {
|
||||
mLine.ssrcGroups.forEach(function (ssrcGroup) {
|
||||
if (typeof ssrcGroup.ssrcs !== 'undefined' &&
|
||||
Array.isArray(ssrcGroup.ssrcs)) {
|
||||
ssrcGroup.ssrcs = ssrcGroup.ssrcs.join(' ');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// join group mids
|
||||
if (typeof session !== 'undefined' &&
|
||||
typeof session.groups !== 'undefined' && Array.isArray(session.groups)) {
|
||||
|
||||
session.groups.forEach(function (g) {
|
||||
if (typeof g.mids !== 'undefined' && Array.isArray(g.mids)) {
|
||||
g.mids = g.mids.join(' ');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return transform.write(session, opts);
|
||||
};
|
||||
|
||||
exports.parse = function(sdp) {
|
||||
var session = transform.parse(sdp);
|
||||
|
||||
if (typeof session !== 'undefined' && typeof session.media !== 'undefined' &&
|
||||
Array.isArray(session.media)) {
|
||||
|
||||
session.media.forEach(function (mLine) {
|
||||
// group sources attributes by ssrc
|
||||
if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) {
|
||||
mLine.sources = {};
|
||||
mLine.ssrcs.forEach(function (ssrc) {
|
||||
if (!mLine.sources[ssrc.id])
|
||||
mLine.sources[ssrc.id] = {};
|
||||
mLine.sources[ssrc.id][ssrc.attribute] = ssrc.value;
|
||||
});
|
||||
|
||||
delete mLine.ssrcs;
|
||||
}
|
||||
|
||||
// split ssrcs in ssrc groups
|
||||
if (typeof mLine.ssrcGroups !== 'undefined' &&
|
||||
Array.isArray(mLine.ssrcGroups)) {
|
||||
mLine.ssrcGroups.forEach(function (ssrcGroup) {
|
||||
if (typeof ssrcGroup.ssrcs === 'string') {
|
||||
ssrcGroup.ssrcs = ssrcGroup.ssrcs.split(' ');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// split group mids
|
||||
if (typeof session !== 'undefined' &&
|
||||
typeof session.groups !== 'undefined' && Array.isArray(session.groups)) {
|
||||
|
||||
session.groups.forEach(function (g) {
|
||||
if (typeof g.mids === 'string') {
|
||||
g.mids = g.mids.split(' ');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return session;
|
||||
};
|
||||
|
|
@ -5,6 +5,7 @@ var SDPUtil = require("./SDPUtil");
|
|||
var SDP = require("./SDP");
|
||||
var RTCBrowserType = require("../../service/RTC/RTCBrowserType");
|
||||
var async = require("async");
|
||||
var transform = require("sdp-transform");
|
||||
|
||||
// Jingle stuff
|
||||
function JingleSession(me, sid, connection, service) {
|
||||
|
@ -95,8 +96,8 @@ JingleSession.prototype.initiate = function (peerjid, isInitiator) {
|
|||
};
|
||||
this.peerconnection.onaddstream = function (event) {
|
||||
if (event.stream.id !== 'default') {
|
||||
console.log("REMOTE STREAM ADDED: " + event.stream + " - " + event.stream.id);
|
||||
self.remoteStreamAdded(event);
|
||||
console.log("REMOTE STREAM ADDED: " + event.stream + " - " + event.stream.id);
|
||||
self.remoteStreamAdded(event);
|
||||
} else {
|
||||
// This is a recvonly stream. Clients that implement Unified Plan,
|
||||
// such as Firefox use recvonly "streams/channels/tracks" for
|
||||
|
@ -199,7 +200,6 @@ JingleSession.prototype.accept = function () {
|
|||
// FIXME: change any inactive to sendrecv or whatever they were originally
|
||||
pranswer.sdp = pranswer.sdp.replace('a=inactive', 'a=sendrecv');
|
||||
}
|
||||
pranswer = APP.simulcast.reverseTransformLocalDescription(pranswer);
|
||||
var prsdp = new SDP(pranswer.sdp);
|
||||
var accept = $iq({to: this.peerjid,
|
||||
type: 'set'})
|
||||
|
@ -652,9 +652,7 @@ JingleSession.prototype.createdAnswer = function (sdp, provisional) {
|
|||
initiator: self.initiator,
|
||||
responder: self.responder,
|
||||
sid: self.sid });
|
||||
var publicLocalDesc = APP.simulcast.reverseTransformLocalDescription(sdp);
|
||||
var publicLocalSDP = new SDP(publicLocalDesc.sdp);
|
||||
publicLocalSDP.toJingle(accept, self.initiator == self.me ? 'initiator' : 'responder', ssrcs);
|
||||
self.localSDP.toJingle(accept, self.initiator == self.me ? 'initiator' : 'responder', ssrcs);
|
||||
self.connection.sendIQ(accept,
|
||||
function () {
|
||||
var ack = {};
|
||||
|
@ -1011,7 +1009,7 @@ JingleSession.prototype.switchStreams = function (new_stream, oldStream, success
|
|||
}
|
||||
|
||||
if(!isAudio)
|
||||
APP.RTC.switchVideoStreams(new_stream, oldStream);
|
||||
APP.RTC.switchVideoStreams(new_stream, oldStream);
|
||||
|
||||
// Conference is not active
|
||||
if(!oldSdp || !self.peerconnection) {
|
||||
|
@ -1239,14 +1237,17 @@ JingleSession.onJingleFatalError = function (session, error)
|
|||
JingleSession.prototype.setLocalDescription = function () {
|
||||
// put our ssrcs into presence so other clients can identify our stream
|
||||
var newssrcs = [];
|
||||
var media = APP.simulcast.parseMedia(this.peerconnection.localDescription);
|
||||
media.forEach(function (media) {
|
||||
var session = transform.parse(this.peerconnection.localDescription.sdp);
|
||||
session.media.forEach(function (media) {
|
||||
|
||||
if(Object.keys(media.sources).length > 0) {
|
||||
if (media.ssrcs != null && media.ssrcs.length > 0) {
|
||||
// TODO(gp) maybe exclude FID streams?
|
||||
Object.keys(media.sources).forEach(function (ssrc) {
|
||||
media.ssrcs.forEach(function (ssrc) {
|
||||
if (ssrc.attribute !== 'cname') {
|
||||
return;
|
||||
}
|
||||
newssrcs.push({
|
||||
'ssrc': ssrc,
|
||||
'ssrc': ssrc.id,
|
||||
'type': media.type,
|
||||
'direction': media.direction
|
||||
});
|
||||
|
|
|
@ -8,6 +8,8 @@ function TraceablePeerConnection(ice_config, constraints) {
|
|||
this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
|
||||
var Interop = require('sdp-interop').Interop;
|
||||
this.interop = new Interop();
|
||||
var Simulcast = require('sdp-simulcast');
|
||||
this.simulcast = new Simulcast({numOfLayers: 3, explodeRemoteSimulcast: false});
|
||||
|
||||
// override as desired
|
||||
this.trace = function (what, info) {
|
||||
|
@ -111,34 +113,31 @@ if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) {
|
|||
TraceablePeerConnection.prototype.__defineGetter__('signalingState', function() { return this.peerconnection.signalingState; });
|
||||
TraceablePeerConnection.prototype.__defineGetter__('iceConnectionState', function() { return this.peerconnection.iceConnectionState; });
|
||||
TraceablePeerConnection.prototype.__defineGetter__('localDescription', function() {
|
||||
this.trace('getLocalDescription::preTransform (Plan A)', dumpSDP(this.peerconnection.localDescription));
|
||||
// if we're running on FF, transform to Plan B first.
|
||||
var desc = this.peerconnection.localDescription;
|
||||
this.trace('getLocalDescription::preTransform', dumpSDP(desc));
|
||||
|
||||
// if we're running on FF, transform to Plan B first.
|
||||
if (navigator.mozGetUserMedia) {
|
||||
desc = this.interop.toPlanB(desc);
|
||||
} else {
|
||||
desc = APP.simulcast.reverseTransformLocalDescription(this.peerconnection.localDescription);
|
||||
this.trace('getLocalDescription::postTransform (Plan B)', dumpSDP(desc));
|
||||
}
|
||||
this.trace('getLocalDescription::postTransform (Plan B)', dumpSDP(desc));
|
||||
return desc;
|
||||
});
|
||||
TraceablePeerConnection.prototype.__defineGetter__('remoteDescription', function() {
|
||||
this.trace('getRemoteDescription::preTransform (Plan A)', dumpSDP(this.peerconnection.remoteDescription));
|
||||
// if we're running on FF, transform to Plan B first.
|
||||
var desc = this.peerconnection.remoteDescription;
|
||||
this.trace('getRemoteDescription::preTransform', dumpSDP(desc));
|
||||
|
||||
// if we're running on FF, transform to Plan B first.
|
||||
if (navigator.mozGetUserMedia) {
|
||||
desc = this.interop.toPlanB(desc);
|
||||
} else {
|
||||
desc = APP.simulcast.reverseTransformRemoteDescription(this.peerconnection.remoteDescription);
|
||||
this.trace('getRemoteDescription::postTransform (Plan B)', dumpSDP(desc));
|
||||
}
|
||||
this.trace('getRemoteDescription::postTransform (Plan B)', dumpSDP(desc));
|
||||
return desc;
|
||||
});
|
||||
}
|
||||
|
||||
TraceablePeerConnection.prototype.addStream = function (stream) {
|
||||
this.trace('addStream', stream.id);
|
||||
APP.simulcast.resetSender();
|
||||
try
|
||||
{
|
||||
this.peerconnection.addStream(stream);
|
||||
|
@ -152,7 +151,6 @@ TraceablePeerConnection.prototype.addStream = function (stream) {
|
|||
|
||||
TraceablePeerConnection.prototype.removeStream = function (stream, stopStreams) {
|
||||
this.trace('removeStream', stream.id);
|
||||
APP.simulcast.resetSender();
|
||||
if(stopStreams) {
|
||||
stream.getAudioTracks().forEach(function (track) {
|
||||
track.stop();
|
||||
|
@ -176,14 +174,13 @@ TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
|
|||
};
|
||||
|
||||
TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
|
||||
this.trace('setLocalDescription::preTransform (Plan B)', dumpSDP(description));
|
||||
this.trace('setLocalDescription::preTransform', dumpSDP(description));
|
||||
// if we're running on FF, transform to Plan A first.
|
||||
if (navigator.mozGetUserMedia) {
|
||||
description = this.interop.toUnifiedPlan(description);
|
||||
} else {
|
||||
description = APP.simulcast.transformLocalDescription(description);
|
||||
this.trace('setLocalDescription::postTransform (Plan A)', dumpSDP(description));
|
||||
}
|
||||
this.trace('setLocalDescription::postTransform (Plan A)', dumpSDP(description));
|
||||
|
||||
var self = this;
|
||||
this.peerconnection.setLocalDescription(description,
|
||||
function () {
|
||||
|
@ -203,15 +200,16 @@ TraceablePeerConnection.prototype.setLocalDescription = function (description, s
|
|||
};
|
||||
|
||||
TraceablePeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) {
|
||||
this.trace('setRemoteDescription::preTransform (Plan B)', dumpSDP(description));
|
||||
this.trace('setRemoteDescription::preTransform', dumpSDP(description));
|
||||
// TODO the focus should squeze or explode the remote simulcast
|
||||
description = this.simulcast.mungeRemoteDescription(description);
|
||||
this.trace('setRemoteDescription::postTransform (simulcast)', dumpSDP(description));
|
||||
|
||||
// if we're running on FF, transform to Plan A first.
|
||||
if (navigator.mozGetUserMedia) {
|
||||
description = this.interop.toUnifiedPlan(description);
|
||||
this.trace('setRemoteDescription::postTransform (Plan A)', dumpSDP(description));
|
||||
}
|
||||
else {
|
||||
description = APP.simulcast.transformRemoteDescription(description);
|
||||
}
|
||||
this.trace('setRemoteDescription::postTransform (Plan A)', dumpSDP(description));
|
||||
var self = this;
|
||||
this.peerconnection.setRemoteDescription(description,
|
||||
function () {
|
||||
|
@ -244,12 +242,19 @@ TraceablePeerConnection.prototype.createOffer = function (successCallback, failu
|
|||
this.trace('createOffer', JSON.stringify(constraints, null, ' '));
|
||||
this.peerconnection.createOffer(
|
||||
function (offer) {
|
||||
self.trace('createOfferOnSuccess::preTransform (Plan A)', dumpSDP(offer));
|
||||
self.trace('createOfferOnSuccess::preTransform', dumpSDP(offer));
|
||||
// if we're running on FF, transform to Plan B first.
|
||||
// NOTE this is not tested because in meet the focus generates the
|
||||
// offer.
|
||||
if (navigator.mozGetUserMedia) {
|
||||
offer = self.interop.toPlanB(offer);
|
||||
self.trace('createOfferOnSuccess::postTransform (Plan B)', dumpSDP(offer));
|
||||
}
|
||||
|
||||
if (config.enableSimulcast && self.simulcast.isSupported()) {
|
||||
offer = self.simulcast.mungeLocalDescription(offer);
|
||||
self.trace('createOfferOnSuccess::postTransform (simulcast)', dumpSDP(offer));
|
||||
}
|
||||
self.trace('createOfferOnSuccess::postTransform (Plan B)', dumpSDP(offer));
|
||||
successCallback(offer);
|
||||
},
|
||||
function(err) {
|
||||
|
@ -265,14 +270,16 @@ TraceablePeerConnection.prototype.createAnswer = function (successCallback, fail
|
|||
this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
|
||||
this.peerconnection.createAnswer(
|
||||
function (answer) {
|
||||
self.trace('createAnswerOnSuccess::preTransfom (Plan A)', dumpSDP(answer));
|
||||
self.trace('createAnswerOnSuccess::preTransfom', dumpSDP(answer));
|
||||
// if we're running on FF, transform to Plan A first.
|
||||
if (navigator.mozGetUserMedia) {
|
||||
answer = self.interop.toPlanB(answer);
|
||||
} else {
|
||||
answer = APP.simulcast.transformAnswer(answer);
|
||||
self.trace('createAnswerOnSuccess::postTransfom (Plan B)', dumpSDP(answer));
|
||||
}
|
||||
if (config.enableSimulcast && self.simulcast.isSupported()) {
|
||||
answer = self.simulcast.mungeLocalDescription(answer);
|
||||
self.trace('createAnswerOnSuccess::postTransfom (simulcast)', dumpSDP(answer));
|
||||
}
|
||||
self.trace('createAnswerOnSuccess::postTransfom (Plan B)', dumpSDP(answer));
|
||||
successCallback(answer);
|
||||
},
|
||||
function(err) {
|
||||
|
|
|
@ -191,6 +191,10 @@ var Moderator = {
|
|||
{ name: 'startVideoMuted', value: config.startVideoMuted})
|
||||
.up();
|
||||
}
|
||||
elem.c(
|
||||
'property',
|
||||
{ name: 'simulcastMode', value: 'rewriting'})
|
||||
.up();
|
||||
elem.up();
|
||||
return elem;
|
||||
},
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"i18next-client": "1.7.7",
|
||||
"sdp-interop": "0.1.4",
|
||||
"sdp-transform": "1.4.0",
|
||||
"sdp-simulcast": "0.1.0",
|
||||
"async": "0.9.0",
|
||||
"retry": "0.6.1"
|
||||
},
|
||||
|
|
|
@ -2,10 +2,6 @@ var RTCEvents = {
|
|||
LASTN_CHANGED: "rtc.lastn_changed",
|
||||
DOMINANTSPEAKER_CHANGED: "rtc.dominantspeaker_changed",
|
||||
LASTN_ENDPOINT_CHANGED: "rtc.lastn_endpoint_changed",
|
||||
SIMULCAST_LAYER_CHANGED: "rtc.simulcast_layer_changed",
|
||||
SIMULCAST_LAYER_CHANGING: "rtc.simulcast_layer_changing",
|
||||
SIMULCAST_START: "rtc.simlcast_start",
|
||||
SIMULCAST_STOP: "rtc.simlcast_stop",
|
||||
AVAILABLE_DEVICES_CHANGED: "rtc.available_devices_changed"
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue