Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

ReceiveVideoController.js 15KB

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