mirror of
https://github.com/zoriya/v10.git
synced 2026-08-09 07:37:48 +00:00
fix(hls): audio selection forgotten
This commit is contained in:
@@ -56,6 +56,8 @@ export function HlsJsMediaMediaTracksMixin<Base extends Constructor<MediaTracksH
|
||||
|
||||
engine.on(Hls.Events.MANIFEST_PARSED, this.#onManifestParsed);
|
||||
engine.on(Hls.Events.AUDIO_TRACKS_UPDATED, this.#onAudioTracksUpdated);
|
||||
engine.on(Hls.Events.AUDIO_TRACK_SWITCHING, this.#onAudioTrackSwitched);
|
||||
engine.on(Hls.Events.AUDIO_TRACK_SWITCHED, this.#onAudioTrackSwitched);
|
||||
engine.on(Hls.Events.LEVELS_UPDATED, this.#onLevelsUpdated);
|
||||
engine.on(Hls.Events.LEVEL_SWITCHED, this.#onLevelSwitched);
|
||||
engine.once(Hls.Events.DESTROYING, this.#teardown);
|
||||
@@ -87,16 +89,38 @@ export function HlsJsMediaMediaTracksMixin<Base extends Constructor<MediaTracksH
|
||||
};
|
||||
|
||||
#onAudioTracksUpdated = (_event: string, data: { audioTracks: HlsJsMediaAudioTrack[] }) => {
|
||||
if (this.#audioTracksMatch(data.audioTracks)) return;
|
||||
|
||||
this.#removeAudioTracks();
|
||||
|
||||
for (const hlsAudioTrack of data.audioTracks) {
|
||||
const kind = hlsAudioTrack.default ? 'main' : 'alternative';
|
||||
const audioTrack = this.addAudioTrack(kind, hlsAudioTrack.name, hlsAudioTrack.lang);
|
||||
audioTrack.id = `${hlsAudioTrack.id}`;
|
||||
audioTrack.enabled = Boolean(hlsAudioTrack.default);
|
||||
}
|
||||
};
|
||||
|
||||
#onAudioTrackSwitched = (_event: string, data: { id: number }) => {
|
||||
const selectedId = `${data.id}`;
|
||||
for (const track of this.audioTracks) {
|
||||
track.enabled = track.id === selectedId;
|
||||
}
|
||||
};
|
||||
|
||||
#audioTracksMatch(incoming: HlsJsMediaAudioTrack[]): boolean {
|
||||
const current = [...this.audioTracks];
|
||||
if (current.length !== incoming.length) return false;
|
||||
|
||||
return incoming.every((hlsAudioTrack, index) => {
|
||||
const existing = current[index];
|
||||
return (
|
||||
existing?.id === `${hlsAudioTrack.id}` &&
|
||||
existing.label === (hlsAudioTrack.name ?? '') &&
|
||||
existing.language === (hlsAudioTrack.lang ?? '')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#switchAudioTrack = () => {
|
||||
const { engine } = this;
|
||||
if (!engine) return;
|
||||
@@ -153,6 +177,8 @@ export function HlsJsMediaMediaTracksMixin<Base extends Constructor<MediaTracksH
|
||||
|
||||
engine?.off(Hls.Events.MANIFEST_PARSED, this.#onManifestParsed);
|
||||
engine?.off(Hls.Events.AUDIO_TRACKS_UPDATED, this.#onAudioTracksUpdated);
|
||||
engine?.off(Hls.Events.AUDIO_TRACK_SWITCHING, this.#onAudioTrackSwitched);
|
||||
engine?.off(Hls.Events.AUDIO_TRACK_SWITCHED, this.#onAudioTrackSwitched);
|
||||
engine?.off(Hls.Events.LEVELS_UPDATED, this.#onLevelsUpdated);
|
||||
engine?.off(Hls.Events.LEVEL_SWITCHED, this.#onLevelSwitched);
|
||||
engine?.off(Hls.Events.DESTROYING, this.#teardown);
|
||||
|
||||
@@ -48,6 +48,9 @@ const manifestParsed = (engine: Hls, levels: Array<Record<string, unknown>>) =>
|
||||
const audioTracksUpdated = (engine: Hls, audioTracks: Array<Record<string, unknown>>) =>
|
||||
(engine as any).emit(Hls.Events.AUDIO_TRACKS_UPDATED, { audioTracks });
|
||||
|
||||
const audioTrackSwitched = (engine: Hls, track: Record<string, unknown>) =>
|
||||
(engine as any).emit(Hls.Events.AUDIO_TRACK_SWITCHED, track);
|
||||
|
||||
function flush() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -69,7 +72,7 @@ describe('HlsJsMediaMediaTracksMixin', () => {
|
||||
expect([...host.videoRenditions].map((rendition) => rendition.height)).toEqual([1080, 360]);
|
||||
});
|
||||
|
||||
it('mirrors alternate audio tracks and enables the default', () => {
|
||||
it('mirrors alternate audio tracks', () => {
|
||||
const engine = createEngine();
|
||||
const host = new HlsJsMediaMediaTracks(engine);
|
||||
|
||||
@@ -79,9 +82,9 @@ describe('HlsJsMediaMediaTracksMixin', () => {
|
||||
]);
|
||||
|
||||
expect(host.audioTracks.length).toBe(2);
|
||||
expect(host.audioTracks[0]?.enabled).toBe(true);
|
||||
expect(host.audioTracks[1]?.enabled).toBe(false);
|
||||
expect(host.audioTracks[1]?.label).toBe('Spanish');
|
||||
expect([...host.audioTracks].map((track) => track.id)).toEqual(['0', '1']);
|
||||
expect([...host.audioTracks].map((track) => track.label)).toEqual(['English', 'Spanish']);
|
||||
expect([...host.audioTracks].map((track) => track.language)).toEqual(['en', 'es']);
|
||||
});
|
||||
|
||||
it('forwards a rendition selection to engine.nextLevel', async () => {
|
||||
@@ -170,6 +173,105 @@ describe('HlsJsMediaMediaTracksMixin', () => {
|
||||
expect(spanish!.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves the audio selection when an identical track list is re-emitted', async () => {
|
||||
const engine = createEngine();
|
||||
const host = new HlsJsMediaMediaTracks(engine);
|
||||
|
||||
audioTracksUpdated(engine, [
|
||||
{ id: 0, default: true, name: 'English', lang: 'en' },
|
||||
{ id: 1, name: 'Spanish', lang: 'es' },
|
||||
]);
|
||||
|
||||
// User picks the non-default track.
|
||||
const [english, spanish] = [...host.audioTracks];
|
||||
english!.enabled = false;
|
||||
spanish!.enabled = true;
|
||||
await flush();
|
||||
expect(engine.audioTrack).toBe(1);
|
||||
|
||||
// A rendition switch moves to a different audio group; hls.js re-emits the
|
||||
// same set of languages (reindexed to the same ids). The selection must
|
||||
// survive rather than snap back to the default.
|
||||
audioTracksUpdated(engine, [
|
||||
{ id: 0, default: true, name: 'English', lang: 'en' },
|
||||
{ id: 1, name: 'Spanish', lang: 'es' },
|
||||
]);
|
||||
await flush();
|
||||
|
||||
expect(host.audioTracks[0]?.enabled).toBe(false);
|
||||
expect(host.audioTracks[1]?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('reflects the engine audio selection from the switch event', async () => {
|
||||
const engine = createEngine();
|
||||
const host = new HlsJsMediaMediaTracks(engine);
|
||||
|
||||
audioTracksUpdated(engine, [
|
||||
{ id: 0, default: true, name: 'English', lang: 'en' },
|
||||
{ id: 1, name: 'Spanish', lang: 'es' },
|
||||
]);
|
||||
|
||||
// hls.js selects the non-default track on its own (e.g. audioPreference).
|
||||
(engine as any).audioTrack = 1;
|
||||
audioTrackSwitched(engine, { id: 1, name: 'Spanish', lang: 'es' });
|
||||
await flush();
|
||||
|
||||
expect(host.audioTracks[0]?.enabled).toBe(false);
|
||||
expect(host.audioTracks[1]?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the selection across a group switch that clears engine.audioTrack', async () => {
|
||||
const engine = createEngine();
|
||||
const host = new HlsJsMediaMediaTracks(engine);
|
||||
|
||||
audioTracksUpdated(engine, [
|
||||
{ id: 0, default: true, name: 'English', lang: 'en' },
|
||||
{ id: 1, name: 'Spanish', lang: 'es' },
|
||||
]);
|
||||
|
||||
// User picks Spanish.
|
||||
const [english, spanish] = [...host.audioTracks];
|
||||
english!.enabled = false;
|
||||
spanish!.enabled = true;
|
||||
await flush();
|
||||
expect(engine.audioTrack).toBe(1);
|
||||
|
||||
// Rendition switch → new audio group. hls.js clears its selection (-1),
|
||||
// re-emits the same list, then re-applies the matched track. `.default`
|
||||
// (English) must not win.
|
||||
(engine as any).audioTrack = -1;
|
||||
audioTracksUpdated(engine, [
|
||||
{ id: 0, default: true, name: 'English', lang: 'en' },
|
||||
{ id: 1, name: 'Spanish', lang: 'es' },
|
||||
]);
|
||||
(engine as any).audioTrack = 1;
|
||||
audioTrackSwitched(engine, { id: 1, name: 'Spanish', lang: 'es' });
|
||||
await flush();
|
||||
|
||||
expect(host.audioTracks[0]?.enabled).toBe(false);
|
||||
expect(host.audioTracks[1]?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('rebuilds the list when the audio track set changes', () => {
|
||||
const engine = createEngine();
|
||||
const host = new HlsJsMediaMediaTracks(engine);
|
||||
|
||||
audioTracksUpdated(engine, [
|
||||
{ id: 0, default: true, name: 'English', lang: 'en' },
|
||||
{ id: 1, name: 'Spanish', lang: 'es' },
|
||||
]);
|
||||
|
||||
// A genuinely different list (extra language) forces a rebuild.
|
||||
audioTracksUpdated(engine, [
|
||||
{ id: 0, default: true, name: 'English', lang: 'en' },
|
||||
{ id: 1, name: 'Spanish', lang: 'es' },
|
||||
{ id: 2, name: 'French', lang: 'fr' },
|
||||
]);
|
||||
|
||||
expect(host.audioTracks.length).toBe(3);
|
||||
expect([...host.audioTracks].map((track) => track.label)).toEqual(['English', 'Spanish', 'French']);
|
||||
});
|
||||
|
||||
it('clears all media tracks on DESTROYING', () => {
|
||||
const engine = createEngine();
|
||||
const host = new HlsJsMediaMediaTracks(engine);
|
||||
|
||||
Reference in New Issue
Block a user