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

subscriber.ts 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. import debounce from 'lodash/debounce';
  2. import { IReduxState, IStore } from '../app/types';
  3. import { _handleParticipantError } from '../base/conference/functions';
  4. import { getSsrcRewritingFeatureFlag } from '../base/config/functions.any';
  5. import { MEDIA_TYPE } from '../base/media/constants';
  6. import {
  7. getLocalParticipant,
  8. getSourceNamesByMediaType
  9. } from '../base/participants/functions';
  10. import StateListenerRegistry from '../base/redux/StateListenerRegistry';
  11. import { getTrackSourceNameByMediaTypeAndParticipant } from '../base/tracks/functions';
  12. import { reportError } from '../base/util/helpers';
  13. import {
  14. getActiveParticipantsIds,
  15. getScreenshareFilmstripParticipantId,
  16. isTopPanelEnabled
  17. } from '../filmstrip/functions';
  18. import { LAYOUTS } from '../video-layout/constants';
  19. import {
  20. getCurrentLayout,
  21. getVideoQualityForLargeVideo,
  22. getVideoQualityForResizableFilmstripThumbnails,
  23. getVideoQualityForScreenSharingFilmstrip,
  24. getVideoQualityForStageThumbnails,
  25. shouldDisplayTileView
  26. } from '../video-layout/functions';
  27. import {
  28. setMaxReceiverVideoQualityForLargeVideo,
  29. setMaxReceiverVideoQualityForScreenSharingFilmstrip,
  30. setMaxReceiverVideoQualityForStageFilmstrip,
  31. setMaxReceiverVideoQualityForTileView,
  32. setMaxReceiverVideoQualityForVerticalFilmstrip
  33. } from './actions';
  34. import { MAX_VIDEO_QUALITY, VIDEO_QUALITY_LEVELS, VIDEO_QUALITY_UNLIMITED } from './constants';
  35. import { getReceiverVideoQualityLevel } from './functions';
  36. import logger from './logger';
  37. import { getMinHeightForQualityLvlMap } from './selector';
  38. /**
  39. * Handles changes in the visible participants in the filmstrip. The listener is debounced
  40. * so that the client doesn't end up sending too many bridge messages when the user is
  41. * scrolling through the thumbnails prompting updates to the selected endpoints.
  42. */
  43. StateListenerRegistry.register(
  44. /* selector */ state => state['features/filmstrip'].visibleRemoteParticipants,
  45. /* listener */ debounce((visibleRemoteParticipants, store) => {
  46. _updateReceiverVideoConstraints(store);
  47. }, 100));
  48. StateListenerRegistry.register(
  49. /* selector */ state => state['features/base/tracks'],
  50. /* listener */(remoteTracks, store) => {
  51. _updateReceiverVideoConstraints(store);
  52. });
  53. /**
  54. * Handles the use case when the on-stage participant has changed.
  55. */
  56. StateListenerRegistry.register(
  57. state => state['features/large-video'].participantId,
  58. (participantId, store) => {
  59. _updateReceiverVideoConstraints(store);
  60. }
  61. );
  62. /**
  63. * Handles the use case when we have set some of the constraints in redux but the conference object wasn't available
  64. * and we haven't been able to pass the constraints to lib-jitsi-meet.
  65. */
  66. StateListenerRegistry.register(
  67. state => state['features/base/conference'].conference,
  68. (conference, store) => {
  69. _updateReceiverVideoConstraints(store);
  70. }
  71. );
  72. /**
  73. * StateListenerRegistry provides a reliable way of detecting changes to
  74. * lastn state and dispatching additional actions.
  75. */
  76. StateListenerRegistry.register(
  77. /* selector */ state => state['features/base/lastn'].lastN,
  78. /* listener */ (lastN, store) => {
  79. _updateReceiverVideoConstraints(store);
  80. });
  81. /**
  82. * Updates the receiver constraints when the stage participants change.
  83. */
  84. StateListenerRegistry.register(
  85. state => getActiveParticipantsIds(state).sort(),
  86. (_, store) => {
  87. _updateReceiverVideoConstraints(store);
  88. }, {
  89. deepEquals: true
  90. }
  91. );
  92. /**
  93. * Updates the receiver constraints when new video sources are added to the conference.
  94. */
  95. StateListenerRegistry.register(
  96. /* selector */ state => state['features/base/participants'].remoteVideoSources,
  97. /* listener */ (remoteVideoSources, store) => {
  98. getSsrcRewritingFeatureFlag(store.getState()) && _updateReceiverVideoConstraints(store);
  99. });
  100. /**
  101. * StateListenerRegistry provides a reliable way of detecting changes to
  102. * maxReceiverVideoQuality* and preferredVideoQuality state and dispatching additional actions.
  103. */
  104. StateListenerRegistry.register(
  105. /* selector */ state => {
  106. const {
  107. maxReceiverVideoQualityForLargeVideo,
  108. maxReceiverVideoQualityForScreenSharingFilmstrip,
  109. maxReceiverVideoQualityForStageFilmstrip,
  110. maxReceiverVideoQualityForTileView,
  111. maxReceiverVideoQualityForVerticalFilmstrip,
  112. preferredVideoQuality
  113. } = state['features/video-quality'];
  114. return {
  115. maxReceiverVideoQualityForLargeVideo,
  116. maxReceiverVideoQualityForScreenSharingFilmstrip,
  117. maxReceiverVideoQualityForStageFilmstrip,
  118. maxReceiverVideoQualityForTileView,
  119. maxReceiverVideoQualityForVerticalFilmstrip,
  120. preferredVideoQuality
  121. };
  122. },
  123. /* listener */ (currentState, store, previousState = {}) => {
  124. const { preferredVideoQuality } = currentState;
  125. const changedPreferredVideoQuality = preferredVideoQuality !== previousState.preferredVideoQuality;
  126. if (changedPreferredVideoQuality) {
  127. _setSenderVideoConstraint(preferredVideoQuality, store);
  128. typeof APP !== 'undefined' && APP.API.notifyVideoQualityChanged(preferredVideoQuality);
  129. }
  130. _updateReceiverVideoConstraints(store);
  131. }, {
  132. deepEquals: true
  133. });
  134. /**
  135. * Implements a state listener in order to calculate max receiver video quality.
  136. */
  137. StateListenerRegistry.register(
  138. /* selector */ state => {
  139. const { reducedUI } = state['features/base/responsive-ui'];
  140. const _shouldDisplayTileView = shouldDisplayTileView(state);
  141. const tileViewThumbnailSize = state['features/filmstrip']?.tileViewDimensions?.thumbnailSize;
  142. const { visibleRemoteParticipants } = state['features/filmstrip'];
  143. const { height: largeVideoHeight } = state['features/large-video'];
  144. const activeParticipantsIds = getActiveParticipantsIds(state);
  145. const {
  146. screenshareFilmstripDimensions: {
  147. thumbnailSize
  148. }
  149. } = state['features/filmstrip'];
  150. const screenshareFilmstripParticipantId = getScreenshareFilmstripParticipantId(state);
  151. return {
  152. activeParticipantsCount: activeParticipantsIds?.length,
  153. displayTileView: _shouldDisplayTileView,
  154. largeVideoHeight,
  155. participantCount: visibleRemoteParticipants?.size || 0,
  156. reducedUI,
  157. screenSharingFilmstripHeight:
  158. screenshareFilmstripParticipantId && getCurrentLayout(state) === LAYOUTS.STAGE_FILMSTRIP_VIEW
  159. ? thumbnailSize?.height : undefined,
  160. stageFilmstripThumbnailHeight: state['features/filmstrip'].stageFilmstripDimensions?.thumbnailSize?.height,
  161. tileViewThumbnailHeight: tileViewThumbnailSize?.height,
  162. verticalFilmstripThumbnailHeight:
  163. state['features/filmstrip'].verticalViewDimensions?.gridView?.thumbnailSize?.height
  164. };
  165. },
  166. /* listener */ ({
  167. activeParticipantsCount,
  168. displayTileView,
  169. largeVideoHeight,
  170. participantCount,
  171. reducedUI,
  172. screenSharingFilmstripHeight,
  173. stageFilmstripThumbnailHeight,
  174. tileViewThumbnailHeight,
  175. verticalFilmstripThumbnailHeight
  176. }, store, previousState = {}) => {
  177. const { dispatch, getState } = store;
  178. const state = getState();
  179. const {
  180. maxReceiverVideoQualityForLargeVideo,
  181. maxReceiverVideoQualityForScreenSharingFilmstrip,
  182. maxReceiverVideoQualityForStageFilmstrip,
  183. maxReceiverVideoQualityForTileView,
  184. maxReceiverVideoQualityForVerticalFilmstrip
  185. } = state['features/video-quality'];
  186. const { maxFullResolutionParticipants = 2 } = state['features/base/config'];
  187. let maxVideoQualityChanged = false;
  188. if (displayTileView) {
  189. let newMaxRecvVideoQuality = VIDEO_QUALITY_LEVELS.STANDARD;
  190. if (reducedUI) {
  191. newMaxRecvVideoQuality = VIDEO_QUALITY_LEVELS.LOW;
  192. } else if (typeof tileViewThumbnailHeight === 'number' && !Number.isNaN(tileViewThumbnailHeight)) {
  193. newMaxRecvVideoQuality
  194. = getReceiverVideoQualityLevel(tileViewThumbnailHeight, getMinHeightForQualityLvlMap(state));
  195. // Override HD level calculated for the thumbnail height when # of participants threshold is exceeded
  196. if (maxFullResolutionParticipants !== -1) {
  197. const override
  198. = participantCount > maxFullResolutionParticipants
  199. && newMaxRecvVideoQuality > VIDEO_QUALITY_LEVELS.STANDARD;
  200. logger.info(`Video quality level for thumbnail height: ${tileViewThumbnailHeight}, `
  201. + `is: ${newMaxRecvVideoQuality}, `
  202. + `override: ${String(override)}, `
  203. + `max full res N: ${maxFullResolutionParticipants}`);
  204. if (override) {
  205. newMaxRecvVideoQuality = VIDEO_QUALITY_LEVELS.STANDARD;
  206. }
  207. }
  208. }
  209. if (maxReceiverVideoQualityForTileView !== newMaxRecvVideoQuality) {
  210. maxVideoQualityChanged = true;
  211. dispatch(setMaxReceiverVideoQualityForTileView(newMaxRecvVideoQuality));
  212. }
  213. } else {
  214. let newMaxRecvVideoQualityForStageFilmstrip;
  215. let newMaxRecvVideoQualityForVerticalFilmstrip;
  216. let newMaxRecvVideoQualityForLargeVideo;
  217. let newMaxRecvVideoQualityForScreenSharingFilmstrip;
  218. if (reducedUI) {
  219. newMaxRecvVideoQualityForVerticalFilmstrip
  220. = newMaxRecvVideoQualityForStageFilmstrip
  221. = newMaxRecvVideoQualityForLargeVideo
  222. = newMaxRecvVideoQualityForScreenSharingFilmstrip
  223. = VIDEO_QUALITY_LEVELS.LOW;
  224. } else {
  225. newMaxRecvVideoQualityForStageFilmstrip
  226. = getVideoQualityForStageThumbnails(stageFilmstripThumbnailHeight, state);
  227. newMaxRecvVideoQualityForVerticalFilmstrip
  228. = getVideoQualityForResizableFilmstripThumbnails(verticalFilmstripThumbnailHeight, state);
  229. newMaxRecvVideoQualityForLargeVideo = getVideoQualityForLargeVideo(largeVideoHeight);
  230. newMaxRecvVideoQualityForScreenSharingFilmstrip
  231. = getVideoQualityForScreenSharingFilmstrip(screenSharingFilmstripHeight, state);
  232. // Override HD level calculated for the thumbnail height when # of participants threshold is exceeded
  233. if (maxFullResolutionParticipants !== -1) {
  234. if (activeParticipantsCount > 0
  235. && newMaxRecvVideoQualityForStageFilmstrip > VIDEO_QUALITY_LEVELS.STANDARD) {
  236. const isScreenSharingFilmstripParticipantFullResolution
  237. = newMaxRecvVideoQualityForScreenSharingFilmstrip > VIDEO_QUALITY_LEVELS.STANDARD;
  238. if (activeParticipantsCount > maxFullResolutionParticipants
  239. - (isScreenSharingFilmstripParticipantFullResolution ? 1 : 0)) {
  240. newMaxRecvVideoQualityForStageFilmstrip = VIDEO_QUALITY_LEVELS.STANDARD;
  241. newMaxRecvVideoQualityForVerticalFilmstrip
  242. = Math.min(VIDEO_QUALITY_LEVELS.STANDARD, newMaxRecvVideoQualityForVerticalFilmstrip);
  243. } else if (newMaxRecvVideoQualityForVerticalFilmstrip > VIDEO_QUALITY_LEVELS.STANDARD
  244. && participantCount > maxFullResolutionParticipants - activeParticipantsCount) {
  245. newMaxRecvVideoQualityForVerticalFilmstrip = VIDEO_QUALITY_LEVELS.STANDARD;
  246. }
  247. } else if (newMaxRecvVideoQualityForVerticalFilmstrip > VIDEO_QUALITY_LEVELS.STANDARD
  248. && participantCount > maxFullResolutionParticipants
  249. - (newMaxRecvVideoQualityForLargeVideo > VIDEO_QUALITY_LEVELS.STANDARD ? 1 : 0)) {
  250. newMaxRecvVideoQualityForVerticalFilmstrip = VIDEO_QUALITY_LEVELS.STANDARD;
  251. }
  252. }
  253. }
  254. if (maxReceiverVideoQualityForStageFilmstrip !== newMaxRecvVideoQualityForStageFilmstrip) {
  255. maxVideoQualityChanged = true;
  256. dispatch(setMaxReceiverVideoQualityForStageFilmstrip(newMaxRecvVideoQualityForStageFilmstrip));
  257. }
  258. if (maxReceiverVideoQualityForVerticalFilmstrip !== newMaxRecvVideoQualityForVerticalFilmstrip) {
  259. maxVideoQualityChanged = true;
  260. dispatch(setMaxReceiverVideoQualityForVerticalFilmstrip(newMaxRecvVideoQualityForVerticalFilmstrip));
  261. }
  262. if (maxReceiverVideoQualityForLargeVideo !== newMaxRecvVideoQualityForLargeVideo) {
  263. maxVideoQualityChanged = true;
  264. dispatch(setMaxReceiverVideoQualityForLargeVideo(newMaxRecvVideoQualityForLargeVideo));
  265. }
  266. if (maxReceiverVideoQualityForScreenSharingFilmstrip !== newMaxRecvVideoQualityForScreenSharingFilmstrip) {
  267. maxVideoQualityChanged = true;
  268. dispatch(
  269. setMaxReceiverVideoQualityForScreenSharingFilmstrip(
  270. newMaxRecvVideoQualityForScreenSharingFilmstrip));
  271. }
  272. }
  273. if (!maxVideoQualityChanged && Boolean(displayTileView) !== Boolean(previousState.displayTileView)) {
  274. _updateReceiverVideoConstraints(store);
  275. }
  276. }, {
  277. deepEquals: true
  278. });
  279. /**
  280. * Returns the source names asociated with the given participants list.
  281. *
  282. * @param {Array<string>} participantList - The list of participants.
  283. * @param {Object} state - The redux state.
  284. * @returns {Array<string>}
  285. */
  286. function _getSourceNames(participantList: Array<string>, state: IReduxState): Array<string> {
  287. const { remoteScreenShares } = state['features/video-layout'];
  288. const tracks = state['features/base/tracks'];
  289. const sourceNamesList: string[] = [];
  290. participantList.forEach(participantId => {
  291. if (getSsrcRewritingFeatureFlag(state)) {
  292. const sourceNames: string[] | undefined
  293. = getSourceNamesByMediaType(state, participantId, MEDIA_TYPE.VIDEO);
  294. sourceNames?.length && sourceNamesList.push(...sourceNames);
  295. } else {
  296. let sourceName: string;
  297. if (remoteScreenShares.includes(participantId)) {
  298. sourceName = participantId;
  299. } else {
  300. sourceName = getTrackSourceNameByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId);
  301. }
  302. if (sourceName) {
  303. sourceNamesList.push(sourceName);
  304. }
  305. }
  306. });
  307. return sourceNamesList;
  308. }
  309. /**
  310. * Helper function for updating the preferred sender video constraint, based on the user preference.
  311. *
  312. * @param {number} preferred - The user preferred max frame height.
  313. * @returns {void}
  314. */
  315. function _setSenderVideoConstraint(preferred: number, { getState }: IStore) {
  316. const state = getState();
  317. const { conference } = state['features/base/conference'];
  318. if (!conference) {
  319. return;
  320. }
  321. logger.info(`Setting sender resolution to ${preferred}`);
  322. conference.setSenderVideoConstraint(preferred)
  323. .catch((error: any) => {
  324. _handleParticipantError(error);
  325. reportError(error, `Changing sender resolution to ${preferred} failed.`);
  326. });
  327. }
  328. /**
  329. * Private helper to calculate the receiver video constraints and set them on the bridge channel.
  330. *
  331. * @param {*} store - The redux store.
  332. * @returns {void}
  333. */
  334. function _updateReceiverVideoConstraints({ getState }: IStore) {
  335. const state = getState();
  336. const { conference } = state['features/base/conference'];
  337. if (!conference) {
  338. return;
  339. }
  340. const { lastN } = state['features/base/lastn'];
  341. const {
  342. maxReceiverVideoQualityForTileView,
  343. maxReceiverVideoQualityForStageFilmstrip,
  344. maxReceiverVideoQualityForVerticalFilmstrip,
  345. maxReceiverVideoQualityForLargeVideo,
  346. maxReceiverVideoQualityForScreenSharingFilmstrip,
  347. preferredVideoQuality
  348. } = state['features/video-quality'];
  349. const { participantId: largeVideoParticipantId = '' } = state['features/large-video'];
  350. const maxFrameHeightForTileView = Math.min(maxReceiverVideoQualityForTileView, preferredVideoQuality);
  351. const maxFrameHeightForStageFilmstrip = Math.min(maxReceiverVideoQualityForStageFilmstrip, preferredVideoQuality);
  352. const maxFrameHeightForVerticalFilmstrip
  353. = Math.min(maxReceiverVideoQualityForVerticalFilmstrip, preferredVideoQuality);
  354. const maxFrameHeightForLargeVideo
  355. = Math.min(maxReceiverVideoQualityForLargeVideo, preferredVideoQuality);
  356. const maxFrameHeightForScreenSharingFilmstrip
  357. = Math.min(maxReceiverVideoQualityForScreenSharingFilmstrip, preferredVideoQuality);
  358. const { remoteScreenShares } = state['features/video-layout'];
  359. const { visibleRemoteParticipants } = state['features/filmstrip'];
  360. const tracks = state['features/base/tracks'];
  361. const localParticipantId = getLocalParticipant(state)?.id;
  362. const activeParticipantsIds = getActiveParticipantsIds(state);
  363. const screenshareFilmstripParticipantId = isTopPanelEnabled(state) && getScreenshareFilmstripParticipantId(state);
  364. const receiverConstraints: any = {
  365. constraints: {},
  366. defaultConstraints: { 'maxHeight': VIDEO_QUALITY_LEVELS.NONE },
  367. lastN
  368. };
  369. let activeParticipantsSources: string[] = [];
  370. let visibleRemoteTrackSourceNames: string[] = [];
  371. let largeVideoSourceName: string | undefined;
  372. receiverConstraints.onStageSources = [];
  373. receiverConstraints.selectedSources = [];
  374. if (visibleRemoteParticipants?.size) {
  375. visibleRemoteTrackSourceNames = _getSourceNames(Array.from(visibleRemoteParticipants), state);
  376. }
  377. if (activeParticipantsIds?.length > 0) {
  378. activeParticipantsSources = _getSourceNames(activeParticipantsIds, state);
  379. }
  380. if (localParticipantId !== largeVideoParticipantId) {
  381. if (remoteScreenShares.includes(largeVideoParticipantId)) {
  382. largeVideoSourceName = largeVideoParticipantId;
  383. } else {
  384. largeVideoSourceName = getSsrcRewritingFeatureFlag(state)
  385. ? getSourceNamesByMediaType(state, largeVideoParticipantId, MEDIA_TYPE.VIDEO)?.[0]
  386. : getTrackSourceNameByMediaTypeAndParticipant(
  387. tracks, MEDIA_TYPE.VIDEO, largeVideoParticipantId);
  388. }
  389. }
  390. // Tile view.
  391. if (shouldDisplayTileView(state)) {
  392. if (!visibleRemoteTrackSourceNames?.length) {
  393. return;
  394. }
  395. visibleRemoteTrackSourceNames.forEach(sourceName => {
  396. receiverConstraints.constraints[sourceName] = { 'maxHeight': maxFrameHeightForTileView };
  397. });
  398. // Prioritize screenshare in tile view.
  399. if (remoteScreenShares?.length) {
  400. receiverConstraints.selectedSources = remoteScreenShares;
  401. }
  402. // Stage view.
  403. } else {
  404. if (!visibleRemoteTrackSourceNames?.length && !largeVideoSourceName && !activeParticipantsSources?.length) {
  405. return;
  406. }
  407. if (visibleRemoteTrackSourceNames?.length) {
  408. visibleRemoteTrackSourceNames.forEach(sourceName => {
  409. receiverConstraints.constraints[sourceName] = { 'maxHeight': maxFrameHeightForVerticalFilmstrip };
  410. });
  411. }
  412. if (getCurrentLayout(state) === LAYOUTS.STAGE_FILMSTRIP_VIEW && activeParticipantsSources.length > 0) {
  413. const onStageSources = [ ...activeParticipantsSources ];
  414. activeParticipantsSources.forEach(sourceName => {
  415. const isScreenSharing = remoteScreenShares.includes(sourceName);
  416. const quality
  417. = isScreenSharing && preferredVideoQuality >= MAX_VIDEO_QUALITY
  418. ? VIDEO_QUALITY_UNLIMITED : maxFrameHeightForStageFilmstrip;
  419. receiverConstraints.constraints[sourceName] = { 'maxHeight': quality };
  420. });
  421. if (screenshareFilmstripParticipantId) {
  422. onStageSources.push(screenshareFilmstripParticipantId);
  423. receiverConstraints.constraints[screenshareFilmstripParticipantId]
  424. = {
  425. 'maxHeight':
  426. preferredVideoQuality >= MAX_VIDEO_QUALITY
  427. ? VIDEO_QUALITY_UNLIMITED : maxFrameHeightForScreenSharingFilmstrip
  428. };
  429. }
  430. receiverConstraints.onStageSources = onStageSources;
  431. } else if (largeVideoSourceName) {
  432. let quality = VIDEO_QUALITY_UNLIMITED;
  433. if (preferredVideoQuality < MAX_VIDEO_QUALITY
  434. || !remoteScreenShares.find(id => id === largeVideoParticipantId)) {
  435. quality = maxFrameHeightForLargeVideo;
  436. }
  437. receiverConstraints.constraints[largeVideoSourceName] = { 'maxHeight': quality };
  438. receiverConstraints.onStageSources = [ largeVideoSourceName ];
  439. }
  440. }
  441. try {
  442. conference.setReceiverConstraints(receiverConstraints);
  443. } catch (error: any) {
  444. _handleParticipantError(error);
  445. reportError(error, `Failed to set receiver video constraints ${JSON.stringify(receiverConstraints)}`);
  446. }
  447. }