Home Reference Source

src/controller/abr-controller.ts

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