You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

middleware.web.js 9.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. // @flow
  2. import { batch } from 'react-redux';
  3. import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
  4. import {
  5. DOMINANT_SPEAKER_CHANGED,
  6. getDominantSpeakerParticipant,
  7. getLocalParticipant,
  8. getLocalScreenShareParticipant,
  9. PARTICIPANT_JOINED,
  10. PARTICIPANT_LEFT
  11. } from '../base/participants';
  12. import { MiddlewareRegistry } from '../base/redux';
  13. import { CLIENT_RESIZED } from '../base/responsive-ui';
  14. import { SETTINGS_UPDATED } from '../base/settings';
  15. import {
  16. getCurrentLayout,
  17. LAYOUTS,
  18. setTileView
  19. } from '../video-layout';
  20. import {
  21. ADD_STAGE_PARTICIPANT,
  22. CLEAR_STAGE_PARTICIPANTS,
  23. REMOVE_STAGE_PARTICIPANT,
  24. SET_MAX_STAGE_PARTICIPANTS,
  25. SET_USER_FILMSTRIP_WIDTH,
  26. TOGGLE_PIN_STAGE_PARTICIPANT
  27. } from './actionTypes';
  28. import {
  29. addStageParticipant,
  30. removeStageParticipant,
  31. setFilmstripWidth,
  32. setStageParticipants
  33. } from './actions';
  34. import {
  35. ACTIVE_PARTICIPANT_TIMEOUT,
  36. DEFAULT_FILMSTRIP_WIDTH,
  37. MAX_ACTIVE_PARTICIPANTS,
  38. MIN_STAGE_VIEW_WIDTH
  39. } from './constants';
  40. import {
  41. isFilmstripResizable,
  42. updateRemoteParticipants,
  43. updateRemoteParticipantsOnLeave,
  44. getActiveParticipantsIds,
  45. getPinnedActiveParticipants,
  46. isStageFilmstripAvailable
  47. } from './functions';
  48. import './subscriber';
  49. /**
  50. * Map of timers.
  51. *
  52. * @type {Map}
  53. */
  54. const timers = new Map();
  55. /**
  56. * The middleware of the feature Filmstrip.
  57. */
  58. MiddlewareRegistry.register(store => next => action => {
  59. if (action.type === PARTICIPANT_LEFT) {
  60. // This has to be executed before we remove the participant from features/base/participants state in order to
  61. // remove the related thumbnail component before we need to re-render it. If we do this after next()
  62. // we will be in sitation where the participant exists in the remoteParticipants array in features/filmstrip
  63. // but doesn't exist in features/base/participants state which will lead to rendering a thumbnail for
  64. // non-existing participant.
  65. updateRemoteParticipantsOnLeave(store, action.participant?.id);
  66. }
  67. let result;
  68. switch (action.type) {
  69. case CLIENT_RESIZED: {
  70. const state = store.getState();
  71. if (isFilmstripResizable(state)) {
  72. const { width: filmstripWidth } = state['features/filmstrip'];
  73. const { clientWidth } = action;
  74. let width;
  75. if (filmstripWidth.current > clientWidth - MIN_STAGE_VIEW_WIDTH) {
  76. width = Math.max(clientWidth - MIN_STAGE_VIEW_WIDTH, DEFAULT_FILMSTRIP_WIDTH);
  77. } else {
  78. width = Math.min(clientWidth - MIN_STAGE_VIEW_WIDTH, filmstripWidth.userSet);
  79. }
  80. if (width !== filmstripWidth.current) {
  81. store.dispatch(setFilmstripWidth(width));
  82. }
  83. }
  84. break;
  85. }
  86. case PARTICIPANT_JOINED: {
  87. result = next(action);
  88. if (action.participant?.isLocalScreenShare) {
  89. break;
  90. }
  91. updateRemoteParticipants(store, action.participant?.id);
  92. break;
  93. }
  94. case SETTINGS_UPDATED: {
  95. if (typeof action.settings?.localFlipX === 'boolean') {
  96. // TODO: This needs to be removed once the large video is Reactified.
  97. VideoLayout.onLocalFlipXChanged();
  98. }
  99. if (action.settings?.disableSelfView) {
  100. const state = store.getState();
  101. const local = getLocalParticipant(state);
  102. const localScreenShare = getLocalScreenShareParticipant(state);
  103. const activeParticipantsIds = getActiveParticipantsIds(state);
  104. if (activeParticipantsIds.find(id => id === local.id)) {
  105. store.dispatch(removeStageParticipant(local.id));
  106. }
  107. if (localScreenShare) {
  108. if (activeParticipantsIds.find(id => id === localScreenShare.id)) {
  109. store.dispatch(removeStageParticipant(localScreenShare.id));
  110. }
  111. }
  112. }
  113. break;
  114. }
  115. case SET_USER_FILMSTRIP_WIDTH: {
  116. VideoLayout.refreshLayout();
  117. break;
  118. }
  119. case ADD_STAGE_PARTICIPANT: {
  120. const { dispatch, getState } = store;
  121. const { participantId, pinned } = action;
  122. const state = getState();
  123. const { activeParticipants, maxStageParticipants } = state['features/filmstrip'];
  124. let queue;
  125. if (activeParticipants.find(p => p.participantId === participantId)) {
  126. queue = activeParticipants.filter(p => p.participantId !== participantId);
  127. queue.push({
  128. participantId,
  129. pinned
  130. });
  131. const tid = timers.get(participantId);
  132. clearTimeout(tid);
  133. timers.delete(participantId);
  134. } else if (activeParticipants.length < maxStageParticipants) {
  135. queue = [ ...activeParticipants, {
  136. participantId,
  137. pinned
  138. } ];
  139. } else {
  140. const notPinnedIndex = activeParticipants.findIndex(p => !p.pinned);
  141. if (notPinnedIndex === -1) {
  142. if (pinned) {
  143. queue = [ ...activeParticipants, {
  144. participantId,
  145. pinned
  146. } ];
  147. queue.shift();
  148. }
  149. } else {
  150. queue = [ ...activeParticipants, {
  151. participantId,
  152. pinned
  153. } ];
  154. queue.splice(notPinnedIndex, 1);
  155. }
  156. }
  157. dispatch(setStageParticipants(queue));
  158. if (!pinned) {
  159. const timeoutId = setTimeout(() => dispatch(removeStageParticipant(participantId)),
  160. ACTIVE_PARTICIPANT_TIMEOUT);
  161. timers.set(participantId, timeoutId);
  162. }
  163. if (getCurrentLayout(state) === LAYOUTS.TILE_VIEW) {
  164. dispatch(setTileView(false));
  165. }
  166. break;
  167. }
  168. case REMOVE_STAGE_PARTICIPANT: {
  169. const state = store.getState();
  170. const { participantId } = action;
  171. const tid = timers.get(participantId);
  172. clearTimeout(tid);
  173. timers.delete(participantId);
  174. const dominant = getDominantSpeakerParticipant(state);
  175. if (participantId === dominant?.id) {
  176. const timeoutId = setTimeout(() => store.dispatch(removeStageParticipant(participantId)),
  177. ACTIVE_PARTICIPANT_TIMEOUT);
  178. timers.set(participantId, timeoutId);
  179. return;
  180. }
  181. break;
  182. }
  183. case DOMINANT_SPEAKER_CHANGED: {
  184. const { id } = action.participant;
  185. const state = store.getState();
  186. const stageFilmstrip = isStageFilmstripAvailable(state);
  187. const local = getLocalParticipant(state);
  188. const currentLayout = getCurrentLayout(state);
  189. if (id === local.id || currentLayout === LAYOUTS.TILE_VIEW) {
  190. break;
  191. }
  192. if (stageFilmstrip) {
  193. const isPinned = getPinnedActiveParticipants(state).some(p => p.participantId === id);
  194. store.dispatch(addStageParticipant(id, Boolean(isPinned)));
  195. }
  196. break;
  197. }
  198. case PARTICIPANT_LEFT: {
  199. const state = store.getState();
  200. const { id } = action.participant;
  201. const activeParticipantsIds = getActiveParticipantsIds(state);
  202. if (activeParticipantsIds.find(pId => pId === id)) {
  203. const tid = timers.get(id);
  204. const { activeParticipants } = state['features/filmstrip'];
  205. clearTimeout(tid);
  206. timers.delete(id);
  207. store.dispatch(setStageParticipants(activeParticipants.filter(p => p.participantId !== id)));
  208. }
  209. break;
  210. }
  211. case SET_MAX_STAGE_PARTICIPANTS: {
  212. const { maxParticipants } = action;
  213. const { activeParticipants } = store.getState()['features/filmstrip'];
  214. const newMax = Math.min(MAX_ACTIVE_PARTICIPANTS, maxParticipants);
  215. action.maxParticipants = newMax;
  216. if (newMax < activeParticipants.length) {
  217. const toRemove = activeParticipants.slice(0, activeParticipants.length - newMax);
  218. batch(() => {
  219. toRemove.forEach(p => store.dispatch(removeStageParticipant(p.participantId)));
  220. });
  221. }
  222. break;
  223. }
  224. case TOGGLE_PIN_STAGE_PARTICIPANT: {
  225. const { dispatch, getState } = store;
  226. const state = getState();
  227. const { participantId } = action;
  228. const pinnedParticipants = getPinnedActiveParticipants(state);
  229. const dominant = getDominantSpeakerParticipant(state);
  230. if (pinnedParticipants.find(p => p.participantId === participantId)) {
  231. if (dominant?.id === participantId) {
  232. const { activeParticipants } = state['features/filmstrip'];
  233. const queue = activeParticipants.map(p => {
  234. if (p.participantId === participantId) {
  235. return {
  236. participantId,
  237. pinned: false
  238. };
  239. }
  240. return p;
  241. });
  242. dispatch(setStageParticipants(queue));
  243. } else {
  244. dispatch(removeStageParticipant(participantId));
  245. }
  246. } else {
  247. dispatch(addStageParticipant(participantId, true));
  248. }
  249. break;
  250. }
  251. case CLEAR_STAGE_PARTICIPANTS: {
  252. const activeParticipants = getActiveParticipantsIds(store.getState());
  253. activeParticipants.forEach(pId => {
  254. const tid = timers.get(pId);
  255. clearTimeout(tid);
  256. timers.delete(pId);
  257. });
  258. }
  259. }
  260. return result ?? next(action);
  261. });