Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

subscriber.ts 22KB

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