Browse Source

feat(Filmstrip): Reorder the visible participants in the filmstrip. (#9707)

* feat(Filmstrip): Reorder the visible participants in the filmstrip.
The participants are ordered alphabetically and the endpoints with screenshares, shared-videos and dominant speakers (in that order) are bumped to the top of the list. The local participant is also moved to the top left corner as opposed to the bottom right corner.

* squash: Implement review comments.

* squash: store alphabetically sorted list in redux and move shared videos to top.

* squash: Use the DEFAULT_REMOTE_DISPLAY_NAME from interfaceConfig for users without a display name.
master
Jaya Allamsetty 3 years ago
parent
commit
40099e97ec
No account linked to committer's email address

+ 1
- 1
conference.js View File

2106
 
2106
 
2107
         room.on(
2107
         room.on(
2108
             JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
2108
             JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
2109
-            id => APP.store.dispatch(dominantSpeakerChanged(id, room)));
2109
+            (dominant, previous) => APP.store.dispatch(dominantSpeakerChanged(dominant, previous, room)));
2110
 
2110
 
2111
         room.on(
2111
         room.on(
2112
             JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP,
2112
             JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP,

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

170
 
170
 
171
     conference.on(
171
     conference.on(
172
         JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
172
         JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
173
-        id => dispatch(dominantSpeakerChanged(id, conference)));
173
+        (dominant, previous) => dispatch(dominantSpeakerChanged(dominant, previous, conference)));
174
 
174
 
175
     conference.on(
175
     conference.on(
176
         JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
176
         JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,

+ 3
- 1
react/features/base/participants/actionTypes.js View File

6
  * {
6
  * {
7
  *     type: DOMINANT_SPEAKER_CHANGED,
7
  *     type: DOMINANT_SPEAKER_CHANGED,
8
  *     participant: {
8
  *     participant: {
9
- *         id: string
9
+ *         conference: JitsiConference,
10
+ *         id: string,
11
+ *         previousSpeakers: Array<string>
10
  *     }
12
  *     }
11
  * }
13
  * }
12
  */
14
  */

+ 7
- 4
react/features/base/participants/actions.js View File

31
 /**
31
 /**
32
  * Create an action for when dominant speaker changes.
32
  * Create an action for when dominant speaker changes.
33
  *
33
  *
34
- * @param {string} id - Participant's ID.
34
+ * @param {string} dominantSpeaker - Participant ID of the dominant speaker.
35
+ * @param {Array<string>} previousSpeakers - Participant IDs of the previous speakers.
35
  * @param {JitsiConference} conference - The {@code JitsiConference} associated
36
  * @param {JitsiConference} conference - The {@code JitsiConference} associated
36
  * with the participant identified by the specified {@code id}. Only the local
37
  * with the participant identified by the specified {@code id}. Only the local
37
  * participant is allowed to not specify an associated {@code JitsiConference}
38
  * participant is allowed to not specify an associated {@code JitsiConference}
40
  *     type: DOMINANT_SPEAKER_CHANGED,
41
  *     type: DOMINANT_SPEAKER_CHANGED,
41
  *     participant: {
42
  *     participant: {
42
  *         conference: JitsiConference,
43
  *         conference: JitsiConference,
43
- *         id: string
44
+ *         id: string,
45
+ *         previousSpeakers: Array<string>
44
  *     }
46
  *     }
45
  * }}
47
  * }}
46
  */
48
  */
47
-export function dominantSpeakerChanged(id, conference) {
49
+export function dominantSpeakerChanged(dominantSpeaker, previousSpeakers, conference) {
48
     return {
50
     return {
49
         type: DOMINANT_SPEAKER_CHANGED,
51
         type: DOMINANT_SPEAKER_CHANGED,
50
         participant: {
52
         participant: {
51
             conference,
53
             conference,
52
-            id
54
+            id: dominantSpeaker,
55
+            previousSpeakers
53
         }
56
         }
54
     };
57
     };
55
 }
58
 }

+ 39
- 12
react/features/base/participants/reducer.js View File

14
 import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
14
 import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
15
 import { isParticipantModerator } from './functions';
15
 import { isParticipantModerator } from './functions';
16
 
16
 
17
+declare var interfaceConfig: Object;
18
+
17
 /**
19
 /**
18
  * Participant object.
20
  * Participant object.
19
  * @typedef {Object} Participant
21
  * @typedef {Object} Participant
51
 ];
53
 ];
52
 
54
 
53
 const DEFAULT_STATE = {
55
 const DEFAULT_STATE = {
54
-    haveParticipantWithScreenSharingFeature: false,
55
     dominantSpeaker: undefined,
56
     dominantSpeaker: undefined,
56
     everyoneIsModerator: false,
57
     everyoneIsModerator: false,
57
-    pinnedParticipant: undefined,
58
+    fakeParticipants: new Map(),
59
+    haveParticipantWithScreenSharingFeature: false,
58
     local: undefined,
60
     local: undefined,
61
+    pinnedParticipant: undefined,
59
     remote: new Map(),
62
     remote: new Map(),
60
-    fakeParticipants: new Map()
63
+    sortedRemoteParticipants: new Map(),
64
+    speakersList: []
61
 };
65
 };
62
 
66
 
63
 /**
67
 /**
91
     }
95
     }
92
     case DOMINANT_SPEAKER_CHANGED: {
96
     case DOMINANT_SPEAKER_CHANGED: {
93
         const { participant } = action;
97
         const { participant } = action;
94
-        const { id } = participant;
95
-        const { dominantSpeaker } = state;
98
+        const { id, previousSpeakers = [] } = participant;
99
+        const { dominantSpeaker, local } = state;
100
+        const speakersList = [];
101
+
102
+        // Update the speakers list.
103
+        id !== local?.id && speakersList.push(id);
104
+        speakersList.push(...previousSpeakers.filter(p => p !== local?.id));
96
 
105
 
97
         // Only one dominant speaker is allowed.
106
         // Only one dominant speaker is allowed.
98
         if (dominantSpeaker) {
107
         if (dominantSpeaker) {
102
         if (_updateParticipantProperty(state, id, 'dominantSpeaker', true)) {
111
         if (_updateParticipantProperty(state, id, 'dominantSpeaker', true)) {
103
             return {
112
             return {
104
                 ...state,
113
                 ...state,
105
-                dominantSpeaker: id
114
+                dominantSpeaker: id,
115
+                speakersList
106
             };
116
             };
107
         }
117
         }
108
 
118
 
180
     }
190
     }
181
     case PARTICIPANT_JOINED: {
191
     case PARTICIPANT_JOINED: {
182
         const participant = _participantJoined(action);
192
         const participant = _participantJoined(action);
193
+        const { id, isFakeParticipant, name, pinned } = participant;
183
         const { pinnedParticipant, dominantSpeaker } = state;
194
         const { pinnedParticipant, dominantSpeaker } = state;
184
 
195
 
185
-        if (participant.pinned) {
196
+        if (pinned) {
186
             if (pinnedParticipant) {
197
             if (pinnedParticipant) {
187
                 _updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
198
                 _updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
188
             }
199
             }
189
 
200
 
190
-            state.pinnedParticipant = participant.id;
201
+            state.pinnedParticipant = id;
191
         }
202
         }
192
 
203
 
193
         if (participant.dominantSpeaker) {
204
         if (participant.dominantSpeaker) {
194
             if (dominantSpeaker) {
205
             if (dominantSpeaker) {
195
                 _updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
206
                 _updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
196
             }
207
             }
197
-            state.dominantSpeaker = participant.id;
208
+            state.dominantSpeaker = id;
198
         }
209
         }
199
 
210
 
200
         const isModerator = isParticipantModerator(participant);
211
         const isModerator = isParticipantModerator(participant);
213
             };
224
             };
214
         }
225
         }
215
 
226
 
216
-        state.remote.set(participant.id, participant);
227
+        state.remote.set(id, participant);
228
+
229
+        // Insert the new participant.
230
+        const displayName = name
231
+            ?? (typeof interfaceConfig === 'object' ? interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME : 'Fellow Jitser');
232
+        const sortedRemoteParticipants = Array.from(state.sortedRemoteParticipants);
233
+
234
+        sortedRemoteParticipants.push([ id, displayName ]);
235
+        sortedRemoteParticipants.sort((a, b) => a[1].localeCompare(b[1]));
217
 
236
 
218
-        if (participant.isFakeParticipant) {
219
-            state.fakeParticipants.set(participant.id, participant);
237
+        // The sort order of participants is preserved since Map remembers the original insertion order of the keys.
238
+        state.sortedRemoteParticipants = new Map(sortedRemoteParticipants);
239
+
240
+        if (isFakeParticipant) {
241
+            state.fakeParticipants.set(id, participant);
220
         }
242
         }
221
 
243
 
222
         return { ...state };
244
         return { ...state };
242
             return state;
264
             return state;
243
         }
265
         }
244
 
266
 
267
+        state.sortedRemoteParticipants.delete(id);
268
+
245
         if (!state.everyoneIsModerator && !isParticipantModerator(oldParticipant)) {
269
         if (!state.everyoneIsModerator && !isParticipantModerator(oldParticipant)) {
246
             state.everyoneIsModerator = _isEveryoneModerator(state);
270
             state.everyoneIsModerator = _isEveryoneModerator(state);
247
         }
271
         }
272
             state.dominantSpeaker = undefined;
296
             state.dominantSpeaker = undefined;
273
         }
297
         }
274
 
298
 
299
+        // Remove the participant from the list of speakers.
300
+        state.speakersList = state.speakersList.filter(speaker => speaker !== id);
301
+
275
         if (pinnedParticipant === id) {
302
         if (pinnedParticipant === id) {
276
             state.pinnedParticipant = undefined;
303
             state.pinnedParticipant = undefined;
277
         }
304
         }

+ 9
- 0
react/features/filmstrip/actionTypes.js View File

50
  */
50
  */
51
 export const SET_HORIZONTAL_VIEW_DIMENSIONS = 'SET_HORIZONTAL_VIEW_DIMENSIONS';
51
 export const SET_HORIZONTAL_VIEW_DIMENSIONS = 'SET_HORIZONTAL_VIEW_DIMENSIONS';
52
 
52
 
53
+/**
54
+ * The type of (redux) action which sets the reordered list of the remote participants in the filmstrip.
55
+ * {
56
+ *      type: SET_REMOTE_PARTICIPANTS,
57
+ *      participants: Array<string>
58
+ * }
59
+ */
60
+export const SET_REMOTE_PARTICIPANTS = 'SET_REMOTE_PARTICIPANTS';
61
+
53
 /**
62
 /**
54
  * The type of (redux) action which sets the dimensions of the thumbnails in vertical view.
63
  * The type of (redux) action which sets the dimensions of the thumbnails in vertical view.
55
  *
64
  *

+ 18
- 0
react/features/filmstrip/actions.web.js View File

5
 
5
 
6
 import {
6
 import {
7
     SET_HORIZONTAL_VIEW_DIMENSIONS,
7
     SET_HORIZONTAL_VIEW_DIMENSIONS,
8
+    SET_REMOTE_PARTICIPANTS,
8
     SET_TILE_VIEW_DIMENSIONS,
9
     SET_TILE_VIEW_DIMENSIONS,
9
     SET_VERTICAL_VIEW_DIMENSIONS,
10
     SET_VERTICAL_VIEW_DIMENSIONS,
10
     SET_VISIBLE_REMOTE_PARTICIPANTS,
11
     SET_VISIBLE_REMOTE_PARTICIPANTS,
25
     calculateThumbnailSizeForVerticalView
26
     calculateThumbnailSizeForVerticalView
26
 } from './functions';
27
 } from './functions';
27
 
28
 
29
+/**
30
+ * Sets the list of the reordered remote participants based on which the visible participants in the filmstrip will be
31
+ * determined.
32
+ *
33
+ * @param {Array<string>} participants - The list of the remote participant endpoint IDs.
34
+ * @returns {{
35
+        type: SET_REMOTE_PARTICIPANTS,
36
+        participants: Array<string>
37
+    }}
38
+ */
39
+export function setRemoteParticipants(participants: Array<string>) {
40
+    return {
41
+        type: SET_REMOTE_PARTICIPANTS,
42
+        participants
43
+    };
44
+}
45
+
28
 /**
46
 /**
29
  * Sets the dimensions of the tile view grid.
47
  * Sets the dimensions of the tile view grid.
30
  *
48
  *

+ 7
- 4
react/features/filmstrip/components/web/Filmstrip.js View File

269
             return `empty-${index}`;
269
             return `empty-${index}`;
270
         }
270
         }
271
 
271
 
272
-        if (index === _remoteParticipantsLength) {
272
+        if (index === 0) {
273
             return 'local';
273
             return 'local';
274
         }
274
         }
275
 
275
 
276
-        return _remoteParticipants[index];
276
+        return _remoteParticipants[index - 1];
277
     }
277
     }
278
 
278
 
279
     _onListItemsRendered: Object => void;
279
     _onListItemsRendered: Object => void;
287
     _onListItemsRendered({ visibleStartIndex, visibleStopIndex }) {
287
     _onListItemsRendered({ visibleStartIndex, visibleStopIndex }) {
288
         const { dispatch } = this.props;
288
         const { dispatch } = this.props;
289
 
289
 
290
-        dispatch(setVisibleRemoteParticipants(visibleStartIndex, visibleStopIndex));
290
+        dispatch(setVisibleRemoteParticipants(visibleStartIndex, visibleStopIndex + 1));
291
     }
291
     }
292
 
292
 
293
     _onGridItemsRendered: Object => void;
293
     _onGridItemsRendered: Object => void;
305
         visibleRowStopIndex
305
         visibleRowStopIndex
306
     }) {
306
     }) {
307
         const { _columns, dispatch } = this.props;
307
         const { _columns, dispatch } = this.props;
308
-        const startIndex = (visibleRowStartIndex * _columns) + visibleColumnStartIndex;
308
+        let startIndex = (visibleRowStartIndex * _columns) + visibleColumnStartIndex;
309
         const endIndex = (visibleRowStopIndex * _columns) + visibleColumnStopIndex;
309
         const endIndex = (visibleRowStopIndex * _columns) + visibleColumnStopIndex;
310
 
310
 
311
+        // In tile view, the start index needs to be offset by 1 because the first participant is the local
312
+        // participant.
313
+        startIndex = startIndex > 0 ? startIndex - 1 : 0;
311
         dispatch(setVisibleRemoteParticipants(startIndex, endIndex));
314
         dispatch(setVisibleRemoteParticipants(startIndex, endIndex));
312
     }
315
     }
313
 
316
 

+ 3
- 2
react/features/filmstrip/components/web/ThumbnailWrapper.js View File

126
             return {};
126
             return {};
127
         }
127
         }
128
 
128
 
129
-        if (index === remoteParticipantsLength) {
129
+        // Make the local participant as the first thumbnail (top left corner) in tile view.
130
+        if (index === 0) {
130
             return {
131
             return {
131
                 _participantID: 'local',
132
                 _participantID: 'local',
132
                 _horizontalOffset: horizontalOffset
133
                 _horizontalOffset: horizontalOffset
134
         }
135
         }
135
 
136
 
136
         return {
137
         return {
137
-            _participantID: remoteParticipants[index],
138
+            _participantID: remoteParticipants[index - 1],
138
             _horizontalOffset: horizontalOffset
139
             _horizontalOffset: horizontalOffset
139
         };
140
         };
140
     }
141
     }

+ 34
- 0
react/features/filmstrip/functions.web.js View File

16
     isRemoteTrackMuted
16
     isRemoteTrackMuted
17
 } from '../base/tracks/functions';
17
 } from '../base/tracks/functions';
18
 
18
 
19
+import { setRemoteParticipants } from './actions.web';
19
 import {
20
 import {
20
     ASPECT_RATIO_BREAKPOINT,
21
     ASPECT_RATIO_BREAKPOINT,
21
     DISPLAY_AVATAR,
22
     DISPLAY_AVATAR,
265
     // check hovering and change state to avatar with name
266
     // check hovering and change state to avatar with name
266
     return isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
267
     return isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
267
 }
268
 }
269
+
270
+/**
271
+ * Computes the reorderd list of the remote participants.
272
+ *
273
+ * @param {*} store - The redux store.
274
+ * @returns {void}
275
+ * @private
276
+ */
277
+export function updateRemoteParticipants(store: Object) {
278
+    const state = store.getState();
279
+    const { fakeParticipants, sortedRemoteParticipants, speakersList } = state['features/base/participants'];
280
+    const { remoteScreenShares } = state['features/video-layout'];
281
+    const screenShares = (remoteScreenShares || []).slice();
282
+    let speakers = (speakersList || []).slice();
283
+    const remoteParticipants = new Map(sortedRemoteParticipants);
284
+    const sharedVideos = fakeParticipants ? Array.from(fakeParticipants.keys()) : [];
285
+
286
+    for (const screenshare of screenShares) {
287
+        remoteParticipants.delete(screenshare);
288
+        speakers = speakers.filter(speaker => speaker !== screenshare);
289
+    }
290
+    for (const sharedVideo of sharedVideos) {
291
+        remoteParticipants.delete(sharedVideo);
292
+        speakers = speakers.filter(speaker => speaker !== sharedVideo);
293
+    }
294
+    for (const speaker of speakers) {
295
+        remoteParticipants.delete(speaker);
296
+    }
297
+    const reorderedParticipants
298
+        = [ ...screenShares.reverse(), ...sharedVideos, ...speakers, ...Array.from(remoteParticipants.keys()) ];
299
+
300
+    store.dispatch(setRemoteParticipants(reorderedParticipants));
301
+}

+ 35
- 2
react/features/filmstrip/middleware.web.js View File

1
 // @flow
1
 // @flow
2
 
2
 
3
 import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
3
 import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
4
+import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
4
 import { MiddlewareRegistry } from '../base/redux';
5
 import { MiddlewareRegistry } from '../base/redux';
5
 import { CLIENT_RESIZED } from '../base/responsive-ui';
6
 import { CLIENT_RESIZED } from '../base/responsive-ui';
6
 import { SETTINGS_UPDATED } from '../base/settings';
7
 import { SETTINGS_UPDATED } from '../base/settings';
9
     LAYOUTS
10
     LAYOUTS
10
 } from '../video-layout';
11
 } from '../video-layout';
11
 
12
 
12
-import { setHorizontalViewDimensions, setTileViewDimensions, setVerticalViewDimensions } from './actions.web';
13
-
13
+import {
14
+    setHorizontalViewDimensions,
15
+    setRemoteParticipants,
16
+    setTileViewDimensions,
17
+    setVerticalViewDimensions
18
+} from './actions.web';
19
+import { updateRemoteParticipants } from './functions.web';
14
 import './subscriber.web';
20
 import './subscriber.web';
15
 
21
 
16
 /**
22
 /**
41
         }
47
         }
42
         break;
48
         break;
43
     }
49
     }
50
+    case PARTICIPANT_JOINED: {
51
+        updateRemoteParticipants(store);
52
+        break;
53
+    }
54
+    case PARTICIPANT_LEFT: {
55
+        _updateRemoteParticipantsOnLeave(store, action.participant?.id);
56
+        break;
57
+    }
44
     case SETTINGS_UPDATED: {
58
     case SETTINGS_UPDATED: {
45
         if (typeof action.settings?.localFlipX === 'boolean') {
59
         if (typeof action.settings?.localFlipX === 'boolean') {
46
             // TODO: This needs to be removed once the large video is Reactified.
60
             // TODO: This needs to be removed once the large video is Reactified.
53
     return result;
67
     return result;
54
 });
68
 });
55
 
69
 
70
+/**
71
+ * Private helper to calculate the reordered list of remote participants when a participant leaves.
72
+ *
73
+ * @param {*} store - The redux store.
74
+ * @param {string} participantId - The endpoint id of the participant leaving the call.
75
+ * @returns {void}
76
+ * @private
77
+ */
78
+function _updateRemoteParticipantsOnLeave(store, participantId = null) {
79
+    if (!participantId) {
80
+        return;
81
+    }
82
+    const state = store.getState();
83
+    const { remoteParticipants } = state['features/filmstrip'];
84
+    const reorderedParticipants = new Set(remoteParticipants);
85
+
86
+    reorderedParticipants.delete(participantId)
87
+        && store.dispatch(setRemoteParticipants(Array.from(reorderedParticipants)));
88
+}

+ 20
- 45
react/features/filmstrip/reducer.js View File

1
 // @flow
1
 // @flow
2
 
2
 
3
-import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
3
+import { PARTICIPANT_LEFT } from '../base/participants';
4
 import { ReducerRegistry } from '../base/redux';
4
 import { ReducerRegistry } from '../base/redux';
5
 
5
 
6
 import {
6
 import {
7
     SET_FILMSTRIP_ENABLED,
7
     SET_FILMSTRIP_ENABLED,
8
     SET_FILMSTRIP_VISIBLE,
8
     SET_FILMSTRIP_VISIBLE,
9
     SET_HORIZONTAL_VIEW_DIMENSIONS,
9
     SET_HORIZONTAL_VIEW_DIMENSIONS,
10
+    SET_REMOTE_PARTICIPANTS,
10
     SET_TILE_VIEW_DIMENSIONS,
11
     SET_TILE_VIEW_DIMENSIONS,
11
     SET_VERTICAL_VIEW_DIMENSIONS,
12
     SET_VERTICAL_VIEW_DIMENSIONS,
12
     SET_VISIBLE_REMOTE_PARTICIPANTS,
13
     SET_VISIBLE_REMOTE_PARTICIPANTS,
40
     /**
41
     /**
41
      * The ordered IDs of the remote participants displayed in the filmstrip.
42
      * The ordered IDs of the remote participants displayed in the filmstrip.
42
      *
43
      *
43
-     * NOTE: Currently the order will match the one from the base/participants array. But this is good initial step for
44
-     * reordering the remote participants.
44
+     * @public
45
+     * @type {Array<string>}
45
      */
46
      */
46
     remoteParticipants: [],
47
     remoteParticipants: [],
47
 
48
 
78
     visibleParticipantsEndIndex: 0,
79
     visibleParticipantsEndIndex: 0,
79
 
80
 
80
     /**
81
     /**
81
-     * The visible participants in the filmstrip.
82
+     * The start index in the remote participants array that is visible in the filmstrip.
82
      *
83
      *
83
      * @public
84
      * @public
84
-     * @type {Array<string>}
85
+     * @type {number}
85
      */
86
      */
86
-    visibleParticipants: [],
87
-
87
+    visibleParticipantsStartIndex: 0,
88
 
88
 
89
     /**
89
     /**
90
-     * The start index in the remote participants array that is visible in the filmstrip.
90
+     * The visible remote participants in the filmstrip.
91
      *
91
      *
92
      * @public
92
      * @public
93
-     * @type {number}
93
+     * @type {Set<string>}
94
      */
94
      */
95
-    visibleParticipantsStartIndex: 0
95
+    visibleRemoteParticipants: new Set()
96
 };
96
 };
97
 
97
 
98
 ReducerRegistry.register(
98
 ReducerRegistry.register(
116
                 ...state,
116
                 ...state,
117
                 horizontalViewDimensions: action.dimensions
117
                 horizontalViewDimensions: action.dimensions
118
             };
118
             };
119
+        case SET_REMOTE_PARTICIPANTS: {
120
+            const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state;
121
+
122
+            state.remoteParticipants = action.participants;
123
+            state.visibleRemoteParticipants = new Set(state.remoteParticipants.slice(startIndex, endIndex));
124
+
125
+            return { ...state };
126
+        }
119
         case SET_TILE_VIEW_DIMENSIONS:
127
         case SET_TILE_VIEW_DIMENSIONS:
120
             return {
128
             return {
121
                 ...state,
129
                 ...state,
138
                     [action.participantId]: action.volume
146
                     [action.participantId]: action.volume
139
                 }
147
                 }
140
             };
148
             };
141
-        case SET_VISIBLE_REMOTE_PARTICIPANTS:
149
+        case SET_VISIBLE_REMOTE_PARTICIPANTS: {
142
             return {
150
             return {
143
                 ...state,
151
                 ...state,
144
                 visibleParticipantsStartIndex: action.startIndex,
152
                 visibleParticipantsStartIndex: action.startIndex,
145
                 visibleParticipantsEndIndex: action.endIndex,
153
                 visibleParticipantsEndIndex: action.endIndex,
146
-                visibleParticipants: state.remoteParticipants.slice(action.startIndex, action.endIndex + 1)
154
+                visibleRemoteParticipants: new Set(state.remoteParticipants.slice(action.startIndex, action.endIndex))
147
             };
155
             };
148
-        case PARTICIPANT_JOINED: {
149
-            const { id, local } = action.participant;
150
-
151
-            if (!local) {
152
-                state.remoteParticipants = [ ...state.remoteParticipants, id ];
153
-
154
-                const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state;
155
-
156
-                if (state.remoteParticipants.length - 1 <= endIndex) {
157
-                    state.visibleParticipants = state.remoteParticipants.slice(startIndex, endIndex + 1);
158
-                }
159
-            }
160
-
161
-            return state;
162
         }
156
         }
163
         case PARTICIPANT_LEFT: {
157
         case PARTICIPANT_LEFT: {
164
             const { id, local } = action.participant;
158
             const { id, local } = action.participant;
166
             if (local) {
160
             if (local) {
167
                 return state;
161
                 return state;
168
             }
162
             }
169
-
170
-            let removedParticipantIndex = 0;
171
-
172
-            state.remoteParticipants = state.remoteParticipants.filter((participantId, index) => {
173
-                if (participantId === id) {
174
-                    removedParticipantIndex = index;
175
-
176
-                    return false;
177
-                }
178
-
179
-                return true;
180
-            });
181
-
182
-            const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state;
183
-
184
-            if (removedParticipantIndex >= startIndex && removedParticipantIndex <= endIndex) {
185
-                state.visibleParticipants = state.remoteParticipants.slice(startIndex, endIndex + 1);
186
-            }
187
-
188
             delete state.participantsVolume[id];
163
             delete state.participantsVolume[id];
189
 
164
 
190
             return state;
165
             return state;

+ 39
- 1
react/features/filmstrip/subscriber.web.js View File

8
 import { setOverflowDrawer } from '../toolbox/actions.web';
8
 import { setOverflowDrawer } from '../toolbox/actions.web';
9
 import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
9
 import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
10
 
10
 
11
-import { setHorizontalViewDimensions, setTileViewDimensions, setVerticalViewDimensions } from './actions.web';
11
+import {
12
+    setHorizontalViewDimensions,
13
+    setTileViewDimensions,
14
+    setVerticalViewDimensions
15
+} from './actions.web';
12
 import {
16
 import {
13
     ASPECT_RATIO_BREAKPOINT,
17
     ASPECT_RATIO_BREAKPOINT,
14
     DISPLAY_DRAWER_THRESHOLD,
18
     DISPLAY_DRAWER_THRESHOLD,
15
     SINGLE_COLUMN_BREAKPOINT,
19
     SINGLE_COLUMN_BREAKPOINT,
16
     TWO_COLUMN_BREAKPOINT
20
     TWO_COLUMN_BREAKPOINT
17
 } from './constants';
21
 } from './constants';
22
+import { updateRemoteParticipants } from './functions.web';
18
 
23
 
19
 /**
24
 /**
20
  * Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles.
25
  * Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles.
153
             store.dispatch(setTileViewDimensions(gridDimensions));
158
             store.dispatch(setTileViewDimensions(gridDimensions));
154
         }
159
         }
155
     });
160
     });
161
+
162
+/**
163
+ * Listens for changes to the screensharing status of the remote participants to recompute the reordered list of the
164
+ * remote endpoints.
165
+ */
166
+StateListenerRegistry.register(
167
+    /* selector */ state => state['features/video-layout'].remoteScreenShares,
168
+    /* listener */ (remoteScreenShares, store) => updateRemoteParticipants(store));
169
+
170
+/**
171
+ * Listens for changes to the dominant speaker to recompute the reordered list of the remote endpoints.
172
+ */
173
+StateListenerRegistry.register(
174
+    /* selector */ state => state['features/base/participants'].dominantSpeaker,
175
+    /* listener */ (dominantSpeaker, store) => _reorderDominantSpeakers(store));
176
+
177
+/**
178
+ * Private helper function that reorders the remote participants based on dominant speaker changes.
179
+ *
180
+ * @param {*} store - The redux store.
181
+ * @returns {void}
182
+ * @private
183
+ */
184
+function _reorderDominantSpeakers(store) {
185
+    const state = store.getState();
186
+    const { dominantSpeaker, local } = state['features/base/participants'];
187
+    const { visibleRemoteParticipants } = state['features/filmstrip'];
188
+
189
+    // Reorder the participants if the new dominant speaker is currently not visible.
190
+    if (dominantSpeaker !== local?.id && !visibleRemoteParticipants.has(dominantSpeaker)) {
191
+        updateRemoteParticipants(store);
192
+    }
193
+}

+ 9
- 9
react/features/video-quality/subscriber.js View File

22
  * scrolling through the thumbnails prompting updates to the selected endpoints.
22
  * scrolling through the thumbnails prompting updates to the selected endpoints.
23
  */
23
  */
24
 StateListenerRegistry.register(
24
 StateListenerRegistry.register(
25
-    /* selector */ state => state['features/filmstrip'].visibleParticipants,
26
-    /* listener */ debounce((visibleParticipants, store) => {
25
+    /* selector */ state => state['features/filmstrip'].visibleRemoteParticipants,
26
+    /* listener */ debounce((visibleRemoteParticipants, store) => {
27
         _updateReceiverVideoConstraints(store);
27
         _updateReceiverVideoConstraints(store);
28
     }, 100));
28
     }, 100));
29
 
29
 
191
     const { maxReceiverVideoQuality, preferredVideoQuality } = state['features/video-quality'];
191
     const { maxReceiverVideoQuality, preferredVideoQuality } = state['features/video-quality'];
192
     const { participantId: largeVideoParticipantId } = state['features/large-video'];
192
     const { participantId: largeVideoParticipantId } = state['features/large-video'];
193
     const maxFrameHeight = Math.min(maxReceiverVideoQuality, preferredVideoQuality);
193
     const maxFrameHeight = Math.min(maxReceiverVideoQuality, preferredVideoQuality);
194
-    let { visibleParticipants } = state['features/filmstrip'];
194
+    let { visibleRemoteParticipants } = state['features/filmstrip'];
195
 
195
 
196
     // TODO: implement this on mobile.
196
     // TODO: implement this on mobile.
197
     if (navigator.product === 'ReactNative') {
197
     if (navigator.product === 'ReactNative') {
198
-        visibleParticipants = Array.from(state['features/base/participants'].remote.keys());
198
+        visibleRemoteParticipants = new Set(Array.from(state['features/base/participants'].remote.keys()));
199
     }
199
     }
200
 
200
 
201
     const receiverConstraints = {
201
     const receiverConstraints = {
208
 
208
 
209
     // Tile view.
209
     // Tile view.
210
     if (shouldDisplayTileView(state)) {
210
     if (shouldDisplayTileView(state)) {
211
-        if (!visibleParticipants?.length) {
211
+        if (!visibleRemoteParticipants?.size) {
212
             return;
212
             return;
213
         }
213
         }
214
 
214
 
215
-        visibleParticipants.forEach(participantId => {
215
+        visibleRemoteParticipants.forEach(participantId => {
216
             receiverConstraints.constraints[participantId] = { 'maxHeight': maxFrameHeight };
216
             receiverConstraints.constraints[participantId] = { 'maxHeight': maxFrameHeight };
217
         });
217
         });
218
 
218
 
219
     // Stage view.
219
     // Stage view.
220
     } else {
220
     } else {
221
-        if (!visibleParticipants?.length && !largeVideoParticipantId) {
221
+        if (!visibleRemoteParticipants?.size && !largeVideoParticipantId) {
222
             return;
222
             return;
223
         }
223
         }
224
 
224
 
225
-        if (visibleParticipants?.length > 0) {
226
-            visibleParticipants.forEach(participantId => {
225
+        if (visibleRemoteParticipants?.size > 0) {
226
+            visibleRemoteParticipants.forEach(participantId => {
227
                 receiverConstraints.constraints[participantId] = { 'maxHeight': VIDEO_QUALITY_LEVELS.LOW };
227
                 receiverConstraints.constraints[participantId] = { 'maxHeight': VIDEO_QUALITY_LEVELS.LOW };
228
             });
228
             });
229
         }
229
         }

Loading…
Cancel
Save