Home Reference Source

src/controller/audio-track-controller.js

  1. import Event from '../events';
  2. import TaskLoop from '../task-loop';
  3. import { logger } from '../utils/logger';
  4. import { ErrorTypes, ErrorDetails } from '../errors';
  5.  
  6. /**
  7. * @class AudioTrackController
  8. * @implements {EventHandler}
  9. *
  10. * Handles main manifest and audio-track metadata loaded,
  11. * owns and exposes the selectable audio-tracks data-models.
  12. *
  13. * Exposes internal interface to select available audio-tracks.
  14. *
  15. * Handles errors on loading audio-track playlists. Manages fallback mechanism
  16. * with redundants tracks (group-IDs).
  17. *
  18. * Handles level-loading and group-ID switches for video (fallback on video levels),
  19. * and eventually adapts the audio-track group-ID to match.
  20. *
  21. * @fires AUDIO_TRACK_LOADING
  22. * @fires AUDIO_TRACK_SWITCHING
  23. * @fires AUDIO_TRACKS_UPDATED
  24. * @fires ERROR
  25. *
  26. */
  27. class AudioTrackController extends TaskLoop {
  28. constructor (hls) {
  29. super(hls,
  30. Event.MANIFEST_LOADING,
  31. Event.MANIFEST_PARSED,
  32. Event.AUDIO_TRACK_LOADED,
  33. Event.AUDIO_TRACK_SWITCHED,
  34. Event.LEVEL_LOADED,
  35. Event.ERROR
  36. );
  37.  
  38. /**
  39. * @private
  40. * Currently selected index in `tracks`
  41. * @member {number} trackId
  42. */
  43. this._trackId = -1;
  44.  
  45. /**
  46. * @private
  47. * If should select tracks according to default track attribute
  48. * @member {boolean} _selectDefaultTrack
  49. */
  50. this._selectDefaultTrack = true;
  51.  
  52. /**
  53. * @public
  54. * All tracks available
  55. * @member {AudioTrack[]}
  56. */
  57. this.tracks = [];
  58.  
  59. /**
  60. * @public
  61. * List of blacklisted audio track IDs (that have caused failure)
  62. * @member {number[]}
  63. */
  64. this.trackIdBlacklist = Object.create(null);
  65.  
  66. /**
  67. * @public
  68. * The currently running group ID for audio
  69. * (we grab this on manifest-parsed and new level-loaded)
  70. * @member {string}
  71. */
  72. this.audioGroupId = null;
  73. }
  74.  
  75. /**
  76. * Reset audio tracks on new manifest loading.
  77. */
  78. onManifestLoading () {
  79. this.tracks = [];
  80. this._trackId = -1;
  81. this._selectDefaultTrack = true;
  82. }
  83.  
  84. /**
  85. * Store tracks data from manifest parsed data.
  86. *
  87. * Trigger AUDIO_TRACKS_UPDATED event.
  88. *
  89. * @param {*} data
  90. */
  91. onManifestParsed (data) {
  92. const tracks = this.tracks = data.audioTracks || [];
  93. this.hls.trigger(Event.AUDIO_TRACKS_UPDATED, { audioTracks: tracks });
  94.  
  95. this._selectAudioGroup(this.hls.nextLoadLevel);
  96. }
  97.  
  98. /**
  99. * Store track details of loaded track in our data-model.
  100. *
  101. * Set-up metadata update interval task for live-mode streams.
  102. *
  103. * @param {*} data
  104. */
  105. onAudioTrackLoaded (data) {
  106. if (data.id >= this.tracks.length) {
  107. logger.warn('Invalid audio track id:', data.id);
  108. return;
  109. }
  110.  
  111. logger.log(`audioTrack ${data.id} loaded`);
  112.  
  113. this.tracks[data.id].details = data.details;
  114.  
  115. // check if current playlist is a live playlist
  116. // and if we have already our reload interval setup
  117. if (data.details.live && !this.hasInterval()) {
  118. // if live playlist we will have to reload it periodically
  119. // set reload period to playlist target duration
  120. const updatePeriodMs = data.details.targetduration * 1000;
  121. this.setInterval(updatePeriodMs);
  122. }
  123.  
  124. if (!data.details.live && this.hasInterval()) {
  125. // playlist is not live and timer is scheduled: cancel it
  126. this.clearInterval();
  127. }
  128. }
  129.  
  130. /**
  131. * Update the internal group ID to any audio-track we may have set manually
  132. * or because of a failure-handling fallback.
  133. *
  134. * Quality-levels should update to that group ID in this case.
  135. *
  136. * @param {*} data
  137. */
  138. onAudioTrackSwitched (data) {
  139. const audioGroupId = this.tracks[data.id].groupId;
  140. if (audioGroupId && (this.audioGroupId !== audioGroupId)) {
  141. this.audioGroupId = audioGroupId;
  142. }
  143. }
  144.  
  145. /**
  146. * When a level gets loaded, if it has redundant audioGroupIds (in the same ordinality as it's redundant URLs)
  147. * we are setting our audio-group ID internally to the one set, if it is different from the group ID currently set.
  148. *
  149. * If group-ID got update, we re-select the appropriate audio-track with this group-ID matching the currently
  150. * selected one (based on NAME property).
  151. *
  152. * @param {*} data
  153. */
  154. onLevelLoaded (data) {
  155. this._selectAudioGroup(data.level);
  156. }
  157.  
  158. /**
  159. * Handle network errors loading audio track manifests
  160. * and also pausing on any netwok errors.
  161. *
  162. * @param {ErrorEventData} data
  163. */
  164. onError (data) {
  165. // Only handle network errors
  166. if (data.type !== ErrorTypes.NETWORK_ERROR) {
  167. return;
  168. }
  169.  
  170. // If fatal network error, cancel update task
  171. if (data.fatal) {
  172. this.clearInterval();
  173. }
  174.  
  175. // If not an audio-track loading error don't handle further
  176. if (data.details !== ErrorDetails.AUDIO_TRACK_LOAD_ERROR) {
  177. return;
  178. }
  179.  
  180. logger.warn('Network failure on audio-track id:', data.context.id);
  181. this._handleLoadError();
  182. }
  183.  
  184. /**
  185. * @type {AudioTrack[]} Audio-track list we own
  186. */
  187. get audioTracks () {
  188. return this.tracks;
  189. }
  190.  
  191. /**
  192. * @type {number} Index into audio-tracks list of currently selected track.
  193. */
  194. get audioTrack () {
  195. return this._trackId;
  196. }
  197.  
  198. /**
  199. * Select current track by index
  200. */
  201. set audioTrack (newId) {
  202. this._setAudioTrack(newId);
  203. // If audio track is selected from API then don't choose from the manifest default track
  204. this._selectDefaultTrack = false;
  205. }
  206.  
  207. /**
  208. * @private
  209. * @param {number} newId
  210. */
  211. _setAudioTrack (newId) {
  212. // noop on same audio track id as already set
  213. if (this._trackId === newId && this.tracks[this._trackId].details) {
  214. logger.debug('Same id as current audio-track passed, and track details available -> no-op');
  215. return;
  216. }
  217.  
  218. // check if level idx is valid
  219. if (newId < 0 || newId >= this.tracks.length) {
  220. logger.warn('Invalid id passed to audio-track controller');
  221. return;
  222. }
  223.  
  224. const audioTrack = this.tracks[newId];
  225.  
  226. logger.log(`Now switching to audio-track index ${newId}`);
  227.  
  228. // stopping live reloading timer if any
  229. this.clearInterval();
  230. this._trackId = newId;
  231.  
  232. const { url, type, id } = audioTrack;
  233. this.hls.trigger(Event.AUDIO_TRACK_SWITCHING, { id, type, url });
  234. this._loadTrackDetailsIfNeeded(audioTrack);
  235. }
  236.  
  237. /**
  238. * @override
  239. */
  240. doTick () {
  241. this._updateTrack(this._trackId);
  242. }
  243.  
  244. /**
  245. * @param levelId
  246. * @private
  247. */
  248. _selectAudioGroup (levelId) {
  249. const levelInfo = this.hls.levels[levelId];
  250.  
  251. if (!levelInfo || !levelInfo.audioGroupIds) {
  252. return;
  253. }
  254.  
  255. const audioGroupId = levelInfo.audioGroupIds[levelInfo.urlId];
  256. if (this.audioGroupId !== audioGroupId) {
  257. this.audioGroupId = audioGroupId;
  258. this._selectInitialAudioTrack();
  259. }
  260. }
  261.  
  262. /**
  263. * Select initial track
  264. * @private
  265. */
  266. _selectInitialAudioTrack () {
  267. let tracks = this.tracks;
  268. if (!tracks.length) {
  269. return;
  270. }
  271.  
  272. const currentAudioTrack = this.tracks[this._trackId];
  273.  
  274. let name = null;
  275. if (currentAudioTrack) {
  276. name = currentAudioTrack.name;
  277. }
  278.  
  279. // Pre-select default tracks if there are any
  280. if (this._selectDefaultTrack) {
  281. const defaultTracks = tracks.filter((track) => track.default);
  282. if (defaultTracks.length) {
  283. tracks = defaultTracks;
  284. } else {
  285. logger.warn('No default audio tracks defined');
  286. }
  287. }
  288.  
  289. let trackFound = false;
  290.  
  291. const traverseTracks = () => {
  292. // Select track with right group ID
  293. tracks.forEach((track) => {
  294. if (trackFound) {
  295. return;
  296. }
  297. // We need to match the (pre-)selected group ID
  298. // and the NAME of the current track.
  299. if ((!this.audioGroupId || track.groupId === this.audioGroupId) &&
  300. (!name || name === track.name)) {
  301. // If there was a previous track try to stay with the same `NAME`.
  302. // It should be unique across tracks of same group, and consistent through redundant track groups.
  303. this._setAudioTrack(track.id);
  304. trackFound = true;
  305. }
  306. });
  307. };
  308.  
  309. traverseTracks();
  310.  
  311. if (!trackFound) {
  312. name = null;
  313. traverseTracks();
  314. }
  315.  
  316. if (!trackFound) {
  317. logger.error(`No track found for running audio group-ID: ${this.audioGroupId}`);
  318.  
  319. this.hls.trigger(Event.ERROR, {
  320. type: ErrorTypes.MEDIA_ERROR,
  321. details: ErrorDetails.AUDIO_TRACK_LOAD_ERROR,
  322. fatal: true
  323. });
  324. }
  325. }
  326.  
  327. /**
  328. * @private
  329. * @param {AudioTrack} audioTrack
  330. * @returns {boolean}
  331. */
  332. _needsTrackLoading (audioTrack) {
  333. const { details, url } = audioTrack;
  334.  
  335. if (!details || details.live) {
  336. // check if we face an audio track embedded in main playlist (audio track without URI attribute)
  337. return !!url;
  338. }
  339.  
  340. return false;
  341. }
  342.  
  343. /**
  344. * @private
  345. * @param {AudioTrack} audioTrack
  346. */
  347. _loadTrackDetailsIfNeeded (audioTrack) {
  348. if (this._needsTrackLoading(audioTrack)) {
  349. const { url, id } = audioTrack;
  350. // track not retrieved yet, or live playlist we need to (re)load it
  351. logger.log(`loading audio-track playlist for id: ${id}`);
  352. this.hls.trigger(Event.AUDIO_TRACK_LOADING, { url, id });
  353. }
  354. }
  355.  
  356. /**
  357. * @private
  358. * @param {number} newId
  359. */
  360. _updateTrack (newId) {
  361. // check if level idx is valid
  362. if (newId < 0 || newId >= this.tracks.length) {
  363. return;
  364. }
  365.  
  366. // stopping live reloading timer if any
  367. this.clearInterval();
  368. this._trackId = newId;
  369. logger.log(`trying to update audio-track ${newId}`);
  370. const audioTrack = this.tracks[newId];
  371. this._loadTrackDetailsIfNeeded(audioTrack);
  372. }
  373.  
  374. /**
  375. * @private
  376. */
  377. _handleLoadError () {
  378. // First, let's black list current track id
  379. this.trackIdBlacklist[this._trackId] = true;
  380.  
  381. // Let's try to fall back on a functional audio-track with the same group ID
  382. const previousId = this._trackId;
  383. const { name, language, groupId } = this.tracks[previousId];
  384.  
  385. logger.warn(`Loading failed on audio track id: ${previousId}, group-id: ${groupId}, name/language: "${name}" / "${language}"`);
  386.  
  387. // Find a non-blacklisted track ID with the same NAME
  388. // At least a track that is not blacklisted, thus on another group-ID.
  389. let newId = previousId;
  390. for (let i = 0; i < this.tracks.length; i++) {
  391. if (this.trackIdBlacklist[i]) {
  392. continue;
  393. }
  394. const newTrack = this.tracks[i];
  395. if (newTrack.name === name) {
  396. newId = i;
  397. break;
  398. }
  399. }
  400.  
  401. if (newId === previousId) {
  402. logger.warn(`No fallback audio-track found for name/language: "${name}" / "${language}"`);
  403. return;
  404. }
  405.  
  406. logger.log('Attempting audio-track fallback id:', newId, 'group-id:', this.tracks[newId].groupId);
  407.  
  408. this._setAudioTrack(newId);
  409. }
  410. }
  411.  
  412. export default AudioTrackController;