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 { HdcpLevel, HdcpLevels, 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 levels from lowest to highest
  166. levels.sort((a, b) => {
  167. if (a.attrs['HDCP-LEVEL'] !== b.attrs['HDCP-LEVEL']) {
  168. return (a.attrs['HDCP-LEVEL'] || '') > (b.attrs['HDCP-LEVEL'] || '')
  169. ? 1
  170. : -1;
  171. }
  172. if (a.bitrate !== b.bitrate) {
  173. return a.bitrate - b.bitrate;
  174. }
  175. if (a.attrs.SCORE !== b.attrs.SCORE) {
  176. return (
  177. a.attrs.decimalFloatingPoint('SCORE') -
  178. b.attrs.decimalFloatingPoint('SCORE')
  179. );
  180. }
  181. if (resolutionFound && a.height !== b.height) {
  182. return a.height - b.height;
  183. }
  184. return 0;
  185. });
  186. this._levels = levels;
  187. // find index of first level in sorted levels
  188. for (let i = 0; i < levels.length; i++) {
  189. if (levels[i].bitrate === bitrateStart) {
  190. this._firstLevel = i;
  191. this.log(
  192. `manifest loaded, ${levels.length} level(s) found, first bitrate: ${bitrateStart}`
  193. );
  194. break;
  195. }
  196. }
  197.  
  198. // Audio is only alternate if manifest include a URI along with the audio group tag,
  199. // and this is not an audio-only stream where levels contain audio-only
  200. const audioOnly = audioCodecFound && !videoCodecFound;
  201. const edata: ManifestParsedData = {
  202. levels,
  203. audioTracks,
  204. subtitleTracks,
  205. sessionData: data.sessionData,
  206. sessionKeys: data.sessionKeys,
  207. firstLevel: this._firstLevel,
  208. stats: data.stats,
  209. audio: audioCodecFound,
  210. video: videoCodecFound,
  211. altAudio: !audioOnly && audioTracks.some((t) => !!t.url),
  212. };
  213. this.hls.trigger(Events.MANIFEST_PARSED, edata);
  214.  
  215. // Initiate loading after all controllers have received MANIFEST_PARSED
  216. if (this.hls.config.autoStartLoad || this.hls.forceStartLoad) {
  217. this.hls.startLoad(this.hls.config.startPosition);
  218. }
  219. } else {
  220. this.hls.trigger(Events.ERROR, {
  221. type: ErrorTypes.MEDIA_ERROR,
  222. details: ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR,
  223. fatal: true,
  224. url: data.url,
  225. reason: 'no level with compatible codecs found in manifest',
  226. });
  227. }
  228. }
  229.  
  230. get levels(): Level[] | null {
  231. if (this._levels.length === 0) {
  232. return null;
  233. }
  234. return this._levels;
  235. }
  236.  
  237. get level(): number {
  238. return this.currentLevelIndex;
  239. }
  240.  
  241. set level(newLevel: number) {
  242. const levels = this._levels;
  243. if (levels.length === 0) {
  244. return;
  245. }
  246. if (this.currentLevelIndex === newLevel && levels[newLevel]?.details) {
  247. return;
  248. }
  249. // check if level idx is valid
  250. if (newLevel < 0 || newLevel >= levels.length) {
  251. // invalid level id given, trigger error
  252. const fatal = newLevel < 0;
  253. this.hls.trigger(Events.ERROR, {
  254. type: ErrorTypes.OTHER_ERROR,
  255. details: ErrorDetails.LEVEL_SWITCH_ERROR,
  256. level: newLevel,
  257. fatal,
  258. reason: 'invalid level idx',
  259. });
  260. if (fatal) {
  261. return;
  262. }
  263. newLevel = Math.min(newLevel, levels.length - 1);
  264. }
  265.  
  266. // stopping live reloading timer if any
  267. this.clearTimer();
  268.  
  269. const lastLevelIndex = this.currentLevelIndex;
  270. const lastLevel = levels[lastLevelIndex];
  271. const level = levels[newLevel];
  272. this.log(`switching to level ${newLevel} from ${lastLevelIndex}`);
  273. this.currentLevelIndex = newLevel;
  274.  
  275. const levelSwitchingData: LevelSwitchingData = Object.assign({}, level, {
  276. level: newLevel,
  277. maxBitrate: level.maxBitrate,
  278. uri: level.uri,
  279. urlId: level.urlId,
  280. });
  281. // @ts-ignore
  282. delete levelSwitchingData._urlId;
  283. this.hls.trigger(Events.LEVEL_SWITCHING, levelSwitchingData);
  284. // check if we need to load playlist for this level
  285. const levelDetails = level.details;
  286. if (!levelDetails || levelDetails.live) {
  287. // level not retrieved yet, or live playlist we need to (re)load it
  288. const hlsUrlParameters = this.switchParams(level.uri, lastLevel?.details);
  289. this.loadPlaylist(hlsUrlParameters);
  290. }
  291. }
  292.  
  293. get manualLevel(): number {
  294. return this.manualLevelIndex;
  295. }
  296.  
  297. set manualLevel(newLevel) {
  298. this.manualLevelIndex = newLevel;
  299. if (this._startLevel === undefined) {
  300. this._startLevel = newLevel;
  301. }
  302.  
  303. if (newLevel !== -1) {
  304. this.level = newLevel;
  305. }
  306. }
  307.  
  308. get firstLevel(): number {
  309. return this._firstLevel;
  310. }
  311.  
  312. set firstLevel(newLevel) {
  313. this._firstLevel = newLevel;
  314. }
  315.  
  316. get startLevel() {
  317. // hls.startLevel takes precedence over config.startLevel
  318. // if none of these values are defined, fallback on this._firstLevel (first quality level appearing in variant manifest)
  319. if (this._startLevel === undefined) {
  320. const configStartLevel = this.hls.config.startLevel;
  321. if (configStartLevel !== undefined) {
  322. return configStartLevel;
  323. } else {
  324. return this._firstLevel;
  325. }
  326. } else {
  327. return this._startLevel;
  328. }
  329. }
  330.  
  331. set startLevel(newLevel) {
  332. this._startLevel = newLevel;
  333. }
  334.  
  335. protected onError(event: Events.ERROR, data: ErrorData) {
  336. super.onError(event, data);
  337. if (data.fatal) {
  338. return;
  339. }
  340.  
  341. // Switch to redundant level when track fails to load
  342. const context = data.context;
  343. const level = this._levels[this.currentLevelIndex];
  344. if (
  345. context &&
  346. ((context.type === PlaylistContextType.AUDIO_TRACK &&
  347. level.audioGroupIds &&
  348. context.groupId === level.audioGroupIds[level.urlId]) ||
  349. (context.type === PlaylistContextType.SUBTITLE_TRACK &&
  350. level.textGroupIds &&
  351. context.groupId === level.textGroupIds[level.urlId]))
  352. ) {
  353. this.redundantFailover(this.currentLevelIndex);
  354. return;
  355. }
  356.  
  357. let levelError = false;
  358. let levelSwitch = true;
  359. let levelIndex;
  360.  
  361. // try to recover not fatal errors
  362. switch (data.details) {
  363. case ErrorDetails.FRAG_LOAD_ERROR:
  364. case ErrorDetails.FRAG_LOAD_TIMEOUT:
  365. case ErrorDetails.KEY_LOAD_ERROR:
  366. case ErrorDetails.KEY_LOAD_TIMEOUT:
  367. if (data.frag) {
  368. // Share fragment error count accross media options (main, audio, subs)
  369. // This allows for level based rendition switching when media option assets fail
  370. const variantLevelIndex =
  371. data.frag.type === PlaylistLevelType.MAIN
  372. ? data.frag.level
  373. : this.currentLevelIndex;
  374. const level = this._levels[variantLevelIndex];
  375. // Set levelIndex when we're out of fragment retries
  376. if (level) {
  377. level.fragmentError++;
  378. if (level.fragmentError > this.hls.config.fragLoadingMaxRetry) {
  379. levelIndex = variantLevelIndex;
  380. }
  381. } else {
  382. levelIndex = variantLevelIndex;
  383. }
  384. }
  385. break;
  386. case ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED: {
  387. const restrictedHdcpLevel = level.attrs['HDCP-LEVEL'];
  388. if (restrictedHdcpLevel) {
  389. this.hls.maxHdcpLevel =
  390. HdcpLevels[
  391. HdcpLevels.indexOf(restrictedHdcpLevel as HdcpLevel) - 1
  392. ];
  393. this.warn(
  394. `Restricting playback to HDCP-LEVEL of "${this.hls.maxHdcpLevel}" or lower`
  395. );
  396. }
  397. }
  398. // eslint-disable-next-line no-fallthrough
  399. case ErrorDetails.FRAG_PARSING_ERROR:
  400. case ErrorDetails.KEY_SYSTEM_NO_SESSION:
  401. levelIndex =
  402. data.frag?.type === PlaylistLevelType.MAIN
  403. ? data.frag.level
  404. : this.currentLevelIndex;
  405. // Do not retry level. Escalate to fatal if switching levels fails.
  406. data.levelRetry = false;
  407. break;
  408. case ErrorDetails.LEVEL_LOAD_ERROR:
  409. case ErrorDetails.LEVEL_LOAD_TIMEOUT:
  410. // Do not perform level switch if an error occurred using delivery directives
  411. // Attempt to reload level without directives first
  412. if (context) {
  413. if (context.deliveryDirectives) {
  414. levelSwitch = false;
  415. }
  416. levelIndex = context.level;
  417. }
  418. levelError = true;
  419. break;
  420. case ErrorDetails.REMUX_ALLOC_ERROR:
  421. levelIndex = data.level ?? this.currentLevelIndex;
  422. levelError = true;
  423. break;
  424. }
  425.  
  426. if (levelIndex !== undefined) {
  427. this.recoverLevel(data, levelIndex, levelError, levelSwitch);
  428. }
  429. }
  430.  
  431. /**
  432. * Switch to a redundant stream if any available.
  433. * If redundant stream is not available, emergency switch down if ABR mode is enabled.
  434. */
  435. private recoverLevel(
  436. errorEvent: ErrorData,
  437. levelIndex: number,
  438. levelError: boolean,
  439. levelSwitch: boolean
  440. ): void {
  441. const { details: errorDetails } = errorEvent;
  442. const level = this._levels[levelIndex];
  443.  
  444. level.loadError++;
  445.  
  446. if (levelError) {
  447. const retrying = this.retryLoadingOrFail(errorEvent);
  448. if (retrying) {
  449. // boolean used to inform stream controller not to switch back to IDLE on non fatal error
  450. errorEvent.levelRetry = true;
  451. } else {
  452. this.currentLevelIndex = -1;
  453. return;
  454. }
  455. }
  456.  
  457. if (levelSwitch) {
  458. const redundantLevels = level.url.length;
  459. // Try redundant fail-over until level.loadError reaches redundantLevels
  460. if (redundantLevels > 1 && level.loadError < redundantLevels) {
  461. errorEvent.levelRetry = true;
  462. this.redundantFailover(levelIndex);
  463. } else if (this.manualLevelIndex === -1) {
  464. // Search for next level to retry
  465. let nextLevel = -1;
  466. const levels = this._levels;
  467. for (let i = levels.length; i--; ) {
  468. const candidate = (i + this.currentLevelIndex) % levels.length;
  469. if (
  470. candidate !== this.currentLevelIndex &&
  471. levels[candidate].loadError === 0
  472. ) {
  473. nextLevel = candidate;
  474. break;
  475. }
  476. }
  477. if (nextLevel > -1 && this.currentLevelIndex !== nextLevel) {
  478. this.warn(`${errorDetails}: switch to ${nextLevel}`);
  479. errorEvent.levelRetry = true;
  480. this.hls.nextAutoLevel = nextLevel;
  481. } else if (errorEvent.levelRetry === false) {
  482. // No levels to switch to and no more retries
  483. errorEvent.fatal = true;
  484. }
  485. }
  486. }
  487. }
  488.  
  489. private redundantFailover(levelIndex: number) {
  490. const level = this._levels[levelIndex];
  491. const redundantLevels = level.url.length;
  492. if (redundantLevels > 1) {
  493. // Update the url id of all levels so that we stay on the same set of variants when level switching
  494. const newUrlId = (level.urlId + 1) % redundantLevels;
  495. this.warn(`Switching to redundant URL-id ${newUrlId}`);
  496. this._levels.forEach((level) => {
  497. level.urlId = newUrlId;
  498. });
  499. this.level = levelIndex;
  500. }
  501. }
  502.  
  503. // reset errors on the successful load of a fragment
  504. protected onFragLoaded(event: Events.FRAG_LOADED, { frag }: FragLoadedData) {
  505. if (frag !== undefined && frag.type === PlaylistLevelType.MAIN) {
  506. const level = this._levels[frag.level];
  507. if (level !== undefined) {
  508. level.fragmentError = 0;
  509. level.loadError = 0;
  510. }
  511. }
  512. }
  513.  
  514. protected onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
  515. const { level, details } = data;
  516. const curLevel = this._levels[level];
  517.  
  518. if (!curLevel) {
  519. this.warn(`Invalid level index ${level}`);
  520. if (data.deliveryDirectives?.skip) {
  521. details.deltaUpdateFailed = true;
  522. }
  523. return;
  524. }
  525.  
  526. // only process level loaded events matching with expected level
  527. if (level === this.currentLevelIndex) {
  528. // reset level load error counter on successful level loaded only if there is no issues with fragments
  529. if (curLevel.fragmentError === 0) {
  530. curLevel.loadError = 0;
  531. this.retryCount = 0;
  532. }
  533. this.playlistLoaded(level, data, curLevel.details);
  534. } else if (data.deliveryDirectives?.skip) {
  535. // received a delta playlist update that cannot be merged
  536. details.deltaUpdateFailed = true;
  537. }
  538. }
  539.  
  540. protected onAudioTrackSwitched(
  541. event: Events.AUDIO_TRACK_SWITCHED,
  542. data: TrackSwitchedData
  543. ) {
  544. const currentLevel = this.hls.levels[this.currentLevelIndex];
  545. if (!currentLevel) {
  546. return;
  547. }
  548.  
  549. if (currentLevel.audioGroupIds) {
  550. let urlId = -1;
  551. const audioGroupId = this.hls.audioTracks[data.id].groupId;
  552. for (let i = 0; i < currentLevel.audioGroupIds.length; i++) {
  553. if (currentLevel.audioGroupIds[i] === audioGroupId) {
  554. urlId = i;
  555. break;
  556. }
  557. }
  558.  
  559. if (urlId !== currentLevel.urlId) {
  560. currentLevel.urlId = urlId;
  561. this.startLoad();
  562. }
  563. }
  564. }
  565.  
  566. protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters) {
  567. super.loadPlaylist();
  568. const level = this.currentLevelIndex;
  569. const currentLevel = this._levels[level];
  570.  
  571. if (this.canLoad && currentLevel && currentLevel.url.length > 0) {
  572. const id = currentLevel.urlId;
  573. let url = currentLevel.url[id];
  574. if (hlsUrlParameters) {
  575. try {
  576. url = hlsUrlParameters.addDirectives(url);
  577. } catch (error) {
  578. this.warn(
  579. `Could not construct new URL with HLS Delivery Directives: ${error}`
  580. );
  581. }
  582. }
  583.  
  584. this.log(
  585. `Attempt loading level index ${level}${
  586. hlsUrlParameters
  587. ? ' at sn ' +
  588. hlsUrlParameters.msn +
  589. ' part ' +
  590. hlsUrlParameters.part
  591. : ''
  592. } with URL-id ${id} ${url}`
  593. );
  594.  
  595. // console.log('Current audio track group ID:', this.hls.audioTracks[this.hls.audioTrack].groupId);
  596. // console.log('New video quality level audio group id:', levelObject.attrs.AUDIO, level);
  597. this.clearTimer();
  598. this.hls.trigger(Events.LEVEL_LOADING, {
  599. url,
  600. level,
  601. id,
  602. deliveryDirectives: hlsUrlParameters || null,
  603. });
  604. }
  605. }
  606.  
  607. get nextLoadLevel() {
  608. if (this.manualLevelIndex !== -1) {
  609. return this.manualLevelIndex;
  610. } else {
  611. return this.hls.nextAutoLevel;
  612. }
  613. }
  614.  
  615. set nextLoadLevel(nextLevel) {
  616. this.level = nextLevel;
  617. if (this.manualLevelIndex === -1) {
  618. this.hls.nextAutoLevel = nextLevel;
  619. }
  620. }
  621.  
  622. removeLevel(levelIndex, urlId) {
  623. const filterLevelAndGroupByIdIndex = (url, id) => id !== urlId;
  624. const levels = this._levels
  625. .filter((level, index) => {
  626. if (index !== levelIndex) {
  627. return true;
  628. }
  629.  
  630. if (level.url.length > 1 && urlId !== undefined) {
  631. level.url = level.url.filter(filterLevelAndGroupByIdIndex);
  632. if (level.audioGroupIds) {
  633. level.audioGroupIds = level.audioGroupIds.filter(
  634. filterLevelAndGroupByIdIndex
  635. );
  636. }
  637. if (level.textGroupIds) {
  638. level.textGroupIds = level.textGroupIds.filter(
  639. filterLevelAndGroupByIdIndex
  640. );
  641. }
  642. level.urlId = 0;
  643. return true;
  644. }
  645. return false;
  646. })
  647. .map((level, index) => {
  648. const { details } = level;
  649. if (details?.fragments) {
  650. details.fragments.forEach((fragment) => {
  651. fragment.level = index;
  652. });
  653. }
  654. return level;
  655. });
  656. this._levels = levels;
  657.  
  658. this.hls.trigger(Events.LEVELS_UPDATED, { levels });
  659. }
  660. }