Fixes flickering issue with simulcast.

This commit is contained in:
George Politis 2014-10-06 13:35:22 +02:00
parent da1e4183e5
commit 71c08450bb
3 changed files with 113 additions and 106 deletions

View File

@ -26,6 +26,5 @@ var config = {
enableRecording: false, enableRecording: false,
enableWelcomePage: false, enableWelcomePage: false,
enableSimulcast: false, enableSimulcast: false,
useNativeSimulcast: false,
isBrand: false isBrand: false
}; };

View File

@ -8,7 +8,14 @@ function Simulcast() {
"use strict"; "use strict";
// TODO(gp) split the Simulcast class in two classes : NativeSimulcast and ClassicSimulcast. // TODO(gp) split the Simulcast class in two classes : NativeSimulcast and ClassicSimulcast.
this.debugLvl = 1;
// Once we properly support native simulcast, enable it automatically in the
// supported browsers (Chrome).
this.useNativeSimulcast = false;
// TODO(gp) we need a logging framework for javascript à la log4j or the
// java logging framework that allows for selective log display
this.debugLvl = 0;
} }
(function () { (function () {
@ -446,7 +453,7 @@ function Simulcast() {
* @returns {*} * @returns {*}
*/ */
Simulcast.prototype.transformAnswer = function (desc) { Simulcast.prototype.transformAnswer = function (desc) {
if (config.enableSimulcast && config.useNativeSimulcast) { if (config.enableSimulcast && this.useNativeSimulcast) {
var sb = desc.sdp.split('\r\n'); var sb = desc.sdp.split('\r\n');
@ -523,7 +530,7 @@ function Simulcast() {
if (config.enableSimulcast) { if (config.enableSimulcast) {
if (config.useNativeSimulcast) { if (this.useNativeSimulcast) {
sb = desc.sdp.split('\r\n'); sb = desc.sdp.split('\r\n');
this._explodeLocalSimulcastSources(sb); this._explodeLocalSimulcastSources(sb);
@ -595,7 +602,7 @@ function Simulcast() {
* @returns {*} * @returns {*}
*/ */
Simulcast.prototype.transformLocalDescription = function (desc) { Simulcast.prototype.transformLocalDescription = function (desc) {
if (config.enableSimulcast && !config.useNativeSimulcast) { if (config.enableSimulcast && !this.useNativeSimulcast) {
var sb = desc.sdp.split('\r\n'); var sb = desc.sdp.split('\r\n');
@ -632,7 +639,7 @@ function Simulcast() {
this._cacheRemoteVideoSources(sb); this._cacheRemoteVideoSources(sb);
this._removeSimulcastGroup(sb); // NOTE(gp) this needs to be called after updateRemoteMaps because we need the simulcast group in the _updateRemoteMaps() method. this._removeSimulcastGroup(sb); // NOTE(gp) this needs to be called after updateRemoteMaps because we need the simulcast group in the _updateRemoteMaps() method.
if (config.useNativeSimulcast) { if (this.useNativeSimulcast) {
// We don't need the goog conference flag if we're not doing // We don't need the goog conference flag if we're not doing
// native simulcast. // native simulcast.
this._ensureGoogConference(sb); this._ensureGoogConference(sb);
@ -700,7 +707,7 @@ function Simulcast() {
: stream; : stream;
}; };
var stream; var localStream, displayedLocalVideoStream;
/** /**
* GUM for simulcast. * GUM for simulcast.
@ -727,7 +734,7 @@ function Simulcast() {
console.log('HQ constraints: ', constraints); console.log('HQ constraints: ', constraints);
console.log('LQ constraints: ', lqConstraints); console.log('LQ constraints: ', lqConstraints);
if (config.enableSimulcast && !config.useNativeSimulcast) { if (config.enableSimulcast && !this.useNativeSimulcast) {
// NOTE(gp) if we request the lq stream first webkitGetUserMedia // NOTE(gp) if we request the lq stream first webkitGetUserMedia
// fails randomly. Tested with Chrome 37. As fippo suggested, the // fails randomly. Tested with Chrome 37. As fippo suggested, the
@ -736,6 +743,8 @@ function Simulcast() {
navigator.webkitGetUserMedia(constraints, function (hqStream) { navigator.webkitGetUserMedia(constraints, function (hqStream) {
localStream = hqStream;
// reset local maps. // reset local maps.
localMaps.msids = []; localMaps.msids = [];
localMaps.msid2ssrc = {}; localMaps.msid2ssrc = {};
@ -745,6 +754,8 @@ function Simulcast() {
navigator.webkitGetUserMedia(lqConstraints, function (lqStream) { navigator.webkitGetUserMedia(lqConstraints, function (lqStream) {
displayedLocalVideoStream = lqStream;
// NOTE(gp) The specification says Array.forEach() will visit // NOTE(gp) The specification says Array.forEach() will visit
// the array elements in numeric order, and that it doesn't // the array elements in numeric order, and that it doesn't
// visit elements that don't exist. // visit elements that don't exist.
@ -752,9 +763,8 @@ function Simulcast() {
// add lq trackid to local map // add lq trackid to local map
localMaps.msids.splice(0, 0, lqStream.getVideoTracks()[0].id); localMaps.msids.splice(0, 0, lqStream.getVideoTracks()[0].id);
hqStream.addTrack(lqStream.getVideoTracks()[0]); localStream.addTrack(lqStream.getVideoTracks()[0]);
stream = hqStream; success(localStream);
success(hqStream);
}, err); }, err);
}, err); }, err);
} else { } else {
@ -769,8 +779,8 @@ function Simulcast() {
// add hq stream to local map // add hq stream to local map
localMaps.msids.push(hqStream.getVideoTracks()[0].id); localMaps.msids.push(hqStream.getVideoTracks()[0].id);
stream = hqStream; displayedLocalVideoStream = localStream = hqStream;
success(hqStream); success(localStream);
}, err); }, err);
} }
}; };
@ -801,7 +811,7 @@ function Simulcast() {
trackid = tid; trackid = tid;
return true; return true;
} }
}) && stream.getVideoTracks().some(function(track) { }) && localStream.getVideoTracks().some(function(track) {
// Start/stop the track that corresponds to the track id // Start/stop the track that corresponds to the track id
if (track.id === trackid) { if (track.id === trackid) {
track.enabled = enabled; track.enabled = enabled;
@ -818,15 +828,7 @@ function Simulcast() {
}; };
Simulcast.prototype.getLocalVideoStream = function() { Simulcast.prototype.getLocalVideoStream = function() {
var track; return displayedLocalVideoStream;
stream.getVideoTracks().some(function(t) {
if ((track = t).enabled) {
return true;
}
});
return new webkitMediaStream([track]);
}; };
$(document).bind('simulcastlayerschanged', function (event, endpointSimulcastLayers) { $(document).bind('simulcastlayerschanged', function (event, endpointSimulcastLayers) {

View File

@ -3,7 +3,10 @@ var VideoLayout = (function (my) {
var currentDominantSpeaker = null; var currentDominantSpeaker = null;
var lastNCount = config.channelLastN; var lastNCount = config.channelLastN;
var lastNEndpointsCache = []; var lastNEndpointsCache = [];
var largeVideoNewSrc = ''; var largeVideoState = {
updateInProgress: false,
newSrc: ''
};
my.changeLocalAudio = function(stream) { my.changeLocalAudio = function(stream) {
connection.jingle.localAudio = stream; connection.jingle.localAudio = stream;
@ -66,7 +69,9 @@ var VideoLayout = (function (my) {
localVideoSelector.addClass("flipVideoX"); localVideoSelector.addClass("flipVideoX");
} }
// Attach WebRTC stream // Attach WebRTC stream
RTC.attachMediaStream(localVideoSelector, stream); var simulcast = new Simulcast();
var videoStream = simulcast.getLocalVideoStream();
RTC.attachMediaStream(localVideoSelector, videoStream);
localVideoSrc = localVideo.src; localVideoSrc = localVideo.src;
@ -114,68 +119,91 @@ var VideoLayout = (function (my) {
console.log('hover in', newSrc); console.log('hover in', newSrc);
if ($('#largeVideo').attr('src') != newSrc) { if ($('#largeVideo').attr('src') != newSrc) {
largeVideoNewSrc = newSrc;
var isVisible = $('#largeVideo').is(':visible'); // Due to the simulcast the localVideoSrc may have changed when the
// fadeOut event triggers. In that case the getJidFromVideoSrc and
// isVideoSrcDesktop methods will not function correctly.
//
// Also, again due to the simulcast, the updateLargeVideo method can
// be called multiple times almost simultaneously. Therefore, we
// store the state here and update only once.
// we need this here because after the fade the videoSrc may have largeVideoState.newSrc = newSrc;
// changed. largeVideoState.isVisible = $('#largeVideo').is(':visible');
var isDesktop = isVideoSrcDesktop(newSrc); largeVideoState.isDesktop = isVideoSrcDesktop(newSrc);
largeVideoState.userJid = getJidFromVideoSrc(newSrc);
var userJid = getJidFromVideoSrc(newSrc);
// we want the notification to trigger even if userJid is undefined,
// or null.
$(document).trigger("selectedendpointchanged", [userJid]);
$('#largeVideo').fadeOut(300, function () {
var oldSrc = $(this).attr('src');
$(this).attr('src', newSrc);
// Screen stream is already rotated // Screen stream is already rotated
var flipX = (newSrc === localVideoSrc) && flipXLocalVideo; largeVideoState.flipX = (newSrc === localVideoSrc) && flipXLocalVideo;
var oldSrc = $('#largeVideo').attr('src');
largeVideoState.oldJid = getJidFromVideoSrc(oldSrc);
var fade = false;
if (largeVideoState.oldJid != largeVideoState.userJid) {
fade = true;
// we want the notification to trigger even if userJid is undefined,
// or null.
$(document).trigger("selectedendpointchanged", [largeVideoState.userJid]);
}
if (!largeVideoState.updateInProgress) {
largeVideoState.updateInProgress = true;
var doUpdate = function () {
$('#largeVideo').attr('src', largeVideoState.newSrc);
var videoTransform = document.getElementById('largeVideo') var videoTransform = document.getElementById('largeVideo')
.style.webkitTransform; .style.webkitTransform;
if (flipX && videoTransform !== 'scaleX(-1)') { if (largeVideoState.flipX && videoTransform !== 'scaleX(-1)') {
document.getElementById('largeVideo').style.webkitTransform document.getElementById('largeVideo').style.webkitTransform
= "scaleX(-1)"; = "scaleX(-1)";
} }
else if (!flipX && videoTransform === 'scaleX(-1)') { else if (!largeVideoState.flipX && videoTransform === 'scaleX(-1)') {
document.getElementById('largeVideo').style.webkitTransform document.getElementById('largeVideo').style.webkitTransform
= "none"; = "none";
} }
// Change the way we'll be measuring and positioning large video // Change the way we'll be measuring and positioning large video
getVideoSize = isDesktop getVideoSize = largeVideoState.isDesktop
? getDesktopVideoSize ? getDesktopVideoSize
: getCameraVideoSize; : getCameraVideoSize;
getVideoPosition = isDesktop getVideoPosition = largeVideoState.isDesktop
? getDesktopVideoPosition ? getDesktopVideoPosition
: getCameraVideoPosition; : getCameraVideoPosition;
if (isVisible) { if (largeVideoState.isVisible) {
// Only if the large video is currently visible. // Only if the large video is currently visible.
// Disable previous dominant speaker video. // Disable previous dominant speaker video.
var oldJid = getJidFromVideoSrc(oldSrc); if (largeVideoState.oldJid) {
if (oldJid) { var oldResourceJid = Strophe.getResourceFromJid(largeVideoState.oldJid);
var oldResourceJid = Strophe.getResourceFromJid(oldJid);
VideoLayout.enableDominantSpeaker(oldResourceJid, false); VideoLayout.enableDominantSpeaker(oldResourceJid, false);
} }
// Enable new dominant speaker in the remote videos section. // Enable new dominant speaker in the remote videos section.
var userJid = getJidFromVideoSrc(newSrc); if (largeVideoState.userJid) {
if (userJid) var resourceJid = Strophe.getResourceFromJid(largeVideoState.userJid);
{
var resourceJid = Strophe.getResourceFromJid(userJid);
VideoLayout.enableDominantSpeaker(resourceJid, true); VideoLayout.enableDominantSpeaker(resourceJid, true);
} }
largeVideoState.updateInProgress = false;
if (fade) {
// using "this" should be ok because we're called
// from within the fadeOut event.
$(this).fadeIn(300); $(this).fadeIn(300);
} }
}); }
};
if (fade) {
$('#largeVideo').fadeOut(300, doUpdate);
} else {
doUpdate();
}
}
} }
}; };
@ -814,15 +842,6 @@ var VideoLayout = (function (my) {
* disabled * disabled
*/ */
my.enableDominantSpeaker = function(resourceJid, isEnable) { my.enableDominantSpeaker = function(resourceJid, isEnable) {
var displayName = resourceJid;
var nameSpan = $('#participant_' + resourceJid + '>span.displayname');
if (nameSpan.length > 0)
displayName = nameSpan.text();
console.log("UI enable dominant speaker",
displayName,
resourceJid,
isEnable);
var videoSpanId = null; var videoSpanId = null;
var videoContainerId = null; var videoContainerId = null;
@ -836,6 +855,16 @@ var VideoLayout = (function (my) {
videoContainerId = videoSpanId; videoContainerId = videoSpanId;
} }
var displayName = resourceJid;
var nameSpan = $('#' + videoContainerId + '>span.displayname');
if (nameSpan.length > 0)
displayName = nameSpan.text();
console.log("UI enable dominant speaker",
displayName,
resourceJid,
isEnable);
videoSpan = document.getElementById(videoContainerId); videoSpan = document.getElementById(videoContainerId);
if (!videoSpan) { if (!videoSpan) {
@ -1322,29 +1351,6 @@ var VideoLayout = (function (my) {
} }
}); });
$(document).bind('simulcastlayerstarted simulcastlayerstopped', function(event) {
var localVideoSelector = $('#' + 'localVideo_' + connection.jingle.localVideo.id);
var simulcast = new Simulcast();
var stream = simulcast.getLocalVideoStream();
var updateLargeVideo = (connection.emuc.myroomjid
== getJidFromVideoSrc(largeVideoNewSrc));
var updateFocusedVideoSrc = (localVideoSrc == focusedVideoSrc);
// Attach WebRTC stream
RTC.attachMediaStream(localVideoSelector, stream);
localVideoSrc = $(localVideoSelector).attr('src');
if (updateLargeVideo) {
VideoLayout.updateLargeVideo(localVideoSrc);
}
if (updateFocusedVideoSrc) {
focusedVideoSrc = localVideoSrc;
}
});
/** /**
* On simulcast layers changed event. * On simulcast layers changed event.
*/ */
@ -1402,7 +1408,7 @@ var VideoLayout = (function (my) {
var selRemoteVideo = $(['#', 'remoteVideo_', session.sid, '_', msidParts[0]].join('')); var selRemoteVideo = $(['#', 'remoteVideo_', session.sid, '_', msidParts[0]].join(''));
var updateLargeVideo = (ssrc2jid[videoSrcToSsrc[selRemoteVideo.attr('src')]] var updateLargeVideo = (ssrc2jid[videoSrcToSsrc[selRemoteVideo.attr('src')]]
== ssrc2jid[videoSrcToSsrc[largeVideoNewSrc]]); == ssrc2jid[videoSrcToSsrc[largeVideoState.newSrc]]);
var updateFocusedVideoSrc = (selRemoteVideo.attr('src') == focusedVideoSrc); var updateFocusedVideoSrc = (selRemoteVideo.attr('src') == focusedVideoSrc);
var electedStreamUrl = webkitURL.createObjectURL(electedStream); var electedStreamUrl = webkitURL.createObjectURL(electedStream);