fix(plex): subtitles draw only the player's own track, lifted in the player's own pixels
A direct-played file's own subtitle stream (WebKit lists an mp4's mov_text) was switched to hidden and drawn under any choice, Off included: only the player's <track> is kept hidden and read, other subtitle tracks are switched off. The lift was measured in screen pixels and applied inside the x1.25 windowed player, and re-measured off a box carrying its own (or a mid-slide) transform: an untransformed outer box is measured, the distance divided by the scale, and a cue that just appeared is placed, not slid from where the last one was left.
This commit is contained in:
@@ -440,9 +440,10 @@ 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", () => {
|
||||
/** A <video> whose text-track list fires `addtrack` / `change` like a browser's. */
|
||||
/** A <video> whose text-track list fires `addtrack` / `change` like a browser's. Each mode is a
|
||||
* track of the player's own <track>; `mediaAdds` adds one from the media itself instead. */
|
||||
function videoWithTracks(...modes: TextTrackMode[]) {
|
||||
const list: { mode: TextTrackMode }[] = modes.map((mode) => ({ mode }));
|
||||
const list: { kind: string; mode: TextTrackMode }[] = [];
|
||||
const tracks = Object.assign(new EventTarget(), {
|
||||
get length() {
|
||||
return list.length;
|
||||
@@ -451,10 +452,20 @@ describe("usePlexTracks — who draws the cues", () => {
|
||||
});
|
||||
const video = document.createElement("video");
|
||||
Object.defineProperty(video, "textTracks", { value: tracks });
|
||||
const add = (mode: TextTrackMode, own: boolean) => {
|
||||
const track = { kind: "subtitles", mode };
|
||||
if (own) {
|
||||
const el = document.createElement("track");
|
||||
Object.defineProperty(el, "track", { value: track });
|
||||
video.appendChild(el);
|
||||
}
|
||||
list.push(track);
|
||||
};
|
||||
for (const mode of modes) add(mode, true);
|
||||
// What a browser does with a freshly mounted `default` <track>: it switches it on by itself.
|
||||
const browserAddsShowing = () =>
|
||||
act(() => {
|
||||
list.push({ mode: "showing" });
|
||||
add("showing", true);
|
||||
tracks.dispatchEvent(new Event("addtrack"));
|
||||
});
|
||||
const browserShows = () =>
|
||||
@@ -462,7 +473,13 @@ describe("usePlexTracks — who draws the cues", () => {
|
||||
list[0]!.mode = "showing";
|
||||
tracks.dispatchEvent(new Event("change"));
|
||||
});
|
||||
return { video, list, browserAddsShowing, browserShows };
|
||||
// A direct-played file's own subtitle stream, which WebKit lists — and may switch on.
|
||||
const mediaAdds = (mode: TextTrackMode) =>
|
||||
act(() => {
|
||||
add(mode, false);
|
||||
tracks.dispatchEvent(new Event("addtrack"));
|
||||
});
|
||||
return { video, list, browserAddsShowing, browserShows, mediaAdds };
|
||||
}
|
||||
|
||||
function mountOn(video: HTMLVideoElement) {
|
||||
@@ -504,4 +521,13 @@ describe("usePlexTracks — who draws the cues", () => {
|
||||
expect(list[0]!.mode).toBe("hidden");
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("switches off a subtitle track that is not the player's own, and leaves its own hidden", () => {
|
||||
const { video, list, mediaAdds } = videoWithTracks("disabled");
|
||||
const { unmount } = mountOn(video);
|
||||
mediaAdds("hidden");
|
||||
mediaAdds("showing");
|
||||
expect(list.map((t) => t.mode)).toEqual(["hidden", "disabled", "disabled"]);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import { qk } from "./queryKeys";
|
||||
import { useLatest } from "./useLatest";
|
||||
import i18n, { plexLang } from "../i18n";
|
||||
import { markerSkip } from "./plexMarkers";
|
||||
import { ownTextTracks } from "./plexSubTracks";
|
||||
import { type PlexPlayerPrefs } from "./plexPlayerPrefs";
|
||||
import { PLEX_SCALE, normalizeVolumeIn, stepVolumeIn } from "./playerVolume";
|
||||
|
||||
@@ -852,23 +853,30 @@ export function usePlexTracks(opts: {
|
||||
// sessionK is already lag-compensated (see HlsEngine), so it is the true content-zero the VTT
|
||||
// must be shifted by; the user offset nudges on top.
|
||||
const subTrackOffset = sessionK - subOffApplied;
|
||||
// Keep every track "hidden": loaded and its cues tracked, but drawn by PlexSubtitles, not by the
|
||||
// browser. Not once per selection: the <track> is remounted on its own key (a grown window, a
|
||||
// retry), and the browser switches a fresh `default` track to "showing" a task later — which drew
|
||||
// the cues a second time, natively, under ours. So it is re-applied on every track the list gains
|
||||
// and on every mode change the browser makes.
|
||||
// Keep our own track "hidden": loaded and its cues tracked, but drawn by PlexSubtitles, not by the
|
||||
// browser. Any other subtitle track (a direct-played file's own, see ownTextTracks) is switched
|
||||
// off, so neither the browser nor PlexSubtitles draws it. Not once per selection: the <track> is
|
||||
// remounted on its own key (a grown window, a retry), and the browser switches a fresh `default`
|
||||
// track to "showing" a task later — which drew the cues a second time, natively, under ours. So it
|
||||
// is re-applied on every track the list gains and on every mode change the browser makes.
|
||||
useEffect(() => {
|
||||
const tracks = videoRef.current?.textTracks;
|
||||
if (!tracks) return;
|
||||
const hide = () => {
|
||||
for (const tr of Array.from(tracks)) if (tr.mode !== "hidden") tr.mode = "hidden";
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
const tracks = video.textTracks;
|
||||
const settle = () => {
|
||||
const own = ownTextTracks(video);
|
||||
for (const tr of Array.from(tracks)) {
|
||||
const subtitle = tr.kind === "subtitles" || tr.kind === "captions";
|
||||
const want = own.has(tr) ? "hidden" : subtitle ? "disabled" : tr.mode;
|
||||
if (tr.mode !== want) tr.mode = want;
|
||||
}
|
||||
};
|
||||
hide();
|
||||
tracks.addEventListener("addtrack", hide);
|
||||
tracks.addEventListener("change", hide);
|
||||
settle();
|
||||
tracks.addEventListener("addtrack", settle);
|
||||
tracks.addEventListener("change", settle);
|
||||
return () => {
|
||||
tracks.removeEventListener("addtrack", hide);
|
||||
tracks.removeEventListener("change", hide);
|
||||
tracks.removeEventListener("addtrack", settle);
|
||||
tracks.removeEventListener("change", settle);
|
||||
};
|
||||
}, [subOrd, id, videoRef]);
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/** The text tracks of the Plex player's own <track> elements. A <video> can expose others too: the
|
||||
* subtitle stream of a direct-played file (WebKit lists an mp4's mov_text in `textTracks`), or a
|
||||
* metadata track hls.js adds. Those are neither shown nor drawn: the viewer picks the subtitles. */
|
||||
export function ownTextTracks(video: HTMLVideoElement): Set<TextTrack> {
|
||||
return new Set(Array.from(video.querySelectorAll("track"), (el) => el.track));
|
||||
}
|
||||
Reference in New Issue
Block a user