Browse Source

Merge pull request #1802 from jitsi/start_in_audio_only

Start in audio only
j8
Saúl Ibarra Corretgé 7 years ago
parent
commit
2525bb2805
5 changed files with 177 additions and 50 deletions
  1. 108
    41
      conference.js
  2. 1
    0
      config.js
  3. 3
    1
      modules/API/API.js
  4. 32
    7
      modules/UI/videolayout/VideoLayout.js
  5. 33
    1
      react/features/base/conference/middleware.js

+ 108
- 41
conference.js View File

25
     conferenceFailed,
25
     conferenceFailed,
26
     conferenceJoined,
26
     conferenceJoined,
27
     conferenceLeft,
27
     conferenceLeft,
28
+    toggleAudioOnly,
28
     EMAIL_COMMAND,
29
     EMAIL_COMMAND,
29
     lockStateChanged
30
     lockStateChanged
30
 } from './react/features/base/conference';
31
 } from './react/features/base/conference';
74
 let room;
75
 let room;
75
 let connection;
76
 let connection;
76
 let localAudio, localVideo;
77
 let localAudio, localVideo;
77
-let initialAudioMutedState = false, initialVideoMutedState = false;
78
+let initialAudioMutedState = false;
78
 
79
 
79
 import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/VideoContainer";
80
 import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/VideoContainer";
80
 
81
 
177
  * result of user interaction
178
  * result of user interaction
178
  */
179
  */
179
 function muteLocalAudio(muted) {
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
     const method = muted ? 'mute' : 'unmute';
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
  * Mute or unmute local video stream if it exists.
205
  * Mute or unmute local video stream if it exists.
197
  * @param {boolean} muted if video stream should be muted or unmuted.
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
 function muteLocalVideo(muted) {
213
 function muteLocalVideo(muted) {
200
-    muteLocalMedia(localVideo, muted, 'Video');
214
+    return muteLocalMedia(localVideo, muted);
201
 }
215
 }
202
 
216
 
203
 /**
217
 /**
424
 }
438
 }
425
 
439
 
426
 export default {
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
     isModerator: false,
447
     isModerator: false,
428
     audioMuted: false,
448
     audioMuted: false,
429
     videoMuted: false,
449
     videoMuted: false,
462
      * Creates local media tracks and connects to a room. Will show error
482
      * Creates local media tracks and connects to a room. Will show error
463
      * dialogs in case accessing the local microphone and/or camera failed. Will
483
      * dialogs in case accessing the local microphone and/or camera failed. Will
464
      * show guidance overlay for users on how to give access to camera and/or
484
      * show guidance overlay for users on how to give access to camera and/or
465
-     * microphone,
485
+     * microphone.
466
      * @param {string} roomName
486
      * @param {string} roomName
467
      * @param {object} options
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
      * @returns {Promise.<JitsiLocalTrack[], JitsiConnection>}
493
      * @returns {Promise.<JitsiLocalTrack[], JitsiConnection>}
471
      */
494
      */
472
     createInitialLocalTracksAndConnect(roomName, options = {}) {
495
     createInitialLocalTracksAndConnect(roomName, options = {}) {
486
         let tryCreateLocalTracks;
509
         let tryCreateLocalTracks;
487
 
510
 
488
         // FIXME the logic about trying to go audio only on error is duplicated
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
             tryCreateLocalTracks = this._createDesktopTrack()
526
             tryCreateLocalTracks = this._createDesktopTrack()
491
                 .then(desktopStream => {
527
                 .then(desktopStream => {
492
                     return createLocalTracks({ devices: ['audio'] }, true)
528
                     return createLocalTracks({ devices: ['audio'] }, true)
594
                 analytics.init();
630
                 analytics.init();
595
                 return this.createInitialLocalTracksAndConnect(
631
                 return this.createInitialLocalTracksAndConnect(
596
                     options.roomName, {
632
                     options.roomName, {
633
+                        startAudioOnly: config.startAudioOnly,
597
                         startScreenSharing: config.startScreenSharing
634
                         startScreenSharing: config.startScreenSharing
598
                     });
635
                     });
599
             }).then(([tracks, con]) => {
636
             }).then(([tracks, con]) => {
600
                 tracks.forEach(track => {
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
                         track.mute();
641
                         track.mute();
604
                     }
642
                     }
605
                 });
643
                 });
606
                 logger.log('initialized with %s local tracks', tracks.length);
644
                 logger.log('initialized with %s local tracks', tracks.length);
645
+                this._localTracksInitialized = true;
607
                 con.addEventListener(
646
                 con.addEventListener(
608
                     ConnectionEvents.CONNECTION_FAILED,
647
                     ConnectionEvents.CONNECTION_FAILED,
609
                     _connectionFailedHandler);
648
                     _connectionFailedHandler);
695
      */
734
      */
696
     toggleAudioMuted(force = false) {
735
     toggleAudioMuted(force = false) {
697
         if(!localAudio && force) {
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
             initialAudioMutedState = !initialAudioMutedState;
739
             initialAudioMutedState = !initialAudioMutedState;
699
             return;
740
             return;
700
         }
741
         }
703
     /**
744
     /**
704
      * Simulates toolbar button click for video mute. Used by shortcuts and API.
745
      * Simulates toolbar button click for video mute. Used by shortcuts and API.
705
      * @param mute true for mute and false for unmute.
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
      * Simulates toolbar button click for video mute. Used by shortcuts and API.
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
      * Retrieve list of conference participants (without local user).
803
      * Retrieve list of conference participants (without local user).
1721
         APP.UI.addListener(UIEvents.VIDEO_MUTED, muted => {
1800
         APP.UI.addListener(UIEvents.VIDEO_MUTED, muted => {
1722
             if (this.isAudioOnly() && !muted) {
1801
             if (this.isAudioOnly() && !muted) {
1723
                 this._displayAudioOnlyTooltip('videoMute');
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
             } else {
1803
             } else {
1737
-                muteLocalVideo(muted);
1804
+                this.muteVideo(muted);
1738
             }
1805
             }
1739
         });
1806
         });
1740
 
1807
 
1927
         );
1994
         );
1928
 
1995
 
1929
         APP.UI.addListener(UIEvents.TOGGLE_AUDIO_ONLY, audioOnly => {
1996
         APP.UI.addListener(UIEvents.TOGGLE_AUDIO_ONLY, audioOnly => {
1930
-            muteLocalVideo(audioOnly);
1997
+            this.muteVideo(audioOnly);
1931
 
1998
 
1932
             // Immediately update the UI by having remote videos and the large
1999
             // Immediately update the UI by having remote videos and the large
1933
             // video update themselves instead of waiting for some other event
2000
             // video update themselves instead of waiting for some other event
2038
                     JitsiMeetJS.mediaDevices.enumerateDevices(devices => {
2105
                     JitsiMeetJS.mediaDevices.enumerateDevices(devices => {
2039
                         // Ugly way to synchronize real device IDs with local
2106
                         // Ugly way to synchronize real device IDs with local
2040
                         // storage and settings menu. This is a workaround until
2107
                         // storage and settings menu. This is a workaround until
2041
-                        // getConstraints() method will be implemented 
2108
+                        // getConstraints() method will be implemented
2042
                         // in browsers.
2109
                         // in browsers.
2043
                         if (localAudio) {
2110
                         if (localAudio) {
2044
                             APP.settings.setMicDeviceId(
2111
                             APP.settings.setMicDeviceId(

+ 1
- 0
config.js View File

76
                               // page redirection when call is hangup
76
                               // page redirection when call is hangup
77
     disableSimulcast: false,
77
     disableSimulcast: false,
78
 //    requireDisplayName: true, // Forces the participants that doesn't have display name to enter it when they enter the room.
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
     startScreenSharing: false, // Will try to start with screensharing instead of camera
80
     startScreenSharing: false, // Will try to start with screensharing instead of camera
80
 //    startAudioMuted: 10, // every participant after the Nth will start audio muted
81
 //    startAudioMuted: 10, // every participant after the Nth will start audio muted
81
 //    startVideoMuted: 10, // every participant after the Nth will start video muted
82
 //    startVideoMuted: 10, // every participant after the Nth will start video muted

+ 3
- 1
modules/API/API.js View File

36
         'display-name':
36
         'display-name':
37
             APP.conference.changeLocalDisplayName.bind(APP.conference),
37
             APP.conference.changeLocalDisplayName.bind(APP.conference),
38
         'toggle-audio': () => APP.conference.toggleAudioMuted(true),
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
         'toggle-film-strip': APP.UI.toggleFilmstrip,
42
         'toggle-film-strip': APP.UI.toggleFilmstrip,
41
         'toggle-chat': APP.UI.toggleChat,
43
         'toggle-chat': APP.UI.toggleChat,
42
         'toggle-contact-list': APP.UI.toggleContactList,
44
         'toggle-contact-list': APP.UI.toggleContactList,

+ 32
- 7
modules/UI/videolayout/VideoLayout.js View File

336
 
336
 
337
         remoteVideo.addRemoteStreamElement(stream);
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
         if (remoteVideo) {
351
         if (remoteVideo) {
354
             remoteVideo.removeRemoteStreamElement(stream);
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
         this._setRemoteControlProperties(user, remoteVideo);
468
         this._setRemoteControlProperties(user, remoteVideo);
447
         this.addRemoteVideoContainer(id, remoteVideo);
469
         this.addRemoteVideoContainer(id, remoteVideo);
448
 
470
 
471
+        this.updateMutedForNoTracks(id, 'audio');
472
+        this.updateMutedForNoTracks(id, 'video');
473
+
449
         const remoteVideosCount = Object.keys(remoteVideos).length;
474
         const remoteVideosCount = Object.keys(remoteVideos).length;
450
 
475
 
451
         if (remoteVideosCount === 1) {
476
         if (remoteVideosCount === 1) {

+ 33
- 1
react/features/base/conference/middleware.js View File

15
     _setAudioOnlyVideoMuted,
15
     _setAudioOnlyVideoMuted,
16
     setLastN
16
     setLastN
17
 } from './actions';
17
 } from './actions';
18
-import { SET_AUDIO_ONLY, SET_LASTN } from './actionTypes';
18
+import { CONFERENCE_JOINED, SET_AUDIO_ONLY, SET_LASTN } from './actionTypes';
19
 import {
19
 import {
20
     _addLocalTracksToConference,
20
     _addLocalTracksToConference,
21
     _handleParticipantError,
21
     _handleParticipantError,
33
     case CONNECTION_ESTABLISHED:
33
     case CONNECTION_ESTABLISHED:
34
         return _connectionEstablished(store, next, action);
34
         return _connectionEstablished(store, next, action);
35
 
35
 
36
+    case CONFERENCE_JOINED:
37
+        return _conferenceJoined(store, next, action);
38
+
36
     case PIN_PARTICIPANT:
39
     case PIN_PARTICIPANT:
37
         return _pinParticipant(store, next, action);
40
         return _pinParticipant(store, next, action);
38
 
41
 
76
     return result;
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
  * Notifies the feature base/conference that the action PIN_PARTICIPANT is being
112
  * Notifies the feature base/conference that the action PIN_PARTICIPANT is being
81
  * dispatched within a specific Redux store. Pins the specified remote
113
  * dispatched within a specific Redux store. Pins the specified remote

Loading…
Cancel
Save