|  | @@ -11,6 +11,7 @@ import * as RemoteControlEvents
 | 
		
	
		
			
			| 11 | 11 |      from './service/remotecontrol/RemoteControlEvents';
 | 
		
	
		
			
			| 12 | 12 |  import UIEvents from './service/UI/UIEvents';
 | 
		
	
		
			
			| 13 | 13 |  import UIUtil from './modules/UI/util/UIUtil';
 | 
		
	
		
			
			|  | 14 | +import { createTaskQueue } from './modules/util/helpers';
 | 
		
	
		
			
			| 14 | 15 |  import * as JitsiMeetConferenceEvents from './ConferenceEvents';
 | 
		
	
		
			
			| 15 | 16 |  
 | 
		
	
		
			
			| 16 | 17 |  import {
 | 
		
	
	
		
			
			|  | @@ -274,6 +275,27 @@ function redirectToStaticPage(pathname) {
 | 
		
	
		
			
			| 274 | 275 |      windowLocation.pathname = newPathname;
 | 
		
	
		
			
			| 275 | 276 |  }
 | 
		
	
		
			
			| 276 | 277 |  
 | 
		
	
		
			
			|  | 278 | +/**
 | 
		
	
		
			
			|  | 279 | + * A queue for the async replaceLocalTrack action so that multiple audio
 | 
		
	
		
			
			|  | 280 | + * replacements cannot happen simultaneously. This solves the issue where
 | 
		
	
		
			
			|  | 281 | + * replaceLocalTrack is called multiple times with an oldTrack of null, causing
 | 
		
	
		
			
			|  | 282 | + * multiple local tracks of the same type to be used.
 | 
		
	
		
			
			|  | 283 | + *
 | 
		
	
		
			
			|  | 284 | + * @private
 | 
		
	
		
			
			|  | 285 | + * @type {Object}
 | 
		
	
		
			
			|  | 286 | + */
 | 
		
	
		
			
			|  | 287 | +const _replaceLocalAudioTrackQueue = createTaskQueue();
 | 
		
	
		
			
			|  | 288 | +
 | 
		
	
		
			
			|  | 289 | +/**
 | 
		
	
		
			
			|  | 290 | + * A task queue for replacement local video tracks. This separate queue exists
 | 
		
	
		
			
			|  | 291 | + * so video replacement is not blocked by audio replacement tasks in the queue
 | 
		
	
		
			
			|  | 292 | + * {@link _replaceLocalAudioTrackQueue}.
 | 
		
	
		
			
			|  | 293 | + *
 | 
		
	
		
			
			|  | 294 | + * @private
 | 
		
	
		
			
			|  | 295 | + * @type {Object}
 | 
		
	
		
			
			|  | 296 | + */
 | 
		
	
		
			
			|  | 297 | +const _replaceLocalVideoTrackQueue = createTaskQueue();
 | 
		
	
		
			
			|  | 298 | +
 | 
		
	
		
			
			| 277 | 299 |  /**
 | 
		
	
		
			
			| 278 | 300 |   *
 | 
		
	
		
			
			| 279 | 301 |   */
 | 
		
	
	
		
			
			|  | @@ -856,9 +878,6 @@ export default {
 | 
		
	
		
			
			| 856 | 878 |              return;
 | 
		
	
		
			
			| 857 | 879 |          }
 | 
		
	
		
			
			| 858 | 880 |  
 | 
		
	
		
			
			| 859 |  | -        // FIXME it is possible to queue this task twice, but it's not causing
 | 
		
	
		
			
			| 860 |  | -        // any issues. Specifically this can happen when the previous
 | 
		
	
		
			
			| 861 |  | -        // get user media call is blocked on "ask user for permissions" dialog.
 | 
		
	
		
			
			| 862 | 881 |          if (!this.localVideo && !mute) {
 | 
		
	
		
			
			| 863 | 882 |              const maybeShowErrorDialog = error => {
 | 
		
	
		
			
			| 864 | 883 |                  showUI && APP.UI.showCameraErrorNotification(error);
 | 
		
	
	
		
			
			|  | @@ -1261,16 +1280,23 @@ export default {
 | 
		
	
		
			
			| 1261 | 1280 |       * @returns {Promise}
 | 
		
	
		
			
			| 1262 | 1281 |       */
 | 
		
	
		
			
			| 1263 | 1282 |      useVideoStream(newStream) {
 | 
		
	
		
			
			| 1264 |  | -        return APP.store.dispatch(
 | 
		
	
		
			
			| 1265 |  | -            replaceLocalTrack(this.localVideo, newStream, room))
 | 
		
	
		
			
			| 1266 |  | -            .then(() => {
 | 
		
	
		
			
			| 1267 |  | -                this.localVideo = newStream;
 | 
		
	
		
			
			| 1268 |  | -                this._setSharingScreen(newStream);
 | 
		
	
		
			
			| 1269 |  | -                if (newStream) {
 | 
		
	
		
			
			| 1270 |  | -                    APP.UI.addLocalStream(newStream);
 | 
		
	
		
			
			| 1271 |  | -                }
 | 
		
	
		
			
			| 1272 |  | -                this.setVideoMuteStatus(this.isLocalVideoMuted());
 | 
		
	
		
			
			|  | 1283 | +        return new Promise((resolve, reject) => {
 | 
		
	
		
			
			|  | 1284 | +            _replaceLocalVideoTrackQueue.enqueue(onFinish => {
 | 
		
	
		
			
			|  | 1285 | +                APP.store.dispatch(
 | 
		
	
		
			
			|  | 1286 | +                replaceLocalTrack(this.localVideo, newStream, room))
 | 
		
	
		
			
			|  | 1287 | +                    .then(() => {
 | 
		
	
		
			
			|  | 1288 | +                        this.localVideo = newStream;
 | 
		
	
		
			
			|  | 1289 | +                        this._setSharingScreen(newStream);
 | 
		
	
		
			
			|  | 1290 | +                        if (newStream) {
 | 
		
	
		
			
			|  | 1291 | +                            APP.UI.addLocalStream(newStream);
 | 
		
	
		
			
			|  | 1292 | +                        }
 | 
		
	
		
			
			|  | 1293 | +                        this.setVideoMuteStatus(this.isLocalVideoMuted());
 | 
		
	
		
			
			|  | 1294 | +                    })
 | 
		
	
		
			
			|  | 1295 | +                    .then(resolve)
 | 
		
	
		
			
			|  | 1296 | +                    .catch(reject)
 | 
		
	
		
			
			|  | 1297 | +                    .then(onFinish);
 | 
		
	
		
			
			| 1273 | 1298 |              });
 | 
		
	
		
			
			|  | 1299 | +        });
 | 
		
	
		
			
			| 1274 | 1300 |      },
 | 
		
	
		
			
			| 1275 | 1301 |  
 | 
		
	
		
			
			| 1276 | 1302 |      /**
 | 
		
	
	
		
			
			|  | @@ -1300,15 +1326,22 @@ export default {
 | 
		
	
		
			
			| 1300 | 1326 |       * @returns {Promise}
 | 
		
	
		
			
			| 1301 | 1327 |       */
 | 
		
	
		
			
			| 1302 | 1328 |      useAudioStream(newStream) {
 | 
		
	
		
			
			| 1303 |  | -        return APP.store.dispatch(
 | 
		
	
		
			
			| 1304 |  | -            replaceLocalTrack(this.localAudio, newStream, room))
 | 
		
	
		
			
			| 1305 |  | -            .then(() => {
 | 
		
	
		
			
			| 1306 |  | -                this.localAudio = newStream;
 | 
		
	
		
			
			| 1307 |  | -                if (newStream) {
 | 
		
	
		
			
			| 1308 |  | -                    APP.UI.addLocalStream(newStream);
 | 
		
	
		
			
			| 1309 |  | -                }
 | 
		
	
		
			
			| 1310 |  | -                this.setAudioMuteStatus(this.isLocalAudioMuted());
 | 
		
	
		
			
			|  | 1329 | +        return new Promise((resolve, reject) => {
 | 
		
	
		
			
			|  | 1330 | +            _replaceLocalAudioTrackQueue.enqueue(onFinish => {
 | 
		
	
		
			
			|  | 1331 | +                APP.store.dispatch(
 | 
		
	
		
			
			|  | 1332 | +                replaceLocalTrack(this.localAudio, newStream, room))
 | 
		
	
		
			
			|  | 1333 | +                    .then(() => {
 | 
		
	
		
			
			|  | 1334 | +                        this.localAudio = newStream;
 | 
		
	
		
			
			|  | 1335 | +                        if (newStream) {
 | 
		
	
		
			
			|  | 1336 | +                            APP.UI.addLocalStream(newStream);
 | 
		
	
		
			
			|  | 1337 | +                        }
 | 
		
	
		
			
			|  | 1338 | +                        this.setAudioMuteStatus(this.isLocalAudioMuted());
 | 
		
	
		
			
			|  | 1339 | +                    })
 | 
		
	
		
			
			|  | 1340 | +                    .then(resolve)
 | 
		
	
		
			
			|  | 1341 | +                    .catch(reject)
 | 
		
	
		
			
			|  | 1342 | +                    .then(onFinish);
 | 
		
	
		
			
			| 1311 | 1343 |              });
 | 
		
	
		
			
			|  | 1344 | +        });
 | 
		
	
		
			
			| 1312 | 1345 |      },
 | 
		
	
		
			
			| 1313 | 1346 |  
 | 
		
	
		
			
			| 1314 | 1347 |      /**
 | 
		
	
	
		
			
			|  | @@ -2375,11 +2408,24 @@ export default {
 | 
		
	
		
			
			| 2375 | 2408 |                      createLocalTracksF,
 | 
		
	
		
			
			| 2376 | 2409 |                      newDevices.videoinput,
 | 
		
	
		
			
			| 2377 | 2410 |                      newDevices.audioinput)
 | 
		
	
		
			
			| 2378 |  | -                .then(tracks =>
 | 
		
	
		
			
			| 2379 |  | -                    Promise.all(this._setLocalAudioVideoStreams(tracks)))
 | 
		
	
		
			
			|  | 2411 | +                .then(tracks => {
 | 
		
	
		
			
			|  | 2412 | +                    // If audio or video muted before, or we unplugged current
 | 
		
	
		
			
			|  | 2413 | +                    // device and selected new one, then mute new track.
 | 
		
	
		
			
			|  | 2414 | +                    const muteSyncPromises = tracks.map(track => {
 | 
		
	
		
			
			|  | 2415 | +                        if ((track.isVideoTrack() && videoWasMuted)
 | 
		
	
		
			
			|  | 2416 | +                            || (track.isAudioTrack() && audioWasMuted)) {
 | 
		
	
		
			
			|  | 2417 | +                            return track.mute();
 | 
		
	
		
			
			|  | 2418 | +                        }
 | 
		
	
		
			
			|  | 2419 | +
 | 
		
	
		
			
			|  | 2420 | +                        return Promise.resolve();
 | 
		
	
		
			
			|  | 2421 | +                    });
 | 
		
	
		
			
			|  | 2422 | +
 | 
		
	
		
			
			|  | 2423 | +                    return Promise.all(muteSyncPromises)
 | 
		
	
		
			
			|  | 2424 | +                        .then(() => Promise.all(
 | 
		
	
		
			
			|  | 2425 | +                            this._setLocalAudioVideoStreams(tracks)));
 | 
		
	
		
			
			|  | 2426 | +                })
 | 
		
	
		
			
			| 2380 | 2427 |                  .then(() => {
 | 
		
	
		
			
			| 2381 |  | -                    // If audio was muted before, or we unplugged current device
 | 
		
	
		
			
			| 2382 |  | -                    // and selected new one, then mute new audio track.
 | 
		
	
		
			
			|  | 2428 | +                    // Log and sync known mute state.
 | 
		
	
		
			
			| 2383 | 2429 |                      if (audioWasMuted) {
 | 
		
	
		
			
			| 2384 | 2430 |                          sendAnalytics(createTrackMutedEvent(
 | 
		
	
		
			
			| 2385 | 2431 |                              'audio',
 | 
		
	
	
		
			
			|  | @@ -2388,8 +2434,6 @@ export default {
 | 
		
	
		
			
			| 2388 | 2434 |                          muteLocalAudio(true);
 | 
		
	
		
			
			| 2389 | 2435 |                      }
 | 
		
	
		
			
			| 2390 | 2436 |  
 | 
		
	
		
			
			| 2391 |  | -                    // If video was muted before, or we unplugged current device
 | 
		
	
		
			
			| 2392 |  | -                    // and selected new one, then mute new video track.
 | 
		
	
		
			
			| 2393 | 2437 |                      if (!this.isSharingScreen && videoWasMuted) {
 | 
		
	
		
			
			| 2394 | 2438 |                          sendAnalytics(createTrackMutedEvent(
 | 
		
	
		
			
			| 2395 | 2439 |                              'video',
 |