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 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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 = 2160;
  7. const LASTN_UNLIMITED = -1;
  8. const ASSUMED_BANDWIDTH_BPS = -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. * @param {Object} options - The instance options:
  17. * - lastN: Number of videos to be requested from the bridge.
  18. * - assumedBandwidthBps: Number of bps to be requested from the bridge.
  19. */
  20. constructor(options) {
  21. const { lastN, assumedBandwidthBps } = options;
  22. // The number of videos requested from the bridge.
  23. this._lastN = lastN ?? LASTN_UNLIMITED;
  24. // The number representing the maximum video height the local client should receive from the bridge/peer.
  25. this._maxFrameHeight = MAX_HEIGHT;
  26. // The number representing the assumed count of bps the local client should receive from the bridge.
  27. this._assumedBandwidthBps = assumedBandwidthBps ?? ASSUMED_BANDWIDTH_BPS;
  28. this._receiverVideoConstraints = {
  29. assumedBandwidthBps: this._assumedBandwidthBps,
  30. constraints: {},
  31. defaultConstraints: { 'maxHeight': this._maxFrameHeight },
  32. lastN: this._lastN
  33. };
  34. }
  35. /**
  36. * Returns the receiver video constraints that need to be sent on the bridge channel or to the remote peer.
  37. */
  38. get constraints() {
  39. this._receiverVideoConstraints.assumedBandwidthBps = this._assumedBandwidthBps;
  40. this._receiverVideoConstraints.lastN = this._lastN;
  41. const individualConstraints = this._receiverVideoConstraints.constraints;
  42. if (individualConstraints && Object.keys(individualConstraints).length) {
  43. /* eslint-disable no-unused-vars */
  44. for (const [ key, value ] of Object.entries(individualConstraints)) {
  45. value.maxHeight = this._maxFrameHeight;
  46. }
  47. } else {
  48. this._receiverVideoConstraints.defaultConstraints = { 'maxHeight': this._maxFrameHeight };
  49. }
  50. return this._receiverVideoConstraints;
  51. }
  52. /**
  53. * Updates the assumed bandwidth bps of the ReceiverVideoConstraints sent to the bridge.
  54. *
  55. * @param {number} assumedBandwidthBps
  56. * @requires {boolean} Returns true if the the value has been updated, false otherwise.
  57. */
  58. updateAssumedBandwidthBps(assumedBandwidthBps) {
  59. const changed = this._assumedBandwidthBps !== assumedBandwidthBps;
  60. if (changed) {
  61. this._assumedBandwidthBps = assumedBandwidthBps;
  62. logger.debug(`Updating receive assumedBandwidthBps: ${assumedBandwidthBps}`);
  63. }
  64. return changed;
  65. }
  66. /**
  67. * Updates the lastN field of the ReceiverVideoConstraints sent to the bridge.
  68. *
  69. * @param {number} value
  70. * @returns {boolean} Returns true if the the value has been updated, false otherwise.
  71. */
  72. updateLastN(value) {
  73. const changed = this._lastN !== value;
  74. if (changed) {
  75. this._lastN = value;
  76. logger.debug(`Updating ReceiverVideoConstraints lastN(${value})`);
  77. }
  78. return changed;
  79. }
  80. /**
  81. * Updates the resolution (height requested) in the contraints field of the ReceiverVideoConstraints
  82. * sent to the bridge.
  83. *
  84. * @param {number} maxFrameHeight
  85. * @requires {boolean} Returns true if the the value has been updated, false otherwise.
  86. */
  87. updateReceiveResolution(maxFrameHeight) {
  88. const changed = this._maxFrameHeight !== maxFrameHeight;
  89. if (changed) {
  90. this._maxFrameHeight = maxFrameHeight;
  91. logger.debug(`Updating receive maxFrameHeight: ${maxFrameHeight}`);
  92. }
  93. return changed;
  94. }
  95. /**
  96. * Updates the receiver constraints sent to the bridge.
  97. *
  98. * @param {Object} videoConstraints
  99. * @returns {boolean} Returns true if the the value has been updated, false otherwise.
  100. */
  101. updateReceiverVideoConstraints(videoConstraints) {
  102. const changed = !isEqual(this._receiverVideoConstraints, videoConstraints);
  103. if (changed) {
  104. this._receiverVideoConstraints = videoConstraints;
  105. if (videoConstraints.defaultConstraints?.maxHeight) {
  106. this.updateReceiveResolution(videoConstraints.defaultConstraints.maxHeight);
  107. }
  108. logger.debug(`Updating ReceiverVideoConstraints ${JSON.stringify(videoConstraints)}`);
  109. }
  110. return changed;
  111. }
  112. }
  113. /**
  114. * This class manages the receive video contraints for a given {@link JitsiConference}. These constraints are
  115. * determined by the application based on how the remote video streams need to be displayed. This class is responsible
  116. * for communicating these constraints to the bridge over the bridge channel.
  117. */
  118. export default class ReceiveVideoController {
  119. /**
  120. * Creates a new instance for a given conference.
  121. *
  122. * @param {JitsiConference} conference the conference instance for which the new instance will be managing
  123. * the receive video quality constraints.
  124. * @param {RTC} rtc the rtc instance which is responsible for initializing the bridge channel.
  125. */
  126. constructor(conference, rtc) {
  127. this._conference = conference;
  128. this._rtc = rtc;
  129. const { config } = conference.options;
  130. // The number of videos requested from the bridge, -1 represents unlimited or all available videos.
  131. this._lastN = config?.startLastN ?? (config?.channelLastN || LASTN_UNLIMITED);
  132. // The number representing the maximum video height the local client should receive from the bridge.
  133. this._maxFrameHeight = MAX_HEIGHT;
  134. /**
  135. * The map that holds the max frame height requested per remote source for p2p connection.
  136. *
  137. * @type Map<string, number>
  138. */
  139. this._sourceReceiverConstraints = new Map();
  140. /**
  141. * The number of bps requested from the bridge.
  142. */
  143. this._assumedBandwidthBps = ASSUMED_BANDWIDTH_BPS;
  144. // The default receiver video constraints.
  145. this._receiverVideoConstraints = new ReceiverVideoConstraints({
  146. lastN: this._lastN,
  147. assumedBandwidthBps: this._assumedBandwidthBps
  148. });
  149. this._conference.on(
  150. JitsiConferenceEvents._MEDIA_SESSION_STARTED,
  151. session => this._onMediaSessionStarted(session));
  152. }
  153. /**
  154. * Returns a map of all the remote source names and the corresponding max frame heights.
  155. *
  156. * @param {JingleSessionPC} mediaSession - the media session.
  157. * @param {number} maxFrameHeight - the height to be requested for remote sources.
  158. * @returns
  159. */
  160. _getDefaultSourceReceiverConstraints(mediaSession, maxFrameHeight) {
  161. const height = maxFrameHeight ?? MAX_HEIGHT;
  162. const remoteVideoTracks = mediaSession.peerconnection?.getRemoteTracks(null, MediaType.VIDEO) || [];
  163. const receiverConstraints = new Map();
  164. for (const track of remoteVideoTracks) {
  165. receiverConstraints.set(track.getSourceName(), height);
  166. }
  167. return receiverConstraints;
  168. }
  169. /**
  170. * Handles the {@link JitsiConferenceEvents.MEDIA_SESSION_STARTED}, that is when the conference creates new media
  171. * session. The preferred receive frameHeight is applied on the media session.
  172. *
  173. * @param {JingleSessionPC} mediaSession - the started media session.
  174. * @returns {void}
  175. * @private
  176. */
  177. _onMediaSessionStarted(mediaSession) {
  178. if (mediaSession.isP2P) {
  179. mediaSession.setReceiverVideoConstraint(this._getDefaultSourceReceiverConstraints(mediaSession));
  180. } else {
  181. this._rtc.setReceiverVideoConstraints(this._receiverVideoConstraints.constraints);
  182. }
  183. }
  184. /**
  185. * Returns the lastN value for the conference.
  186. *
  187. * @returns {number}
  188. */
  189. getLastN() {
  190. return this._lastN;
  191. }
  192. /**
  193. * Sets the assumed bandwidth bps the local participant should receive from remote participants.
  194. *
  195. * @param {number|undefined} assumedBandwidthBps - the new value.
  196. * @returns {void}
  197. */
  198. setAssumedBandwidthBps(assumedBandwidthBps) {
  199. if (this._receiverVideoConstraints.updateAssumedBandwidthBps(assumedBandwidthBps)) {
  200. this._rtc.setReceiverVideoConstraints(this._receiverVideoConstraints.constraints);
  201. }
  202. }
  203. /**
  204. * Selects a new value for "lastN". The requested amount of videos are going to be delivered after the value is
  205. * in effect. Set to -1 for unlimited or all available videos.
  206. *
  207. * @param {number} value the new value for lastN.
  208. * @returns {void}
  209. */
  210. setLastN(value) {
  211. if (this._lastN !== value) {
  212. this._lastN = value;
  213. if (this._receiverVideoConstraints.updateLastN(value)) {
  214. this._rtc.setReceiverVideoConstraints(this._receiverVideoConstraints.constraints);
  215. }
  216. }
  217. }
  218. /**
  219. * Sets the maximum video resolution the local participant should receive from remote participants.
  220. *
  221. * @param {number|undefined} maxFrameHeight - the new value.
  222. * @returns {void}
  223. */
  224. setPreferredReceiveMaxFrameHeight(maxFrameHeight) {
  225. this._maxFrameHeight = maxFrameHeight;
  226. for (const session of this._conference.getMediaSessions()) {
  227. if (session.isP2P) {
  228. session.setReceiverVideoConstraint(this._getDefaultSourceReceiverConstraints(session, maxFrameHeight));
  229. } else if (this._receiverVideoConstraints.updateReceiveResolution(maxFrameHeight)) {
  230. this._rtc.setReceiverVideoConstraints(this._receiverVideoConstraints.constraints);
  231. }
  232. }
  233. }
  234. /**
  235. * Sets the receiver constraints for the conference.
  236. *
  237. * @param {Object} constraints The video constraints.
  238. */
  239. setReceiverConstraints(constraints) {
  240. if (!constraints) {
  241. return;
  242. }
  243. const isEndpointsFormat = Object.keys(constraints).includes('onStageEndpoints', 'selectedEndpoints');
  244. if (isEndpointsFormat) {
  245. throw new Error(
  246. '"onStageEndpoints" and "selectedEndpoints" are not supported when sourceNameSignaling is enabled.'
  247. );
  248. }
  249. const constraintsChanged = this._receiverVideoConstraints.updateReceiverVideoConstraints(constraints);
  250. if (constraintsChanged) {
  251. this._assumedBandwidthBps = constraints.assumedBandwidthBps ?? this._assumedBandwidthBps;
  252. this._lastN = constraints.lastN ?? this._lastN;
  253. // Send the contraints on the bridge channel.
  254. this._rtc.setReceiverVideoConstraints(constraints);
  255. const p2pSession = this._conference.getMediaSessions().find(session => session.isP2P);
  256. if (!p2pSession || !constraints.constraints) {
  257. return;
  258. }
  259. const mappedConstraints = Array.from(Object.entries(constraints.constraints))
  260. .map(constraint => {
  261. constraint[1] = constraint[1].maxHeight;
  262. return constraint;
  263. });
  264. this._sourceReceiverConstraints = new Map(mappedConstraints);
  265. // Send the receiver constraints to the peer through a "content-modify" message.
  266. p2pSession.setReceiverVideoConstraint(this._sourceReceiverConstraints);
  267. }
  268. }
  269. }