fix(plex): subtitles survive hls.js and a remounted track

hls.js emptied the cues of every text track on the element when a manifest
loaded or the media attached, our own <track> included, so a VTT that loaded
first showed nothing for the session; its subtitle and caption controllers are
no longer built. A remounted default <track> was switched to showing by the
browser after the hide had run, drawing the cues natively under ours; the hide
now follows every added track and mode change.
This commit is contained in:
2026-10-08 19:35:47 +02:00
parent dae0b80f6a
commit 57a81c5643
4 changed files with 115 additions and 13 deletions
@@ -440,14 +440,36 @@ describe("usePlexTracks — the debounced A/V-offset restart", () => {
// The browser no longer draws the cues: its rendering sat under the control bar on an iPad, and a cue
// placed by `line` cannot move out of the way. The track stays loaded, as `hidden`, for PlexSubtitles.
describe("usePlexTracks — who draws the cues", () => {
it("keeps the mounted track hidden: loaded and tracked, not drawn by the browser", () => {
const track = { mode: "disabled" as TextTrackMode };
/** A <video> whose text-track list fires `addtrack` / `change` like a browser's. */
function videoWithTracks(...modes: TextTrackMode[]) {
const list: { mode: TextTrackMode }[] = modes.map((mode) => ({ mode }));
const tracks = Object.assign(new EventTarget(), {
get length() {
return list.length;
},
[Symbol.iterator]: () => list[Symbol.iterator](),
});
const video = document.createElement("video");
Object.defineProperty(video, "textTracks", { value: [track] });
Object.defineProperty(video, "textTracks", { value: tracks });
// What a browser does with a freshly mounted `default` <track>: it switches it on by itself.
const browserAddsShowing = () =>
act(() => {
list.push({ mode: "showing" });
tracks.dispatchEvent(new Event("addtrack"));
});
const browserShows = () =>
act(() => {
list[0]!.mode = "showing";
tracks.dispatchEvent(new Event("change"));
});
return { video, list, browserAddsShowing, browserShows };
}
function mountOn(video: HTMLVideoElement) {
const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={client()}>{children}</QueryClientProvider>
);
const { unmount } = renderHook(
return renderHook(
() =>
usePlexTracks({
id: RK,
@@ -464,7 +486,22 @@ describe("usePlexTracks — who draws the cues", () => {
}),
{ wrapper },
);
expect(track.mode).toBe("hidden");
}
it("keeps the mounted track hidden: loaded and tracked, not drawn by the browser", () => {
const { video, list } = videoWithTracks("disabled");
const { unmount } = mountOn(video);
expect(list[0]!.mode).toBe("hidden");
unmount();
});
it("hides a remounted track the browser switched on by itself", () => {
const { video, list, browserAddsShowing, browserShows } = videoWithTracks("disabled");
const { unmount } = mountOn(video);
browserAddsShowing();
expect(list.map((t) => t.mode)).toEqual(["hidden", "hidden"]);
browserShows();
expect(list[0]!.mode).toBe("hidden");
unmount();
});
});