Home Reference Source

src/controller/abr-controller.ts

  1. import EwmaBandWidthEstimator from '../utils/ewma-bandwidth-estimator';
  2. import { Events } from '../events';
  3. import { BufferHelper } from '../utils/buffer-helper';
  4. import { ErrorDetails } from '../errors';
  5. import { PlaylistLevelType } from '../types/loader';
  6. import { logger } from '../utils/logger';
  7. import type { Bufferable } from '../utils/buffer-helper';
  8. import type { Fragment } from '../loader/fragment';
  9. import type { Part } from '../loader/fragment';
  10. import type { LoaderStats } from '../types/loader';
  11. import type Hls from '../hls';
  12. import type {
  13. FragLoadingData,
  14. FragLoadedData,
  15. FragBufferedData,
  16. ErrorData,
  17. LevelLoadedData,
  18. } from '../types/events';
  19. import type { ComponentAPI } from '../types/component-api';
  20.  
  21. class AbrController implements ComponentAPI {
  22. protected hls: Hls;
  23. private lastLoadedFragLevel: number = 0;
  24. private _nextAutoLevel: number = -1;
  25. private timer?: number;
  26. private onCheck: Function = this._abandonRulesCheck.bind(this);
  27. private fragCurrent: Fragment | null = null;
  28. private partCurrent: Part | null = null;
  29. private bitrateTestDelay: number = 0;
  30.  
  31. public readonly bwEstimator: EwmaBandWidthEstimator;
  32.  
  33. constructor(hls: Hls) {
  34. this.hls = hls;
  35.  
  36. const config = hls.config;
  37. this.bwEstimator = new EwmaBandWidthEstimator(
  38. config.abrEwmaSlowVoD,
  39. config.abrEwmaFastVoD,
  40. config.abrEwmaDefaultEstimate
  41. );
  42.  
  43. this.registerListeners();
  44. }
  45.  
  46. protected registerListeners() {
  47. const { hls } = this;
  48. hls.on(Events.FRAG_LOADING, this.onFragLoading, this);
  49. hls.on(Events.FRAG_LOADED, this.onFragLoaded, this);
  50. hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
  51. hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
  52. hls.on(Events.ERROR, this.onError, this);
  53. }
  54.  
  55. protected unregisterListeners() {
  56. const { hls } = this;
  57. hls.off(Events.FRAG_LOADING, this.onFragLoading, this);
  58. hls.off(Events.FRAG_LOADED, this.onFragLoaded, this);
  59. hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
  60. hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
  61. hls.off(Events.ERROR, this.onError, this);
  62. }
  63.  
  64. public destroy() {
  65. this.unregisterListeners();
  66. this.clearTimer();
  67. // @ts-ignore
  68. this.hls = this.onCheck = null;
  69. this.fragCurrent = this.partCurrent = null;
  70. }
  71.  
  72. protected onFragLoading(event: Events.FRAG_LOADING, data: FragLoadingData) {
  73. const frag = data.frag;
  74. if (frag.type === PlaylistLevelType.MAIN) {
  75. if (!this.timer) {
  76. this.fragCurrent = frag;
  77. this.partCurrent = data.part ?? null;
  78. this.timer = self.setInterval(this.onCheck, 100);
  79. }
  80. }
  81. }
  82.  
  83. protected onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
  84. const config = this.hls.config;
  85. if (data.details.live) {
  86. this.bwEstimator.update(config.abrEwmaSlowLive, config.abrEwmaFastLive);
  87. } else {
  88. this.bwEstimator.update(config.abrEwmaSlowVoD, config.abrEwmaFastVoD);
  89. }
  90. }
  91.  
  92. /*
  93. This method monitors the download rate of the current fragment, and will downswitch if that fragment will not load
  94. quickly enough to prevent underbuffering
  95. */
  96. private _abandonRulesCheck() {
  97. const { fragCurrent: frag, partCurrent: part, hls } = this;
  98. const { autoLevelEnabled, config, media } = hls;
  99. if (!frag || !media) {
  100. return;
  101. }
  102.  
  103. const stats: LoaderStats = part ? part.stats : frag.stats;
  104. const duration = part ? part.duration : frag.duration;
  105. // If loading has been aborted and not in lowLatencyMode, stop timer and return
  106. if (stats.aborted) {
  107. logger.warn('frag loader destroy or aborted, disarm abandonRules');
  108. this.clearTimer();
  109. // reset forced auto level value so that next level will be selected
  110. this._nextAutoLevel = -1;
  111. return;
  112. }
  113.  
  114. // This check only runs if we're in ABR mode and actually playing
  115. if (
  116. !autoLevelEnabled ||
  117. media.paused ||
  118. !media.playbackRate ||
  119. !media.readyState
  120. ) {
  121. return;
  122. }
  123.  
  124. const requestDelay = performance.now() - stats.loading.start;
  125. const playbackRate = Math.abs(media.playbackRate);
  126. // In order to work with a stable bandwidth, only begin monitoring bandwidth after half of the fragment has been loaded
  127. if (requestDelay <= (500 * duration) / playbackRate) {
  128. return;
  129. }
  130.  
  131. const { levels, minAutoLevel } = hls;
  132. const level = levels[frag.level];
  133. const expectedLen =
  134. stats.total ||
  135. Math.max(stats.loaded, Math.round((duration * level.maxBitrate) / 8));
  136. const loadRate = Math.max(
  137. 1,
  138. stats.bwEstimate
  139. ? stats.bwEstimate / 8
  140. : (stats.loaded * 1000) / requestDelay
  141. );
  142. // fragLoadDelay is an estimate of the time (in seconds) it will take to buffer the entire fragment
  143. const fragLoadedDelay = (expectedLen - stats.loaded) / loadRate;
  144.  
  145. const pos = media.currentTime;
  146. // bufferStarvationDelay is an estimate of the amount time (in seconds) it will take to exhaust the buffer
  147. const bufferStarvationDelay =
  148. (BufferHelper.bufferInfo(media, pos, config.maxBufferHole).end - pos) /
  149. playbackRate;
  150.  
  151. // Attempt an emergency downswitch only if less than 2 fragment lengths are buffered, and the time to finish loading
  152. // the current fragment is greater than the amount of buffer we have left
  153. if (
  154. bufferStarvationDelay >= (2 * duration) / playbackRate ||
  155. fragLoadedDelay <= bufferStarvationDelay
  156. ) {
  157. return;
  158. }
  159.  
  160. let fragLevelNextLoadedDelay: number = Number.POSITIVE_INFINITY;
  161. let nextLoadLevel: number;
  162. // Iterate through lower level and try to find the largest one that avoids rebuffering
  163. for (
  164. nextLoadLevel = frag.level - 1;
  165. nextLoadLevel > minAutoLevel;
  166. nextLoadLevel--
  167. ) {
  168. // compute time to load next fragment at lower level
  169. // 0.8 : consider only 80% of current bw to be conservative
  170. // 8 = bits per byte (bps/Bps)
  171. const levelNextBitrate = levels[nextLoadLevel].maxBitrate;
  172. fragLevelNextLoadedDelay =
  173. (duration * levelNextBitrate) / (8 * 0.8 * loadRate);
  174.  
  175. if (fragLevelNextLoadedDelay < bufferStarvationDelay) {
  176. break;
  177. }
  178. }
  179. // Only emergency switch down if it takes less time to load a new fragment at lowest level instead of continuing
  180. // to load the current one
  181. if (fragLevelNextLoadedDelay >= fragLoadedDelay) {
  182. return;
  183. }
  184. const bwEstimate: number = this.bwEstimator.getEstimate();
  185. logger.warn(`Fragment ${frag.sn}${
  186. part ? ' part ' + part.index : ''
  187. } of level ${
  188. frag.level
  189. } is loading too slowly and will cause an underbuffer; aborting and switching to level ${nextLoadLevel}
  190. Current BW estimate: ${
  191. Number.isFinite(bwEstimate) ? (bwEstimate / 1024).toFixed(3) : 'Unknown'
  192. } Kb/s
  193. Estimated load time for current fragment: ${fragLoadedDelay.toFixed(3)} s
  194. Estimated load time for the next fragment: ${fragLevelNextLoadedDelay.toFixed(
  195. 3
  196. )} s
  197. Time to underbuffer: ${bufferStarvationDelay.toFixed(3)} s`);
  198. hls.nextLoadLevel = nextLoadLevel;
  199. this.bwEstimator.sample(requestDelay, stats.loaded);
  200. this.clearTimer();
  201. if (frag.loader) {
  202. this.fragCurrent = this.partCurrent = null;
  203. frag.loader.abort();
  204. }
  205. hls.trigger(Events.FRAG_LOAD_EMERGENCY_ABORTED, { frag, part, stats });
  206. }
  207.  
  208. protected onFragLoaded(
  209. event: Events.FRAG_LOADED,
  210. { frag, part }: FragLoadedData
  211. ) {
  212. if (
  213. frag.type === PlaylistLevelType.MAIN &&
  214. Number.isFinite(frag.sn as number)
  215. ) {
  216. const stats = part ? part.stats : frag.stats;
  217. const duration = part ? part.duration : frag.duration;
  218. // stop monitoring bw once frag loaded
  219. this.clearTimer();
  220. // store level id after successful fragment load
  221. this.lastLoadedFragLevel = frag.level;
  222. // reset forced auto level value so that next level will be selected
  223. this._nextAutoLevel = -1;
  224.  
  225. // compute level average bitrate
  226. if (this.hls.config.abrMaxWithRealBitrate) {
  227. const level = this.hls.levels[frag.level];
  228. const loadedBytes =
  229. (level.loaded ? level.loaded.bytes : 0) + stats.loaded;
  230. const loadedDuration =
  231. (level.loaded ? level.loaded.duration : 0) + duration;
  232. level.loaded = { bytes: loadedBytes, duration: loadedDuration };
  233. level.realBitrate = Math.round((8 * loadedBytes) / loadedDuration);
  234. }
  235. if (frag.bitrateTest) {
  236. const fragBufferedData: FragBufferedData = {
  237. stats,
  238. frag,
  239. part,
  240. id: frag.type,
  241. };
  242. this.onFragBuffered(Events.FRAG_BUFFERED, fragBufferedData);
  243. frag.bitrateTest = false;
  244. }
  245. }
  246. }
  247.  
  248. protected onFragBuffered(
  249. event: Events.FRAG_BUFFERED,
  250. data: FragBufferedData
  251. ) {
  252. const { frag, part } = data;
  253. const stats = part ? part.stats : frag.stats;
  254.  
  255. if (stats.aborted) {
  256. return;
  257. }
  258. // Only count non-alt-audio frags which were actually buffered in our BW calculations
  259. if (frag.type !== PlaylistLevelType.MAIN || frag.sn === 'initSegment') {
  260. return;
  261. }
  262. // Use the difference between parsing and request instead of buffering and request to compute fragLoadingProcessing;
  263. // rationale is that buffer appending only happens once media is attached. This can happen when config.startFragPrefetch
  264. // is used. If we used buffering in that case, our BW estimate sample will be very large.
  265. const processingMs = stats.parsing.end - stats.loading.start;
  266. this.bwEstimator.sample(processingMs, stats.loaded);
  267. stats.bwEstimate = this.bwEstimator.getEstimate();
  268. if (frag.bitrateTest) {
  269. this.bitrateTestDelay = processingMs / 1000;
  270. } else {
  271. this.bitrateTestDelay = 0;
  272. }
  273. }
  274.  
  275. protected onError(event: Events.ERROR, data: ErrorData) {
  276. // stop timer in case of frag loading error
  277. switch (data.details) {
  278. case ErrorDetails.FRAG_LOAD_ERROR:
  279. case ErrorDetails.FRAG_LOAD_TIMEOUT:
  280. this.clearTimer();
  281. break;
  282. default:
  283. break;
  284. }
  285. }
  286.  
  287. clearTimer() {
  288. self.clearInterval(this.timer);
  289. this.timer = undefined;
  290. }
  291.  
  292. // return next auto level
  293. get nextAutoLevel() {
  294. const forcedAutoLevel = this._nextAutoLevel;
  295. const bwEstimator = this.bwEstimator;
  296. // in case next auto level has been forced, and bw not available or not reliable, return forced value
  297. if (
  298. forcedAutoLevel !== -1 &&
  299. (!bwEstimator || !bwEstimator.canEstimate())
  300. ) {
  301. return forcedAutoLevel;
  302. }
  303.  
  304. // compute next level using ABR logic
  305. let nextABRAutoLevel = this.getNextABRAutoLevel();
  306. // if forced auto level has been defined, use it to cap ABR computed quality level
  307. if (forcedAutoLevel !== -1) {
  308. nextABRAutoLevel = Math.min(forcedAutoLevel, nextABRAutoLevel);
  309. }
  310.  
  311. return nextABRAutoLevel;
  312. }
  313.  
  314. private getNextABRAutoLevel() {
  315. const { fragCurrent, partCurrent, hls } = this;
  316. const { maxAutoLevel, config, minAutoLevel, media } = hls;
  317. const currentFragDuration = partCurrent
  318. ? partCurrent.duration
  319. : fragCurrent
  320. ? fragCurrent.duration
  321. : 0;
  322. const pos = media ? media.currentTime : 0;
  323.  
  324. // playbackRate is the absolute value of the playback rate; if media.playbackRate is 0, we use 1 to load as
  325. // if we're playing back at the normal rate.
  326. const playbackRate =
  327. media && media.playbackRate !== 0 ? Math.abs(media.playbackRate) : 1.0;
  328. const avgbw = this.bwEstimator
  329. ? this.bwEstimator.getEstimate()
  330. : config.abrEwmaDefaultEstimate;
  331. // bufferStarvationDelay is the wall-clock time left until the playback buffer is exhausted.
  332. const bufferStarvationDelay =
  333. (BufferHelper.bufferInfo(media as Bufferable, pos, config.maxBufferHole)
  334. .end -
  335. pos) /
  336. playbackRate;
  337.  
  338. // First, look to see if we can find a level matching with our avg bandwidth AND that could also guarantee no rebuffering at all
  339. let bestLevel = this.findBestLevel(
  340. avgbw,
  341. minAutoLevel,
  342. maxAutoLevel,
  343. bufferStarvationDelay,
  344. config.abrBandWidthFactor,
  345. config.abrBandWidthUpFactor
  346. );
  347. if (bestLevel >= 0) {
  348. return bestLevel;
  349. }
  350. logger.trace(
  351. `${
  352. bufferStarvationDelay ? 'rebuffering expected' : 'buffer is empty'
  353. }, finding optimal quality level`
  354. );
  355. // not possible to get rid of rebuffering ... let's try to find level that will guarantee less than maxStarvationDelay of rebuffering
  356. // if no matching level found, logic will return 0
  357. let maxStarvationDelay = currentFragDuration
  358. ? Math.min(currentFragDuration, config.maxStarvationDelay)
  359. : config.maxStarvationDelay;
  360. let bwFactor = config.abrBandWidthFactor;
  361. let bwUpFactor = config.abrBandWidthUpFactor;
  362.  
  363. if (!bufferStarvationDelay) {
  364. // in case buffer is empty, let's check if previous fragment was loaded to perform a bitrate test
  365. const bitrateTestDelay = this.bitrateTestDelay;
  366. if (bitrateTestDelay) {
  367. // if it is the case, then we need to adjust our max starvation delay using maxLoadingDelay config value
  368. // max video loading delay used in automatic start level selection :
  369. // in that mode ABR controller will ensure that video loading time (ie the time to fetch the first fragment at lowest quality level +
  370. // the time to fetch the fragment at the appropriate quality level is less than ```maxLoadingDelay``` )
  371. // cap maxLoadingDelay and ensure it is not bigger 'than bitrate test' frag duration
  372. const maxLoadingDelay = currentFragDuration
  373. ? Math.min(currentFragDuration, config.maxLoadingDelay)
  374. : config.maxLoadingDelay;
  375. maxStarvationDelay = maxLoadingDelay - bitrateTestDelay;
  376. logger.trace(
  377. `bitrate test took ${Math.round(
  378. 1000 * bitrateTestDelay
  379. )}ms, set first fragment max fetchDuration to ${Math.round(
  380. 1000 * maxStarvationDelay
  381. )} ms`
  382. );
  383. // don't use conservative factor on bitrate test
  384. bwFactor = bwUpFactor = 1;
  385. }
  386. }
  387. bestLevel = this.findBestLevel(
  388. avgbw,
  389. minAutoLevel,
  390. maxAutoLevel,
  391. bufferStarvationDelay + maxStarvationDelay,
  392. bwFactor,
  393. bwUpFactor
  394. );
  395. return Math.max(bestLevel, 0);
  396. }
  397.  
  398. private findBestLevel(
  399. currentBw: number,
  400. minAutoLevel: number,
  401. maxAutoLevel: number,
  402. maxFetchDuration: number,
  403. bwFactor: number,
  404. bwUpFactor: number
  405. ): number {
  406. const {
  407. fragCurrent,
  408. partCurrent,
  409. lastLoadedFragLevel: currentLevel,
  410. } = this;
  411. const { levels } = this.hls;
  412. const level = levels[currentLevel];
  413. const live = !!level?.details?.live;
  414. const currentCodecSet = level?.codecSet;
  415.  
  416. const currentFragDuration = partCurrent
  417. ? partCurrent.duration
  418. : fragCurrent
  419. ? fragCurrent.duration
  420. : 0;
  421. for (let i = maxAutoLevel; i >= minAutoLevel; i--) {
  422. const levelInfo = levels[i];
  423.  
  424. if (
  425. !levelInfo ||
  426. (currentCodecSet && levelInfo.codecSet !== currentCodecSet)
  427. ) {
  428. continue;
  429. }
  430.  
  431. const levelDetails = levelInfo.details;
  432. const avgDuration =
  433. (partCurrent
  434. ? levelDetails?.partTarget
  435. : levelDetails?.averagetargetduration) || currentFragDuration;
  436.  
  437. let adjustedbw: number;
  438. // follow algorithm captured from stagefright :
  439. // https://android.googlesource.com/platform/frameworks/av/+/master/media/libstagefright/httplive/LiveSession.cpp
  440. // Pick the highest bandwidth stream below or equal to estimated bandwidth.
  441. // consider only 80% of the available bandwidth, but if we are switching up,
  442. // be even more conservative (70%) to avoid overestimating and immediately
  443. // switching back.
  444. if (i <= currentLevel) {
  445. adjustedbw = bwFactor * currentBw;
  446. } else {
  447. adjustedbw = bwUpFactor * currentBw;
  448. }
  449.  
  450. const bitrate: number = levels[i].maxBitrate;
  451. const fetchDuration: number = (bitrate * avgDuration) / adjustedbw;
  452.  
  453. logger.trace(
  454. `level/adjustedbw/bitrate/avgDuration/maxFetchDuration/fetchDuration: ${i}/${Math.round(
  455. adjustedbw
  456. )}/${bitrate}/${avgDuration}/${maxFetchDuration}/${fetchDuration}`
  457. );
  458. // if adjusted bw is greater than level bitrate AND
  459. if (
  460. adjustedbw > bitrate &&
  461. // fragment fetchDuration unknown OR live stream OR fragment fetchDuration less than max allowed fetch duration, then this level matches
  462. // we don't account for max Fetch Duration for live streams, this is to avoid switching down when near the edge of live sliding window ...
  463. // special case to support startLevel = -1 (bitrateTest) on live streams : in that case we should not exit loop so that findBestLevel will return -1
  464. (!fetchDuration ||
  465. (live && !this.bitrateTestDelay) ||
  466. fetchDuration < maxFetchDuration)
  467. ) {
  468. // as we are looping from highest to lowest, this will return the best achievable quality level
  469. return i;
  470. }
  471. }
  472. // not enough time budget even with quality level 0 ... rebuffering might happen
  473. return -1;
  474. }
  475.  
  476. set nextAutoLevel(nextLevel) {
  477. this._nextAutoLevel = nextLevel;
  478. }
  479. }
  480.  
  481. export default AbrController;