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.

SendVideoController.ts 6.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import { getLogger } from '@jitsi/logger';
  2. import JitsiConference from '../../JitsiConference';
  3. import JingleSessionPC from '../xmpp/JingleSessionPC';
  4. import MediaSessionEvents from '../xmpp/MediaSessionEvents';
  5. const logger = getLogger('modules/qualitycontrol/SendVideoController');
  6. const MAX_LOCAL_RESOLUTION = 2160;
  7. export interface IVideoConstraint {
  8. maxHeight: number;
  9. sourceName: string;
  10. }
  11. /**
  12. * The class manages send video constraints across media sessions({@link JingleSessionPC}) which belong to
  13. * {@link JitsiConference}. It finds the lowest common value, between the local user's send preference and
  14. * the remote party's receive preference. Also this module will consider only the active session's receive value,
  15. * because local tracks are shared and while JVB may have no preference, the remote p2p may have and they may be totally
  16. * different.
  17. */
  18. export default class SendVideoController {
  19. private _conference: JitsiConference;
  20. private _preferredSendMaxFrameHeight: number;
  21. /**
  22. * Source name based sender constraints.
  23. * @type {Map<string, number>};
  24. */
  25. private _sourceSenderConstraints: Map<string, number>;
  26. /**
  27. * Creates new instance for a given conference.
  28. *
  29. * @param {JitsiConference} conference - the conference instance for which the new instance will be managing
  30. * the send video quality constraints.
  31. */
  32. constructor(conference: JitsiConference) {
  33. this._conference = conference;
  34. this._preferredSendMaxFrameHeight = MAX_LOCAL_RESOLUTION;
  35. /**
  36. * Source name based sender constraints.
  37. * @type {Map<string, number>};
  38. */
  39. this._sourceSenderConstraints = new Map();
  40. }
  41. /**
  42. * Figures out the send video constraint as specified by {@link _selectSendMaxFrameHeight} and sets it on all media
  43. * sessions for the reasons mentioned in this class description.
  44. *
  45. * @param {string} sourceName - The source for which sender constraints have changed.
  46. * @returns {Promise<void>}
  47. * @private
  48. */
  49. async _propagateSendMaxFrameHeight(sourceName: string): Promise<void> {
  50. if (!sourceName) {
  51. throw new Error('sourceName missing for calculating the sendMaxHeight for video tracks');
  52. }
  53. const sendMaxFrameHeight = this._selectSendMaxFrameHeight(sourceName);
  54. const promises = [];
  55. if (sendMaxFrameHeight !== undefined && sendMaxFrameHeight >= 0) {
  56. for (const session of this._conference.getMediaSessions()) {
  57. promises.push(session.setSenderVideoConstraint(sendMaxFrameHeight, sourceName));
  58. }
  59. }
  60. await Promise.all(promises);
  61. }
  62. /**
  63. * Selects the lowest common value for the local video send constraint by looking at local user's preference and
  64. * the active media session's receive preference set by the remote party.
  65. *
  66. * @param {string} sourceName - The source for which sender constraints have changed.
  67. * @returns {number|undefined}
  68. * @private
  69. */
  70. _selectSendMaxFrameHeight(sourceName: string): number | undefined {
  71. if (!sourceName) {
  72. throw new Error('sourceName missing for calculating the sendMaxHeight for video tracks');
  73. }
  74. const activeMediaSession = this._conference.getActiveMediaSession();
  75. const remoteRecvMaxFrameHeight = activeMediaSession
  76. ? this._sourceSenderConstraints.get(sourceName)
  77. : undefined;
  78. if (this._preferredSendMaxFrameHeight >= 0 && remoteRecvMaxFrameHeight !== undefined && remoteRecvMaxFrameHeight >= 0) {
  79. return Math.min(this._preferredSendMaxFrameHeight, remoteRecvMaxFrameHeight);
  80. } else if (remoteRecvMaxFrameHeight !== undefined && remoteRecvMaxFrameHeight >= 0) {
  81. return remoteRecvMaxFrameHeight;
  82. }
  83. return this._preferredSendMaxFrameHeight;
  84. }
  85. /**
  86. * Configures the video encodings on the local sources when a media connection is established or becomes active.
  87. *
  88. * @returns {void}
  89. */
  90. configureConstraintsForLocalSources(): void {
  91. for (const track of this._conference.getLocalVideoTracks()) {
  92. const sourceName = track.getSourceName();
  93. sourceName && this._propagateSendMaxFrameHeight(sourceName);
  94. }
  95. }
  96. /**
  97. * Handles the {@link JitsiConferenceEvents.MEDIA_SESSION_STARTED}, that is when the conference creates new media
  98. * session. It doesn't mean it's already active though. For example the JVB connection may be created after
  99. * the conference has entered the p2p mode already.
  100. *
  101. * @param {JingleSessionPC} mediaSession - the started media session.
  102. */
  103. onMediaSessionStarted(mediaSession: JingleSessionPC): void {
  104. mediaSession.addListener(
  105. MediaSessionEvents.REMOTE_SOURCE_CONSTRAINTS_CHANGED,
  106. (session: JingleSessionPC, sourceConstraints: Array<IVideoConstraint>) => {
  107. session === this._conference.getActiveMediaSession()
  108. && sourceConstraints.forEach(constraint => this.onSenderConstraintsReceived(constraint));
  109. });
  110. }
  111. /**
  112. * Propagates the video constraints if they have changed.
  113. *
  114. * @param {IVideoConstraint} videoConstraints - The sender video constraints received from the bridge.
  115. * @returns {Promise<void>}
  116. */
  117. async onSenderConstraintsReceived(videoConstraints: IVideoConstraint): Promise<void> {
  118. const { maxHeight, sourceName } = videoConstraints;
  119. const localVideoTracks = this._conference.getLocalVideoTracks() ?? [];
  120. for (const track of localVideoTracks) {
  121. // Propagate the sender constraint only if it has changed.
  122. if (track.getSourceName() === sourceName
  123. && this._sourceSenderConstraints.get(sourceName) !== maxHeight) {
  124. this._sourceSenderConstraints.set(
  125. sourceName,
  126. maxHeight === -1
  127. ? Math.min(MAX_LOCAL_RESOLUTION, this._preferredSendMaxFrameHeight)
  128. : maxHeight);
  129. logger.debug(`Sender constraints for source:${sourceName} changed to maxHeight:${maxHeight}`);
  130. await this._propagateSendMaxFrameHeight(sourceName);
  131. }
  132. }
  133. }
  134. /**
  135. * Sets local preference for max send video frame height.
  136. *
  137. * @param {number} maxFrameHeight - the new value to set.
  138. * @returns {Promise<void>} - resolved when the operation is complete.
  139. */
  140. async setPreferredSendMaxFrameHeight(maxFrameHeight: number): Promise<void> {
  141. this._preferredSendMaxFrameHeight = maxFrameHeight;
  142. const promises: Promise<void>[] = [];
  143. for (const sourceName of this._sourceSenderConstraints.keys()) {
  144. promises.push(this._propagateSendMaxFrameHeight(sourceName));
  145. }
  146. await Promise.allSettled(promises);
  147. }
  148. }