forked from video-dev/hls.js
-
Notifications
You must be signed in to change notification settings - Fork 90
/
audio-track-controller.js
384 lines (331 loc) · 9.93 KB
/
audio-track-controller.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
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;
/**
* @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;
}
/**
* 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 });
}
/**
* 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) {
// FIXME: crashes because currentLevel is undefined
// const levelInfo = this.hls.levels[this.hls.currentLevel];
const levelInfo = this.hls.levels[data.level];
if (!levelInfo.audioGroupIds) {
return;
}
const audioGroupId = levelInfo.audioGroupIds[levelInfo.urlId];
if (this.audioGroupId !== audioGroupId) {
this.audioGroupId = audioGroupId;
this._selectInitialAudioTrack();
}
}
/**
* 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) {
// 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);
}
/**
* 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
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.audioTrack = 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 } = audioTrack;
if (!details) {
return true;
} else if (details.live) {
return true;
}
}
/**
* @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.audioTrack = newId;
}
}
export default AudioTrackController;