Browse Source

feat(conference) Implement audio/video mute disable when sender limit is reached.

* feat(conference) Impl audio/video mute disable when sender limit is reached.
Jicofo sends a presence when the audio/video sender limit is reached in the conference. The client can then proceed to disable the audio and video mute buttons when this occurs.

* squash: use a different action type and show notification.
master
Jaya Allamsetty 3 years ago
parent
commit
c7765cc1b7
No account linked to committer's email address

+ 26
- 1
conference.js View File

@@ -76,12 +76,16 @@ import {
76 76
 import {
77 77
     getStartWithAudioMuted,
78 78
     getStartWithVideoMuted,
79
+    isAudioMuted,
80
+    isVideoMuted,
79 81
     isVideoMutedByUser,
80 82
     MEDIA_TYPE,
81 83
     setAudioAvailable,
82 84
     setAudioMuted,
85
+    setAudioUnmutePermissions,
83 86
     setVideoAvailable,
84
-    setVideoMuted
87
+    setVideoMuted,
88
+    setVideoUnmutePermissions
85 89
 } from './react/features/base/media';
86 90
 import {
87 91
     dominantSpeakerChanged,
@@ -2257,6 +2261,27 @@ export default {
2257 2261
             APP.store.dispatch(suspendDetected());
2258 2262
         });
2259 2263
 
2264
+        room.on(
2265
+            JitsiConferenceEvents.AUDIO_UNMUTE_PERMISSIONS_CHANGED,
2266
+            disableAudioMuteChange => {
2267
+                const muted = isAudioMuted(APP.store.getState());
2268
+
2269
+                // Disable the mute button only if its muted.
2270
+                if (!disableAudioMuteChange || (disableAudioMuteChange && muted)) {
2271
+                    APP.store.dispatch(setAudioUnmutePermissions(disableAudioMuteChange));
2272
+                }
2273
+            });
2274
+        room.on(
2275
+            JitsiConferenceEvents.VIDEO_UNMUTE_PERMISSIONS_CHANGED,
2276
+            disableVideoMuteChange => {
2277
+                const muted = isVideoMuted(APP.store.getState());
2278
+
2279
+                // Disable the mute button only if its muted.
2280
+                if (!disableVideoMuteChange || (disableVideoMuteChange && muted)) {
2281
+                    APP.store.dispatch(setVideoUnmutePermissions(disableVideoMuteChange));
2282
+                }
2283
+            });
2284
+
2260 2285
         APP.UI.addListener(UIEvents.AUDIO_MUTED, muted => {
2261 2286
             this.muteAudio(muted);
2262 2287
         });

+ 5
- 1
lang/main.json View File

@@ -572,6 +572,8 @@
572 572
     "notify": {
573 573
         "allowAction": "Allow",
574 574
         "allowedUnmute": "You can unmute your microphone, start your camera or share your screen.",
575
+        "audioUnmuteBlockedTitle": "Mic unmute blocked!",
576
+        "audioUnmuteBlockedDescription": "Mic unmute operation has been temporarily blocked because of system limits.",
575 577
         "connectedOneMember": "{{name}} joined the meeting",
576 578
         "connectedThreePlusMembers": "{{name}} and many others joined the meeting",
577 579
         "connectedTwoMembers": "{{first}} and {{second}} joined the meeting",
@@ -622,7 +624,9 @@
622 624
         "moderationToggleDescription": "by {{participantDisplayName}}",
623 625
         "raiseHandAction": "Raise hand",
624 626
         "reactionSounds": "Disable sounds",
625
-        "groupTitle": "Notifications"
627
+        "groupTitle": "Notifications",
628
+        "videoUnmuteBlockedTitle": "Camera unmute blocked!",
629
+        "videoUnmuteBlockedDescription": "Camera unmute operation has been temporarily blocked because of system limits."
626 630
     },
627 631
     "participantsPane": {
628 632
         "close": "Close",

+ 30
- 1
react/features/base/conference/actions.js View File

@@ -10,7 +10,15 @@ import { endpointMessageReceived } from '../../subtitles';
10 10
 import { getReplaceParticipant } from '../config/functions';
11 11
 import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection';
12 12
 import { JitsiConferenceEvents } from '../lib-jitsi-meet';
13
-import { MEDIA_TYPE, setAudioMuted, setVideoMuted } from '../media';
13
+import {
14
+    MEDIA_TYPE,
15
+    isAudioMuted,
16
+    isVideoMuted,
17
+    setAudioMuted,
18
+    setAudioUnmutePermissions,
19
+    setVideoMuted,
20
+    setVideoUnmutePermissions
21
+} from '../media';
14 22
 import {
15 23
     dominantSpeakerChanged,
16 24
     getNormalizedDisplayName,
@@ -146,6 +154,27 @@ function _addConferenceListeners(conference, dispatch, state) {
146 154
             }
147 155
         });
148 156
 
157
+    conference.on(
158
+        JitsiConferenceEvents.AUDIO_UNMUTE_PERMISSIONS_CHANGED,
159
+        disableAudioMuteChange => {
160
+            const muted = isAudioMuted(state);
161
+
162
+            // Disable the mute button only if its muted.
163
+            if (!disableAudioMuteChange || (disableAudioMuteChange && muted)) {
164
+                APP.store.dispatch(setAudioUnmutePermissions(disableAudioMuteChange));
165
+            }
166
+        });
167
+    conference.on(
168
+        JitsiConferenceEvents.VIDEO_UNMUTE_PERMISSIONS_CHANGED,
169
+        disableVideoMuteChange => {
170
+            const muted = isVideoMuted(state);
171
+
172
+            // Disable the mute button only if its muted.
173
+            if (!disableVideoMuteChange || (disableVideoMuteChange && muted)) {
174
+                APP.store.dispatch(setVideoUnmutePermissions(disableVideoMuteChange));
175
+            }
176
+        });
177
+
149 178
     // Dispatches into features/base/tracks follow:
150 179
 
151 180
     conference.on(

+ 25
- 4
react/features/base/media/actionTypes.js View File

@@ -1,3 +1,14 @@
1
+
2
+/**
3
+ * The type of (redux) action to adjust the availability of the local audio.
4
+ *
5
+ * {
6
+ *     type: SET_AUDIO_AVAILABLE,
7
+ *     muted: boolean
8
+ * }
9
+ */
10
+export const SET_AUDIO_AVAILABLE = 'SET_AUDIO_AVAILABLE';
11
+
1 12
 /**
2 13
  * The type of (redux) action to set the muted state of the local audio.
3 14
  *
@@ -9,14 +20,14 @@
9 20
 export const SET_AUDIO_MUTED = 'SET_AUDIO_MUTED';
10 21
 
11 22
 /**
12
- * The type of (redux) action to adjust the availability of the local audio.
23
+ * The type of (redux) action to enable/disable the audio mute icon.
13 24
  *
14 25
  * {
15
- *     type: SET_AUDIO_AVAILABLE,
16
- *     muted: boolean
26
+ *     type: SET_AUDIO_UNMUTE_PERMISSIONS,
27
+ *     blocked: boolean
17 28
  * }
18 29
  */
19
-export const SET_AUDIO_AVAILABLE = 'SET_AUDIO_AVAILABLE';
30
+export const SET_AUDIO_UNMUTE_PERMISSIONS = 'SET_AUDIO_UNMUTE_PERMISSIONS';
20 31
 
21 32
 /**
22 33
  * The type of (redux) action to set the facing mode of the local video camera
@@ -61,6 +72,16 @@ export const SET_VIDEO_MUTED = 'SET_VIDEO_MUTED';
61 72
  */
62 73
 export const STORE_VIDEO_TRANSFORM = 'STORE_VIDEO_TRANSFORM';
63 74
 
75
+/**
76
+ * The type of (redux) action to enable/disable the video mute icon.
77
+ *
78
+ * {
79
+ *     type: SET_VIDEO_UNMUTE_PERMISSIONS,
80
+ *     blocked: boolean
81
+ * }
82
+ */
83
+ export const SET_VIDEO_UNMUTE_PERMISSIONS = 'SET_VIDEO_UNMUTE_PERMISSIONS';
84
+
64 85
 /**
65 86
  * The type of (redux) action to toggle the local video camera facing mode. In
66 87
  * contrast to SET_CAMERA_FACING_MODE, allows the toggling to be optimally

+ 28
- 0
react/features/base/media/actions.js View File

@@ -9,9 +9,11 @@ import { isModerationNotificationDisplayed } from '../../notifications';
9 9
 import {
10 10
     SET_AUDIO_MUTED,
11 11
     SET_AUDIO_AVAILABLE,
12
+    SET_AUDIO_UNMUTE_PERMISSIONS,
12 13
     SET_CAMERA_FACING_MODE,
13 14
     SET_VIDEO_AVAILABLE,
14 15
     SET_VIDEO_MUTED,
16
+    SET_VIDEO_UNMUTE_PERMISSIONS,
15 17
     STORE_VIDEO_TRANSFORM,
16 18
     TOGGLE_CAMERA_FACING_MODE
17 19
 } from './actionTypes';
@@ -59,6 +61,19 @@ export function setAudioMuted(muted: boolean, ensureTrack: boolean = false) {
59 61
     };
60 62
 }
61 63
 
64
+/**
65
+ * Action to disable/enable the audio mute icon.
66
+ *
67
+ * @param {boolean} blocked - True if the audio mute icon needs to be disabled.
68
+ * @returns {Function}
69
+ */
70
+export function setAudioUnmutePermissions(blocked: boolean) {
71
+    return {
72
+        type: SET_AUDIO_UNMUTE_PERMISSIONS,
73
+        blocked
74
+    };
75
+}
76
+
62 77
 /**
63 78
  * Action to set the facing mode of the local camera.
64 79
  *
@@ -136,6 +151,19 @@ export function setVideoMuted(
136 151
     };
137 152
 }
138 153
 
154
+/**
155
+ * Action to disable/enable the video mute icon.
156
+ *
157
+ * @param {boolean} blocked - True if the video mute icon needs to be disabled.
158
+ * @returns {Function}
159
+ */
160
+export function setVideoUnmutePermissions(blocked: boolean) {
161
+    return {
162
+        type: SET_VIDEO_UNMUTE_PERMISSIONS,
163
+        blocked
164
+    };
165
+}
166
+
139 167
 /**
140 168
  * Creates an action to store the last video {@link Transform} applied to a
141 169
  * stream.

+ 10
- 0
react/features/base/media/functions.js View File

@@ -88,6 +88,16 @@ export function getStartWithVideoMuted(stateful: Object | Function) {
88 88
     return Boolean(getPropertyValue(stateful, 'startWithVideoMuted', START_WITH_AUDIO_VIDEO_MUTED_SOURCES));
89 89
 }
90 90
 
91
+/**
92
+ * Determines whether video is currently muted.
93
+ *
94
+ * @param {Function|Object} stateful - The redux store, state, or {@code getState} function.
95
+ * @returns {boolean}
96
+ */
97
+export function isVideoMuted(stateful: Function | Object) {
98
+    return Boolean(toState(stateful)['features/base/media'].video.muted);
99
+}
100
+
91 101
 /**
92 102
  * Determines whether video is currently muted by the user authority.
93 103
  *

+ 34
- 1
react/features/base/media/middleware.js View File

@@ -8,6 +8,10 @@ import {
8 8
     sendAnalytics
9 9
 } from '../../analytics';
10 10
 import { APP_STATE_CHANGED } from '../../mobile/background';
11
+import {
12
+    NOTIFICATION_TIMEOUT_TYPE,
13
+    showWarningNotification
14
+} from '../../notifications';
11 15
 import { isForceMuted } from '../../participants-pane/functions';
12 16
 import { SET_AUDIO_ONLY, setAudioOnly } from '../audio-only';
13 17
 import { isRoomValid, SET_ROOM } from '../conference';
@@ -21,7 +25,12 @@ import {
21 25
     TRACK_ADDED
22 26
 } from '../tracks';
23 27
 
24
-import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from './actionTypes';
28
+import {
29
+    SET_AUDIO_MUTED,
30
+    SET_AUDIO_UNMUTE_PERMISSIONS,
31
+    SET_VIDEO_MUTED,
32
+    SET_VIDEO_UNMUTE_PERMISSIONS
33
+} from './actionTypes';
25 34
 import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
26 35
 import {
27 36
     CAMERA_FACING_MODE,
@@ -74,6 +83,18 @@ MiddlewareRegistry.register(store => next => action => {
74 83
         break;
75 84
     }
76 85
 
86
+    case SET_AUDIO_UNMUTE_PERMISSIONS: {
87
+        const { blocked } = action;
88
+
89
+        if (blocked) {
90
+            store.dispatch(showWarningNotification({
91
+                descriptionKey: 'notify.audioUnmuteBlockedDescription',
92
+                titleKey: 'notify.audioUnmuteBlockedTitle'
93
+            }, NOTIFICATION_TIMEOUT_TYPE.LONG));
94
+        }
95
+        break;
96
+    }
97
+
77 98
     case SET_VIDEO_MUTED: {
78 99
         const state = store.getState();
79 100
         const participant = getLocalParticipant(state);
@@ -83,6 +104,18 @@ MiddlewareRegistry.register(store => next => action => {
83 104
         }
84 105
         break;
85 106
     }
107
+
108
+    case SET_VIDEO_UNMUTE_PERMISSIONS: {
109
+        const { blocked } = action;
110
+
111
+        if (blocked) {
112
+            store.dispatch(showWarningNotification({
113
+                descriptionKey: 'notify.videoUnmuteBlockedDescription',
114
+                titleKey: 'notify.videoUnmuteBlockedTitle'
115
+            }, NOTIFICATION_TIMEOUT_TYPE.LONG));
116
+        }
117
+        break;
118
+    }
86 119
     }
87 120
 
88 121
     return next(action);

+ 16
- 0
react/features/base/media/reducer.js View File

@@ -7,9 +7,11 @@ import { TRACK_REMOVED } from '../tracks/actionTypes';
7 7
 import {
8 8
     SET_AUDIO_AVAILABLE,
9 9
     SET_AUDIO_MUTED,
10
+    SET_AUDIO_UNMUTE_PERMISSIONS,
10 11
     SET_CAMERA_FACING_MODE,
11 12
     SET_VIDEO_AVAILABLE,
12 13
     SET_VIDEO_MUTED,
14
+    SET_VIDEO_UNMUTE_PERMISSIONS,
13 15
     STORE_VIDEO_TRANSFORM,
14 16
     TOGGLE_CAMERA_FACING_MODE
15 17
 } from './actionTypes';
@@ -33,6 +35,7 @@ import { CAMERA_FACING_MODE } from './constants';
33 35
  */
34 36
 export const _AUDIO_INITIAL_MEDIA_STATE = {
35 37
     available: true,
38
+    blocked: false,
36 39
     muted: false
37 40
 };
38 41
 
@@ -59,6 +62,12 @@ function _audio(state = _AUDIO_INITIAL_MEDIA_STATE, action) {
59 62
             muted: action.muted
60 63
         };
61 64
 
65
+    case SET_AUDIO_UNMUTE_PERMISSIONS:
66
+        return {
67
+            ...state,
68
+            blocked: action.blocked
69
+        };
70
+
62 71
     default:
63 72
         return state;
64 73
     }
@@ -83,6 +92,7 @@ function _audio(state = _AUDIO_INITIAL_MEDIA_STATE, action) {
83 92
  */
84 93
 export const _VIDEO_INITIAL_MEDIA_STATE = {
85 94
     available: true,
95
+    blocked: false,
86 96
     facingMode: CAMERA_FACING_MODE.USER,
87 97
     muted: 0,
88 98
 
@@ -126,6 +136,12 @@ function _video(state = _VIDEO_INITIAL_MEDIA_STATE, action) {
126 136
             muted: action.muted
127 137
         };
128 138
 
139
+    case SET_VIDEO_UNMUTE_PERMISSIONS:
140
+        return {
141
+            ...state,
142
+            blocked: action.blocked
143
+        };
144
+
129 145
     case STORE_VIDEO_TRANSFORM:
130 146
         return _storeVideoTransform(state, action);
131 147
 

+ 18
- 14
react/features/talk-while-muted/middleware.js View File

@@ -13,6 +13,7 @@ import {
13 13
     showNotification
14 14
 } from '../notifications';
15 15
 import { isForceMuted } from '../participants-pane/functions';
16
+import { isAudioMuteButtonDisabled } from '../toolbox/functions.any';
16 17
 
17 18
 import { setCurrentNotificationUid } from './actions';
18 19
 import { TALK_WHILE_MUTED_SOUND_ID } from './constants';
@@ -46,24 +47,27 @@ MiddlewareRegistry.register(store => next => action => {
46 47
             JitsiConferenceEvents.TALK_WHILE_MUTED, async () => {
47 48
                 const state = getState();
48 49
                 const local = getLocalParticipant(state);
49
-                const forceMuted = isForceMuted(local, MEDIA_TYPE.AUDIO, state);
50
-                const notification = await dispatch(showNotification({
51
-                    titleKey: 'toolbar.talkWhileMutedPopup',
52
-                    customActionNameKey: forceMuted ? 'notify.raiseHandAction' : 'notify.unmute',
53
-                    customActionHandler: () => dispatch(forceMuted ? raiseHand(true) : setAudioMuted(false))
54
-                }, NOTIFICATION_TIMEOUT_TYPE.LONG));
55 50
 
56
-                const { soundsTalkWhileMuted } = getState()['features/base/settings'];
51
+                // Display the talk while muted notification only when the audio button is not disabled.
52
+                if (!isAudioMuteButtonDisabled(state)) {
53
+                    const forceMuted = isForceMuted(local, MEDIA_TYPE.AUDIO, state);
54
+                    const notification = await dispatch(showNotification({
55
+                        titleKey: 'toolbar.talkWhileMutedPopup',
56
+                        customActionNameKey: forceMuted ? 'notify.raiseHandAction' : 'notify.unmute',
57
+                        customActionHandler: () => dispatch(forceMuted ? raiseHand(true) : setAudioMuted(false))
58
+                    }, NOTIFICATION_TIMEOUT_TYPE.LONG));
57 59
 
58
-                if (soundsTalkWhileMuted) {
59
-                    dispatch(playSound(TALK_WHILE_MUTED_SOUND_ID));
60
-                }
60
+                    const { soundsTalkWhileMuted } = getState()['features/base/settings'];
61 61
 
62
+                    if (soundsTalkWhileMuted) {
63
+                        dispatch(playSound(TALK_WHILE_MUTED_SOUND_ID));
64
+                    }
62 65
 
63
-                if (notification) {
64
-                    // we store the last start muted notification id that we showed,
65
-                    // so we can hide it when unmuted mic is detected
66
-                    dispatch(setCurrentNotificationUid(notification.uid));
66
+                    if (notification) {
67
+                        // we store the last start muted notification id that we showed,
68
+                        // so we can hide it when unmuted mic is detected
69
+                        dispatch(setCurrentNotificationUid(notification.uid));
70
+                    }
67 71
                 }
68 72
             });
69 73
         break;

+ 2
- 1
react/features/toolbox/components/AudioMuteButton.js View File

@@ -14,6 +14,7 @@ import { AbstractAudioMuteButton } from '../../base/toolbox/components';
14 14
 import type { AbstractButtonProps } from '../../base/toolbox/components';
15 15
 import { isLocalTrackMuted } from '../../base/tracks';
16 16
 import { muteLocal } from '../../video-menu/actions';
17
+import { isAudioMuteButtonDisabled } from '../functions';
17 18
 
18 19
 declare var APP: Object;
19 20
 
@@ -151,7 +152,7 @@ class AudioMuteButton extends AbstractAudioMuteButton<Props, *> {
151 152
  */
152 153
 function _mapStateToProps(state): Object {
153 154
     const _audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
154
-    const _disabled = state['features/base/config'].startSilent;
155
+    const _disabled = state['features/base/config'].startSilent || isAudioMuteButtonDisabled(state);
155 156
     const enabledFlag = getFeatureFlag(state, AUDIO_MUTE_BUTTON_ENABLED, true);
156 157
 
157 158
     return {

+ 13
- 0
react/features/toolbox/functions.any.js View File

@@ -0,0 +1,13 @@
1
+// @flow
2
+
3
+/**
4
+ * Indicates if the audio mute button is disabled or not.
5
+ *
6
+ * @param {Object} state - The state from the Redux store.
7
+ * @returns {boolean}
8
+ */
9
+export function isAudioMuteButtonDisabled(state: Object) {
10
+    const { audio } = state['features/base/media'];
11
+
12
+    return !(audio?.available && !audio?.blocked);
13
+}

+ 5
- 1
react/features/toolbox/functions.native.js View File

@@ -6,6 +6,8 @@ import { getParticipantCountWithFake } from '../base/participants';
6 6
 import { toState } from '../base/redux';
7 7
 import { isLocalVideoTrackDesktop } from '../base/tracks';
8 8
 
9
+export * from './functions.any';
10
+
9 11
 const WIDTH = {
10 12
     FIT_9_ICONS: 560,
11 13
     FIT_8_ICONS: 500,
@@ -78,5 +80,7 @@ export function isToolboxVisible(stateful: Object | Function) {
78 80
  * @returns {boolean}
79 81
  */
80 82
 export function isVideoMuteButtonDisabled(state: Object) {
81
-    return !hasAvailableDevices(state, 'videoInput') || isLocalVideoTrackDesktop(state);
83
+    const { video } = state['features/base/media'];
84
+
85
+    return !hasAvailableDevices(state, 'videoInput') || video?.blocked || isLocalVideoTrackDesktop(state);
82 86
 }

+ 8
- 3
react/features/toolbox/functions.web.js View File

@@ -5,6 +5,8 @@ import { hasAvailableDevices } from '../base/devices';
5 5
 
6 6
 import { TOOLBAR_TIMEOUT } from './constants';
7 7
 
8
+export * from './functions.any';
9
+
8 10
 /**
9 11
  * Helper for getting the height of the toolbox.
10 12
  *
@@ -58,8 +60,9 @@ export function isToolboxVisible(state: Object) {
58 60
  * @returns {boolean}
59 61
  */
60 62
 export function isAudioSettingsButtonDisabled(state: Object) {
61
-    return (!hasAvailableDevices(state, 'audioInput')
62
-          && !hasAvailableDevices(state, 'audioOutput'))
63
+
64
+    return !(hasAvailableDevices(state, 'audioInput')
65
+          && hasAvailableDevices(state, 'audioOutput'))
63 66
           || state['features/base/config'].startSilent;
64 67
 }
65 68
 
@@ -80,7 +83,9 @@ export function isVideoSettingsButtonDisabled(state: Object) {
80 83
  * @returns {boolean}
81 84
  */
82 85
 export function isVideoMuteButtonDisabled(state: Object) {
83
-    return !hasAvailableDevices(state, 'videoInput');
86
+    const { video } = state['features/base/media'];
87
+
88
+    return !hasAvailableDevices(state, 'videoInput') || video?.blocked;
84 89
 }
85 90
 
86 91
 /**

Loading…
Cancel
Save