Skip to content

Commit d8b20b4

Browse files
authored
fix: multi-audio tracks (#874)
* fix: initAudioTrackInfo * chore: cleanup * chore: handle possible race condition * chore: selectAudioTrack
1 parent e6bf0e7 commit d8b20b4

File tree

1 file changed

+78
-14
lines changed

1 file changed

+78
-14
lines changed

src/plugins/adaptive-streaming/quality-levels.js

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -290,14 +290,69 @@ const qualityLevels = (player, options) => {
290290
}
291291
};
292292

293+
// Helper to synchronise Video.js AudioTrackList with hls.js active track
294+
const updateVjsAudioTracksEnabled = activeIndex => {
295+
const list = player.audioTracks();
296+
if (!list) return;
297+
for (let i = 0; i < list.length; i++) {
298+
list[i].enabled = i === activeIndex;
299+
}
300+
// Sync complete – log for debug
301+
logAudioTrackInfo();
302+
};
303+
304+
// Flush buffered audio by performing a tiny seek outside current range
305+
const flushBufferedAudio = () => {
306+
try {
307+
const cur = player.currentTime();
308+
const delta = 0.5; // 500 ms gap to escape current buffer
309+
const target = Math.min(cur + delta, player.duration() - 0.1);
310+
player.currentTime(target);
311+
player.currentTime(cur);
312+
} catch (e) {
313+
debugLog('Error while flushing buffered audio', e);
314+
}
315+
};
316+
317+
// Force hls.js to switch track and immediately start loading fragments for it
318+
const selectAudioTrack = index => {
319+
var tech = player.tech({ IWillNotUseThisInPlugins: true });
320+
if (
321+
typeof tech.sourceHandler_ != 'undefined' &&
322+
typeof tech.sourceHandler_.hls != 'undefined' &&
323+
tech.sourceHandler_.hls != null
324+
) {
325+
const hls = tech.sourceHandler_.hls;
326+
if (hls.audioTrack === index) {
327+
return;
328+
}
329+
hls.audioTrack = index;
330+
// Quickly restart loading to fill the buffer of the new audio group
331+
if (hls.media) {
332+
try {
333+
hls.stopLoad();
334+
hls.startLoad(0);
335+
} catch (e) {
336+
debugLog('Error while reloading after audioTrack switch', e);
337+
}
338+
}
339+
340+
logAudioTrackInfo();
341+
// will flush once switch is confirmed (AUDIO_TRACK_SWITCHED)
342+
hls.once(Hls.Events.AUDIO_TRACK_SWITCHED, () => {
343+
flushBufferedAudio();
344+
});
345+
}
346+
};
347+
293348
// Audio track handling
294-
const addAudioTrackVideojs = track => {
295-
var vjsTrack = new videojs.AudioTrack({
349+
const addAudioTrackVideojs = (track, hls) => {
350+
const vjsTrack = new videojs.AudioTrack({
296351
id: `${track.type}-id_${track.id}-groupId_${track.groupId}-${track.name}`,
297352
kind: 'translation',
298353
label: track.name,
299354
language: track.lang,
300-
enabled: track.enabled,
355+
enabled: hls.audioTrack === hls.audioTracks.indexOf(track),
301356
default: track.default
302357
});
303358

@@ -315,7 +370,7 @@ const qualityLevels = (player, options) => {
315370
const hls = tech.sourceHandler_.hls;
316371
const len = hls.audioTracks.length;
317372
for (let i = 0; i < len; i++) {
318-
addAudioTrackVideojs(hls.audioTracks[i]);
373+
addAudioTrackVideojs(hls.audioTracks[i], hls);
319374
}
320375
}
321376

@@ -328,11 +383,10 @@ const qualityLevels = (player, options) => {
328383
typeof tech.sourceHandler_.hls != 'undefined' &&
329384
tech.sourceHandler_.hls != null
330385
) {
331-
const hls = tech.sourceHandler_.hls;
332386
for (var i = 0; i < audioTrackList.length; i++) {
333387
var track = audioTrackList[i];
334388
if (track.enabled) {
335-
hls.audioTrack = i;
389+
selectAudioTrack(i);
336390
return;
337391
}
338392
}
@@ -354,25 +408,35 @@ const qualityLevels = (player, options) => {
354408
tech.sourceHandler_.hls != null
355409
) {
356410
const hls = tech.sourceHandler_.hls;
357-
hls.on(Hls.Events.MANIFEST_LOADED, (eventName, data) => {
411+
412+
const manifestLoadedHandler = (eventName, data) => {
358413
debugLog(`HLS event: ${eventName}`, data);
359414
populateLevels(hls.levels, 'hls');
360-
});
415+
initAudioTrackInfo();
416+
hls.off(Hls.Events.MANIFEST_LOADED, manifestLoadedHandler);
417+
};
418+
419+
// If manifest is already loaded, populate levels immediately.
420+
if (hls.levels && hls.levels.length > 0) {
421+
manifestLoadedHandler('MANUAL_MANIFEST_LOADED', {});
422+
} else {
423+
hls.on(Hls.Events.MANIFEST_LOADED, manifestLoadedHandler);
424+
}
361425

362426
hls.on(Hls.Events.LEVEL_SWITCHED, (eventName, data) => {
363427
debugLog(`HLS event: ${eventName}`, data);
364428
populateQualityLevelsChange(data.level);
365429
populateQualityChangedEvent(data.level);
366430
});
367431

368-
hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, (eventName, data) => {
369-
debugLog(`HLS event: ${eventName}`, data);
370-
initAudioTrackInfo();
371-
});
372-
373432
hls.on(Hls.Events.AUDIO_TRACK_SWITCHED, (eventName, data) => {
374433
debugLog(`HLS event: ${eventName}`, data);
375-
logAudioTrackInfo();
434+
// Make sure Video.js AudioTracks reflect the newly-selected HLS track
435+
if (typeof data.id !== 'undefined') {
436+
updateVjsAudioTracksEnabled(data.id);
437+
}
438+
// Ensure buffer flushed when switch originates from hls.js (e.g. default track)
439+
flushBufferedAudio();
376440
});
377441

378442
hls.on(Hls.Events.ERROR, (eventName, data) => {

0 commit comments

Comments
 (0)