Home Reference Source

src/controller/level-controller.js

  1. /*
  2. * Level Controller
  3. */
  4.  
  5. import Event from '../events';
  6. import EventHandler from '../event-handler';
  7. import { logger } from '../utils/logger';
  8. import { ErrorTypes, ErrorDetails } from '../errors';
  9. import { isCodecSupportedInMp4 } from '../utils/codecs';
  10. import { addGroupId, computeReloadInterval } from './level-helper';
  11.  
  12. let chromeOrFirefox;
  13.  
  14. export default class LevelController extends EventHandler {
  15. constructor (hls) {
  16. super(hls,
  17. Event.MANIFEST_LOADED,
  18. Event.LEVEL_LOADED,
  19. Event.AUDIO_TRACK_SWITCHED,
  20. Event.FRAG_LOADED,
  21. Event.ERROR);
  22.  
  23. this.canload = false;
  24. this.currentLevelIndex = null;
  25. this.manualLevelIndex = -1;
  26. this.timer = null;
  27.  
  28. chromeOrFirefox = /chrome|firefox/.test(navigator.userAgent.toLowerCase());
  29. }
  30.  
  31. onHandlerDestroying () {
  32. this.clearTimer();
  33. this.manualLevelIndex = -1;
  34. }
  35.  
  36. clearTimer () {
  37. if (this.timer !== null) {
  38. clearTimeout(this.timer);
  39. this.timer = null;
  40. }
  41. }
  42.  
  43. startLoad () {
  44. let levels = this._levels;
  45.  
  46. this.canload = true;
  47. this.levelRetryCount = 0;
  48.  
  49. // clean up live level details to force reload them, and reset load errors
  50. if (levels) {
  51. levels.forEach(level => {
  52. level.loadError = 0;
  53. });
  54. }
  55. // speed up live playlist refresh if timer exists
  56. if (this.timer !== null) {
  57. this.loadLevel();
  58. }
  59. }
  60.  
  61. stopLoad () {
  62. this.canload = false;
  63. }
  64.  
  65. onManifestLoaded (data) {
  66. let levels = [];
  67. let audioTracks = [];
  68. let bitrateStart;
  69. let levelSet = {};
  70. let levelFromSet = null;
  71. let videoCodecFound = false;
  72. let audioCodecFound = false;
  73.  
  74. // regroup redundant levels together
  75. data.levels.forEach(level => {
  76. const attributes = level.attrs;
  77. level.loadError = 0;
  78. level.fragmentError = false;
  79.  
  80. videoCodecFound = videoCodecFound || !!level.videoCodec;
  81. audioCodecFound = audioCodecFound || !!level.audioCodec;
  82.  
  83. // erase audio codec info if browser does not support mp4a.40.34.
  84. // demuxer will autodetect codec and fallback to mpeg/audio
  85. if (chromeOrFirefox && level.audioCodec && level.audioCodec.indexOf('mp4a.40.34') !== -1) {
  86. level.audioCodec = undefined;
  87. }
  88.  
  89. levelFromSet = levelSet[level.bitrate]; // FIXME: we would also have to match the resolution here
  90.  
  91. if (!levelFromSet) {
  92. level.url = [level.url];
  93. level.urlId = 0;
  94. levelSet[level.bitrate] = level;
  95. levels.push(level);
  96. } else {
  97. levelFromSet.url.push(level.url);
  98. }
  99.  
  100. if (attributes) {
  101. if (attributes.AUDIO) {
  102. addGroupId(levelFromSet || level, 'audio', attributes.AUDIO);
  103. }
  104. if (attributes.SUBTITLES) {
  105. addGroupId(levelFromSet || level, 'text', attributes.SUBTITLES);
  106. }
  107. }
  108. });
  109.  
  110. // remove audio-only level if we also have levels with audio+video codecs signalled
  111. if (videoCodecFound && audioCodecFound) {
  112. levels = levels.filter(({ videoCodec }) => !!videoCodec);
  113. }
  114.  
  115. // only keep levels with supported audio/video codecs
  116. levels = levels.filter(({ audioCodec, videoCodec }) => {
  117. return (!audioCodec || isCodecSupportedInMp4(audioCodec, 'audio')) && (!videoCodec || isCodecSupportedInMp4(videoCodec, 'video'));
  118. });
  119.  
  120. if (data.audioTracks) {
  121. audioTracks = data.audioTracks.filter(track => !track.audioCodec || isCodecSupportedInMp4(track.audioCodec, 'audio'));
  122. // Reassign id's after filtering since they're used as array indices
  123. audioTracks.forEach((track, index) => {
  124. track.id = index;
  125. });
  126. }
  127.  
  128. if (levels.length > 0) {
  129. // start bitrate is the first bitrate of the manifest
  130. bitrateStart = levels[0].bitrate;
  131. // sort level on bitrate
  132. levels.sort((a, b) => a.bitrate - b.bitrate);
  133. this._levels = levels;
  134. // find index of first level in sorted levels
  135. for (let i = 0; i < levels.length; i++) {
  136. if (levels[i].bitrate === bitrateStart) {
  137. this._firstLevel = i;
  138. logger.log(`manifest loaded,${levels.length} level(s) found, first bitrate:${bitrateStart}`);
  139. break;
  140. }
  141. }
  142.  
  143. // Audio is only alternate if manifest include a URI along with the audio group tag,
  144. // and this is not an audio-only stream where levels contain audio-only
  145. const audioOnly = audioCodecFound && !videoCodecFound;
  146. this.hls.trigger(Event.MANIFEST_PARSED, {
  147. levels,
  148. audioTracks,
  149. firstLevel: this._firstLevel,
  150. stats: data.stats,
  151. audio: audioCodecFound,
  152. video: videoCodecFound,
  153. altAudio: !audioOnly && audioTracks.some(t => !!t.url)
  154. });
  155. } else {
  156. this.hls.trigger(Event.ERROR, {
  157. type: ErrorTypes.MEDIA_ERROR,
  158. details: ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR,
  159. fatal: true,
  160. url: this.hls.url,
  161. reason: 'no level with compatible codecs found in manifest'
  162. });
  163. }
  164. }
  165.  
  166. get levels () {
  167. return this._levels;
  168. }
  169.  
  170. get level () {
  171. return this.currentLevelIndex;
  172. }
  173.  
  174. set level (newLevel) {
  175. let levels = this._levels;
  176. if (levels) {
  177. newLevel = Math.min(newLevel, levels.length - 1);
  178. if (this.currentLevelIndex !== newLevel || !levels[newLevel].details) {
  179. this.setLevelInternal(newLevel);
  180. }
  181. }
  182. }
  183.  
  184. setLevelInternal (newLevel) {
  185. const levels = this._levels;
  186. const hls = this.hls;
  187. // check if level idx is valid
  188. if (newLevel >= 0 && newLevel < levels.length) {
  189. // stopping live reloading timer if any
  190. this.clearTimer();
  191. if (this.currentLevelIndex !== newLevel) {
  192. logger.log(`switching to level ${newLevel}`);
  193. this.currentLevelIndex = newLevel;
  194. const levelProperties = levels[newLevel];
  195. levelProperties.level = newLevel;
  196. hls.trigger(Event.LEVEL_SWITCHING, levelProperties);
  197. }
  198. const level = levels[newLevel];
  199. const levelDetails = level.details;
  200.  
  201. // check if we need to load playlist for this level
  202. if (!levelDetails || levelDetails.live) {
  203. // level not retrieved yet, or live playlist we need to (re)load it
  204. let urlId = level.urlId;
  205. hls.trigger(Event.LEVEL_LOADING, { url: level.url[urlId], level: newLevel, id: urlId });
  206. }
  207. } else {
  208. // invalid level id given, trigger error
  209. hls.trigger(Event.ERROR, {
  210. type: ErrorTypes.OTHER_ERROR,
  211. details: ErrorDetails.LEVEL_SWITCH_ERROR,
  212. level: newLevel,
  213. fatal: false,
  214. reason: 'invalid level idx'
  215. });
  216. }
  217. }
  218.  
  219. get manualLevel () {
  220. return this.manualLevelIndex;
  221. }
  222.  
  223. set manualLevel (newLevel) {
  224. this.manualLevelIndex = newLevel;
  225. if (this._startLevel === undefined) {
  226. this._startLevel = newLevel;
  227. }
  228.  
  229. if (newLevel !== -1) {
  230. this.level = newLevel;
  231. }
  232. }
  233.  
  234. get firstLevel () {
  235. return this._firstLevel;
  236. }
  237.  
  238. set firstLevel (newLevel) {
  239. this._firstLevel = newLevel;
  240. }
  241.  
  242. get startLevel () {
  243. // hls.startLevel takes precedence over config.startLevel
  244. // if none of these values are defined, fallback on this._firstLevel (first quality level appearing in variant manifest)
  245. if (this._startLevel === undefined) {
  246. let configStartLevel = this.hls.config.startLevel;
  247. if (configStartLevel !== undefined) {
  248. return configStartLevel;
  249. } else {
  250. return this._firstLevel;
  251. }
  252. } else {
  253. return this._startLevel;
  254. }
  255. }
  256.  
  257. set startLevel (newLevel) {
  258. this._startLevel = newLevel;
  259. }
  260.  
  261. onError (data) {
  262. if (data.fatal) {
  263. if (data.type === ErrorTypes.NETWORK_ERROR) {
  264. this.clearTimer();
  265. }
  266.  
  267. return;
  268. }
  269.  
  270. let levelError = false, fragmentError = false;
  271. let levelIndex;
  272.  
  273. // try to recover not fatal errors
  274. switch (data.details) {
  275. case ErrorDetails.FRAG_LOAD_ERROR:
  276. case ErrorDetails.FRAG_LOAD_TIMEOUT:
  277. case ErrorDetails.KEY_LOAD_ERROR:
  278. case ErrorDetails.KEY_LOAD_TIMEOUT:
  279. levelIndex = data.frag.level;
  280. fragmentError = true;
  281. break;
  282. case ErrorDetails.LEVEL_LOAD_ERROR:
  283. case ErrorDetails.LEVEL_LOAD_TIMEOUT:
  284. levelIndex = data.context.level;
  285. levelError = true;
  286. break;
  287. case ErrorDetails.REMUX_ALLOC_ERROR:
  288. levelIndex = data.level;
  289. levelError = true;
  290. break;
  291. }
  292.  
  293. if (levelIndex !== undefined) {
  294. this.recoverLevel(data, levelIndex, levelError, fragmentError);
  295. }
  296. }
  297.  
  298. /**
  299. * Switch to a redundant stream if any available.
  300. * If redundant stream is not available, emergency switch down if ABR mode is enabled.
  301. *
  302. * @param {Object} errorEvent
  303. * @param {Number} levelIndex current level index
  304. * @param {Boolean} levelError
  305. * @param {Boolean} fragmentError
  306. */
  307. // FIXME Find a better abstraction where fragment/level retry management is well decoupled
  308. recoverLevel (errorEvent, levelIndex, levelError, fragmentError) {
  309. let { config } = this.hls;
  310. let { details: errorDetails } = errorEvent;
  311. let level = this._levels[levelIndex];
  312. let redundantLevels, delay, nextLevel;
  313.  
  314. level.loadError++;
  315. level.fragmentError = fragmentError;
  316.  
  317. if (levelError) {
  318. if ((this.levelRetryCount + 1) <= config.levelLoadingMaxRetry) {
  319. // exponential backoff capped to max retry timeout
  320. delay = Math.min(Math.pow(2, this.levelRetryCount) * config.levelLoadingRetryDelay, config.levelLoadingMaxRetryTimeout);
  321. // Schedule level reload
  322. this.timer = setTimeout(() => this.loadLevel(), delay);
  323. // boolean used to inform stream controller not to switch back to IDLE on non fatal error
  324. errorEvent.levelRetry = true;
  325. this.levelRetryCount++;
  326. logger.warn(`level controller, ${errorDetails}, retry in ${delay} ms, current retry count is ${this.levelRetryCount}`);
  327. } else {
  328. logger.error(`level controller, cannot recover from ${errorDetails} error`);
  329. this.currentLevelIndex = null;
  330. // stopping live reloading timer if any
  331. this.clearTimer();
  332. // switch error to fatal
  333. errorEvent.fatal = true;
  334. return;
  335. }
  336. }
  337.  
  338. // Try any redundant streams if available for both errors: level and fragment
  339. // If level.loadError reaches redundantLevels it means that we tried them all, no hope => let's switch down
  340. if (levelError || fragmentError) {
  341. redundantLevels = level.url.length;
  342.  
  343. if (redundantLevels > 1 && level.loadError < redundantLevels) {
  344. level.urlId = (level.urlId + 1) % redundantLevels;
  345. level.details = undefined;
  346.  
  347. logger.warn(`level controller, ${errorDetails} for level ${levelIndex}: switching to redundant URL-id ${level.urlId}`);
  348.  
  349. // console.log('Current audio track group ID:', this.hls.audioTracks[this.hls.audioTrack].groupId);
  350. // console.log('New video quality level audio group id:', level.attrs.AUDIO);
  351. } else {
  352. // Search for available level
  353. if (this.manualLevelIndex === -1) {
  354. // When lowest level has been reached, let's start hunt from the top
  355. nextLevel = (levelIndex === 0) ? this._levels.length - 1 : levelIndex - 1;
  356. logger.warn(`level controller, ${errorDetails}: switch to ${nextLevel}`);
  357. this.hls.nextAutoLevel = this.currentLevelIndex = nextLevel;
  358. } else if (fragmentError) {
  359. // Allow fragment retry as long as configuration allows.
  360. // reset this._level so that another call to set level() will trigger again a frag load
  361. logger.warn(`level controller, ${errorDetails}: reload a fragment`);
  362. this.currentLevelIndex = null;
  363. }
  364. }
  365. }
  366. }
  367.  
  368. // reset errors on the successful load of a fragment
  369. onFragLoaded ({ frag }) {
  370. if (frag !== undefined && frag.type === 'main') {
  371. const level = this._levels[frag.level];
  372. if (level !== undefined) {
  373. level.fragmentError = false;
  374. level.loadError = 0;
  375. this.levelRetryCount = 0;
  376. }
  377. }
  378. }
  379.  
  380. onLevelLoaded (data) {
  381. const { level, details } = data;
  382. // only process level loaded events matching with expected level
  383. if (level !== this.currentLevelIndex) {
  384. return;
  385. }
  386.  
  387. const curLevel = this._levels[level];
  388. // reset level load error counter on successful level loaded only if there is no issues with fragments
  389. if (!curLevel.fragmentError) {
  390. curLevel.loadError = 0;
  391. this.levelRetryCount = 0;
  392. }
  393. // if current playlist is a live playlist, arm a timer to reload it
  394. if (details.live) {
  395. const reloadInterval = computeReloadInterval(curLevel.details, details, data.stats.trequest);
  396. logger.log(`live playlist, reload in ${Math.round(reloadInterval)} ms`);
  397. this.timer = setTimeout(() => this.loadLevel(), reloadInterval);
  398. } else {
  399. this.clearTimer();
  400. }
  401. }
  402.  
  403. onAudioTrackSwitched (data) {
  404. const audioGroupId = this.hls.audioTracks[data.id].groupId;
  405.  
  406. const currentLevel = this.hls.levels[this.currentLevelIndex];
  407. if (!currentLevel) {
  408. return;
  409. }
  410.  
  411. if (currentLevel.audioGroupIds) {
  412. let urlId = -1;
  413.  
  414. for (let i = 0; i < currentLevel.audioGroupIds.length; i++) {
  415. if (currentLevel.audioGroupIds[i] === audioGroupId) {
  416. urlId = i;
  417. break;
  418. }
  419. }
  420.  
  421. if (urlId !== currentLevel.urlId) {
  422. currentLevel.urlId = urlId;
  423. this.startLoad();
  424. }
  425. }
  426. }
  427.  
  428. loadLevel () {
  429. logger.debug('call to loadLevel');
  430.  
  431. if (this.currentLevelIndex !== null && this.canload) {
  432. const levelObject = this._levels[this.currentLevelIndex];
  433.  
  434. if (typeof levelObject === 'object' &&
  435. levelObject.url.length > 0) {
  436. const level = this.currentLevelIndex;
  437. const id = levelObject.urlId;
  438. const url = levelObject.url[id];
  439.  
  440. logger.log(`Attempt loading level index ${level} with URL-id ${id}`);
  441.  
  442. // console.log('Current audio track group ID:', this.hls.audioTracks[this.hls.audioTrack].groupId);
  443. // console.log('New video quality level audio group id:', levelObject.attrs.AUDIO, level);
  444.  
  445. this.hls.trigger(Event.LEVEL_LOADING, { url, level, id });
  446. }
  447. }
  448. }
  449.  
  450. get nextLoadLevel () {
  451. if (this.manualLevelIndex !== -1) {
  452. return this.manualLevelIndex;
  453. } else {
  454. return this.hls.nextAutoLevel;
  455. }
  456. }
  457.  
  458. set nextLoadLevel (nextLevel) {
  459. this.level = nextLevel;
  460. if (this.manualLevelIndex === -1) {
  461. this.hls.nextAutoLevel = nextLevel;
  462. }
  463. }
  464.  
  465. removeLevel (levelIndex, urlId) {
  466. const levels = this.levels.filter((level, index) => {
  467. if (index !== levelIndex) {
  468. return true;
  469. }
  470.  
  471. if (level.url.length > 1 && urlId !== undefined) {
  472. level.url = level.url.filter((url, id) => id !== urlId);
  473. level.urlId = 0;
  474. return true;
  475. }
  476. return false;
  477. }).map((level, index) => {
  478. const { details } = level;
  479. if (details && details.fragments) {
  480. details.fragments.forEach((fragment) => {
  481. fragment.level = index;
  482. });
  483. }
  484. return level;
  485. });
  486.  
  487. this._levels = levels;
  488.  
  489. this.hls.trigger(Event.LEVELS_UPDATED, { levels });
  490. }
  491. }