Переглянути джерело

[RN] Add audio only mode for conferences

The behavior can be triggered with the toggleAudioOnly action, which is
currently fired with a button.

The following aspects of the conference will change when in audio only mode:

- local video is muted
- last N is set to 0 (effectively muting remote video)
- full-screen mode is exited
- audio mode is set to "audio chat" (default output is the earpiece)
- the wake lock is disengaged

One aspect not handled in this patch is disabling the video mute button while in
audio only mode. The user should not be able to turn back video on in that case.
j8
Saúl Ibarra Corretgé 8 роки тому
джерело
коміт
8fe3dce649

+ 25
- 1
react/features/base/conference/actionTypes.js Переглянути файл

@@ -68,6 +68,31 @@ export const CONFERENCE_WILL_LEAVE = Symbol('CONFERENCE_WILL_LEAVE');
68 68
  */
69 69
 export const LOCK_STATE_CHANGED = Symbol('LOCK_STATE_CHANGED');
70 70
 
71
+/**
72
+ * The type of the Redux action which sets the audio-only flag for the current
73
+ * conference.
74
+ *
75
+ * {
76
+ *     type: SET_AUDIO_ONLY,
77
+ *     audioOnly: boolean
78
+ * }
79
+ */
80
+export const SET_AUDIO_ONLY = Symbol('SET_AUDIO_ONLY');
81
+
82
+/**
83
+ * The type of redux action which signals that video will be muted because the
84
+ * audio-only mode was enabled / disabled.
85
+ *
86
+ * {
87
+ *     type: _SET_AUDIO_ONLY_VIDEO_MUTED,
88
+ *     muted: boolean
89
+ * }
90
+ *
91
+ * @protected
92
+ */
93
+export const _SET_AUDIO_ONLY_VIDEO_MUTED
94
+    = Symbol('_SET_AUDIO_ONLY_VIDEO_MUTED');
95
+
71 96
 /**
72 97
  * The type of redux action which sets the video channel's lastN (value).
73 98
  *
@@ -75,7 +100,6 @@ export const LOCK_STATE_CHANGED = Symbol('LOCK_STATE_CHANGED');
75 100
  *     type: SET_LASTN,
76 101
  *     lastN: number
77 102
  * }
78
- *
79 103
  */
80 104
 export const SET_LASTN = Symbol('SET_LASTN');
81 105
 

+ 73
- 0
react/features/base/conference/actions.js Переглянути файл

@@ -1,4 +1,5 @@
1 1
 import { JitsiConferenceEvents } from '../lib-jitsi-meet';
2
+import { setVideoMuted } from '../media';
2 3
 import {
3 4
     dominantSpeakerChanged,
4 5
     getLocalParticipant,
@@ -17,6 +18,8 @@ import {
17 18
     CONFERENCE_WILL_JOIN,
18 19
     CONFERENCE_WILL_LEAVE,
19 20
     LOCK_STATE_CHANGED,
21
+    SET_AUDIO_ONLY,
22
+    _SET_AUDIO_ONLY_VIDEO_MUTED,
20 23
     SET_LASTN,
21 24
     SET_PASSWORD,
22 25
     SET_ROOM
@@ -295,6 +298,63 @@ function _lockStateChanged(conference, locked) {
295 298
     };
296 299
 }
297 300
 
301
+/**
302
+ * Sets the audio-only flag for the current JitsiConference.
303
+ *
304
+ * @param {boolean} audioOnly - True if the conference should be audio only;
305
+ * false, otherwise.
306
+ * @private
307
+ * @returns {{
308
+ *     type: SET_AUDIO_ONLY,
309
+ *     audioOnly: boolean
310
+ * }}
311
+ */
312
+function _setAudioOnly(audioOnly) {
313
+    return {
314
+        type: SET_AUDIO_ONLY,
315
+        audioOnly
316
+    };
317
+}
318
+
319
+/**
320
+ * Signals that the app should mute video because it's now in audio-only mode,
321
+ * or unmute it because it no longer is. If video was already muted, nothing
322
+ * will happen; otherwise, it will be muted. When audio-only mode is disabled,
323
+ * the previous state will be restored.
324
+ *
325
+ * @param {boolean} muted - True if video should be muted; false, otherwise.
326
+ * @protected
327
+ * @returns {Function}
328
+ */
329
+export function _setAudioOnlyVideoMuted(muted: boolean) {
330
+    return (dispatch, getState) => {
331
+        if (muted) {
332
+            const { video } = getState()['features/base/media'];
333
+
334
+            if (video.muted) {
335
+                // Video is already muted, do nothing.
336
+                return;
337
+            }
338
+        } else {
339
+            const { audioOnlyVideoMuted }
340
+                = getState()['features/base/conference'];
341
+
342
+            if (!audioOnlyVideoMuted) {
343
+                // We didn't mute video, do nothing.
344
+                return;
345
+            }
346
+        }
347
+
348
+        // Remember that local video was muted due to the audio-only mode
349
+        // vs user's choice.
350
+        dispatch({
351
+            type: _SET_AUDIO_ONLY_VIDEO_MUTED,
352
+            muted
353
+        });
354
+        dispatch(setVideoMuted(muted));
355
+    };
356
+}
357
+
298 358
 /**
299 359
  * Sets the video channel's last N (value) of the current conference. A value of
300 360
  * undefined shall be used to reset it to the default value.
@@ -403,3 +463,16 @@ export function setRoom(room) {
403 463
         room
404 464
     };
405 465
 }
466
+
467
+/**
468
+ * Toggles the audio-only flag for the current JitsiConference.
469
+ *
470
+ * @returns {Function}
471
+ */
472
+export function toggleAudioOnly() {
473
+    return (dispatch: Dispatch<*>, getState: Function) => {
474
+        const { audioOnly } = getState()['features/base/conference'];
475
+
476
+        return dispatch(_setAudioOnly(!audioOnly));
477
+    };
478
+}

+ 38
- 2
react/features/base/conference/middleware.js Переглянути файл

@@ -8,8 +8,12 @@ import {
8 8
 import { MiddlewareRegistry } from '../redux';
9 9
 import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
10 10
 
11
-import { createConference } from './actions';
12
-import { SET_LASTN } from './actionTypes';
11
+import {
12
+    createConference,
13
+    _setAudioOnlyVideoMuted,
14
+    setLastN
15
+} from './actions';
16
+import { SET_AUDIO_ONLY, SET_LASTN } from './actionTypes';
13 17
 import {
14 18
     _addLocalTracksToConference,
15 19
     _handleParticipantError,
@@ -30,6 +34,9 @@ MiddlewareRegistry.register(store => next => action => {
30 34
     case PIN_PARTICIPANT:
31 35
         return _pinParticipant(store, next, action);
32 36
 
37
+    case SET_AUDIO_ONLY:
38
+        return _setAudioOnly(store, next, action);
39
+
33 40
     case SET_LASTN:
34 41
         return _setLastN(store, next, action);
35 42
 
@@ -116,6 +123,35 @@ function _pinParticipant(store, next, action) {
116 123
     return next(action);
117 124
 }
118 125
 
126
+/**
127
+ * Sets the audio-only flag for the current conference. When audio-only is set,
128
+ * local video is muted and last N is set to 0 to avoid receiving remote video.
129
+ *
130
+ * @param {Store} store - The Redux store in which the specified action is being
131
+ * dispatched.
132
+ * @param {Dispatch} next - The Redux dispatch function to dispatch the
133
+ * specified action to the specified store.
134
+ * @param {Action} action - The Redux action SET_AUDIO_ONLY which is being
135
+ * dispatched in the specified store.
136
+ * @private
137
+ * @returns {Object} The new state that is the result of the reduction of the
138
+ * specified action.
139
+ */
140
+function _setAudioOnly(store, next, action) {
141
+    const result = next(action);
142
+
143
+    const { audioOnly } = action;
144
+
145
+    // Set lastN to 0 in case audio-only is desired; leave it as undefined,
146
+    // otherwise, and the default lastN value will be chosen automatically.
147
+    store.dispatch(setLastN(audioOnly ? 0 : undefined));
148
+
149
+    // Mute local video
150
+    store.dispatch(_setAudioOnlyVideoMuted(audioOnly));
151
+
152
+    return result;
153
+}
154
+
119 155
 /**
120 156
  * Sets the last N (value) of the video channel in the conference.
121 157
  *

+ 41
- 0
react/features/base/conference/reducer.js Переглянути файл

@@ -11,6 +11,8 @@ import {
11 11
     CONFERENCE_LEFT,
12 12
     CONFERENCE_WILL_LEAVE,
13 13
     LOCK_STATE_CHANGED,
14
+    SET_AUDIO_ONLY,
15
+    _SET_AUDIO_ONLY_VIDEO_MUTED,
14 16
     SET_PASSWORD,
15 17
     SET_ROOM
16 18
 } from './actionTypes';
@@ -37,6 +39,12 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
37 39
     case LOCK_STATE_CHANGED:
38 40
         return _lockStateChanged(state, action);
39 41
 
42
+    case SET_AUDIO_ONLY:
43
+        return _setAudioOnly(state, action);
44
+
45
+    case _SET_AUDIO_ONLY_VIDEO_MUTED:
46
+        return _setAudioOnlyVideoMuted(state, action);
47
+
40 48
     case SET_PASSWORD:
41 49
         return _setPassword(state, action);
42 50
 
@@ -71,6 +79,8 @@ function _conferenceFailed(state, action) {
71 79
 
72 80
     return (
73 81
         setStateProperties(state, {
82
+            audioOnly: undefined,
83
+            audioOnlyVideoMuted: undefined,
74 84
             conference: undefined,
75 85
             leaving: undefined,
76 86
             locked: undefined,
@@ -144,6 +154,8 @@ function _conferenceLeft(state, action) {
144 154
 
145 155
     return (
146 156
         setStateProperties(state, {
157
+            audioOnly: undefined,
158
+            audioOnlyVideoMuted: undefined,
147 159
             conference: undefined,
148 160
             leaving: undefined,
149 161
             locked: undefined,
@@ -200,6 +212,35 @@ function _lockStateChanged(state, action) {
200 212
     return setStateProperty(state, 'locked', action.locked || undefined);
201 213
 }
202 214
 
215
+/**
216
+ * Reduces a specific Redux action SET_AUDIO_ONLY of the feature
217
+ * base/conference.
218
+ *
219
+ * @param {Object} state - The Redux state of the feature base/conference.
220
+ * @param {Action} action - The Redux action SET_AUDIO_ONLY to reduce.
221
+ * @private
222
+ * @returns {Object} The new state of the feature base/conference after the
223
+ * reduction of the specified action.
224
+ */
225
+function _setAudioOnly(state, action) {
226
+    return setStateProperty(state, 'audioOnly', action.audioOnly);
227
+}
228
+
229
+/**
230
+ * Reduces a specific Redux action _SET_AUDIO_ONLY_VIDEO_MUTED of the feature
231
+ * base/conference.
232
+ *
233
+ * @param {Object} state - The Redux state of the feature base/conference.
234
+ * @param {Action} action - The Redux action SET_AUDIO_ONLY_VIDEO_MUTED to
235
+ * reduce.
236
+ * @private
237
+ * @returns {Object} The new state of the feature base/conference after the
238
+ * reduction of the specified action.
239
+ */
240
+function _setAudioOnlyVideoMuted(state, action) {
241
+    return setStateProperty(state, 'audioOnlyVideoMuted', action.muted);
242
+}
243
+
203 244
 /**
204 245
  * Reduces a specific Redux action SET_PASSWORD of the feature base/conference.
205 246
  *

+ 9
- 6
react/features/mobile/audio-mode/middleware.js Переглянути файл

@@ -6,7 +6,8 @@ import { APP_WILL_MOUNT } from '../../app';
6 6
 import {
7 7
     CONFERENCE_FAILED,
8 8
     CONFERENCE_LEFT,
9
-    CONFERENCE_WILL_JOIN
9
+    CONFERENCE_WILL_JOIN,
10
+    SET_AUDIO_ONLY
10 11
 } from '../../base/conference';
11 12
 import { MiddlewareRegistry } from '../../base/redux';
12 13
 
@@ -21,8 +22,6 @@ import { MiddlewareRegistry } from '../../base/redux';
21 22
 MiddlewareRegistry.register(store => next => action => {
22 23
     const AudioMode = NativeModules.AudioMode;
23 24
 
24
-    // The react-native module AudioMode is implemented on iOS at the time of
25
-    // this writing.
26 25
     if (AudioMode) {
27 26
         let mode;
28 27
 
@@ -34,14 +33,18 @@ MiddlewareRegistry.register(store => next => action => {
34 33
             break;
35 34
 
36 35
         case CONFERENCE_WILL_JOIN: {
37
-            const conference = store.getState()['features/base/conference'];
36
+            const { audioOnly } = store.getState()['features/base/conference'];
38 37
 
38
+            mode = audioOnly ? AudioMode.AUDIO_CALL : AudioMode.VIDEO_CALL;
39
+            break;
40
+        }
41
+
42
+        case SET_AUDIO_ONLY:
39 43
             mode
40
-                = conference.audioOnly
44
+                = action.audioOnly
41 45
                     ? AudioMode.AUDIO_CALL
42 46
                     : AudioMode.VIDEO_CALL;
43 47
             break;
44
-        }
45 48
 
46 49
         default:
47 50
             mode = null;

+ 4
- 2
react/features/mobile/background/actions.js Переглянути файл

@@ -42,10 +42,12 @@ export function _setBackgroundVideoMuted(muted: boolean) {
42 42
         // for last N will be chosen automatically.
43 43
         const { audioOnly } = getState()['features/base/conference'];
44 44
 
45
-        if (!audioOnly) {
46
-            dispatch(setLastN(muted ? 0 : undefined));
45
+        if (audioOnly) {
46
+            return;
47 47
         }
48 48
 
49
+        dispatch(setLastN(muted ? 0 : undefined));
50
+
49 51
         if (muted) {
50 52
             const { video } = getState()['features/base/media'];
51 53
 

+ 6
- 1
react/features/mobile/full-screen/middleware.js Переглянути файл

@@ -7,7 +7,8 @@ import { APP_STATE_CHANGED } from '../background';
7 7
 import {
8 8
     CONFERENCE_FAILED,
9 9
     CONFERENCE_LEFT,
10
-    CONFERENCE_WILL_JOIN
10
+    CONFERENCE_WILL_JOIN,
11
+    SET_AUDIO_ONLY
11 12
 } from '../../base/conference';
12 13
 import { Platform } from '../../base/react';
13 14
 import { MiddlewareRegistry } from '../../base/redux';
@@ -50,6 +51,10 @@ MiddlewareRegistry.register(store => next => action => {
50 51
     case CONFERENCE_LEFT:
51 52
         fullScreen = false;
52 53
         break;
54
+
55
+    case SET_AUDIO_ONLY:
56
+        fullScreen = !action.audioOnly;
57
+        break;
53 58
     }
54 59
 
55 60
     if (fullScreen !== null) {

+ 8
- 6
react/features/mobile/wake-lock/middleware.js Переглянути файл

@@ -3,7 +3,8 @@ import KeepAwake from 'react-native-keep-awake';
3 3
 import {
4 4
     CONFERENCE_FAILED,
5 5
     CONFERENCE_JOINED,
6
-    CONFERENCE_LEFT
6
+    CONFERENCE_LEFT,
7
+    SET_AUDIO_ONLY
7 8
 } from '../../base/conference';
8 9
 import { MiddlewareRegistry } from '../../base/redux';
9 10
 
@@ -18,12 +19,9 @@ import { MiddlewareRegistry } from '../../base/redux';
18 19
 MiddlewareRegistry.register(store => next => action => {
19 20
     switch (action.type) {
20 21
     case CONFERENCE_JOINED: {
21
-        const state = store.getState()['features/base/conference'];
22
+        const { audioOnly } = store.getState()['features/base/conference'];
22 23
 
23
-        // TODO(saghul): Implement audio-only mode.
24
-        if (!state.audioOnly) {
25
-            _setWakeLock(true);
26
-        }
24
+        _setWakeLock(!audioOnly);
27 25
         break;
28 26
     }
29 27
 
@@ -31,6 +29,10 @@ MiddlewareRegistry.register(store => next => action => {
31 29
     case CONFERENCE_LEFT:
32 30
         _setWakeLock(false);
33 31
         break;
32
+
33
+    case SET_AUDIO_ONLY:
34
+        _setWakeLock(!action.audioOnly);
35
+        break;
34 36
     }
35 37
 
36 38
     return next(action);

+ 31
- 7
react/features/toolbox/components/Toolbox.native.js Переглянути файл

@@ -2,6 +2,7 @@ import React, { Component } from 'react';
2 2
 import { View } from 'react-native';
3 3
 import { connect } from 'react-redux';
4 4
 
5
+import { toggleAudioOnly } from '../../base/conference';
5 6
 import { MEDIA_TYPE, toggleCameraFacingMode } from '../../base/media';
6 7
 import { Container } from '../../base/react';
7 8
 import { ColorPalette } from '../../base/styles';
@@ -40,7 +41,7 @@ class Toolbox extends Component {
40 41
         _onHangup: React.PropTypes.func,
41 42
 
42 43
         /**
43
-         * Handler for room locking.
44
+         * Sets the lock i.e. password protection of the conference/room.
44 45
          */
45 46
         _onRoomLock: React.PropTypes.func,
46 47
 
@@ -50,7 +51,13 @@ class Toolbox extends Component {
50 51
         _onToggleAudio: React.PropTypes.func,
51 52
 
52 53
         /**
53
-         * Handler for toggling camera facing mode.
54
+         * Toggles the audio-only flag of the conference.
55
+         */
56
+        _onToggleAudioOnly: React.PropTypes.func,
57
+
58
+        /**
59
+         * Switches between the front/user-facing and back/environment-facing
60
+         * cameras.
54 61
          */
55 62
         _onToggleCameraFacingMode: React.PropTypes.func,
56 63
 
@@ -198,6 +205,12 @@ class Toolbox extends Component {
198 205
                     onClick = { this.props._onRoomLock }
199 206
                     style = { style }
200 207
                     underlayColor = { underlayColor } />
208
+                <ToolbarButton
209
+                    iconName = 'star'
210
+                    iconStyle = { iconStyle }
211
+                    onClick = { this.props._onToggleAudioOnly }
212
+                    style = { style }
213
+                    underlayColor = { underlayColor } />
201 214
             </View>
202 215
         );
203 216
 
@@ -224,6 +237,7 @@ Object.assign(Toolbox.prototype, {
224 237
  * @param {Function} dispatch - Redux action dispatcher.
225 238
  * @returns {{
226 239
  *     _onRoomLock: Function,
240
+ *     _onToggleAudioOnly: Function,
227 241
  *     _onToggleCameraFacingMode: Function,
228 242
  * }}
229 243
  * @private
@@ -233,11 +247,10 @@ function _mapDispatchToProps(dispatch) {
233 247
         ...abstractMapDispatchToProps(dispatch),
234 248
 
235 249
         /**
236
-         * Dispatches an action to set the lock i.e. password protection of the
237
-         * conference/room.
250
+         * Sets the lock i.e. password protection of the conference/room.
238 251
          *
239 252
          * @private
240
-         * @returns {Object} - Dispatched action.
253
+         * @returns {Object} Dispatched action.
241 254
          * @type {Function}
242 255
          */
243 256
         _onRoomLock() {
@@ -245,11 +258,22 @@ function _mapDispatchToProps(dispatch) {
245 258
         },
246 259
 
247 260
         /**
248
-         * Switches between the front/user-facing and rear/environment-facing
261
+         * Toggles the audio-only flag of the conference.
262
+         *
263
+         * @private
264
+         * @returns {Object} Dispatched action.
265
+         * @type {Function}
266
+         */
267
+        _onToggleAudioOnly() {
268
+            return dispatch(toggleAudioOnly());
269
+        },
270
+
271
+        /**
272
+         * Switches between the front/user-facing and back/environment-facing
249 273
          * cameras.
250 274
          *
251 275
          * @private
252
-         * @returns {Object} - Dispatched action.
276
+         * @returns {Object} Dispatched action.
253 277
          * @type {Function}
254 278
          */
255 279
         _onToggleCameraFacingMode() {

Завантаження…
Відмінити
Зберегти