mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
fix(playback): restore suppressed DASH captions when the preference re-enables
The Shaka bridge dropped the auto-selected text track with selectTextTrack(null) when showCaptions was off, but did not remember it — re-enabling the preference mid-session left captions permanently off (HLS/native bridges already restore). The session now remembers the suppressed track id and reselects it via the bridge's caption-state pass; suppression is also skipped when no track is active. Covered by session and new WebVideoShakaControls specs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
208d8d0fc2
commit
52ed64e45c
5 files changed
+167
-15
No files matched your search
@@ -94,6 +94,9 @@ export class FakeShakaPlayer implements ShakaPlayerLike {
|
||||
|
||||
selectTextTrack(track: ShakaTextTrackLike | null): void {
|
||||
this.selectTextTrackCalls.push(track);
|
||||
for (const candidate of this.textTracks) {
|
||||
candidate.active = candidate === track;
|
||||
}
|
||||
}
|
||||
|
||||
isLive(): boolean {
|
||||
|
||||
@@ -226,13 +226,44 @@ describe('ShakaVideoSession', () => {
|
||||
];
|
||||
expect(slow.destroyCount).toBe(1);
|
||||
expect(fast.loadedUrls).toEqual(['http://example.com/fast.mpd']);
|
||||
// The slow player resolved after being superseded; its post-load
|
||||
// caption suppression must not have been applied.
|
||||
// The slow player resolved after being superseded; nothing may have
|
||||
// touched its text tracks afterwards.
|
||||
expect(slow.selectTextTrackCalls).toEqual([]);
|
||||
expect(fast.selectTextTrackCalls).toEqual([null]);
|
||||
expect(issues).toEqual([]);
|
||||
});
|
||||
|
||||
it('suppresses auto-selected captions on load and restores them on demand', async () => {
|
||||
const environment = createFakeShakaEnvironment({
|
||||
onCreate: (player) => {
|
||||
player.textTracks = [
|
||||
{
|
||||
id: 7,
|
||||
active: true,
|
||||
language: 'en',
|
||||
label: 'English',
|
||||
kind: 'subtitles',
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
const session = createSession(environment);
|
||||
session.start(video, 'http://example.com/subs.mpd');
|
||||
await flush();
|
||||
|
||||
const player = environment.instances[0];
|
||||
// showCaptions() is false: the auto-selected track is hidden…
|
||||
expect(player.selectTextTrackCalls).toEqual([null]);
|
||||
expect(player.textTracks[0].active).toBe(false);
|
||||
|
||||
// …and remembered, so re-enabling the preference restores it.
|
||||
session.restoreSuppressedTextTrack();
|
||||
expect(player.textTracks[0].active).toBe(true);
|
||||
|
||||
// The restore is one-shot.
|
||||
session.restoreSuppressedTextTrack();
|
||||
expect(player.selectTextTrackCalls).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('emits an unsupported-container diagnostic when the browser lacks MSE/EME', async () => {
|
||||
const environment = createFakeShakaEnvironment();
|
||||
environment.browserSupported = false;
|
||||
|
||||
@@ -65,6 +65,7 @@ export class ShakaVideoSession {
|
||||
private readonly refreshListeners = new Set<() => void>();
|
||||
private operationChain: Promise<void> = Promise.resolve();
|
||||
private pendingTeardown: Promise<void> = Promise.resolve();
|
||||
private suppressedTextTrackId: number | null = null;
|
||||
private generation = 0;
|
||||
private destroyed = false;
|
||||
|
||||
@@ -192,11 +193,47 @@ export class ShakaVideoSession {
|
||||
// Shaka 5 shows a text track by selecting it; keep captions off when
|
||||
// the preference is disabled in case the manifest auto-selected one.
|
||||
if (!(this.config.showCaptions?.() ?? false)) {
|
||||
player.selectTextTrack(null);
|
||||
this.suppressTextTracksOn(player);
|
||||
}
|
||||
this.notifyRefresh();
|
||||
}
|
||||
|
||||
/** Hides the active text track, remembering it for a later restore. */
|
||||
suppressTextTracks(): void {
|
||||
const player = this.player;
|
||||
if (player) {
|
||||
this.suppressTextTracksOn(player);
|
||||
}
|
||||
}
|
||||
|
||||
/** Reselects the text track hidden by {@link suppressTextTracks}. */
|
||||
restoreSuppressedTextTrack(): void {
|
||||
const player = this.player;
|
||||
if (!player || this.suppressedTextTrackId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const track = player
|
||||
.getTextTracks()
|
||||
.find((candidate) => candidate.id === this.suppressedTextTrackId);
|
||||
this.suppressedTextTrackId = null;
|
||||
if (track && !track.active) {
|
||||
player.selectTextTrack(track);
|
||||
}
|
||||
}
|
||||
|
||||
private suppressTextTracksOn(player: ShakaPlayerLike): void {
|
||||
const active = player
|
||||
.getTextTracks()
|
||||
.find((candidate) => candidate.active);
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.suppressedTextTrackId = active.id;
|
||||
player.selectTextTrack(null);
|
||||
}
|
||||
|
||||
private handleLoadFailure(
|
||||
generation: number,
|
||||
player: ShakaPlayerLike,
|
||||
@@ -297,6 +334,7 @@ export class ShakaVideoSession {
|
||||
private beginPlayerTeardown(): void {
|
||||
const player = this.player;
|
||||
this.player = null;
|
||||
this.suppressedTextTrackId = null;
|
||||
if (!player) {
|
||||
this.playerErrorListener = null;
|
||||
this.playerRefreshListener = null;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { InlinePlaybackPlayer } from '../playback-diagnostics/playback-diagnostics.model';
|
||||
import {
|
||||
createFakeShakaEnvironment,
|
||||
flushShakaMicrotasks as flush,
|
||||
} from '../shaka-engine/shaka-player-test-double';
|
||||
import { ShakaVideoSession } from '../shaka-engine/shaka-video-session';
|
||||
import { WebVideoShakaControls } from './web-video-shaka-controls';
|
||||
|
||||
describe('WebVideoShakaControls', () => {
|
||||
const video = {} as HTMLVideoElement;
|
||||
|
||||
function createHarness() {
|
||||
const environment = createFakeShakaEnvironment({
|
||||
onCreate: (player) => {
|
||||
player.textTracks = [
|
||||
{
|
||||
id: 3,
|
||||
active: true,
|
||||
language: 'en',
|
||||
label: 'English',
|
||||
kind: 'subtitles',
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
const state = { showCaptions: false };
|
||||
const session = new ShakaVideoSession({
|
||||
player: InlinePlaybackPlayer.Html5,
|
||||
emitPlaybackIssue: () => undefined,
|
||||
showCaptions: () => state.showCaptions,
|
||||
loadShaka: environment.loader,
|
||||
});
|
||||
const controls = new WebVideoShakaControls({
|
||||
showCaptions: () => state.showCaptions,
|
||||
refresh: () => undefined,
|
||||
});
|
||||
return { environment, session, controls, state };
|
||||
}
|
||||
|
||||
it('restores the suppressed caption track when the preference turns on', async () => {
|
||||
const { environment, session, controls, state } = createHarness();
|
||||
controls.bind(session);
|
||||
session.start(video, 'http://example.com/subs.mpd');
|
||||
await flush();
|
||||
|
||||
const player = environment.instances[0];
|
||||
expect(player.textTracks[0].active).toBe(false);
|
||||
|
||||
state.showCaptions = true;
|
||||
controls.refreshInputs();
|
||||
|
||||
expect(player.textTracks[0].active).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps an explicit user subtitle-off choice over the preference', async () => {
|
||||
const { environment, session, controls, state } = createHarness();
|
||||
controls.bind(session);
|
||||
session.start(video, 'http://example.com/subs.mpd');
|
||||
await flush();
|
||||
|
||||
controls.setSubtitleTrack(-1);
|
||||
state.showCaptions = true;
|
||||
controls.refreshInputs();
|
||||
|
||||
const player = environment.instances[0];
|
||||
expect(player.textTracks[0].active).toBe(false);
|
||||
});
|
||||
|
||||
it('lists subtitle tracks with the active flag as selection state', async () => {
|
||||
const { environment, session, controls, state } = createHarness();
|
||||
state.showCaptions = true;
|
||||
controls.bind(session);
|
||||
session.start(video, 'http://example.com/subs.mpd');
|
||||
await flush();
|
||||
|
||||
expect(controls.getSubtitleTracks()).toEqual([
|
||||
{ id: 0, label: 'English', selected: true },
|
||||
]);
|
||||
expect(environment.instances[0].textTracks[0].active).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -106,20 +106,19 @@ export class WebVideoShakaControls {
|
||||
}
|
||||
|
||||
private applyCaptionState(): void {
|
||||
const player = this.getPlayer();
|
||||
if (!player) {
|
||||
const session = this.session;
|
||||
if (!session || this.subtitleOverride !== null) {
|
||||
// A user selection (subtitleOverride) always wins.
|
||||
return;
|
||||
}
|
||||
|
||||
// Without an explicit user choice, keep manifest-auto-selected text
|
||||
// hidden while the captions preference is off (HLS parity). A user
|
||||
// selection (subtitleOverride) always wins.
|
||||
if (
|
||||
this.subtitleOverride === null &&
|
||||
!this.config.showCaptions() &&
|
||||
player.getTextTracks().some((track) => track.active)
|
||||
) {
|
||||
player.selectTextTrack(null);
|
||||
// Without an explicit user choice, mirror the HLS bridge: keep
|
||||
// manifest-auto-selected text hidden while the captions preference is
|
||||
// off, and reselect the suppressed track when it turns back on.
|
||||
if (this.config.showCaptions()) {
|
||||
session.restoreSuppressedTextTrack();
|
||||
} else {
|
||||
session.suppressTextTracks();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user