src/controller/audio-track-controller.js
- import Event from '../events';
- import TaskLoop from '../task-loop';
- import { logger } from '../utils/logger';
- import { ErrorTypes, ErrorDetails } from '../errors';
-
- /**
- * @class AudioTrackController
- * @implements {EventHandler}
- *
- * Handles main manifest and audio-track metadata loaded,
- * owns and exposes the selectable audio-tracks data-models.
- *
- * Exposes internal interface to select available audio-tracks.
- *
- * Handles errors on loading audio-track playlists. Manages fallback mechanism
- * with redundants tracks (group-IDs).
- *
- * Handles level-loading and group-ID switches for video (fallback on video levels),
- * and eventually adapts the audio-track group-ID to match.
- *
- * @fires AUDIO_TRACK_LOADING
- * @fires AUDIO_TRACK_SWITCHING
- * @fires AUDIO_TRACKS_UPDATED
- * @fires ERROR
- *
- */
- class AudioTrackController extends TaskLoop {
- constructor (hls) {
- super(hls,
- Event.MANIFEST_LOADING,
- Event.MANIFEST_PARSED,
- Event.AUDIO_TRACK_LOADED,
- Event.AUDIO_TRACK_SWITCHED,
- Event.LEVEL_LOADED,
- Event.ERROR
- );
-
- /**
- * @private
- * Currently selected index in `tracks`
- * @member {number} trackId
- */
- this._trackId = -1;
-
- /**
- * @private
- * If should select tracks according to default track attribute
- * @member {boolean} _selectDefaultTrack
- */
- this._selectDefaultTrack = true;
-
- /**
- * @public
- * All tracks available
- * @member {AudioTrack[]}
- */
- this.tracks = [];
-
- /**
- * @public
- * List of blacklisted audio track IDs (that have caused failure)
- * @member {number[]}
- */
- this.trackIdBlacklist = Object.create(null);
-
- /**
- * @public
- * The currently running group ID for audio
- * (we grab this on manifest-parsed and new level-loaded)
- * @member {string}
- */
- this.audioGroupId = null;
- }
-
- /**
- * Reset audio tracks on new manifest loading.
- */
- onManifestLoading () {
- this.tracks = [];
- this._trackId = -1;
- this._selectDefaultTrack = true;
- this.audioGroupId = null;
- }
-
- /**
- * Store tracks data from manifest parsed data.
- *
- * Trigger AUDIO_TRACKS_UPDATED event.
- *
- * @param {*} data
- */
- onManifestParsed (data) {
- const tracks = this.tracks = data.audioTracks || [];
- this.hls.trigger(Event.AUDIO_TRACKS_UPDATED, { audioTracks: tracks });
-
- this._selectAudioGroup(this.hls.nextLoadLevel);
- }
-
- /**
- * Store track details of loaded track in our data-model.
- *
- * Set-up metadata update interval task for live-mode streams.
- *
- * @param {*} data
- */
- onAudioTrackLoaded (data) {
- if (data.id >= this.tracks.length) {
- logger.warn('Invalid audio track id:', data.id);
- return;
- }
-
- logger.log(`audioTrack ${data.id} loaded`);
-
- this.tracks[data.id].details = data.details;
-
- // check if current playlist is a live playlist
- // and if we have already our reload interval setup
- if (data.details.live && !this.hasInterval()) {
- // if live playlist we will have to reload it periodically
- // set reload period to playlist target duration
- const updatePeriodMs = data.details.targetduration * 1000;
- this.setInterval(updatePeriodMs);
- }
-
- if (!data.details.live && this.hasInterval()) {
- // playlist is not live and timer is scheduled: cancel it
- this.clearInterval();
- }
- }
-
- /**
- * Update the internal group ID to any audio-track we may have set manually
- * or because of a failure-handling fallback.
- *
- * Quality-levels should update to that group ID in this case.
- *
- * @param {*} data
- */
- onAudioTrackSwitched (data) {
- const audioGroupId = this.tracks[data.id].groupId;
- if (audioGroupId && (this.audioGroupId !== audioGroupId)) {
- this.audioGroupId = audioGroupId;
- }
- }
-
- /**
- * When a level gets loaded, if it has redundant audioGroupIds (in the same ordinality as it's redundant URLs)
- * we are setting our audio-group ID internally to the one set, if it is different from the group ID currently set.
- *
- * If group-ID got update, we re-select the appropriate audio-track with this group-ID matching the currently
- * selected one (based on NAME property).
- *
- * @param {*} data
- */
- onLevelLoaded (data) {
- this._selectAudioGroup(data.level);
- }
-
- /**
- * Handle network errors loading audio track manifests
- * and also pausing on any netwok errors.
- *
- * @param {ErrorEventData} data
- */
- onError (data) {
- // Only handle network errors
- if (data.type !== ErrorTypes.NETWORK_ERROR) {
- return;
- }
-
- // If fatal network error, cancel update task
- if (data.fatal) {
- this.clearInterval();
- }
-
- // If not an audio-track loading error don't handle further
- if (data.details !== ErrorDetails.AUDIO_TRACK_LOAD_ERROR) {
- return;
- }
-
- logger.warn('Network failure on audio-track id:', data.context.id);
- this._handleLoadError();
- }
-
- /**
- * @type {AudioTrack[]} Audio-track list we own
- */
- get audioTracks () {
- return this.tracks;
- }
-
- /**
- * @type {number} Index into audio-tracks list of currently selected track.
- */
- get audioTrack () {
- return this._trackId;
- }
-
- /**
- * Select current track by index
- */
- set audioTrack (newId) {
- this._setAudioTrack(newId);
- // If audio track is selected from API then don't choose from the manifest default track
- this._selectDefaultTrack = false;
- }
-
- /**
- * @private
- * @param {number} newId
- */
- _setAudioTrack (newId) {
- // noop on same audio track id as already set
- if (this._trackId === newId && this.tracks[this._trackId].details) {
- logger.debug('Same id as current audio-track passed, and track details available -> no-op');
- return;
- }
-
- // check if level idx is valid
- if (newId < 0 || newId >= this.tracks.length) {
- logger.warn('Invalid id passed to audio-track controller');
- return;
- }
-
- const audioTrack = this.tracks[newId];
-
- logger.log(`Now switching to audio-track index ${newId}`);
-
- // stopping live reloading timer if any
- this.clearInterval();
- this._trackId = newId;
-
- const { url, type, id } = audioTrack;
- this.hls.trigger(Event.AUDIO_TRACK_SWITCHING, { id, type, url });
- this._loadTrackDetailsIfNeeded(audioTrack);
- }
-
- /**
- * @override
- */
- doTick () {
- this._updateTrack(this._trackId);
- }
-
- /**
- * @param levelId
- * @private
- */
- _selectAudioGroup (levelId) {
- const levelInfo = this.hls.levels[levelId];
-
- if (!levelInfo || !levelInfo.audioGroupIds) {
- return;
- }
-
- const audioGroupId = levelInfo.audioGroupIds[levelInfo.urlId];
- if (this.audioGroupId !== audioGroupId) {
- this.audioGroupId = audioGroupId;
- this._selectInitialAudioTrack();
- }
- }
-
- /**
- * Select initial track
- * @private
- */
- _selectInitialAudioTrack () {
- let tracks = this.tracks;
- if (!tracks.length) {
- return;
- }
-
- const currentAudioTrack = this.tracks[this._trackId];
-
- let name = null;
- if (currentAudioTrack) {
- name = currentAudioTrack.name;
- }
-
- // Pre-select default tracks if there are any
- if (this._selectDefaultTrack) {
- const defaultTracks = tracks.filter((track) => track.default);
- if (defaultTracks.length) {
- tracks = defaultTracks;
- } else {
- logger.warn('No default audio tracks defined');
- }
- }
-
- let trackFound = false;
-
- const traverseTracks = () => {
- // Select track with right group ID
- tracks.forEach((track) => {
- if (trackFound) {
- return;
- }
- // We need to match the (pre-)selected group ID
- // and the NAME of the current track.
- if ((!this.audioGroupId || track.groupId === this.audioGroupId) &&
- (!name || name === track.name)) {
- // If there was a previous track try to stay with the same `NAME`.
- // It should be unique across tracks of same group, and consistent through redundant track groups.
- this._setAudioTrack(track.id);
- trackFound = true;
- }
- });
- };
-
- traverseTracks();
-
- if (!trackFound) {
- name = null;
- traverseTracks();
- }
-
- if (!trackFound) {
- logger.error(`No track found for running audio group-ID: ${this.audioGroupId}`);
-
- this.hls.trigger(Event.ERROR, {
- type: ErrorTypes.MEDIA_ERROR,
- details: ErrorDetails.AUDIO_TRACK_LOAD_ERROR,
- fatal: true
- });
- }
- }
-
- /**
- * @private
- * @param {AudioTrack} audioTrack
- * @returns {boolean}
- */
- _needsTrackLoading (audioTrack) {
- const { details, url } = audioTrack;
-
- if (!details || details.live) {
- // check if we face an audio track embedded in main playlist (audio track without URI attribute)
- return !!url;
- }
-
- return false;
- }
-
- /**
- * @private
- * @param {AudioTrack} audioTrack
- */
- _loadTrackDetailsIfNeeded (audioTrack) {
- if (this._needsTrackLoading(audioTrack)) {
- const { url, id } = audioTrack;
- // track not retrieved yet, or live playlist we need to (re)load it
- logger.log(`loading audio-track playlist for id: ${id}`);
- this.hls.trigger(Event.AUDIO_TRACK_LOADING, { url, id });
- }
- }
-
- /**
- * @private
- * @param {number} newId
- */
- _updateTrack (newId) {
- // check if level idx is valid
- if (newId < 0 || newId >= this.tracks.length) {
- return;
- }
-
- // stopping live reloading timer if any
- this.clearInterval();
- this._trackId = newId;
- logger.log(`trying to update audio-track ${newId}`);
- const audioTrack = this.tracks[newId];
- this._loadTrackDetailsIfNeeded(audioTrack);
- }
-
- /**
- * @private
- */
- _handleLoadError () {
- // First, let's black list current track id
- this.trackIdBlacklist[this._trackId] = true;
-
- // Let's try to fall back on a functional audio-track with the same group ID
- const previousId = this._trackId;
- const { name, language, groupId } = this.tracks[previousId];
-
- logger.warn(`Loading failed on audio track id: ${previousId}, group-id: ${groupId}, name/language: "${name}" / "${language}"`);
-
- // Find a non-blacklisted track ID with the same NAME
- // At least a track that is not blacklisted, thus on another group-ID.
- let newId = previousId;
- for (let i = 0; i < this.tracks.length; i++) {
- if (this.trackIdBlacklist[i]) {
- continue;
- }
- const newTrack = this.tracks[i];
- if (newTrack.name === name) {
- newId = i;
- break;
- }
- }
-
- if (newId === previousId) {
- logger.warn(`No fallback audio-track found for name/language: "${name}" / "${language}"`);
- return;
- }
-
- logger.log('Attempting audio-track fallback id:', newId, 'group-id:', this.tracks[newId].groupId);
-
- this._setAudioTrack(newId);
- }
- }
-
- export default AudioTrackController;