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.

subscriber.js 8.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. // @flow
  2. import debounce from 'lodash/debounce';
  3. import { _handleParticipantError } from '../base/conference';
  4. import { getParticipantCount } from '../base/participants';
  5. import { StateListenerRegistry } from '../base/redux';
  6. import { reportError } from '../base/util';
  7. import { shouldDisplayTileView } from '../video-layout';
  8. import { setMaxReceiverVideoQuality } from './actions';
  9. import { VIDEO_QUALITY_LEVELS } from './constants';
  10. import { getReceiverVideoQualityLevel } from './functions';
  11. import logger from './logger';
  12. import { getMinHeightForQualityLvlMap } from './selector';
  13. declare var APP: Object;
  14. /**
  15. * Handles changes in the visible participants in the filmstrip. The listener is debounced
  16. * so that the client doesn't end up sending too many bridge messages when the user is
  17. * scrolling through the thumbnails prompting updates to the selected endpoints.
  18. */
  19. StateListenerRegistry.register(
  20. /* selector */ state => state['features/filmstrip'].visibleRemoteParticipants,
  21. /* listener */ debounce((visibleRemoteParticipants, store) => {
  22. _updateReceiverVideoConstraints(store);
  23. }, 100));
  24. /**
  25. * Handles the use case when the on-stage participant has changed.
  26. */
  27. StateListenerRegistry.register(
  28. state => state['features/large-video'].participantId,
  29. (participantId, store) => {
  30. _updateReceiverVideoConstraints(store);
  31. }
  32. );
  33. /**
  34. * Handles the use case when we have set some of the constraints in redux but the conference object wasn't available
  35. * and we haven't been able to pass the constraints to lib-jitsi-meet.
  36. */
  37. StateListenerRegistry.register(
  38. state => state['features/base/conference'].conference,
  39. (conference, store) => {
  40. _updateReceiverVideoConstraints(store);
  41. }
  42. );
  43. /**
  44. * Updates the receiver constraints when the layout changes. When we are in stage view we need to handle the
  45. * on-stage participant differently.
  46. */
  47. StateListenerRegistry.register(
  48. /* selector */ state => state['features/video-layout'].tileViewEnabled,
  49. /* listener */ (tileViewEnabled, store) => {
  50. _updateReceiverVideoConstraints(store);
  51. }
  52. );
  53. /**
  54. * StateListenerRegistry provides a reliable way of detecting changes to
  55. * lastn state and dispatching additional actions.
  56. */
  57. StateListenerRegistry.register(
  58. /* selector */ state => state['features/base/lastn'].lastN,
  59. /* listener */ (lastN, store) => {
  60. _updateReceiverVideoConstraints(store);
  61. });
  62. /**
  63. * StateListenerRegistry provides a reliable way of detecting changes to
  64. * maxReceiverVideoQuality and preferredVideoQuality state and dispatching additional actions.
  65. */
  66. StateListenerRegistry.register(
  67. /* selector */ state => {
  68. const {
  69. maxReceiverVideoQuality,
  70. preferredVideoQuality
  71. } = state['features/video-quality'];
  72. return {
  73. maxReceiverVideoQuality,
  74. preferredVideoQuality
  75. };
  76. },
  77. /* listener */ (currentState, store, previousState = {}) => {
  78. const { maxReceiverVideoQuality, preferredVideoQuality } = currentState;
  79. const changedPreferredVideoQuality = preferredVideoQuality !== previousState.preferredVideoQuality;
  80. const changedReceiverVideoQuality = maxReceiverVideoQuality !== previousState.maxReceiverVideoQuality;
  81. if (changedPreferredVideoQuality) {
  82. _setSenderVideoConstraint(preferredVideoQuality, store);
  83. typeof APP !== 'undefined' && APP.API.notifyVideoQualityChanged(preferredVideoQuality);
  84. }
  85. changedReceiverVideoQuality && _updateReceiverVideoConstraints(store);
  86. }, {
  87. deepEquals: true
  88. });
  89. /**
  90. * Implements a state listener in order to calculate max receiver video quality.
  91. */
  92. StateListenerRegistry.register(
  93. /* selector */ state => {
  94. const { reducedUI } = state['features/base/responsive-ui'];
  95. const _shouldDisplayTileView = shouldDisplayTileView(state);
  96. const thumbnailSize = state['features/filmstrip']?.tileViewDimensions?.thumbnailSize;
  97. const participantCount = getParticipantCount(state);
  98. return {
  99. displayTileView: _shouldDisplayTileView,
  100. participantCount,
  101. reducedUI,
  102. thumbnailHeight: thumbnailSize?.height
  103. };
  104. },
  105. /* listener */ ({ displayTileView, participantCount, reducedUI, thumbnailHeight }, { dispatch, getState }) => {
  106. const state = getState();
  107. const { maxReceiverVideoQuality } = state['features/video-quality'];
  108. const { maxFullResolutionParticipants = 2 } = state['features/base/config'];
  109. let newMaxRecvVideoQuality = VIDEO_QUALITY_LEVELS.ULTRA;
  110. if (reducedUI) {
  111. newMaxRecvVideoQuality = VIDEO_QUALITY_LEVELS.LOW;
  112. } else if (displayTileView && !Number.isNaN(thumbnailHeight)) {
  113. newMaxRecvVideoQuality = getReceiverVideoQualityLevel(thumbnailHeight, getMinHeightForQualityLvlMap(state));
  114. // Override HD level calculated for the thumbnail height when # of participants threshold is exceeded
  115. if (maxReceiverVideoQuality !== newMaxRecvVideoQuality && maxFullResolutionParticipants !== -1) {
  116. const override
  117. = participantCount > maxFullResolutionParticipants
  118. && newMaxRecvVideoQuality > VIDEO_QUALITY_LEVELS.STANDARD;
  119. logger.info(`Video quality level for thumbnail height: ${thumbnailHeight}, `
  120. + `is: ${newMaxRecvVideoQuality}, `
  121. + `override: ${String(override)}, `
  122. + `max full res N: ${maxFullResolutionParticipants}`);
  123. if (override) {
  124. newMaxRecvVideoQuality = VIDEO_QUALITY_LEVELS.STANDARD;
  125. }
  126. }
  127. }
  128. if (maxReceiverVideoQuality !== newMaxRecvVideoQuality) {
  129. dispatch(setMaxReceiverVideoQuality(newMaxRecvVideoQuality));
  130. }
  131. }, {
  132. deepEquals: true
  133. });
  134. /**
  135. * Helper function for updating the preferred sender video constraint, based on the user preference.
  136. *
  137. * @param {number} preferred - The user preferred max frame height.
  138. * @returns {void}
  139. */
  140. function _setSenderVideoConstraint(preferred, { getState }) {
  141. const state = getState();
  142. const { conference } = state['features/base/conference'];
  143. if (!conference) {
  144. return;
  145. }
  146. logger.info(`Setting sender resolution to ${preferred}`);
  147. conference.setSenderVideoConstraint(preferred)
  148. .catch(error => {
  149. _handleParticipantError(error);
  150. reportError(error, `Changing sender resolution to ${preferred} failed.`);
  151. });
  152. }
  153. /**
  154. * Private helper to calculate the receiver video constraints and set them on the bridge channel.
  155. *
  156. * @param {*} store - The redux store.
  157. * @returns {void}
  158. */
  159. function _updateReceiverVideoConstraints({ getState }) {
  160. const state = getState();
  161. const { conference } = state['features/base/conference'];
  162. if (!conference) {
  163. return;
  164. }
  165. const { lastN } = state['features/base/lastn'];
  166. const { maxReceiverVideoQuality, preferredVideoQuality } = state['features/video-quality'];
  167. const { participantId: largeVideoParticipantId } = state['features/large-video'];
  168. const maxFrameHeight = Math.min(maxReceiverVideoQuality, preferredVideoQuality);
  169. const { visibleRemoteParticipants } = state['features/filmstrip'];
  170. const receiverConstraints = {
  171. constraints: {},
  172. defaultConstraints: { 'maxHeight': VIDEO_QUALITY_LEVELS.NONE },
  173. lastN,
  174. onStageEndpoints: [],
  175. selectedEndpoints: []
  176. };
  177. // Tile view.
  178. if (shouldDisplayTileView(state)) {
  179. if (!visibleRemoteParticipants?.size) {
  180. return;
  181. }
  182. visibleRemoteParticipants.forEach(participantId => {
  183. receiverConstraints.constraints[participantId] = { 'maxHeight': maxFrameHeight };
  184. });
  185. // Stage view.
  186. } else {
  187. if (!visibleRemoteParticipants?.size && !largeVideoParticipantId) {
  188. return;
  189. }
  190. if (visibleRemoteParticipants?.size > 0) {
  191. visibleRemoteParticipants.forEach(participantId => {
  192. receiverConstraints.constraints[participantId] = { 'maxHeight': VIDEO_QUALITY_LEVELS.LOW };
  193. });
  194. }
  195. if (largeVideoParticipantId) {
  196. receiverConstraints.constraints[largeVideoParticipantId] = { 'maxHeight': maxFrameHeight };
  197. receiverConstraints.onStageEndpoints = [ largeVideoParticipantId ];
  198. }
  199. }
  200. try {
  201. conference.setReceiverConstraints(receiverConstraints);
  202. } catch (error) {
  203. _handleParticipantError(error);
  204. reportError(error, `Failed to set receiver video constraints ${JSON.stringify(receiverConstraints)}`);
  205. }
  206. }