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