Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

QualityController.ts 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. import { getLogger } from '@jitsi/logger';
  2. import JitsiConference from "../../JitsiConference";
  3. import { JitsiConferenceEvents } from "../../JitsiConferenceEvents";
  4. import { CodecMimeType } from "../../service/RTC/CodecMimeType";
  5. import RTCEvents from "../../service/RTC/RTCEvents";
  6. import JitsiLocalTrack from "../RTC/JitsiLocalTrack";
  7. import TraceablePeerConnection from "../RTC/TraceablePeerConnection";
  8. import JingleSessionPC from "../xmpp/JingleSessionPC";
  9. import { CodecSelection } from "./CodecSelection";
  10. import ReceiveVideoController from "./ReceiveVideoController";
  11. import SendVideoController from "./SendVideoController";
  12. import {
  13. DEFAULT_LAST_N,
  14. LAST_N_UNLIMITED,
  15. VIDEO_CODECS_BY_COMPLEXITY,
  16. VIDEO_QUALITY_LEVELS
  17. } from '../../service/RTC/StandardVideoQualitySettings';
  18. const logger = getLogger(__filename);
  19. // Period for which the client will wait for the cpu limitation flag to be reset in the peerconnection stats before it
  20. // attempts to rectify the situation by attempting a codec switch.
  21. const LIMITED_BY_CPU_TIMEOUT = 60000;
  22. // The min. value that lastN will be set to while trying to fix video qaulity issues.
  23. const MIN_LAST_N = 3;
  24. enum QualityLimitationReason {
  25. BANDWIDTH = 'bandwidth',
  26. CPU = 'cpu',
  27. NONE = 'none'
  28. };
  29. interface IResolution {
  30. height: number;
  31. width: number;
  32. }
  33. interface IOutboundRtpStats {
  34. codec: CodecMimeType;
  35. encodeTime: number;
  36. qualityLimitationReason: QualityLimitationReason;
  37. resolution: IResolution;
  38. timestamp: number;
  39. }
  40. interface ISourceStats {
  41. avgEncodeTime: number;
  42. codec: CodecMimeType;
  43. encodeResolution: number;
  44. localTrack: JitsiLocalTrack;
  45. qualityLimitationReason: QualityLimitationReason;
  46. timestamp: number;
  47. tpc: TraceablePeerConnection;
  48. };
  49. interface ITrackStats {
  50. encodeResolution: number
  51. encodeTime: number;
  52. qualityLimitationReason: QualityLimitationReason;
  53. }
  54. interface IVideoConstraints {
  55. maxHeight: number;
  56. sourceName: string;
  57. }
  58. export class FixedSizeArray {
  59. private _data: ISourceStats[];
  60. private _maxSize: number;
  61. constructor(size: number) {
  62. this._maxSize = size;
  63. this._data = [];
  64. }
  65. add(item: ISourceStats): void {
  66. if (this._data.length >= this._maxSize) {
  67. this._data.shift();
  68. }
  69. this._data.push(item);
  70. }
  71. get(index: number): ISourceStats | undefined {
  72. if (index < 0 || index >= this._data.length) {
  73. throw new Error("Index out of bounds");
  74. }
  75. return this._data[index];
  76. }
  77. size(): number {
  78. return this._data.length;
  79. }
  80. }
  81. /**
  82. * QualityController class that is responsible for maintaining optimal video quality experience on the local endpoint
  83. * by controlling the codec, encode resolution and receive resolution of the remote video streams. It also makes
  84. * adjustments based on the outbound and inbound rtp stream stats reported by the underlying peer connection.
  85. */
  86. export class QualityController {
  87. private _codecController: CodecSelection;
  88. private _conference: JitsiConference;
  89. private _enableAdaptiveMode: boolean;
  90. private _encodeTimeStats: Map<number, FixedSizeArray>;
  91. private _isLastNRampupBlocked: boolean;
  92. private _lastNRampupTime: number;
  93. private _lastNRampupTimeout: number | undefined;
  94. private _limitedByCpuTimeout: number | undefined;
  95. private _receiveVideoController: ReceiveVideoController;
  96. private _sendVideoController: SendVideoController;
  97. /**
  98. *
  99. * @param {JitsiConference} conference - The JitsiConference instance.
  100. * @param {Object} options - video quality settings passed through config.js.
  101. */
  102. constructor(conference: JitsiConference, options: {
  103. enableAdaptiveMode: boolean;
  104. jvb: Object;
  105. lastNRampupTime: number;
  106. p2p: Object;
  107. }) {
  108. this._conference = conference;
  109. const { jvb, p2p } = options;
  110. this._codecController = new CodecSelection(conference, { jvb, p2p });
  111. this._enableAdaptiveMode = options.enableAdaptiveMode;
  112. this._encodeTimeStats = new Map();
  113. this._isLastNRampupBlocked = false;
  114. this._lastNRampupTime = options.lastNRampupTime;
  115. this._receiveVideoController = new ReceiveVideoController(conference);
  116. this._sendVideoController = new SendVideoController(conference);
  117. this._conference.on(
  118. JitsiConferenceEvents._MEDIA_SESSION_STARTED,
  119. (session: JingleSessionPC) => {
  120. this._codecController.selectPreferredCodec(session);
  121. this._receiveVideoController.onMediaSessionStarted(session);
  122. this._sendVideoController.onMediaSessionStarted(session);
  123. });
  124. this._conference.on(
  125. JitsiConferenceEvents._MEDIA_SESSION_ACTIVE_CHANGED,
  126. () => this._sendVideoController.configureConstraintsForLocalSources());
  127. this._conference.on(
  128. JitsiConferenceEvents.CONFERENCE_VISITOR_CODECS_CHANGED,
  129. (codecList: CodecMimeType[]) => this._codecController.updateVisitorCodecs(codecList));
  130. // Debounce the calls to codec selection when there is a burst of joins and leaves.
  131. const debouncedSelectCodec = this._debounce(
  132. () => this._codecController.selectPreferredCodec(this._conference.jvbJingleSession),
  133. 1000);
  134. this._conference.on(JitsiConferenceEvents.USER_JOINED, debouncedSelectCodec.bind(this));
  135. this._conference.on(JitsiConferenceEvents.USER_LEFT, debouncedSelectCodec.bind(this));
  136. this._conference.rtc.on(
  137. RTCEvents.SENDER_VIDEO_CONSTRAINTS_CHANGED,
  138. (videoConstraints: IVideoConstraints) => this._sendVideoController.onSenderConstraintsReceived(videoConstraints));
  139. this._conference.on(
  140. JitsiConferenceEvents.ENCODE_TIME_STATS_RECEIVED,
  141. (tpc: TraceablePeerConnection, stats: Map<number, IOutboundRtpStats>) => this._processOutboundRtpStats(tpc, stats));
  142. }
  143. /**
  144. * Creates a debounced function that delays the execution of the provided function until after the specified delay
  145. * has elapsed. Unlike typical debounce implementations, the timer does not reset when the function is called again
  146. * within the delay period.
  147. *
  148. * @param {Function} func - The function to be debounced.
  149. * @param {number} delay - The delay in milliseconds.
  150. * @returns {Function} - The debounced function.
  151. */
  152. _debounce(func: Function, delay: number) {
  153. return function (...args) {
  154. if (!this._timer) {
  155. this._timer = setTimeout(() => {
  156. this._timer = null;
  157. func.apply(this, args);
  158. }, delay);
  159. }
  160. };
  161. }
  162. /**
  163. * Adjusts the lastN value so that fewer remote video sources are received from the bridge in an attempt to improve
  164. * encode resolution of the outbound video streams based on cpuLimited parameter passed. If cpuLimited is false,
  165. * the lastN value will slowly be ramped back up to the channelLastN value set in config.js.
  166. *
  167. * @param {boolean} cpuLimited - whether the endpoint is cpu limited or not.
  168. * @returns boolean - Returns true if an action was taken, false otherwise.
  169. */
  170. _lowerOrRaiseLastN(cpuLimited: boolean): boolean {
  171. const lastN = this.receiveVideoController.getLastN();
  172. let newLastN = lastN;
  173. if (cpuLimited && (lastN !== LAST_N_UNLIMITED && lastN <= MIN_LAST_N)) {
  174. return false;
  175. }
  176. // If channelLastN is not set or set to -1 in config.js, the client will ramp up lastN to only up to 25.
  177. let { channelLastN = DEFAULT_LAST_N } = this._conference.options.config;
  178. channelLastN = channelLastN === LAST_N_UNLIMITED ? DEFAULT_LAST_N : channelLastN;
  179. if (cpuLimited) {
  180. const videoStreamsReceived = this._conference.getForwardedSources().length;
  181. newLastN = Math.floor(videoStreamsReceived / 2);
  182. if (newLastN < MIN_LAST_N) {
  183. newLastN = MIN_LAST_N;
  184. }
  185. // Increment lastN by 1 every LAST_N_RAMPUP_TIME (60) secs.
  186. } else if (lastN < channelLastN) {
  187. newLastN++;
  188. }
  189. if (newLastN === lastN) {
  190. return false;
  191. }
  192. const isStillLimitedByCpu = newLastN < channelLastN;
  193. this.receiveVideoController.setLastNLimitedByCpu(isStillLimitedByCpu);
  194. logger.info(`QualityController - setting lastN=${newLastN}, limitedByCpu=${isStillLimitedByCpu}`);
  195. this.receiveVideoController.setLastN(newLastN);
  196. return true;
  197. }
  198. /**
  199. * Adjusts the requested resolution for remote video sources by updating the receiver constraints in an attempt to
  200. * improve the encode resolution of the outbound video streams.
  201. * @return {void}
  202. */
  203. _maybeLowerReceiveResolution(): void {
  204. const currentConstraints = this.receiveVideoController.getCurrentReceiverConstraints();
  205. const individualConstraints = currentConstraints.constraints;
  206. let maxHeight = 0;
  207. if (individualConstraints && Object.keys(individualConstraints).length) {
  208. for (const value of Object.values(individualConstraints)) {
  209. const v: any = value;
  210. maxHeight = Math.max(maxHeight, v.maxHeight);
  211. }
  212. }
  213. const currentLevel = VIDEO_QUALITY_LEVELS.findIndex(lvl => lvl.height <= maxHeight);
  214. // Do not lower the resolution to less than 180p.
  215. if (VIDEO_QUALITY_LEVELS[currentLevel].height === 180) {
  216. return;
  217. }
  218. this.receiveVideoController.setPreferredReceiveMaxFrameHeight(VIDEO_QUALITY_LEVELS[currentLevel + 1].height);
  219. }
  220. /**
  221. * Updates the codec preference order for the local endpoint on the active media session and switches the video
  222. * codec if needed.
  223. *
  224. * @param {number} trackId - The track ID of the local video track for which stats have been captured.
  225. * @returns {boolean} - Returns true if video codec was changed.
  226. */
  227. _maybeSwitchVideoCodec(trackId: number): boolean {
  228. const stats = this._encodeTimeStats.get(trackId);
  229. const { codec, encodeResolution, localTrack } = stats.get(stats.size() - 1);
  230. const codecsByVideoType = VIDEO_CODECS_BY_COMPLEXITY[localTrack.getVideoType()];
  231. const codecIndex = codecsByVideoType.findIndex(val => val === codec.toLowerCase());
  232. // Do nothing if the encoder is using the lowest complexity codec already.
  233. if (codecIndex === codecsByVideoType.length - 1) {
  234. return false;
  235. }
  236. if (!this._limitedByCpuTimeout) {
  237. this._limitedByCpuTimeout = window.setTimeout(() => {
  238. this._limitedByCpuTimeout = undefined;
  239. const updatedStats = this._encodeTimeStats.get(trackId);
  240. const latestSourceStats: ISourceStats = updatedStats.get(updatedStats.size() - 1);
  241. // If the encoder is still limited by CPU, switch to a lower complexity codec.
  242. if (latestSourceStats.qualityLimitationReason === QualityLimitationReason.CPU
  243. || encodeResolution < Math.min(localTrack.maxEnabledResolution, localTrack.getCaptureResolution())) {
  244. return this.codecController.changeCodecPreferenceOrder(localTrack, codec)
  245. }
  246. }, LIMITED_BY_CPU_TIMEOUT);
  247. }
  248. return false;
  249. }
  250. /**
  251. * Adjusts codec, lastN or receive resolution based on the send resolution (of the outbound streams) and limitation
  252. * reported by the browser in the WebRTC stats. Recovery is also attempted if the limitation goes away. No action
  253. * is taken if the adaptive mode has been disabled through config.js.
  254. *
  255. * @param {ISourceStats} sourceStats - The outbound-rtp stats for a local video track.
  256. * @returns {void}
  257. */
  258. _performQualityOptimizations(sourceStats: ISourceStats): void {
  259. // Do not attempt run time adjustments if the adaptive mode is disabled.
  260. if (!this._enableAdaptiveMode) {
  261. return;
  262. }
  263. const { encodeResolution, localTrack, qualityLimitationReason, tpc } = sourceStats;
  264. const trackId = localTrack.rtcId;
  265. if (encodeResolution === tpc.calculateExpectedSendResolution(localTrack)) {
  266. if (this._limitedByCpuTimeout) {
  267. window.clearTimeout(this._limitedByCpuTimeout);
  268. this._limitedByCpuTimeout = undefined;
  269. }
  270. if (qualityLimitationReason === QualityLimitationReason.NONE
  271. && this.receiveVideoController.isLastNLimitedByCpu()) {
  272. if (!this._lastNRampupTimeout && !this._isLastNRampupBlocked) {
  273. // Ramp up the number of received videos if CPU limitation no longer exists. If the cpu
  274. // limitation returns as a consequence, do not attempt to ramp up again, continue to
  275. // increment the lastN value otherwise until it is equal to the channelLastN value.
  276. this._lastNRampupTimeout = window.setTimeout(() => {
  277. this._lastNRampupTimeout = undefined;
  278. const updatedStats = this._encodeTimeStats.get(trackId);
  279. const latestSourceStats: ISourceStats = updatedStats.get(updatedStats.size() - 1);
  280. if (latestSourceStats.qualityLimitationReason === QualityLimitationReason.CPU) {
  281. this._isLastNRampupBlocked = true;
  282. } else {
  283. this._lowerOrRaiseLastN(false /* raise */);
  284. }
  285. }, this._lastNRampupTime);
  286. }
  287. }
  288. return;
  289. }
  290. // Do nothing if the limitation reason is bandwidth since the browser will dynamically adapt the outbound
  291. // resolution based on available uplink bandwith. Otherwise,
  292. // 1. Switch the codec to the lowest complexity one incrementally.
  293. // 2. Switch to a lower lastN value, cutting the receive videos by half in every iteration until
  294. // MIN_LAST_N value is reached.
  295. // 3. Lower the receive resolution of individual streams up to 180p.
  296. if (qualityLimitationReason === QualityLimitationReason.CPU) {
  297. if (this._lastNRampupTimeout) {
  298. window.clearTimeout(this._lastNRampupTimeout);
  299. this._lastNRampupTimeout = undefined;
  300. this._isLastNRampupBlocked = true;
  301. }
  302. const codecSwitched = this._maybeSwitchVideoCodec(trackId);
  303. if (!codecSwitched && !this._limitedByCpuTimeout) {
  304. const lastNChanged = this._lowerOrRaiseLastN(true /* lower */);
  305. if (!lastNChanged) {
  306. this.receiveVideoController.setReceiveResolutionLimitedByCpu(true);
  307. this._maybeLowerReceiveResolution();
  308. }
  309. }
  310. }
  311. }
  312. /**
  313. * Processes the outbound RTP stream stats as reported by the WebRTC peerconnection and makes runtime adjustments
  314. * to the client for better quality experience if the adaptive mode is enabled.
  315. *
  316. * @param {TraceablePeerConnection} tpc - The underlying WebRTC peerconnection where stats have been captured.
  317. * @param {Map<number, IOutboundRtpStats>} stats - Outbound-rtp stream stats per SSRC.
  318. * @returns void
  319. */
  320. _processOutboundRtpStats(tpc: TraceablePeerConnection, stats: Map<number, IOutboundRtpStats>): void {
  321. const activeSession = this._conference.getActiveMediaSession();
  322. // Process stats only for the active media session.
  323. if (activeSession.peerconnection !== tpc) {
  324. return;
  325. }
  326. const statsPerTrack = new Map();
  327. for (const ssrc of stats.keys()) {
  328. const { codec, encodeTime, qualityLimitationReason, resolution, timestamp } = stats.get(ssrc);
  329. const track = tpc.getTrackBySSRC(ssrc);
  330. const trackId = track.rtcId;
  331. let existingStats = statsPerTrack.get(trackId);
  332. const encodeResolution = Math.min(resolution.height, resolution.width);
  333. const ssrcStats = {
  334. encodeResolution,
  335. encodeTime,
  336. qualityLimitationReason
  337. };
  338. if (existingStats) {
  339. existingStats.codec = codec;
  340. existingStats.timestamp = timestamp;
  341. existingStats.trackStats.push(ssrcStats);
  342. } else {
  343. existingStats = {
  344. codec,
  345. timestamp,
  346. trackStats: [ ssrcStats ]
  347. };
  348. statsPerTrack.set(trackId, existingStats);
  349. }
  350. }
  351. // Aggregate the stats for multiple simulcast streams with different SSRCs but for the same video stream.
  352. for (const trackId of statsPerTrack.keys()) {
  353. const { codec, timestamp, trackStats } = statsPerTrack.get(trackId);
  354. const totalEncodeTime = trackStats
  355. .map((stat: ITrackStats) => stat.encodeTime)
  356. .reduce((totalValue: number, currentValue: number) => totalValue + currentValue, 0);
  357. const avgEncodeTime: number = totalEncodeTime / trackStats.length;
  358. const { qualityLimitationReason = QualityLimitationReason.NONE }
  359. = trackStats
  360. .find((stat: ITrackStats) => stat.qualityLimitationReason !== QualityLimitationReason.NONE) ?? {};
  361. const encodeResolution: number = trackStats
  362. .map((stat: ITrackStats) => stat.encodeResolution)
  363. .reduce((resolution: number, currentValue: number) => Math.max(resolution, currentValue), 0);
  364. const localTrack = this._conference.getLocalVideoTracks().find(t => t.rtcId === trackId);
  365. const exisitingStats: FixedSizeArray = this._encodeTimeStats.get(trackId);
  366. const sourceStats = {
  367. avgEncodeTime,
  368. codec,
  369. encodeResolution,
  370. qualityLimitationReason,
  371. localTrack,
  372. timestamp,
  373. tpc
  374. };
  375. if (exisitingStats) {
  376. exisitingStats.add(sourceStats);
  377. } else {
  378. // Save stats for only the last 5 mins.
  379. const data = new FixedSizeArray(300);
  380. data.add(sourceStats);
  381. this._encodeTimeStats.set(trackId, data);
  382. }
  383. logger.debug(`Encode stats for ${localTrack}: codec=${codec}, time=${avgEncodeTime},`
  384. + `resolution=${encodeResolution}, qualityLimitationReason=${qualityLimitationReason}`);
  385. this._performQualityOptimizations(sourceStats);
  386. }
  387. }
  388. get codecController() {
  389. return this._codecController;
  390. }
  391. get receiveVideoController() {
  392. return this._receiveVideoController;
  393. }
  394. get sendVideoController() {
  395. return this._sendVideoController;
  396. }
  397. }