Przeglądaj źródła

Merge pull request #1802 from jitsi/start_in_audio_only

Start in audio only
j8
Saúl Ibarra Corretgé 7 lat temu
rodzic
commit
2525bb2805

+ 108
- 41
conference.js Wyświetl plik

@@ -25,6 +25,7 @@ import {
25 25
     conferenceFailed,
26 26
     conferenceJoined,
27 27
     conferenceLeft,
28
+    toggleAudioOnly,
28 29
     EMAIL_COMMAND,
29 30
     lockStateChanged
30 31
 } from './react/features/base/conference';
@@ -74,7 +75,7 @@ const eventEmitter = new EventEmitter();
74 75
 let room;
75 76
 let connection;
76 77
 let localAudio, localVideo;
77
-let initialAudioMutedState = false, initialVideoMutedState = false;
78
+let initialAudioMutedState = false;
78 79
 
79 80
 import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/VideoContainer";
80 81
 
@@ -177,27 +178,40 @@ function getDisplayName(id) {
177 178
  * result of user interaction
178 179
  */
179 180
 function muteLocalAudio(muted) {
180
-    muteLocalMedia(localAudio, muted, 'Audio');
181
+    muteLocalMedia(localAudio, muted);
181 182
 }
182 183
 
183
-function muteLocalMedia(localMedia, muted, localMediaTypeString) {
184
-    if (!localMedia) {
185
-        return;
184
+/**
185
+ * Mute or unmute local media stream if it exists.
186
+ * @param {JitsiLocalTrack} localTrack
187
+ * @param {boolean} muted
188
+ *
189
+ * @returns {Promise} resolved in case mute/unmute operations succeeds or
190
+ * rejected with an error if something goes wrong. It is expected that often
191
+ * the error will be of the {@link JitsiTrackError} type, but it's not
192
+ * guaranteed.
193
+ */
194
+function muteLocalMedia(localTrack, muted) {
195
+    if (!localTrack) {
196
+        return Promise.resolve();
186 197
     }
187 198
 
188 199
     const method = muted ? 'mute' : 'unmute';
189 200
 
190
-    localMedia[method]().catch(reason => {
191
-        logger.warn(`${localMediaTypeString} ${method} was rejected:`, reason);
192
-    });
201
+    return localTrack[method]();
193 202
 }
194 203
 
195 204
 /**
196 205
  * Mute or unmute local video stream if it exists.
197 206
  * @param {boolean} muted if video stream should be muted or unmuted.
207
+ *
208
+ * @returns {Promise} resolved in case mute/unmute operations succeeds or
209
+ * rejected with an error if something goes wrong. It is expected that often
210
+ * the error will be of the {@link JitsiTrackError} type, but it's not
211
+ * guaranteed.
198 212
  */
199 213
 function muteLocalVideo(muted) {
200
-    muteLocalMedia(localVideo, muted, 'Video');
214
+    return muteLocalMedia(localVideo, muted);
201 215
 }
202 216
 
203 217
 /**
@@ -424,6 +438,12 @@ function _connectionFailedHandler(error) {
424 438
 }
425 439
 
426 440
 export default {
441
+    /**
442
+     * Flag used to delay modification of the muted status of local media tracks
443
+     * until those are created (or not, but at that point it's certain that
444
+     * the tracks won't exist).
445
+     */
446
+    _localTracksInitialized: false,
427 447
     isModerator: false,
428 448
     audioMuted: false,
429 449
     videoMuted: false,
@@ -462,11 +482,14 @@ export default {
462 482
      * Creates local media tracks and connects to a room. Will show error
463 483
      * dialogs in case accessing the local microphone and/or camera failed. Will
464 484
      * show guidance overlay for users on how to give access to camera and/or
465
-     * microphone,
485
+     * microphone.
466 486
      * @param {string} roomName
467 487
      * @param {object} options
468
-     * @param {boolean} options.startScreenSharing - if <tt>true</tt> should
469
-     * start with screensharing instead of camera video.
488
+     * @param {boolean} options.startAudioOnly=false - if <tt>true</tt> then
489
+     * only audio track will be created and the audio only mode will be turned
490
+     * on.
491
+     * @param {boolean} options.startScreenSharing=false - if <tt>true</tt>
492
+     * should start with screensharing instead of camera video.
470 493
      * @returns {Promise.<JitsiLocalTrack[], JitsiConnection>}
471 494
      */
472 495
     createInitialLocalTracksAndConnect(roomName, options = {}) {
@@ -486,7 +509,20 @@ export default {
486 509
         let tryCreateLocalTracks;
487 510
 
488 511
         // FIXME the logic about trying to go audio only on error is duplicated
489
-        if (options.startScreenSharing) {
512
+        if (options.startAudioOnly) {
513
+            tryCreateLocalTracks
514
+                = createLocalTracks({ devices: ['audio'] }, true)
515
+                    .catch(err => {
516
+                        audioOnlyError = err;
517
+
518
+                        return [];
519
+                    });
520
+
521
+            // Enable audio only mode
522
+            if (config.startAudioOnly) {
523
+                APP.store.dispatch(toggleAudioOnly());
524
+            }
525
+        } else if (options.startScreenSharing) {
490 526
             tryCreateLocalTracks = this._createDesktopTrack()
491 527
                 .then(desktopStream => {
492 528
                     return createLocalTracks({ devices: ['audio'] }, true)
@@ -594,16 +630,19 @@ export default {
594 630
                 analytics.init();
595 631
                 return this.createInitialLocalTracksAndConnect(
596 632
                     options.roomName, {
633
+                        startAudioOnly: config.startAudioOnly,
597 634
                         startScreenSharing: config.startScreenSharing
598 635
                     });
599 636
             }).then(([tracks, con]) => {
600 637
                 tracks.forEach(track => {
601
-                    if((track.isAudioTrack() && initialAudioMutedState)
602
-                        || (track.isVideoTrack() && initialVideoMutedState)) {
638
+                    if (track.isAudioTrack() && initialAudioMutedState) {
639
+                        track.mute();
640
+                    } else if (track.isVideoTrack() && this.videoMuted) {
603 641
                         track.mute();
604 642
                     }
605 643
                 });
606 644
                 logger.log('initialized with %s local tracks', tracks.length);
645
+                this._localTracksInitialized = true;
607 646
                 con.addEventListener(
608 647
                     ConnectionEvents.CONNECTION_FAILED,
609 648
                     _connectionFailedHandler);
@@ -695,6 +734,8 @@ export default {
695 734
      */
696 735
     toggleAudioMuted(force = false) {
697 736
         if(!localAudio && force) {
737
+            // NOTE this logic will be adjusted to the same one as for the video
738
+            // once 'startWithAudioMuted' option is added.
698 739
             initialAudioMutedState = !initialAudioMutedState;
699 740
             return;
700 741
         }
@@ -703,22 +744,60 @@ export default {
703 744
     /**
704 745
      * Simulates toolbar button click for video mute. Used by shortcuts and API.
705 746
      * @param mute true for mute and false for unmute.
747
+     * @param {boolean} [showUI] when set to false will not display any error
748
+     * dialogs in case of media permissions error.
706 749
      */
707
-    muteVideo(mute) {
708
-        muteLocalVideo(mute);
750
+    muteVideo(mute, showUI = true) {
751
+        // Not ready to modify track's state yet
752
+        if (!this._localTracksInitialized) {
753
+            this.videoMuted = mute;
754
+
755
+            return;
756
+        }
757
+
758
+        const maybeShowErrorDialog = (error) => {
759
+            if (showUI) {
760
+                APP.UI.showDeviceErrorDialog(null, error);
761
+            }
762
+        };
763
+
764
+        if (!localVideo && this.videoMuted && !mute) {
765
+            // Try to create local video if there wasn't any.
766
+            // This handles the case when user joined with no video
767
+            // (dismissed screen sharing screen or in audio only mode), but
768
+            // decided to add it later on by clicking on muted video icon or
769
+            // turning off the audio only mode.
770
+            //
771
+            // FIXME when local track creation is moved to react/redux
772
+            // it should take care of the use case described above
773
+            createLocalTracks({ devices: ['video'] }, false)
774
+                .then(([videoTrack]) => videoTrack)
775
+                .catch(error => {
776
+                    // FIXME should send some feedback to the API on error ?
777
+                    maybeShowErrorDialog(error);
778
+
779
+                    // Rollback the video muted status by using null track
780
+                    return null;
781
+                })
782
+                .then(videoTrack => this.useVideoStream(videoTrack));
783
+        } else {
784
+            const oldMutedStatus = this.videoMuted;
785
+
786
+            muteLocalVideo(mute)
787
+                .catch(error => {
788
+                    maybeShowErrorDialog(error);
789
+                    this.videoMuted = oldMutedStatus;
790
+                    APP.UI.setVideoMuted(this.getMyUserId(), this.videoMuted);
791
+                });
792
+        }
709 793
     },
710 794
     /**
711 795
      * Simulates toolbar button click for video mute. Used by shortcuts and API.
712
-     * @param {boolean} force - If the track is not created, the operation
713
-     * will be executed after the track is created. Otherwise the operation
714
-     * will be ignored.
796
+     * @param {boolean} [showUI] when set to false will not display any error
797
+     * dialogs in case of media permissions error.
715 798
      */
716
-    toggleVideoMuted(force = false) {
717
-        if(!localVideo && force) {
718
-            initialVideoMutedState = !initialVideoMutedState;
719
-            return;
720
-        }
721
-        this.muteVideo(!this.videoMuted);
799
+    toggleVideoMuted(showUI = true) {
800
+        this.muteVideo(!this.videoMuted, showUI);
722 801
     },
723 802
     /**
724 803
      * Retrieve list of conference participants (without local user).
@@ -1721,20 +1800,8 @@ export default {
1721 1800
         APP.UI.addListener(UIEvents.VIDEO_MUTED, muted => {
1722 1801
             if (this.isAudioOnly() && !muted) {
1723 1802
                 this._displayAudioOnlyTooltip('videoMute');
1724
-            } else if (!localVideo && this.videoMuted && !muted) {
1725
-                // Maybe try to create local video if there wasn't any ?
1726
-                // This handles the case when user joined with no video
1727
-                // (dismissed screen sharing screen), but decided to add it
1728
-                // later on by clicking on muted video icon.
1729
-                createLocalTracks({ devices: ['video'] }, false)
1730
-                    .then(([videoTrack]) => {
1731
-                        APP.conference.useVideoStream(videoTrack);
1732
-                    })
1733
-                    .catch(error => {
1734
-                        APP.UI.showDeviceErrorDialog(null, error);
1735
-                    });
1736 1803
             } else {
1737
-                muteLocalVideo(muted);
1804
+                this.muteVideo(muted);
1738 1805
             }
1739 1806
         });
1740 1807
 
@@ -1927,7 +1994,7 @@ export default {
1927 1994
         );
1928 1995
 
1929 1996
         APP.UI.addListener(UIEvents.TOGGLE_AUDIO_ONLY, audioOnly => {
1930
-            muteLocalVideo(audioOnly);
1997
+            this.muteVideo(audioOnly);
1931 1998
 
1932 1999
             // Immediately update the UI by having remote videos and the large
1933 2000
             // video update themselves instead of waiting for some other event
@@ -2038,7 +2105,7 @@ export default {
2038 2105
                     JitsiMeetJS.mediaDevices.enumerateDevices(devices => {
2039 2106
                         // Ugly way to synchronize real device IDs with local
2040 2107
                         // storage and settings menu. This is a workaround until
2041
-                        // getConstraints() method will be implemented 
2108
+                        // getConstraints() method will be implemented
2042 2109
                         // in browsers.
2043 2110
                         if (localAudio) {
2044 2111
                             APP.settings.setMicDeviceId(

+ 1
- 0
config.js Wyświetl plik

@@ -76,6 +76,7 @@ var config = { // eslint-disable-line no-unused-vars
76 76
                               // page redirection when call is hangup
77 77
     disableSimulcast: false,
78 78
 //    requireDisplayName: true, // Forces the participants that doesn't have display name to enter it when they enter the room.
79
+    startAudioOnly: false, // Will start the conference in the audio only mode (no video is being received nor sent)
79 80
     startScreenSharing: false, // Will try to start with screensharing instead of camera
80 81
 //    startAudioMuted: 10, // every participant after the Nth will start audio muted
81 82
 //    startVideoMuted: 10, // every participant after the Nth will start video muted

+ 3
- 1
modules/API/API.js Wyświetl plik

@@ -36,7 +36,9 @@ function initCommands() {
36 36
         'display-name':
37 37
             APP.conference.changeLocalDisplayName.bind(APP.conference),
38 38
         'toggle-audio': () => APP.conference.toggleAudioMuted(true),
39
-        'toggle-video': () => APP.conference.toggleVideoMuted(true),
39
+        'toggle-video': () => {
40
+            APP.conference.toggleVideoMuted(false /* no UI */);
41
+        },
40 42
         'toggle-film-strip': APP.UI.toggleFilmstrip,
41 43
         'toggle-chat': APP.UI.toggleChat,
42 44
         'toggle-contact-list': APP.UI.toggleContactList,

+ 32
- 7
modules/UI/videolayout/VideoLayout.js Wyświetl plik

@@ -336,13 +336,11 @@ var VideoLayout = {
336 336
 
337 337
         remoteVideo.addRemoteStreamElement(stream);
338 338
 
339
-        // if track is muted make sure we reflect that
340
-        if(stream.isMuted())
341
-        {
342
-            if(stream.getType() === "audio")
343
-                this.onAudioMute(stream.getParticipantId(), true);
344
-            else
345
-                this.onVideoMute(stream.getParticipantId(), true);
339
+        // Make sure track's muted state is reflected
340
+        if (stream.getType() === "audio") {
341
+            this.onAudioMute(stream.getParticipantId(), stream.isMuted());
342
+        } else {
343
+            this.onVideoMute(stream.getParticipantId(), stream.isMuted());
346 344
         }
347 345
     },
348 346
 
@@ -353,6 +351,30 @@ var VideoLayout = {
353 351
         if (remoteVideo) {
354 352
             remoteVideo.removeRemoteStreamElement(stream);
355 353
         }
354
+        this.updateMutedForNoTracks(id, stream.getType());
355
+    },
356
+
357
+    /**
358
+     * FIXME get rid of this method once muted indicator are reactified (by
359
+     * making sure that user with no tracks is displayed as muted )
360
+     *
361
+     * If participant has no tracks will make the UI display muted status.
362
+     * @param {string} participantId
363
+     * @param {string} mediaType 'audio' or 'video'
364
+     */
365
+    updateMutedForNoTracks(participantId, mediaType) {
366
+        const participant = APP.conference.getParticipantById(participantId);
367
+
368
+        if (participant
369
+                && !participant.getTracksByMediaType(mediaType).length) {
370
+            if (mediaType === 'audio') {
371
+                APP.UI.setAudioMuted(participantId, true);
372
+            } else if (mediaType === 'video') {
373
+                APP.UI.setVideoMuted(participantId, true);
374
+            } else {
375
+                logger.error(`Unsupported media type: ${mediaType}`);
376
+            }
377
+        }
356 378
     },
357 379
 
358 380
     /**
@@ -446,6 +468,9 @@ var VideoLayout = {
446 468
         this._setRemoteControlProperties(user, remoteVideo);
447 469
         this.addRemoteVideoContainer(id, remoteVideo);
448 470
 
471
+        this.updateMutedForNoTracks(id, 'audio');
472
+        this.updateMutedForNoTracks(id, 'video');
473
+
449 474
         const remoteVideosCount = Object.keys(remoteVideos).length;
450 475
 
451 476
         if (remoteVideosCount === 1) {

+ 33
- 1
react/features/base/conference/middleware.js Wyświetl plik

@@ -15,7 +15,7 @@ import {
15 15
     _setAudioOnlyVideoMuted,
16 16
     setLastN
17 17
 } from './actions';
18
-import { SET_AUDIO_ONLY, SET_LASTN } from './actionTypes';
18
+import { CONFERENCE_JOINED, SET_AUDIO_ONLY, SET_LASTN } from './actionTypes';
19 19
 import {
20 20
     _addLocalTracksToConference,
21 21
     _handleParticipantError,
@@ -33,6 +33,9 @@ MiddlewareRegistry.register(store => next => action => {
33 33
     case CONNECTION_ESTABLISHED:
34 34
         return _connectionEstablished(store, next, action);
35 35
 
36
+    case CONFERENCE_JOINED:
37
+        return _conferenceJoined(store, next, action);
38
+
36 39
     case PIN_PARTICIPANT:
37 40
         return _pinParticipant(store, next, action);
38 41
 
@@ -76,6 +79,35 @@ function _connectionEstablished(store, next, action) {
76 79
     return result;
77 80
 }
78 81
 
82
+/**
83
+ * Does extra sync up on properties that may need to be updated, after
84
+ * the conference was joined.
85
+ *
86
+ * @param {Store} store - The Redux store in which the specified action is being
87
+ * dispatched.
88
+ * @param {Dispatch} next - The Redux dispatch function to dispatch the
89
+ * specified action to the specified store.
90
+ * @param {Action} action - The Redux action CONFERENCE_JOINED which is being
91
+ * dispatched in the specified store.
92
+ * @private
93
+ * @returns {Object} The new state that is the result of the reduction of the
94
+ * specified action.
95
+ */
96
+function _conferenceJoined(store, next, action) {
97
+    const result = next(action);
98
+    const { audioOnly, conference }
99
+        = store.getState()['features/base/conference'];
100
+
101
+    // FIXME On Web the audio only mode for "start audio only" is toggled before
102
+    // conference is added to the redux store ("on conference joined" action)
103
+    // and the LastN value needs to be synchronized here.
104
+    if (audioOnly && conference.getLastN() !== 0) {
105
+        store.dispatch(setLastN(0));
106
+    }
107
+
108
+    return result;
109
+}
110
+
79 111
 /**
80 112
  * Notifies the feature base/conference that the action PIN_PARTICIPANT is being
81 113
  * dispatched within a specific Redux store. Pins the specified remote

Ładowanie…
Anuluj
Zapisz