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.js 6.8KB

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