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

ReceiveVideoController.js 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import { getLogger } from '@jitsi/logger';
  2. import isEqual from 'lodash.isequal';
  3. import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
  4. import FeatureFlags from '../flags/FeatureFlags';
  5. const logger = getLogger(__filename);
  6. const MAX_HEIGHT_ONSTAGE = 2160;
  7. const MAX_HEIGHT_THUMBNAIL = 180;
  8. const LASTN_UNLIMITED = -1;
  9. /**
  10. * This class translates the legacy signaling format between the client and the bridge (that affects bandwidth
  11. * allocation) to the new format described here https://github.com/jitsi/jitsi-videobridge/blob/master/doc/allocation.md
  12. */
  13. class ReceiverVideoConstraints {
  14. /**
  15. * Creates a new instance.
  16. */
  17. constructor() {
  18. // Default constraints used for endpoints that are not explicitly included in constraints.
  19. // These constraints are used for endpoints that are thumbnails in the stage view.
  20. this._defaultConstraints = { 'maxHeight': MAX_HEIGHT_THUMBNAIL };
  21. // The number of videos requested from the bridge.
  22. this._lastN = LASTN_UNLIMITED;
  23. // The number representing the maximum video height the local client should receive from the bridge.
  24. this._maxFrameHeight = MAX_HEIGHT_ONSTAGE;
  25. // The endpoint IDs of the participants that are currently selected.
  26. this._selectedEndpoints = [];
  27. this._receiverVideoConstraints = {
  28. constraints: {},
  29. defaultConstraints: this.defaultConstraints,
  30. lastN: this._lastN,
  31. onStageEndpoints: [],
  32. selectedEndpoints: this._selectedEndpoints
  33. };
  34. }
  35. /**
  36. * Returns the receiver video constraints that need to be sent on the bridge channel.
  37. */
  38. get constraints() {
  39. this._receiverVideoConstraints.lastN = this._lastN;
  40. if (!this._selectedEndpoints.length) {
  41. return this._receiverVideoConstraints;
  42. }
  43. // The client is assumed to be in TileView if it has selected more than one endpoint, otherwise it is
  44. // assumed to be in StageView.
  45. this._receiverVideoConstraints.constraints = {};
  46. if (this._selectedEndpoints.length > 1) {
  47. /**
  48. * Tile view.
  49. * Only the default constraints are specified here along with lastN (if it is set).
  50. * {
  51. * 'colibriClass': 'ReceiverVideoConstraints',
  52. * 'defaultConstraints': { 'maxHeight': 360 }
  53. * }
  54. */
  55. this._receiverVideoConstraints.defaultConstraints = { 'maxHeight': this._maxFrameHeight };
  56. this._receiverVideoConstraints.onStageEndpoints = [];
  57. this._receiverVideoConstraints.selectedEndpoints = [];
  58. } else {
  59. /**
  60. * Stage view.
  61. * The participant on stage is specified in onStageEndpoints and a higher maxHeight is specified
  62. * for that endpoint while a default maxHeight of 180 is applied to all the other endpoints.
  63. * {
  64. * 'colibriClass': 'ReceiverVideoConstraints',
  65. * 'onStageEndpoints': ['A'],
  66. * 'defaultConstraints': { 'maxHeight': 180 },
  67. * 'constraints': {
  68. * 'A': { 'maxHeight': 720 }
  69. * }
  70. * }
  71. */
  72. this._receiverVideoConstraints.constraints[this._selectedEndpoints[0]] = {
  73. 'maxHeight': this._maxFrameHeight
  74. };
  75. this._receiverVideoConstraints.defaultConstraints = this._defaultConstraints;
  76. this._receiverVideoConstraints.onStageEndpoints = this._selectedEndpoints;
  77. this._receiverVideoConstraints.selectedEndpoints = [];
  78. }
  79. return this._receiverVideoConstraints;
  80. }
  81. /**
  82. * Updates the lastN field of the ReceiverVideoConstraints sent to the bridge.
  83. *
  84. * @param {number} value
  85. * @returns {boolean} Returns true if the the value has been updated, false otherwise.
  86. */
  87. updateLastN(value) {
  88. const changed = this._lastN !== value;
  89. if (changed) {
  90. this._lastN = value;
  91. logger.debug(`Updating ReceiverVideoConstraints lastN(${value})`);
  92. }
  93. return changed;
  94. }
  95. /**
  96. * Updates the resolution (height requested) in the contraints field of the ReceiverVideoConstraints
  97. * sent to the bridge.
  98. *
  99. * @param {number} maxFrameHeight
  100. * @requires {boolean} Returns true if the the value has been updated, false otherwise.
  101. */
  102. updateReceiveResolution(maxFrameHeight) {
  103. const changed = this._maxFrameHeight !== maxFrameHeight;
  104. if (changed) {
  105. this._maxFrameHeight = maxFrameHeight;
  106. logger.debug(`Updating receive maxFrameHeight: ${maxFrameHeight}`);
  107. }
  108. return changed;
  109. }
  110. /**
  111. * Updates the receiver constraints sent to the bridge.
  112. *
  113. * @param {Object} videoConstraints
  114. * @returns {boolean} Returns true if the the value has been updated, false otherwise.
  115. */
  116. updateReceiverVideoConstraints(videoConstraints) {
  117. const changed = !isEqual(this._receiverVideoConstraints, videoConstraints);
  118. if (changed) {
  119. this._receiverVideoConstraints = videoConstraints;
  120. logger.debug(`Updating ReceiverVideoConstraints ${JSON.stringify(videoConstraints)}`);
  121. }
  122. return changed;
  123. }
  124. /**
  125. * Updates the list of selected endpoints.
  126. *
  127. * @param {Array<string>} ids
  128. * @returns {void}
  129. */
  130. updateSelectedEndpoints(ids) {
  131. logger.debug(`Updating selected endpoints: ${JSON.stringify(ids)}`);
  132. this._selectedEndpoints = ids;
  133. }
  134. }
  135. /**
  136. * This class manages the receive video contraints for a given {@link JitsiConference}. These constraints are
  137. * determined by the application based on how the remote video streams need to be displayed. This class is responsible
  138. * for communicating these constraints to the bridge over the bridge channel.
  139. */
  140. export default class ReceiveVideoController {
  141. /**
  142. * Creates a new instance for a given conference.
  143. *
  144. * @param {JitsiConference} conference the conference instance for which the new instance will be managing
  145. * the receive video quality constraints.
  146. * @param {RTC} rtc the rtc instance which is responsible for initializing the bridge channel.
  147. */
  148. constructor(conference, rtc) {
  149. this._conference = conference;
  150. this._rtc = rtc;
  151. const { config } = conference.options;
  152. // The number of videos requested from the bridge, -1 represents unlimited or all available videos.
  153. this._lastN = config?.startLastN ?? (config?.channelLastN || LASTN_UNLIMITED);
  154. // The number representing the maximum video height the local client should receive from the bridge.
  155. this._maxFrameHeight = MAX_HEIGHT_ONSTAGE;
  156. // Enable new receiver constraints by default unless it is explicitly disabled through config.js.
  157. const useNewReceiverConstraints = config?.useNewBandwidthAllocationStrategy ?? true;
  158. if (useNewReceiverConstraints) {
  159. this._receiverVideoConstraints = new ReceiverVideoConstraints();
  160. const lastNUpdated = this._receiverVideoConstraints.updateLastN(this._lastN);
  161. lastNUpdated && this._rtc.setNewReceiverVideoConstraints(this._receiverVideoConstraints.constraints);
  162. } else {
  163. this._rtc.setLastN(this._lastN);
  164. }
  165. // The endpoint IDs of the participants that are currently selected.
  166. this._selectedEndpoints = [];
  167. this._conference.on(
  168. JitsiConferenceEvents._MEDIA_SESSION_STARTED,
  169. session => this._onMediaSessionStarted(session));
  170. }
  171. /**
  172. * Handles the {@link JitsiConferenceEvents.MEDIA_SESSION_STARTED}, that is when the conference creates new media
  173. * session. The preferred receive frameHeight is applied on the media session.
  174. *
  175. * @param {JingleSessionPC} mediaSession - the started media session.
  176. * @returns {void}
  177. * @private
  178. */
  179. _onMediaSessionStarted(mediaSession) {
  180. if (mediaSession.isP2P || !this._receiverVideoConstraints) {
  181. mediaSession.setReceiverVideoConstraint(this._maxFrameHeight);
  182. } else {
  183. this._receiverVideoConstraints.updateReceiveResolution(this._maxFrameHeight);
  184. this._rtc.setNewReceiverVideoConstraints(this._receiverVideoConstraints.constraints);
  185. }
  186. }
  187. /**
  188. * Returns the lastN value for the conference.
  189. *
  190. * @returns {number}
  191. */
  192. getLastN() {
  193. return this._lastN;
  194. }
  195. /**
  196. * Elects the participants with the given ids to be the selected participants in order to always receive video
  197. * for this participant (even when last n is enabled).
  198. *
  199. * @param {Array<string>} ids - The user ids.
  200. * @returns {void}
  201. */
  202. selectEndpoints(ids) {
  203. this._selectedEndpoints = ids;
  204. if (this._receiverVideoConstraints) {
  205. // Filter out the local endpointId from the list of selected endpoints.
  206. const remoteEndpointIds = ids.filter(id => id !== this._conference.myUserId());
  207. const oldConstraints = JSON.parse(JSON.stringify(this._receiverVideoConstraints.constraints));
  208. remoteEndpointIds.length && this._receiverVideoConstraints.updateSelectedEndpoints(remoteEndpointIds);
  209. const newConstraints = this._receiverVideoConstraints.constraints;
  210. // Send bridge message only when the constraints change.
  211. if (!isEqual(newConstraints, oldConstraints)) {
  212. this._rtc.setNewReceiverVideoConstraints(newConstraints);
  213. }
  214. return;
  215. }
  216. this._rtc.selectEndpoints(ids);
  217. }
  218. /**
  219. * Selects a new value for "lastN". The requested amount of videos are going to be delivered after the value is
  220. * in effect. Set to -1 for unlimited or all available videos.
  221. *
  222. * @param {number} value the new value for lastN.
  223. * @returns {void}
  224. */
  225. setLastN(value) {
  226. if (this._lastN !== value) {
  227. this._lastN = value;
  228. if (this._receiverVideoConstraints) {
  229. const lastNUpdated = this._receiverVideoConstraints.updateLastN(value);
  230. // Send out the message on the bridge channel if lastN was updated.
  231. lastNUpdated && this._rtc.setNewReceiverVideoConstraints(this._receiverVideoConstraints.constraints);
  232. return;
  233. }
  234. this._rtc.setLastN(value);
  235. }
  236. }
  237. /**
  238. * Sets the maximum video resolution the local participant should receive from remote participants.
  239. *
  240. * @param {number|undefined} maxFrameHeight - the new value.
  241. * @returns {void}
  242. */
  243. setPreferredReceiveMaxFrameHeight(maxFrameHeight) {
  244. this._maxFrameHeight = maxFrameHeight;
  245. for (const session of this._conference.getMediaSessions()) {
  246. if (session.isP2P || !this._receiverVideoConstraints) {
  247. maxFrameHeight && session.setReceiverVideoConstraint(maxFrameHeight);
  248. } else {
  249. const resolutionUpdated = this._receiverVideoConstraints.updateReceiveResolution(maxFrameHeight);
  250. resolutionUpdated
  251. && this._rtc.setNewReceiverVideoConstraints(this._receiverVideoConstraints.constraints);
  252. }
  253. }
  254. }
  255. /**
  256. * Sets the receiver constraints for the conference.
  257. *
  258. * @param {Object} constraints The video constraints.
  259. */
  260. setReceiverConstraints(constraints) {
  261. if (!this._receiverVideoConstraints) {
  262. this._receiverVideoConstraints = new ReceiverVideoConstraints();
  263. }
  264. const isEndpointsFormat = Object.keys(constraints).includes('onStageEndpoints', 'selectedEndpoints');
  265. const isSourcesFormat = Object.keys(constraints).includes('onStageSources', 'selectedSources');
  266. if (!FeatureFlags.isSourceNameSignalingEnabled() && isSourcesFormat) {
  267. throw new Error(
  268. '"onStageSources" and "selectedSources" are not supported when sourceNameSignaling is disabled.'
  269. );
  270. }
  271. if (FeatureFlags.isSourceNameSignalingEnabled() && isEndpointsFormat) {
  272. throw new Error(
  273. '"onStageEndpoints" and "selectedEndpoints" are not supported when sourceNameSignaling is enabled.'
  274. );
  275. }
  276. const constraintsChanged = this._receiverVideoConstraints.updateReceiverVideoConstraints(constraints);
  277. if (constraintsChanged) {
  278. this._lastN = constraints.lastN ?? this._lastN;
  279. this._selectedEndpoints = constraints.selectedEndpoints ?? this._selectedEndpoints;
  280. this._rtc.setNewReceiverVideoConstraints(constraints);
  281. const p2pSession = this._conference.getMediaSessions().find(session => session.isP2P);
  282. if (p2pSession) {
  283. let maxFrameHeight = Object.values(constraints.constraints)[0]?.maxHeight;
  284. if (!maxFrameHeight) {
  285. maxFrameHeight = constraints.defaultConstraints?.maxHeight;
  286. }
  287. maxFrameHeight && p2pSession.setReceiverVideoConstraint(maxFrameHeight);
  288. }
  289. }
  290. }
  291. }