Home Reference Source

src/controller/level-controller.ts

  1. /*
  2. * Level Controller
  3. */
  4.  
  5. import {
  6. ManifestLoadedData,
  7. ManifestParsedData,
  8. LevelLoadedData,
  9. TrackSwitchedData,
  10. FragLoadedData,
  11. ErrorData,
  12. LevelSwitchingData,
  13. } from '../types/events';
  14. import { Level } from '../types/level';
  15. import { Events } from '../events';
  16. import { ErrorTypes, ErrorDetails } from '../errors';
  17. import { isCodecSupportedInMp4 } from '../utils/codecs';
  18. import { addGroupId, assignTrackIdsByGroup } from './level-helper';
  19. import BasePlaylistController from './base-playlist-controller';
  20. import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
  21. import type Hls from '../hls';
  22. import type { HlsUrlParameters, LevelParsed } from '../types/level';
  23. import type { MediaPlaylist } from '../types/media-playlist';
  24.  
  25. const chromeOrFirefox: boolean = /chrome|firefox/.test(
  26. navigator.userAgent.toLowerCase()
  27. );
  28.  
  29. export default class LevelController extends BasePlaylistController {
  30. private _levels: Level[] = [];
  31. private _firstLevel: number = -1;
  32. private _startLevel?: number;
  33. private currentLevelIndex: number = -1;
  34. private manualLevelIndex: number = -1;
  35.  
  36. public onParsedComplete!: Function;
  37.  
  38. constructor(hls: Hls) {
  39. super(hls, '[level-controller]');
  40. this._registerListeners();
  41. }
  42.  
  43. private _registerListeners() {
  44. const { hls } = this;
  45. hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
  46. hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
  47. hls.on(Events.AUDIO_TRACK_SWITCHED, this.onAudioTrackSwitched, this);
  48. hls.on(Events.FRAG_LOADED, this.onFragLoaded, this);
  49. hls.on(Events.ERROR, this.onError, this);
  50. }
  51.  
  52. private _unregisterListeners() {
  53. const { hls } = this;
  54. hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
  55. hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
  56. hls.off(Events.AUDIO_TRACK_SWITCHED, this.onAudioTrackSwitched, this);
  57. hls.off(Events.FRAG_LOADED, this.onFragLoaded, this);
  58. hls.off(Events.ERROR, this.onError, this);
  59. }
  60.  
  61. public destroy() {
  62. this._unregisterListeners();
  63. this.manualLevelIndex = -1;
  64. this._levels.length = 0;
  65. super.destroy();
  66. }
  67.  
  68. public startLoad(): void {
  69. const levels = this._levels;
  70.  
  71. // clean up live level details to force reload them, and reset load errors
  72. levels.forEach((level) => {
  73. level.loadError = 0;
  74. });
  75.  
  76. super.startLoad();
  77. }
  78.  
  79. protected onManifestLoaded(
  80. event: Events.MANIFEST_LOADED,
  81. data: ManifestLoadedData
  82. ): void {
  83. let levels: Level[] = [];
  84. let audioTracks: MediaPlaylist[] = [];
  85. let subtitleTracks: MediaPlaylist[] = [];
  86. let bitrateStart: number | undefined;
  87. const levelSet: { [key: string]: Level } = {};
  88. let levelFromSet: Level;
  89. let resolutionFound = false;
  90. let videoCodecFound = false;
  91. let audioCodecFound = false;
  92.  
  93. // regroup redundant levels together
  94. data.levels.forEach((levelParsed: LevelParsed) => {
  95. const attributes = levelParsed.attrs;
  96.  
  97. resolutionFound =
  98. resolutionFound || !!(levelParsed.width && levelParsed.height);
  99. videoCodecFound = videoCodecFound || !!levelParsed.videoCodec;
  100. audioCodecFound = audioCodecFound || !!levelParsed.audioCodec;
  101.  
  102. // erase audio codec info if browser does not support mp4a.40.34.
  103. // demuxer will autodetect codec and fallback to mpeg/audio
  104. if (
  105. chromeOrFirefox &&
  106. levelParsed.audioCodec &&
  107. levelParsed.audioCodec.indexOf('mp4a.40.34') !== -1
  108. ) {
  109. levelParsed.audioCodec = undefined;
  110. }
  111.  
  112. const levelKey = `${levelParsed.bitrate}-${levelParsed.attrs.RESOLUTION}-${levelParsed.attrs.CODECS}`;
  113. levelFromSet = levelSet[levelKey];
  114.  
  115. if (!levelFromSet) {
  116. levelFromSet = new Level(levelParsed);
  117. levelSet[levelKey] = levelFromSet;
  118. levels.push(levelFromSet);
  119. } else {
  120. levelFromSet.url.push(levelParsed.url);
  121. }
  122.  
  123. if (attributes) {
  124. if (attributes.AUDIO) {
  125. addGroupId(levelFromSet, 'audio', attributes.AUDIO);
  126. }
  127. if (attributes.SUBTITLES) {
  128. addGroupId(levelFromSet, 'text', attributes.SUBTITLES);
  129. }
  130. }
  131. });
  132.  
  133. // remove audio-only level if we also have levels with video codecs or RESOLUTION signalled
  134. if ((resolutionFound || videoCodecFound) && audioCodecFound) {
  135. levels = levels.filter(
  136. ({ videoCodec, width, height }) => !!videoCodec || !!(width && height)
  137. );
  138. }
  139.  
  140. // only keep levels with supported audio/video codecs
  141. levels = levels.filter(({ audioCodec, videoCodec }) => {
  142. return (
  143. (!audioCodec || isCodecSupportedInMp4(audioCodec, 'audio')) &&
  144. (!videoCodec || isCodecSupportedInMp4(videoCodec, 'video'))
  145. );
  146. });
  147.  
  148. if (data.audioTracks) {
  149. audioTracks = data.audioTracks.filter(
  150. (track) =>
  151. !track.audioCodec || isCodecSupportedInMp4(track.audioCodec, 'audio')
  152. );
  153. // Assign ids after filtering as array indices by group-id
  154. assignTrackIdsByGroup(audioTracks);
  155. }
  156.  
  157. if (data.subtitles) {
  158. subtitleTracks = data.subtitles;
  159. assignTrackIdsByGroup(subtitleTracks);
  160. }
  161.  
  162. if (levels.length > 0) {
  163. // start bitrate is the first bitrate of the manifest
  164. bitrateStart = levels[0].bitrate;
  165. // sort level on bitrate
  166. levels.sort((a, b) => a.bitrate - b.bitrate);
  167. this._levels = levels;
  168. // find index of first level in sorted levels
  169. for (let i = 0; i < levels.length; i++) {
  170. if (levels[i].bitrate === bitrateStart) {
  171. this._firstLevel = i;
  172. this.log(
  173. `manifest loaded, ${levels.length} level(s) found, first bitrate: ${bitrateStart}`
  174. );
  175. break;
  176. }
  177. }
  178.  
  179. // Audio is only alternate if manifest include a URI along with the audio group tag,
  180. // and this is not an audio-only stream where levels contain audio-only
  181. const audioOnly = audioCodecFound && !videoCodecFound;
  182. const edata: ManifestParsedData = {
  183. levels,
  184. audioTracks,
  185. subtitleTracks,
  186. sessionData: data.sessionData,
  187. sessionKeys: data.sessionKeys,
  188. firstLevel: this._firstLevel,
  189. stats: data.stats,
  190. audio: audioCodecFound,
  191. video: videoCodecFound,
  192. altAudio: !audioOnly && audioTracks.some((t) => !!t.url),
  193. };
  194. this.hls.trigger(Events.MANIFEST_PARSED, edata);
  195.  
  196. // Initiate loading after all controllers have received MANIFEST_PARSED
  197. if (this.hls.config.autoStartLoad || this.hls.forceStartLoad) {
  198. this.hls.startLoad(this.hls.config.startPosition);
  199. }
  200. } else {
  201. this.hls.trigger(Events.ERROR, {
  202. type: ErrorTypes.MEDIA_ERROR,
  203. details: ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR,
  204. fatal: true,
  205. url: data.url,
  206. reason: 'no level with compatible codecs found in manifest',
  207. });
  208. }
  209. }
  210.  
  211. get levels(): Level[] | null {
  212. if (this._levels.length === 0) {
  213. return null;
  214. }
  215. return this._levels;
  216. }
  217.  
  218. get level(): number {
  219. return this.currentLevelIndex;
  220. }
  221.  
  222. set level(newLevel: number) {
  223. const levels = this._levels;
  224. if (levels.length === 0) {
  225. return;
  226. }
  227. if (this.currentLevelIndex === newLevel && levels[newLevel]?.details) {
  228. return;
  229. }
  230. // check if level idx is valid
  231. if (newLevel < 0 || newLevel >= levels.length) {
  232. // invalid level id given, trigger error
  233. const fatal = newLevel < 0;
  234. this.hls.trigger(Events.ERROR, {
  235. type: ErrorTypes.OTHER_ERROR,
  236. details: ErrorDetails.LEVEL_SWITCH_ERROR,
  237. level: newLevel,
  238. fatal,
  239. reason: 'invalid level idx',
  240. });
  241. if (fatal) {
  242. return;
  243. }
  244. newLevel = Math.min(newLevel, levels.length - 1);
  245. }
  246.  
  247. // stopping live reloading timer if any
  248. this.clearTimer();
  249.  
  250. const lastLevelIndex = this.currentLevelIndex;
  251. const lastLevel = levels[lastLevelIndex];
  252. const level = levels[newLevel];
  253. this.log(`switching to level ${newLevel} from ${lastLevelIndex}`);
  254. this.currentLevelIndex = newLevel;
  255.  
  256. const levelSwitchingData: LevelSwitchingData = Object.assign({}, level, {
  257. level: newLevel,
  258. maxBitrate: level.maxBitrate,
  259. uri: level.uri,
  260. urlId: level.urlId,
  261. });
  262. // @ts-ignore
  263. delete levelSwitchingData._urlId;
  264. this.hls.trigger(Events.LEVEL_SWITCHING, levelSwitchingData);
  265. // check if we need to load playlist for this level
  266. const levelDetails = level.details;
  267. if (!levelDetails || levelDetails.live) {
  268. // level not retrieved yet, or live playlist we need to (re)load it
  269. const hlsUrlParameters = this.switchParams(level.uri, lastLevel?.details);
  270. this.loadPlaylist(hlsUrlParameters);
  271. }
  272. }
  273.  
  274. get manualLevel(): number {
  275. return this.manualLevelIndex;
  276. }
  277.  
  278. set manualLevel(newLevel) {
  279. this.manualLevelIndex = newLevel;
  280. if (this._startLevel === undefined) {
  281. this._startLevel = newLevel;
  282. }
  283.  
  284. if (newLevel !== -1) {
  285. this.level = newLevel;
  286. }
  287. }
  288.  
  289. get firstLevel(): number {
  290. return this._firstLevel;
  291. }
  292.  
  293. set firstLevel(newLevel) {
  294. this._firstLevel = newLevel;
  295. }
  296.  
  297. get startLevel() {
  298. // hls.startLevel takes precedence over config.startLevel
  299. // if none of these values are defined, fallback on this._firstLevel (first quality level appearing in variant manifest)
  300. if (this._startLevel === undefined) {
  301. const configStartLevel = this.hls.config.startLevel;
  302. if (configStartLevel !== undefined) {
  303. return configStartLevel;
  304. } else {
  305. return this._firstLevel;
  306. }
  307. } else {
  308. return this._startLevel;
  309. }
  310. }
  311.  
  312. set startLevel(newLevel) {
  313. this._startLevel = newLevel;
  314. }
  315.  
  316. protected onError(event: Events.ERROR, data: ErrorData) {
  317. super.onError(event, data);
  318. if (data.fatal) {
  319. return;
  320. }
  321.  
  322. // Switch to redundant level when track fails to load
  323. const context = data.context;
  324. const level = this._levels[this.currentLevelIndex];
  325. if (
  326. context &&
  327. ((context.type === PlaylistContextType.AUDIO_TRACK &&
  328. level.audioGroupIds &&
  329. context.groupId === level.audioGroupIds[level.urlId]) ||
  330. (context.type === PlaylistContextType.SUBTITLE_TRACK &&
  331. level.textGroupIds &&
  332. context.groupId === level.textGroupIds[level.urlId]))
  333. ) {
  334. this.redundantFailover(this.currentLevelIndex);
  335. return;
  336. }
  337.  
  338. let levelError = false;
  339. let levelSwitch = true;
  340. let levelIndex;
  341.  
  342. // try to recover not fatal errors
  343. switch (data.details) {
  344. case ErrorDetails.FRAG_LOAD_ERROR:
  345. case ErrorDetails.FRAG_LOAD_TIMEOUT:
  346. case ErrorDetails.KEY_LOAD_ERROR:
  347. case ErrorDetails.KEY_LOAD_TIMEOUT:
  348. if (data.frag) {
  349. // Share fragment error count accross media options (main, audio, subs)
  350. // This allows for level based rendition switching when media option assets fail
  351. const variantLevelIndex =
  352. data.frag.type === PlaylistLevelType.MAIN
  353. ? data.frag.level
  354. : this.currentLevelIndex;
  355. const level = this._levels[variantLevelIndex];
  356. // Set levelIndex when we're out of fragment retries
  357. if (level) {
  358. level.fragmentError++;
  359. if (level.fragmentError > this.hls.config.fragLoadingMaxRetry) {
  360. levelIndex = variantLevelIndex;
  361. }
  362. } else {
  363. levelIndex = variantLevelIndex;
  364. }
  365. }
  366. break;
  367. case ErrorDetails.FRAG_PARSING_ERROR:
  368. case ErrorDetails.KEY_SYSTEM_NO_SESSION:
  369. case ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED:
  370. levelIndex =
  371. data.frag?.type === PlaylistLevelType.MAIN
  372. ? data.frag.level
  373. : this.currentLevelIndex;
  374. // Do not retry level. Escalate to fatal if switching levels fails.
  375. data.levelRetry = false;
  376. break;
  377. case ErrorDetails.LEVEL_LOAD_ERROR:
  378. case ErrorDetails.LEVEL_LOAD_TIMEOUT:
  379. // Do not perform level switch if an error occurred using delivery directives
  380. // Attempt to reload level without directives first
  381. if (context) {
  382. if (context.deliveryDirectives) {
  383. levelSwitch = false;
  384. }
  385. levelIndex = context.level;
  386. }
  387. levelError = true;
  388. break;
  389. case ErrorDetails.REMUX_ALLOC_ERROR:
  390. levelIndex = data.level ?? this.currentLevelIndex;
  391. levelError = true;
  392. break;
  393. }
  394.  
  395. if (levelIndex !== undefined) {
  396. this.recoverLevel(data, levelIndex, levelError, levelSwitch);
  397. }
  398. }
  399.  
  400. /**
  401. * Switch to a redundant stream if any available.
  402. * If redundant stream is not available, emergency switch down if ABR mode is enabled.
  403. */
  404. private recoverLevel(
  405. errorEvent: ErrorData,
  406. levelIndex: number,
  407. levelError: boolean,
  408. levelSwitch: boolean
  409. ): void {
  410. const { details: errorDetails } = errorEvent;
  411. const level = this._levels[levelIndex];
  412.  
  413. level.loadError++;
  414.  
  415. if (levelError) {
  416. const retrying = this.retryLoadingOrFail(errorEvent);
  417. if (retrying) {
  418. // boolean used to inform stream controller not to switch back to IDLE on non fatal error
  419. errorEvent.levelRetry = true;
  420. } else {
  421. this.currentLevelIndex = -1;
  422. return;
  423. }
  424. }
  425.  
  426. if (levelSwitch) {
  427. const redundantLevels = level.url.length;
  428. // Try redundant fail-over until level.loadError reaches redundantLevels
  429. if (redundantLevels > 1 && level.loadError < redundantLevels) {
  430. errorEvent.levelRetry = true;
  431. this.redundantFailover(levelIndex);
  432. } else if (this.manualLevelIndex === -1) {
  433. // Search for next level to retry
  434. let nextLevel = -1;
  435. const levels = this._levels;
  436. for (let i = levels.length; i--; ) {
  437. const candidate = (i + this.currentLevelIndex) % levels.length;
  438. if (
  439. candidate !== this.currentLevelIndex &&
  440. levels[candidate].loadError === 0
  441. ) {
  442. nextLevel = candidate;
  443. break;
  444. }
  445. }
  446. if (nextLevel > -1 && this.currentLevelIndex !== nextLevel) {
  447. this.warn(`${errorDetails}: switch to ${nextLevel}`);
  448. errorEvent.levelRetry = true;
  449. this.hls.nextAutoLevel = nextLevel;
  450. } else if (errorEvent.levelRetry === false) {
  451. // No levels to switch to and no more retries
  452. errorEvent.fatal = true;
  453. }
  454. }
  455. }
  456. }
  457.  
  458. private redundantFailover(levelIndex: number) {
  459. const level = this._levels[levelIndex];
  460. const redundantLevels = level.url.length;
  461. if (redundantLevels > 1) {
  462. // Update the url id of all levels so that we stay on the same set of variants when level switching
  463. const newUrlId = (level.urlId + 1) % redundantLevels;
  464. this.warn(`Switching to redundant URL-id ${newUrlId}`);
  465. this._levels.forEach((level) => {
  466. level.urlId = newUrlId;
  467. });
  468. this.level = levelIndex;
  469. }
  470. }
  471.  
  472. // reset errors on the successful load of a fragment
  473. protected onFragLoaded(event: Events.FRAG_LOADED, { frag }: FragLoadedData) {
  474. if (frag !== undefined && frag.type === PlaylistLevelType.MAIN) {
  475. const level = this._levels[frag.level];
  476. if (level !== undefined) {
  477. level.fragmentError = 0;
  478. level.loadError = 0;
  479. }
  480. }
  481. }
  482.  
  483. protected onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
  484. const { level, details } = data;
  485. const curLevel = this._levels[level];
  486.  
  487. if (!curLevel) {
  488. this.warn(`Invalid level index ${level}`);
  489. if (data.deliveryDirectives?.skip) {
  490. details.deltaUpdateFailed = true;
  491. }
  492. return;
  493. }
  494.  
  495. // only process level loaded events matching with expected level
  496. if (level === this.currentLevelIndex) {
  497. // reset level load error counter on successful level loaded only if there is no issues with fragments
  498. if (curLevel.fragmentError === 0) {
  499. curLevel.loadError = 0;
  500. this.retryCount = 0;
  501. }
  502. this.playlistLoaded(level, data, curLevel.details);
  503. } else if (data.deliveryDirectives?.skip) {
  504. // received a delta playlist update that cannot be merged
  505. details.deltaUpdateFailed = true;
  506. }
  507. }
  508.  
  509. protected onAudioTrackSwitched(
  510. event: Events.AUDIO_TRACK_SWITCHED,
  511. data: TrackSwitchedData
  512. ) {
  513. const currentLevel = this.hls.levels[this.currentLevelIndex];
  514. if (!currentLevel) {
  515. return;
  516. }
  517.  
  518. if (currentLevel.audioGroupIds) {
  519. let urlId = -1;
  520. const audioGroupId = this.hls.audioTracks[data.id].groupId;
  521. for (let i = 0; i < currentLevel.audioGroupIds.length; i++) {
  522. if (currentLevel.audioGroupIds[i] === audioGroupId) {
  523. urlId = i;
  524. break;
  525. }
  526. }
  527.  
  528. if (urlId !== currentLevel.urlId) {
  529. currentLevel.urlId = urlId;
  530. this.startLoad();
  531. }
  532. }
  533. }
  534.  
  535. protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters) {
  536. super.loadPlaylist();
  537. const level = this.currentLevelIndex;
  538. const currentLevel = this._levels[level];
  539.  
  540. if (this.canLoad && currentLevel && currentLevel.url.length > 0) {
  541. const id = currentLevel.urlId;
  542. let url = currentLevel.url[id];
  543. if (hlsUrlParameters) {
  544. try {
  545. url = hlsUrlParameters.addDirectives(url);
  546. } catch (error) {
  547. this.warn(
  548. `Could not construct new URL with HLS Delivery Directives: ${error}`
  549. );
  550. }
  551. }
  552.  
  553. this.log(
  554. `Attempt loading level index ${level}${
  555. hlsUrlParameters
  556. ? ' at sn ' +
  557. hlsUrlParameters.msn +
  558. ' part ' +
  559. hlsUrlParameters.part
  560. : ''
  561. } with URL-id ${id} ${url}`
  562. );
  563.  
  564. // console.log('Current audio track group ID:', this.hls.audioTracks[this.hls.audioTrack].groupId);
  565. // console.log('New video quality level audio group id:', levelObject.attrs.AUDIO, level);
  566. this.clearTimer();
  567. this.hls.trigger(Events.LEVEL_LOADING, {
  568. url,
  569. level,
  570. id,
  571. deliveryDirectives: hlsUrlParameters || null,
  572. });
  573. }
  574. }
  575.  
  576. get nextLoadLevel() {
  577. if (this.manualLevelIndex !== -1) {
  578. return this.manualLevelIndex;
  579. } else {
  580. return this.hls.nextAutoLevel;
  581. }
  582. }
  583.  
  584. set nextLoadLevel(nextLevel) {
  585. this.level = nextLevel;
  586. if (this.manualLevelIndex === -1) {
  587. this.hls.nextAutoLevel = nextLevel;
  588. }
  589. }
  590.  
  591. removeLevel(levelIndex, urlId) {
  592. const filterLevelAndGroupByIdIndex = (url, id) => id !== urlId;
  593. const levels = this._levels
  594. .filter((level, index) => {
  595. if (index !== levelIndex) {
  596. return true;
  597. }
  598.  
  599. if (level.url.length > 1 && urlId !== undefined) {
  600. level.url = level.url.filter(filterLevelAndGroupByIdIndex);
  601. if (level.audioGroupIds) {
  602. level.audioGroupIds = level.audioGroupIds.filter(
  603. filterLevelAndGroupByIdIndex
  604. );
  605. }
  606. if (level.textGroupIds) {
  607. level.textGroupIds = level.textGroupIds.filter(
  608. filterLevelAndGroupByIdIndex
  609. );
  610. }
  611. level.urlId = 0;
  612. return true;
  613. }
  614. return false;
  615. })
  616. .map((level, index) => {
  617. const { details } = level;
  618. if (details?.fragments) {
  619. details.fragments.forEach((fragment) => {
  620. fragment.level = index;
  621. });
  622. }
  623. return level;
  624. });
  625. this._levels = levels;
  626.  
  627. this.hls.trigger(Events.LEVELS_UPDATED, { levels });
  628. }
  629. }