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