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. // Share fragment error count accross media options (main, audio, subs)
  348. // This allows for level based rendition switching when media option assets fail
  349. const variantLevelIndex =
  350. data.frag.type === PlaylistLevelType.MAIN
  351. ? data.frag.level
  352. : this.currentLevelIndex;
  353. const level = this._levels[variantLevelIndex];
  354. // Set levelIndex when we're out of fragment retries
  355. if (level) {
  356. level.fragmentError++;
  357. if (level.fragmentError > this.hls.config.fragLoadingMaxRetry) {
  358. levelIndex = variantLevelIndex;
  359. }
  360. } else {
  361. levelIndex = variantLevelIndex;
  362. }
  363. }
  364. break;
  365. case ErrorDetails.LEVEL_LOAD_ERROR:
  366. case ErrorDetails.LEVEL_LOAD_TIMEOUT:
  367. // Do not perform level switch if an error occurred using delivery directives
  368. // Attempt to reload level without directives first
  369. if (context) {
  370. if (context.deliveryDirectives) {
  371. levelSwitch = false;
  372. }
  373. levelIndex = context.level;
  374. }
  375. levelError = true;
  376. break;
  377. case ErrorDetails.REMUX_ALLOC_ERROR:
  378. levelIndex = data.level ?? this.currentLevelIndex;
  379. levelError = true;
  380. break;
  381. }
  382.  
  383. if (levelIndex !== undefined) {
  384. this.recoverLevel(data, levelIndex, levelError, levelSwitch);
  385. }
  386. }
  387.  
  388. /**
  389. * Switch to a redundant stream if any available.
  390. * If redundant stream is not available, emergency switch down if ABR mode is enabled.
  391. */
  392. private recoverLevel(
  393. errorEvent: ErrorData,
  394. levelIndex: number,
  395. levelError: boolean,
  396. levelSwitch: boolean
  397. ): void {
  398. const { details: errorDetails } = errorEvent;
  399. const level = this._levels[levelIndex];
  400.  
  401. level.loadError++;
  402.  
  403. if (levelError) {
  404. const retrying = this.retryLoadingOrFail(errorEvent);
  405. if (retrying) {
  406. // boolean used to inform stream controller not to switch back to IDLE on non fatal error
  407. errorEvent.levelRetry = true;
  408. } else {
  409. this.currentLevelIndex = -1;
  410. return;
  411. }
  412. }
  413.  
  414. if (levelSwitch) {
  415. const redundantLevels = level.url.length;
  416. // Try redundant fail-over until level.loadError reaches redundantLevels
  417. if (redundantLevels > 1 && level.loadError < redundantLevels) {
  418. errorEvent.levelRetry = true;
  419. this.redundantFailover(levelIndex);
  420. } else if (this.manualLevelIndex === -1) {
  421. // Search for next level to retry
  422. let nextLevel = -1;
  423. const levels = this._levels;
  424. for (let i = levels.length; i--; ) {
  425. const candidate = (i + this.currentLevelIndex) % levels.length;
  426. if (
  427. candidate !== this.currentLevelIndex &&
  428. levels[candidate].loadError === 0
  429. ) {
  430. nextLevel = candidate;
  431. break;
  432. }
  433. }
  434. if (nextLevel > -1 && this.currentLevelIndex !== nextLevel) {
  435. this.warn(`${errorDetails}: switch to ${nextLevel}`);
  436. errorEvent.levelRetry = true;
  437. this.hls.nextAutoLevel = nextLevel;
  438. }
  439. }
  440. }
  441. }
  442.  
  443. private redundantFailover(levelIndex: number) {
  444. const level = this._levels[levelIndex];
  445. const redundantLevels = level.url.length;
  446. if (redundantLevels > 1) {
  447. // Update the url id of all levels so that we stay on the same set of variants when level switching
  448. const newUrlId = (level.urlId + 1) % redundantLevels;
  449. this.warn(`Switching to redundant URL-id ${newUrlId}`);
  450. this._levels.forEach((level) => {
  451. level.urlId = newUrlId;
  452. });
  453. this.level = levelIndex;
  454. }
  455. }
  456.  
  457. // reset errors on the successful load of a fragment
  458. protected onFragLoaded(event: Events.FRAG_LOADED, { frag }: FragLoadedData) {
  459. if (frag !== undefined && frag.type === PlaylistLevelType.MAIN) {
  460. const level = this._levels[frag.level];
  461. if (level !== undefined) {
  462. level.fragmentError = 0;
  463. level.loadError = 0;
  464. }
  465. }
  466. }
  467.  
  468. protected onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
  469. const { level, details } = data;
  470. const curLevel = this._levels[level];
  471.  
  472. if (!curLevel) {
  473. this.warn(`Invalid level index ${level}`);
  474. if (data.deliveryDirectives?.skip) {
  475. details.deltaUpdateFailed = true;
  476. }
  477. return;
  478. }
  479.  
  480. // only process level loaded events matching with expected level
  481. if (level === this.currentLevelIndex) {
  482. // reset level load error counter on successful level loaded only if there is no issues with fragments
  483. if (curLevel.fragmentError === 0) {
  484. curLevel.loadError = 0;
  485. this.retryCount = 0;
  486. }
  487. this.playlistLoaded(level, data, curLevel.details);
  488. } else if (data.deliveryDirectives?.skip) {
  489. // received a delta playlist update that cannot be merged
  490. details.deltaUpdateFailed = true;
  491. }
  492. }
  493.  
  494. protected onAudioTrackSwitched(
  495. event: Events.AUDIO_TRACK_SWITCHED,
  496. data: TrackSwitchedData
  497. ) {
  498. const currentLevel = this.hls.levels[this.currentLevelIndex];
  499. if (!currentLevel) {
  500. return;
  501. }
  502.  
  503. if (currentLevel.audioGroupIds) {
  504. let urlId = -1;
  505. const audioGroupId = this.hls.audioTracks[data.id].groupId;
  506. for (let i = 0; i < currentLevel.audioGroupIds.length; i++) {
  507. if (currentLevel.audioGroupIds[i] === audioGroupId) {
  508. urlId = i;
  509. break;
  510. }
  511. }
  512.  
  513. if (urlId !== currentLevel.urlId) {
  514. currentLevel.urlId = urlId;
  515. this.startLoad();
  516. }
  517. }
  518. }
  519.  
  520. protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters) {
  521. const level = this.currentLevelIndex;
  522. const currentLevel = this._levels[level];
  523.  
  524. if (this.canLoad && currentLevel && currentLevel.url.length > 0) {
  525. const id = currentLevel.urlId;
  526. let url = currentLevel.url[id];
  527. if (hlsUrlParameters) {
  528. try {
  529. url = hlsUrlParameters.addDirectives(url);
  530. } catch (error) {
  531. this.warn(
  532. `Could not construct new URL with HLS Delivery Directives: ${error}`
  533. );
  534. }
  535. }
  536.  
  537. this.log(
  538. `Attempt loading level index ${level}${
  539. hlsUrlParameters
  540. ? ' at sn ' +
  541. hlsUrlParameters.msn +
  542. ' part ' +
  543. hlsUrlParameters.part
  544. : ''
  545. } with URL-id ${id} ${url}`
  546. );
  547.  
  548. // console.log('Current audio track group ID:', this.hls.audioTracks[this.hls.audioTrack].groupId);
  549. // console.log('New video quality level audio group id:', levelObject.attrs.AUDIO, level);
  550. this.clearTimer();
  551. this.hls.trigger(Events.LEVEL_LOADING, {
  552. url,
  553. level,
  554. id,
  555. deliveryDirectives: hlsUrlParameters || null,
  556. });
  557. }
  558. }
  559.  
  560. get nextLoadLevel() {
  561. if (this.manualLevelIndex !== -1) {
  562. return this.manualLevelIndex;
  563. } else {
  564. return this.hls.nextAutoLevel;
  565. }
  566. }
  567.  
  568. set nextLoadLevel(nextLevel) {
  569. this.level = nextLevel;
  570. if (this.manualLevelIndex === -1) {
  571. this.hls.nextAutoLevel = nextLevel;
  572. }
  573. }
  574.  
  575. removeLevel(levelIndex, urlId) {
  576. const filterLevelAndGroupByIdIndex = (url, id) => id !== urlId;
  577. const levels = this._levels
  578. .filter((level, index) => {
  579. if (index !== levelIndex) {
  580. return true;
  581. }
  582.  
  583. if (level.url.length > 1 && urlId !== undefined) {
  584. level.url = level.url.filter(filterLevelAndGroupByIdIndex);
  585. if (level.audioGroupIds) {
  586. level.audioGroupIds = level.audioGroupIds.filter(
  587. filterLevelAndGroupByIdIndex
  588. );
  589. }
  590. if (level.textGroupIds) {
  591. level.textGroupIds = level.textGroupIds.filter(
  592. filterLevelAndGroupByIdIndex
  593. );
  594. }
  595. level.urlId = 0;
  596. return true;
  597. }
  598. return false;
  599. })
  600. .map((level, index) => {
  601. const { details } = level;
  602. if (details?.fragments) {
  603. details.fragments.forEach((fragment) => {
  604. fragment.level = index;
  605. });
  606. }
  607. return level;
  608. });
  609. this._levels = levels;
  610.  
  611. this.hls.trigger(Events.LEVELS_UPDATED, { levels });
  612. }
  613. }