Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

middleware.js 7.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. // @flow
  2. import _ from 'lodash';
  3. import { CONFERENCE_JOIN_IN_PROGRESS } from '../base/conference/actionTypes';
  4. import {
  5. PARTICIPANT_LEFT,
  6. getParticipantById,
  7. getPinnedParticipant,
  8. pinParticipant
  9. } from '../base/participants';
  10. import { MiddlewareRegistry } from '../base/redux';
  11. import { updateSettings } from '../base/settings';
  12. import { setFilmstripVisible } from '../filmstrip';
  13. import { addStageParticipant } from '../filmstrip/actions.web';
  14. import { setTileView } from '../video-layout';
  15. import {
  16. setFollowMeModerator,
  17. setFollowMeState
  18. } from './actions';
  19. import { FOLLOW_ME_COMMAND } from './constants';
  20. import { isFollowMeActive } from './functions';
  21. import logger from './logger';
  22. import './subscriber';
  23. declare var APP: Object;
  24. /**
  25. * The timeout after which a follow-me command that has been received will be
  26. * ignored if not consumed.
  27. *
  28. * @type {number} in seconds
  29. * @private
  30. */
  31. const _FOLLOW_ME_RECEIVED_TIMEOUT = 30;
  32. /**
  33. * An instance of a timeout used as a workaround when attempting to pin a
  34. * non-existent particapant, which may be caused by participant join information
  35. * not being received yet.
  36. *
  37. * @type {TimeoutID}
  38. */
  39. let nextOnStageTimeout;
  40. /**
  41. * A count of how many seconds the nextOnStageTimeout has ticked while waiting
  42. * for a participant to be discovered that should be pinned. This variable
  43. * works in conjunction with {@code _FOLLOW_ME_RECEIVED_TIMEOUT} and
  44. * {@code nextOnStageTimeout}.
  45. *
  46. * @type {number}
  47. */
  48. let nextOnStageTimer = 0;
  49. /**
  50. * Represents "Follow Me" feature which enables a moderator to (partially)
  51. * control the user experience/interface (e.g. Filmstrip visibility) of (other)
  52. * non-moderator participant.
  53. */
  54. MiddlewareRegistry.register(store => next => action => {
  55. switch (action.type) {
  56. case CONFERENCE_JOIN_IN_PROGRESS: {
  57. const { conference } = action;
  58. conference.addCommandListener(
  59. FOLLOW_ME_COMMAND, ({ attributes }, id) => {
  60. _onFollowMeCommand(attributes, id, store);
  61. });
  62. break;
  63. }
  64. case PARTICIPANT_LEFT:
  65. if (store.getState()['features/follow-me'].moderator === action.participant.id) {
  66. store.dispatch(setFollowMeModerator());
  67. }
  68. break;
  69. }
  70. return next(action);
  71. });
  72. /**
  73. * Notifies this instance about a "Follow Me" command received by the Jitsi
  74. * conference.
  75. *
  76. * @param {Object} attributes - The attributes carried by the command.
  77. * @param {string} id - The identifier of the participant who issuing the
  78. * command. A notable idiosyncrasy to be mindful of here is that the command
  79. * may be issued by the local participant.
  80. * @param {Object} store - The redux store. Used to calculate and dispatch
  81. * updates.
  82. * @private
  83. * @returns {void}
  84. */
  85. function _onFollowMeCommand(attributes = {}, id, store) {
  86. const state = store.getState();
  87. // We require to know who issued the command because (1) only a
  88. // moderator is allowed to send commands and (2) a command MUST be
  89. // issued by a defined commander.
  90. if (typeof id === 'undefined') {
  91. return;
  92. }
  93. const participantSendingCommand = getParticipantById(state, id);
  94. if (participantSendingCommand) {
  95. // The Command(s) API will send us our own commands and we don't want
  96. // to act upon them.
  97. if (participantSendingCommand.local) {
  98. return;
  99. }
  100. if (participantSendingCommand.role !== 'moderator') {
  101. logger.warn('Received follow-me command not from moderator');
  102. return;
  103. }
  104. } else {
  105. // This is the case of jibri receiving commands from a hidden participant.
  106. const { iAmRecorder } = state['features/base/config'];
  107. const { conference } = state['features/base/conference'];
  108. // As this participant is not stored in redux store we do the checks on the JitsiParticipant from lib-jitsi-meet
  109. const participant = conference.getParticipantById(id);
  110. if (!iAmRecorder || !participant || participant.getRole() !== 'moderator'
  111. || !participant.isHiddenFromRecorder()) {
  112. logger.warn('Something went wrong with follow-me command');
  113. return;
  114. }
  115. }
  116. if (!isFollowMeActive(state)) {
  117. store.dispatch(setFollowMeModerator(id));
  118. }
  119. // just a command that follow me was turned off
  120. if (attributes.off) {
  121. store.dispatch(setFollowMeModerator());
  122. return;
  123. }
  124. const oldState = state['features/follow-me'].state || {};
  125. store.dispatch(setFollowMeState(attributes));
  126. // XMPP will translate all booleans to strings, so explicitly check against
  127. // the string form of the boolean {@code true}.
  128. if (oldState.filmstripVisible !== attributes.filmstripVisible) {
  129. store.dispatch(setFilmstripVisible(attributes.filmstripVisible === 'true'));
  130. }
  131. if (oldState.tileViewEnabled !== attributes.tileViewEnabled) {
  132. store.dispatch(setTileView(attributes.tileViewEnabled === 'true'));
  133. }
  134. // For now gate etherpad checks behind a web-app check to be extra safe
  135. // against calling a web-app global.
  136. if (typeof APP !== 'undefined'
  137. && oldState.sharedDocumentVisible !== attributes.sharedDocumentVisible) {
  138. const isEtherpadVisible = attributes.sharedDocumentVisible === 'true';
  139. const documentManager = APP.UI.getSharedDocumentManager();
  140. if (documentManager
  141. && isEtherpadVisible !== state['features/etherpad'].editing) {
  142. documentManager.toggleEtherpad();
  143. }
  144. }
  145. const pinnedParticipant = getPinnedParticipant(state);
  146. const idOfParticipantToPin = attributes.nextOnStage;
  147. if (typeof idOfParticipantToPin !== 'undefined'
  148. && (!pinnedParticipant || idOfParticipantToPin !== pinnedParticipant.id)
  149. && oldState.nextOnStage !== attributes.nextOnStage) {
  150. _pinVideoThumbnailById(store, idOfParticipantToPin);
  151. } else if (typeof idOfParticipantToPin === 'undefined' && pinnedParticipant) {
  152. store.dispatch(pinParticipant(null));
  153. }
  154. if (attributes.pinnedStageParticipants !== undefined) {
  155. const stageParticipants = JSON.parse(attributes.pinnedStageParticipants);
  156. if (!_.isEqual(stageParticipants, oldState.pinnedStageParticipants)) {
  157. stageParticipants.forEach(p => store.dispatch(addStageParticipant(p.participantId, true)));
  158. }
  159. }
  160. if (attributes.maxStageParticipants !== undefined
  161. && oldState.maxStageParticipants !== attributes.maxStageParticipants) {
  162. store.dispatch(updateSettings({
  163. maxStageParticipants: Number(attributes.maxStageParticipants)
  164. }));
  165. }
  166. }
  167. /**
  168. * Pins the video thumbnail given by clickId.
  169. *
  170. * @param {Object} store - The redux store.
  171. * @param {string} clickId - The identifier of the participant to pin.
  172. * @private
  173. * @returns {void}
  174. */
  175. function _pinVideoThumbnailById(store, clickId) {
  176. if (getParticipantById(store.getState(), clickId)) {
  177. clearTimeout(nextOnStageTimeout);
  178. nextOnStageTimer = 0;
  179. store.dispatch(pinParticipant(clickId));
  180. } else {
  181. nextOnStageTimeout = setTimeout(() => {
  182. if (nextOnStageTimer > _FOLLOW_ME_RECEIVED_TIMEOUT) {
  183. nextOnStageTimer = 0;
  184. return;
  185. }
  186. nextOnStageTimer++;
  187. _pinVideoThumbnailById(store, clickId);
  188. }, 1000);
  189. }
  190. }