選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

subscriber.js 21KB

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