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