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.

ReceiveVideoController.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. import { getLogger } from '@jitsi/logger';
  2. import isEqual from 'lodash.isequal';
  3. import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
  4. import { MediaType } from '../../service/RTC/MediaType';
  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. /**
  157. * The map that holds the max frame height requested for each remote source when source-name signaling is
  158. * enabled.
  159. *
  160. * @type Map<string, number>
  161. */
  162. this._sourceReceiverConstraints = new Map();
  163. // Enable new receiver constraints by default unless it is explicitly disabled through config.js.
  164. const useNewReceiverConstraints = config?.useNewBandwidthAllocationStrategy ?? true;
  165. if (useNewReceiverConstraints) {
  166. this._receiverVideoConstraints = new ReceiverVideoConstraints();
  167. const lastNUpdated = this._receiverVideoConstraints.updateLastN(this._lastN);
  168. lastNUpdated && this._rtc.setNewReceiverVideoConstraints(this._receiverVideoConstraints.constraints);
  169. } else {
  170. this._rtc.setLastN(this._lastN);
  171. }
  172. // The endpoint IDs of the participants that are currently selected.
  173. this._selectedEndpoints = [];
  174. this._conference.on(
  175. JitsiConferenceEvents._MEDIA_SESSION_STARTED,
  176. session => this._onMediaSessionStarted(session));
  177. }
  178. /**
  179. * Returns a map of all the remote source names and the corresponding max frame heights.
  180. *
  181. * @param {number} maxFrameHeight
  182. * @returns
  183. */
  184. _getDefaultSourceReceiverConstraints(mediaSession, maxFrameHeight) {
  185. const remoteVideoTracks = mediaSession.peerconnection?.getRemoteTracks(null, MediaType.VIDEO) || [];
  186. const receiverConstraints = new Map();
  187. for (const track of remoteVideoTracks) {
  188. receiverConstraints.set(track.getSourceName(), maxFrameHeight);
  189. }
  190. return receiverConstraints;
  191. }
  192. /**
  193. * Handles the {@link JitsiConferenceEvents.MEDIA_SESSION_STARTED}, that is when the conference creates new media
  194. * session. The preferred receive frameHeight is applied on the media session.
  195. *
  196. * @param {JingleSessionPC} mediaSession - the started media session.
  197. * @returns {void}
  198. * @private
  199. */
  200. _onMediaSessionStarted(mediaSession) {
  201. if (mediaSession.isP2P || !this._receiverVideoConstraints) {
  202. mediaSession.setReceiverVideoConstraint(this._maxFrameHeight, this._sourceReceiverConstraints);
  203. } else {
  204. this._receiverVideoConstraints.updateReceiveResolution(this._maxFrameHeight);
  205. this._rtc.setNewReceiverVideoConstraints(this._receiverVideoConstraints.constraints);
  206. }
  207. }
  208. /**
  209. * Returns the lastN value for the conference.
  210. *
  211. * @returns {number}
  212. */
  213. getLastN() {
  214. return this._lastN;
  215. }
  216. /**
  217. * Elects the participants with the given ids to be the selected participants in order to always receive video
  218. * for this participant (even when last n is enabled).
  219. *
  220. * @param {Array<string>} ids - The user ids.
  221. * @returns {void}
  222. */
  223. selectEndpoints(ids) {
  224. this._selectedEndpoints = ids;
  225. if (this._receiverVideoConstraints) {
  226. // Filter out the local endpointId from the list of selected endpoints.
  227. const remoteEndpointIds = ids.filter(id => id !== this._conference.myUserId());
  228. const oldConstraints = JSON.parse(JSON.stringify(this._receiverVideoConstraints.constraints));
  229. remoteEndpointIds.length && this._receiverVideoConstraints.updateSelectedEndpoints(remoteEndpointIds);
  230. const newConstraints = this._receiverVideoConstraints.constraints;
  231. // Send bridge message only when the constraints change.
  232. if (!isEqual(newConstraints, oldConstraints)) {
  233. this._rtc.setNewReceiverVideoConstraints(newConstraints);
  234. }
  235. return;
  236. }
  237. this._rtc.selectEndpoints(ids);
  238. }
  239. /**
  240. * Selects a new value for "lastN". The requested amount of videos are going to be delivered after the value is
  241. * in effect. Set to -1 for unlimited or all available videos.
  242. *
  243. * @param {number} value the new value for lastN.
  244. * @returns {void}
  245. */
  246. setLastN(value) {
  247. if (this._lastN !== value) {
  248. this._lastN = value;
  249. if (this._receiverVideoConstraints) {
  250. const lastNUpdated = this._receiverVideoConstraints.updateLastN(value);
  251. // Send out the message on the bridge channel if lastN was updated.
  252. lastNUpdated && this._rtc.setNewReceiverVideoConstraints(this._receiverVideoConstraints.constraints);
  253. return;
  254. }
  255. this._rtc.setLastN(value);
  256. }
  257. }
  258. /**
  259. * Sets the maximum video resolution the local participant should receive from remote participants.
  260. *
  261. * @param {number|undefined} maxFrameHeight - the new value.
  262. * @returns {void}
  263. */
  264. setPreferredReceiveMaxFrameHeight(maxFrameHeight) {
  265. this._maxFrameHeight = maxFrameHeight;
  266. for (const session of this._conference.getMediaSessions()) {
  267. if (session.isP2P || !this._receiverVideoConstraints) {
  268. session.setReceiverVideoConstraint(
  269. maxFrameHeight,
  270. this._getDefaultSourceReceiverConstraints(this._maxFrameHeight));
  271. } else {
  272. const resolutionUpdated = this._receiverVideoConstraints.updateReceiveResolution(maxFrameHeight);
  273. resolutionUpdated
  274. && this._rtc.setNewReceiverVideoConstraints(this._receiverVideoConstraints.constraints);
  275. }
  276. }
  277. }
  278. /**
  279. * Sets the receiver constraints for the conference.
  280. *
  281. * @param {Object} constraints The video constraints.
  282. */
  283. setReceiverConstraints(constraints) {
  284. if (!constraints) {
  285. return;
  286. }
  287. const isEndpointsFormat = Object.keys(constraints).includes('onStageEndpoints', 'selectedEndpoints');
  288. if (isEndpointsFormat) {
  289. throw new Error(
  290. '"onStageEndpoints" and "selectedEndpoints" are not supported when sourceNameSignaling is enabled.'
  291. );
  292. }
  293. const constraintsChanged = this._receiverVideoConstraints.updateReceiverVideoConstraints(constraints);
  294. if (constraintsChanged) {
  295. this._lastN = constraints.lastN ?? this._lastN;
  296. this._selectedEndpoints = constraints.selectedEndpoints ?? this._selectedEndpoints;
  297. this._rtc.setNewReceiverVideoConstraints(constraints);
  298. const p2pSession = this._conference.getMediaSessions().find(session => session.isP2P);
  299. if (!p2pSession) {
  300. return;
  301. }
  302. const mappedConstraints = Array.from(Object.entries(constraints.constraints))
  303. .map(constraint => {
  304. constraint[1] = constraint[1].maxHeight;
  305. return constraint;
  306. });
  307. this._sourceReceiverConstraints = new Map(mappedConstraints);
  308. // Send the receiver constraints to the peer through a "content-modify" message.
  309. p2pSession.setReceiverVideoConstraint(null, this._sourceReceiverConstraints);
  310. }
  311. }
  312. }