Merge pull request #3419 from virtuacoplenny/lenny/queue-replace-track

Queue replaceLocalTrack
This commit is contained in:
virtuacoplenny 2018-09-05 10:20:12 -07:00 committed by GitHub
commit 2043845d52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 144 additions and 40 deletions

View File

@ -11,6 +11,7 @@ import * as RemoteControlEvents
from './service/remotecontrol/RemoteControlEvents'; from './service/remotecontrol/RemoteControlEvents';
import UIEvents from './service/UI/UIEvents'; import UIEvents from './service/UI/UIEvents';
import UIUtil from './modules/UI/util/UIUtil'; import UIUtil from './modules/UI/util/UIUtil';
import { createTaskQueue } from './modules/util/helpers';
import * as JitsiMeetConferenceEvents from './ConferenceEvents'; import * as JitsiMeetConferenceEvents from './ConferenceEvents';
import { import {
@ -274,6 +275,27 @@ function redirectToStaticPage(pathname) {
windowLocation.pathname = newPathname; windowLocation.pathname = newPathname;
} }
/**
* A queue for the async replaceLocalTrack action so that multiple audio
* replacements cannot happen simultaneously. This solves the issue where
* replaceLocalTrack is called multiple times with an oldTrack of null, causing
* multiple local tracks of the same type to be used.
*
* @private
* @type {Object}
*/
const _replaceLocalAudioTrackQueue = createTaskQueue();
/**
* A task queue for replacement local video tracks. This separate queue exists
* so video replacement is not blocked by audio replacement tasks in the queue
* {@link _replaceLocalAudioTrackQueue}.
*
* @private
* @type {Object}
*/
const _replaceLocalVideoTrackQueue = createTaskQueue();
/** /**
* *
*/ */
@ -856,9 +878,6 @@ export default {
return; return;
} }
// FIXME it is possible to queue this task twice, but it's not causing
// any issues. Specifically this can happen when the previous
// get user media call is blocked on "ask user for permissions" dialog.
if (!this.localVideo && !mute) { if (!this.localVideo && !mute) {
const maybeShowErrorDialog = error => { const maybeShowErrorDialog = error => {
showUI && APP.UI.showCameraErrorNotification(error); showUI && APP.UI.showCameraErrorNotification(error);
@ -1261,16 +1280,23 @@ export default {
* @returns {Promise} * @returns {Promise}
*/ */
useVideoStream(newStream) { useVideoStream(newStream) {
return APP.store.dispatch( return new Promise((resolve, reject) => {
replaceLocalTrack(this.localVideo, newStream, room)) _replaceLocalVideoTrackQueue.enqueue(onFinish => {
.then(() => { APP.store.dispatch(
this.localVideo = newStream; replaceLocalTrack(this.localVideo, newStream, room))
this._setSharingScreen(newStream); .then(() => {
if (newStream) { this.localVideo = newStream;
APP.UI.addLocalStream(newStream); this._setSharingScreen(newStream);
} if (newStream) {
this.setVideoMuteStatus(this.isLocalVideoMuted()); APP.UI.addLocalStream(newStream);
}
this.setVideoMuteStatus(this.isLocalVideoMuted());
})
.then(resolve)
.catch(reject)
.then(onFinish);
}); });
});
}, },
/** /**
@ -1300,15 +1326,22 @@ export default {
* @returns {Promise} * @returns {Promise}
*/ */
useAudioStream(newStream) { useAudioStream(newStream) {
return APP.store.dispatch( return new Promise((resolve, reject) => {
replaceLocalTrack(this.localAudio, newStream, room)) _replaceLocalAudioTrackQueue.enqueue(onFinish => {
.then(() => { APP.store.dispatch(
this.localAudio = newStream; replaceLocalTrack(this.localAudio, newStream, room))
if (newStream) { .then(() => {
APP.UI.addLocalStream(newStream); this.localAudio = newStream;
} if (newStream) {
this.setAudioMuteStatus(this.isLocalAudioMuted()); APP.UI.addLocalStream(newStream);
}
this.setAudioMuteStatus(this.isLocalAudioMuted());
})
.then(resolve)
.catch(reject)
.then(onFinish);
}); });
});
}, },
/** /**
@ -2375,11 +2408,24 @@ export default {
createLocalTracksF, createLocalTracksF,
newDevices.videoinput, newDevices.videoinput,
newDevices.audioinput) newDevices.audioinput)
.then(tracks => .then(tracks => {
Promise.all(this._setLocalAudioVideoStreams(tracks))) // If audio or video muted before, or we unplugged current
// device and selected new one, then mute new track.
const muteSyncPromises = tracks.map(track => {
if ((track.isVideoTrack() && videoWasMuted)
|| (track.isAudioTrack() && audioWasMuted)) {
return track.mute();
}
return Promise.resolve();
});
return Promise.all(muteSyncPromises)
.then(() => Promise.all(
this._setLocalAudioVideoStreams(tracks)));
})
.then(() => { .then(() => {
// If audio was muted before, or we unplugged current device // Log and sync known mute state.
// and selected new one, then mute new audio track.
if (audioWasMuted) { if (audioWasMuted) {
sendAnalytics(createTrackMutedEvent( sendAnalytics(createTrackMutedEvent(
'audio', 'audio',
@ -2388,8 +2434,6 @@ export default {
muteLocalAudio(true); muteLocalAudio(true);
} }
// If video was muted before, or we unplugged current device
// and selected new one, then mute new video track.
if (!this.isSharingScreen && videoWasMuted) { if (!this.isSharingScreen && videoWasMuted) {
sendAnalytics(createTrackMutedEvent( sendAnalytics(createTrackMutedEvent(
'video', 'video',

View File

@ -216,8 +216,6 @@ export class VideoContainer extends LargeContainer {
this.emitter = emitter; this.emitter = emitter;
this.resizeContainer = resizeContainer; this.resizeContainer = resizeContainer;
this.isVisible = false;
/** /**
* Whether the background should fit the height of the container * Whether the background should fit the height of the container
* (portrait) or fit the width of the container (landscape). * (portrait) or fit the width of the container (landscape).
@ -603,17 +601,11 @@ export class VideoContainer extends LargeContainer {
* TODO: refactor this since Temasys is no longer supported. * TODO: refactor this since Temasys is no longer supported.
*/ */
show() { show() {
// its already visible
if (this.isVisible) {
return Promise.resolve();
}
return new Promise(resolve => { return new Promise(resolve => {
this.$wrapperParent.css('visibility', 'visible').fadeTo( this.$wrapperParent.css('visibility', 'visible').fadeTo(
FADE_DURATION_MS, FADE_DURATION_MS,
1, 1,
() => { () => {
this.isVisible = true;
resolve(); resolve();
} }
); );
@ -628,15 +620,9 @@ export class VideoContainer extends LargeContainer {
// hide its avatar // hide its avatar
this.showAvatar(false); this.showAvatar(false);
// its already hidden
if (!this.isVisible) {
return Promise.resolve();
}
return new Promise(resolve => { return new Promise(resolve => {
this.$wrapperParent.fadeTo(FADE_DURATION_MS, 0, () => { this.$wrapperParent.fadeTo(FADE_DURATION_MS, 0, () => {
this.$wrapperParent.css('visibility', 'hidden'); this.$wrapperParent.css('visibility', 'hidden');
this.isVisible = false;
resolve(); resolve();
}); });
}); });

63
modules/util/TaskQueue.js Normal file
View File

@ -0,0 +1,63 @@
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Manages a queue of functions where the current function in progress will
* automatically execute the next queued function.
*/
export class TaskQueue {
/**
* Creates a new instance of {@link TaskQueue} and sets initial instance
* variable values.
*/
constructor() {
this._queue = [];
this._currentTask = null;
this._onTaskComplete = this._onTaskComplete.bind(this);
}
/**
* Adds a new function to the queue. It will be immediately invoked if no
* other functions are queued.
*
* @param {Function} taskFunction - The function to be queued for execution.
* @private
* @returns {void}
*/
enqueue(taskFunction) {
this._queue.push(taskFunction);
this._executeNext();
}
/**
* If no queued task is currently executing, invokes the first task in the
* queue if any.
*
* @private
* @returns {void}
*/
_executeNext() {
if (this._currentTask) {
logger.warn('Task queued while a task is in progress.');
return;
}
this._currentTask = this._queue.shift() || null;
if (this._currentTask) {
this._currentTask(this._onTaskComplete);
}
}
/**
* Prepares to invoke the next function in the queue.
*
* @private
* @returns {void}
*/
_onTaskComplete() {
this._currentTask = null;
this._executeNext();
}
}

View File

@ -1,3 +1,5 @@
import { TaskQueue } from './TaskQueue';
/** /**
* Create deferred object. * Create deferred object.
* *
@ -13,3 +15,12 @@ export function createDeferred() {
return deferred; return deferred;
} }
/**
* Returns an instance of {@link TaskQueue}.
*
* @returns {Object}
*/
export function createTaskQueue() {
return new TaskQueue();
}