2020-01-24 16:28:47 +00:00
|
|
|
// @flow
|
|
|
|
|
2022-04-07 08:31:53 +00:00
|
|
|
import { batch } from 'react-redux';
|
|
|
|
|
2021-01-21 20:46:47 +00:00
|
|
|
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
2022-03-29 08:45:09 +00:00
|
|
|
import {
|
|
|
|
DOMINANT_SPEAKER_CHANGED,
|
|
|
|
getDominantSpeakerParticipant,
|
|
|
|
getLocalParticipant,
|
2022-04-05 15:17:25 +00:00
|
|
|
getLocalScreenShareParticipant,
|
2022-03-29 08:45:09 +00:00
|
|
|
PARTICIPANT_JOINED,
|
|
|
|
PARTICIPANT_LEFT
|
|
|
|
} from '../base/participants';
|
2020-01-24 16:28:47 +00:00
|
|
|
import { MiddlewareRegistry } from '../base/redux';
|
|
|
|
import { CLIENT_RESIZED } from '../base/responsive-ui';
|
2021-01-21 20:46:47 +00:00
|
|
|
import { SETTINGS_UPDATED } from '../base/settings';
|
2020-01-24 16:28:47 +00:00
|
|
|
import {
|
|
|
|
getCurrentLayout,
|
2022-03-29 08:45:09 +00:00
|
|
|
LAYOUTS,
|
|
|
|
setTileView
|
2020-01-24 16:28:47 +00:00
|
|
|
} from '../video-layout';
|
|
|
|
|
2022-04-07 08:31:53 +00:00
|
|
|
import {
|
|
|
|
ADD_STAGE_PARTICIPANT,
|
2022-04-12 13:19:10 +00:00
|
|
|
CLEAR_STAGE_PARTICIPANTS,
|
2022-04-07 08:31:53 +00:00
|
|
|
REMOVE_STAGE_PARTICIPANT,
|
|
|
|
SET_MAX_STAGE_PARTICIPANTS,
|
2022-04-12 06:57:01 +00:00
|
|
|
SET_USER_FILMSTRIP_WIDTH,
|
|
|
|
TOGGLE_PIN_STAGE_PARTICIPANT
|
2022-04-07 08:31:53 +00:00
|
|
|
} from './actionTypes';
|
2021-08-18 22:34:01 +00:00
|
|
|
import {
|
2022-03-29 08:45:09 +00:00
|
|
|
addStageParticipant,
|
|
|
|
removeStageParticipant,
|
2022-06-29 13:59:49 +00:00
|
|
|
setFilmstripHeight,
|
2022-02-24 12:20:37 +00:00
|
|
|
setFilmstripWidth,
|
2022-03-31 09:13:33 +00:00
|
|
|
setStageParticipants
|
2021-08-23 23:02:41 +00:00
|
|
|
} from './actions';
|
2022-03-29 08:45:09 +00:00
|
|
|
import {
|
|
|
|
ACTIVE_PARTICIPANT_TIMEOUT,
|
|
|
|
DEFAULT_FILMSTRIP_WIDTH,
|
|
|
|
MAX_ACTIVE_PARTICIPANTS,
|
2022-06-29 13:59:49 +00:00
|
|
|
MIN_STAGE_VIEW_HEIGHT,
|
|
|
|
MIN_STAGE_VIEW_WIDTH,
|
|
|
|
TOP_FILMSTRIP_HEIGHT
|
2022-03-29 08:45:09 +00:00
|
|
|
} from './constants';
|
|
|
|
import {
|
|
|
|
isFilmstripResizable,
|
|
|
|
updateRemoteParticipants,
|
2022-04-12 13:19:10 +00:00
|
|
|
updateRemoteParticipantsOnLeave,
|
|
|
|
getActiveParticipantsIds,
|
|
|
|
getPinnedActiveParticipants,
|
|
|
|
isStageFilmstripAvailable
|
2022-03-29 08:45:09 +00:00
|
|
|
} from './functions';
|
2021-08-23 23:02:41 +00:00
|
|
|
import './subscriber';
|
2022-03-29 08:45:09 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Map of timers.
|
|
|
|
*
|
|
|
|
* @type {Map}
|
|
|
|
*/
|
|
|
|
const timers = new Map();
|
2020-06-04 14:09:13 +00:00
|
|
|
|
2020-01-24 16:28:47 +00:00
|
|
|
/**
|
|
|
|
* The middleware of the feature Filmstrip.
|
|
|
|
*/
|
|
|
|
MiddlewareRegistry.register(store => next => action => {
|
2021-09-10 17:37:05 +00:00
|
|
|
if (action.type === PARTICIPANT_LEFT) {
|
|
|
|
// This has to be executed before we remove the participant from features/base/participants state in order to
|
|
|
|
// remove the related thumbnail component before we need to re-render it. If we do this after next()
|
|
|
|
// we will be in sitation where the participant exists in the remoteParticipants array in features/filmstrip
|
|
|
|
// but doesn't exist in features/base/participants state which will lead to rendering a thumbnail for
|
|
|
|
// non-existing participant.
|
|
|
|
updateRemoteParticipantsOnLeave(store, action.participant?.id);
|
|
|
|
}
|
|
|
|
|
2022-03-29 08:45:09 +00:00
|
|
|
let result;
|
2020-01-24 16:28:47 +00:00
|
|
|
|
|
|
|
switch (action.type) {
|
|
|
|
case CLIENT_RESIZED: {
|
|
|
|
const state = store.getState();
|
2022-02-24 12:20:37 +00:00
|
|
|
|
|
|
|
if (isFilmstripResizable(state)) {
|
2022-06-29 13:59:49 +00:00
|
|
|
const { width: filmstripWidth, topPanelHeight } = state['features/filmstrip'];
|
|
|
|
const { clientWidth, clientHeight } = action;
|
|
|
|
let height, width;
|
2022-02-24 12:20:37 +00:00
|
|
|
|
|
|
|
if (filmstripWidth.current > clientWidth - MIN_STAGE_VIEW_WIDTH) {
|
|
|
|
width = Math.max(clientWidth - MIN_STAGE_VIEW_WIDTH, DEFAULT_FILMSTRIP_WIDTH);
|
|
|
|
} else {
|
|
|
|
width = Math.min(clientWidth - MIN_STAGE_VIEW_WIDTH, filmstripWidth.userSet);
|
|
|
|
}
|
|
|
|
if (width !== filmstripWidth.current) {
|
|
|
|
store.dispatch(setFilmstripWidth(width));
|
|
|
|
}
|
2022-06-29 13:59:49 +00:00
|
|
|
|
|
|
|
if (topPanelHeight.current > clientHeight - MIN_STAGE_VIEW_HEIGHT) {
|
|
|
|
height = Math.max(clientHeight - MIN_STAGE_VIEW_HEIGHT, TOP_FILMSTRIP_HEIGHT);
|
|
|
|
} else {
|
|
|
|
height = Math.min(clientHeight - MIN_STAGE_VIEW_HEIGHT, topPanelHeight.userSet);
|
|
|
|
}
|
|
|
|
if (height !== topPanelHeight.current) {
|
|
|
|
store.dispatch(setFilmstripHeight(height));
|
|
|
|
}
|
2022-02-24 12:20:37 +00:00
|
|
|
}
|
2020-01-24 16:28:47 +00:00
|
|
|
break;
|
|
|
|
}
|
2021-08-18 22:34:01 +00:00
|
|
|
case PARTICIPANT_JOINED: {
|
2022-03-29 08:45:09 +00:00
|
|
|
result = next(action);
|
2022-04-04 18:57:58 +00:00
|
|
|
if (action.participant?.isLocalScreenShare) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2021-08-19 21:56:45 +00:00
|
|
|
updateRemoteParticipants(store, action.participant?.id);
|
2021-08-18 22:34:01 +00:00
|
|
|
break;
|
|
|
|
}
|
2021-01-21 20:46:47 +00:00
|
|
|
case SETTINGS_UPDATED: {
|
|
|
|
if (typeof action.settings?.localFlipX === 'boolean') {
|
|
|
|
// TODO: This needs to be removed once the large video is Reactified.
|
|
|
|
VideoLayout.onLocalFlipXChanged();
|
2020-01-24 16:28:47 +00:00
|
|
|
}
|
2022-03-29 08:45:09 +00:00
|
|
|
if (action.settings?.disableSelfView) {
|
|
|
|
const state = store.getState();
|
|
|
|
const local = getLocalParticipant(state);
|
2022-04-05 15:17:25 +00:00
|
|
|
const localScreenShare = getLocalScreenShareParticipant(state);
|
2022-03-29 08:45:09 +00:00
|
|
|
const activeParticipantsIds = getActiveParticipantsIds(state);
|
|
|
|
|
|
|
|
if (activeParticipantsIds.find(id => id === local.id)) {
|
|
|
|
store.dispatch(removeStageParticipant(local.id));
|
|
|
|
}
|
2022-04-05 15:17:25 +00:00
|
|
|
|
2022-04-06 13:35:18 +00:00
|
|
|
if (localScreenShare) {
|
|
|
|
if (activeParticipantsIds.find(id => id === localScreenShare.id)) {
|
|
|
|
store.dispatch(removeStageParticipant(localScreenShare.id));
|
|
|
|
}
|
2022-04-05 15:17:25 +00:00
|
|
|
}
|
2022-03-29 08:45:09 +00:00
|
|
|
}
|
2020-01-24 16:28:47 +00:00
|
|
|
break;
|
|
|
|
}
|
2022-02-24 12:20:37 +00:00
|
|
|
case SET_USER_FILMSTRIP_WIDTH: {
|
|
|
|
VideoLayout.refreshLayout();
|
2022-03-29 08:45:09 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case ADD_STAGE_PARTICIPANT: {
|
|
|
|
const { dispatch, getState } = store;
|
|
|
|
const { participantId, pinned } = action;
|
|
|
|
const state = getState();
|
2022-04-07 08:31:53 +00:00
|
|
|
const { activeParticipants, maxStageParticipants } = state['features/filmstrip'];
|
2022-03-29 08:45:09 +00:00
|
|
|
let queue;
|
|
|
|
|
|
|
|
if (activeParticipants.find(p => p.participantId === participantId)) {
|
|
|
|
queue = activeParticipants.filter(p => p.participantId !== participantId);
|
|
|
|
queue.push({
|
|
|
|
participantId,
|
|
|
|
pinned
|
|
|
|
});
|
|
|
|
const tid = timers.get(participantId);
|
|
|
|
|
|
|
|
clearTimeout(tid);
|
2022-04-12 06:57:01 +00:00
|
|
|
timers.delete(participantId);
|
2022-04-07 08:31:53 +00:00
|
|
|
} else if (activeParticipants.length < maxStageParticipants) {
|
2022-03-29 08:45:09 +00:00
|
|
|
queue = [ ...activeParticipants, {
|
|
|
|
participantId,
|
|
|
|
pinned
|
|
|
|
} ];
|
|
|
|
} else {
|
|
|
|
const notPinnedIndex = activeParticipants.findIndex(p => !p.pinned);
|
|
|
|
|
|
|
|
if (notPinnedIndex === -1) {
|
|
|
|
if (pinned) {
|
|
|
|
queue = [ ...activeParticipants, {
|
|
|
|
participantId,
|
|
|
|
pinned
|
|
|
|
} ];
|
|
|
|
queue.shift();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
queue = [ ...activeParticipants, {
|
|
|
|
participantId,
|
|
|
|
pinned
|
|
|
|
} ];
|
|
|
|
queue.splice(notPinnedIndex, 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-06 21:32:09 +00:00
|
|
|
// If queue is undefined we haven't made any changes to the active participants. This will mostly happen
|
|
|
|
// if the participant that we are trying to add is not pinned and all slots are currently taken by pinned
|
|
|
|
// participants.
|
|
|
|
// IMPORTANT: setting active participants to undefined will crash jitsi-meet.
|
|
|
|
if (typeof queue !== 'undefined') {
|
|
|
|
dispatch(setStageParticipants(queue));
|
|
|
|
if (!pinned) {
|
|
|
|
const timeoutId = setTimeout(() => dispatch(removeStageParticipant(participantId)),
|
|
|
|
ACTIVE_PARTICIPANT_TIMEOUT);
|
|
|
|
|
|
|
|
timers.set(participantId, timeoutId);
|
|
|
|
}
|
2022-03-29 08:45:09 +00:00
|
|
|
}
|
2022-05-06 21:32:09 +00:00
|
|
|
|
2022-03-29 08:45:09 +00:00
|
|
|
if (getCurrentLayout(state) === LAYOUTS.TILE_VIEW) {
|
|
|
|
dispatch(setTileView(false));
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case REMOVE_STAGE_PARTICIPANT: {
|
|
|
|
const state = store.getState();
|
|
|
|
const { participantId } = action;
|
|
|
|
const tid = timers.get(participantId);
|
|
|
|
|
|
|
|
clearTimeout(tid);
|
|
|
|
timers.delete(participantId);
|
|
|
|
const dominant = getDominantSpeakerParticipant(state);
|
|
|
|
|
|
|
|
if (participantId === dominant?.id) {
|
|
|
|
const timeoutId = setTimeout(() => store.dispatch(removeStageParticipant(participantId)),
|
|
|
|
ACTIVE_PARTICIPANT_TIMEOUT);
|
|
|
|
|
|
|
|
timers.set(participantId, timeoutId);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case DOMINANT_SPEAKER_CHANGED: {
|
|
|
|
const { id } = action.participant;
|
|
|
|
const state = store.getState();
|
2022-04-12 13:19:10 +00:00
|
|
|
const stageFilmstrip = isStageFilmstripAvailable(state);
|
2022-04-05 13:00:32 +00:00
|
|
|
const local = getLocalParticipant(state);
|
2022-04-12 13:19:10 +00:00
|
|
|
const currentLayout = getCurrentLayout(state);
|
2022-04-05 13:00:32 +00:00
|
|
|
|
2022-04-12 13:19:10 +00:00
|
|
|
if (id === local.id || currentLayout === LAYOUTS.TILE_VIEW) {
|
2022-04-05 13:00:32 +00:00
|
|
|
break;
|
|
|
|
}
|
2022-03-29 08:45:09 +00:00
|
|
|
|
2022-04-12 13:19:10 +00:00
|
|
|
if (stageFilmstrip) {
|
2022-04-05 13:00:32 +00:00
|
|
|
const isPinned = getPinnedActiveParticipants(state).some(p => p.participantId === id);
|
|
|
|
|
|
|
|
store.dispatch(addStageParticipant(id, Boolean(isPinned)));
|
2022-03-29 08:45:09 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case PARTICIPANT_LEFT: {
|
2022-04-12 06:57:01 +00:00
|
|
|
const state = store.getState();
|
2022-03-29 08:45:09 +00:00
|
|
|
const { id } = action.participant;
|
2022-04-12 06:57:01 +00:00
|
|
|
const activeParticipantsIds = getActiveParticipantsIds(state);
|
2022-03-29 08:45:09 +00:00
|
|
|
|
|
|
|
if (activeParticipantsIds.find(pId => pId === id)) {
|
2022-04-12 06:57:01 +00:00
|
|
|
const tid = timers.get(id);
|
|
|
|
const { activeParticipants } = state['features/filmstrip'];
|
|
|
|
|
|
|
|
clearTimeout(tid);
|
|
|
|
timers.delete(id);
|
|
|
|
store.dispatch(setStageParticipants(activeParticipants.filter(p => p.participantId !== id)));
|
2022-03-29 08:45:09 +00:00
|
|
|
}
|
|
|
|
break;
|
2022-02-24 12:20:37 +00:00
|
|
|
}
|
2022-04-07 08:31:53 +00:00
|
|
|
case SET_MAX_STAGE_PARTICIPANTS: {
|
|
|
|
const { maxParticipants } = action;
|
|
|
|
const { activeParticipants } = store.getState()['features/filmstrip'];
|
|
|
|
const newMax = Math.min(MAX_ACTIVE_PARTICIPANTS, maxParticipants);
|
|
|
|
|
|
|
|
action.maxParticipants = newMax;
|
|
|
|
|
|
|
|
if (newMax < activeParticipants.length) {
|
|
|
|
const toRemove = activeParticipants.slice(0, activeParticipants.length - newMax);
|
|
|
|
|
|
|
|
batch(() => {
|
|
|
|
toRemove.forEach(p => store.dispatch(removeStageParticipant(p.participantId)));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2022-04-12 06:57:01 +00:00
|
|
|
case TOGGLE_PIN_STAGE_PARTICIPANT: {
|
|
|
|
const { dispatch, getState } = store;
|
|
|
|
const state = getState();
|
|
|
|
const { participantId } = action;
|
|
|
|
const pinnedParticipants = getPinnedActiveParticipants(state);
|
|
|
|
const dominant = getDominantSpeakerParticipant(state);
|
|
|
|
|
|
|
|
if (pinnedParticipants.find(p => p.participantId === participantId)) {
|
|
|
|
if (dominant?.id === participantId) {
|
|
|
|
const { activeParticipants } = state['features/filmstrip'];
|
|
|
|
const queue = activeParticipants.map(p => {
|
|
|
|
if (p.participantId === participantId) {
|
|
|
|
return {
|
|
|
|
participantId,
|
|
|
|
pinned: false
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return p;
|
|
|
|
});
|
|
|
|
|
|
|
|
dispatch(setStageParticipants(queue));
|
|
|
|
} else {
|
|
|
|
dispatch(removeStageParticipant(participantId));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
dispatch(addStageParticipant(participantId, true));
|
|
|
|
}
|
2022-04-12 13:19:10 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case CLEAR_STAGE_PARTICIPANTS: {
|
|
|
|
const activeParticipants = getActiveParticipantsIds(store.getState());
|
|
|
|
|
|
|
|
activeParticipants.forEach(pId => {
|
|
|
|
const tid = timers.get(pId);
|
2022-04-12 06:57:01 +00:00
|
|
|
|
2022-04-12 13:19:10 +00:00
|
|
|
clearTimeout(tid);
|
|
|
|
timers.delete(pId);
|
|
|
|
});
|
2022-04-12 06:57:01 +00:00
|
|
|
}
|
2020-01-24 16:28:47 +00:00
|
|
|
}
|
|
|
|
|
2022-03-29 08:45:09 +00:00
|
|
|
return result ?? next(action);
|
2020-01-24 16:28:47 +00:00
|
|
|
});
|