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:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState, type RefObject } from "react";
|
||||
import { subShadow } from "../lib/plexPlayerPrefs";
|
||||
import { ownTextTracks } from "../lib/plexSubTracks";
|
||||
|
||||
// The Plex player's subtitles, drawn by us instead of the browser. The track stays mounted (hls.js and
|
||||
// the VTT fetch need it) but `hidden`: the browser parses the cues and reports the active ones, and
|
||||
@@ -16,7 +17,7 @@ const CUE_EM_OF_HEIGHT = 0.05;
|
||||
// The air left between the text and what it avoids (the bar's top, or the video's bottom edge).
|
||||
const CLEARANCE_PX = 8;
|
||||
|
||||
/** The cues showing now, from every track of `video` that is not switched off. */
|
||||
/** The cues showing now, from the player's own track(s) of `video` that are not switched off. */
|
||||
function useActiveCues(video: HTMLVideoElement | null): Cue[] {
|
||||
const [cues, setCues] = useState<Cue[]>([]);
|
||||
useEffect(() => {
|
||||
@@ -24,8 +25,9 @@ function useActiveCues(video: HTMLVideoElement | null): Cue[] {
|
||||
const tracks = video.textTracks;
|
||||
const read = () => {
|
||||
const now: Cue[] = [];
|
||||
const own = ownTextTracks(video);
|
||||
for (const tr of Array.from(tracks)) {
|
||||
if (tr.mode === "disabled") continue;
|
||||
if (!own.has(tr) || tr.mode === "disabled") continue;
|
||||
for (const c of Array.from(tr.activeCues ?? [])) now.push(c as Cue);
|
||||
}
|
||||
setCues((prev) =>
|
||||
@@ -38,12 +40,11 @@ function useActiveCues(video: HTMLVideoElement | null): Cue[] {
|
||||
bound.add(tr);
|
||||
tr.addEventListener("cuechange", read);
|
||||
};
|
||||
for (const tr of Array.from(tracks)) bind(tr);
|
||||
const onAdd = (e: Event) => {
|
||||
const tr = (e as TrackEvent).track;
|
||||
if (tr) bind(tr as TextTrack);
|
||||
const onAdd = () => {
|
||||
for (const tr of Array.from(tracks)) bind(tr);
|
||||
read();
|
||||
};
|
||||
for (const tr of Array.from(tracks)) bind(tr);
|
||||
tracks.addEventListener("addtrack", onAdd);
|
||||
tracks.addEventListener("removetrack", read);
|
||||
read();
|
||||
@@ -115,20 +116,29 @@ export default function PlexSubtitles({
|
||||
const cues = useActiveCues(video);
|
||||
const height = useHeight(video);
|
||||
const boxRef = useRef<HTMLDivElement>(null);
|
||||
const [lift, setLift] = useState(0);
|
||||
const liftRef = useRef(0);
|
||||
// `slide` is false for the first placement of a box that just appeared: it is put where it belongs,
|
||||
// not slid there from where the previous cue was left.
|
||||
const [lift, setLift] = useState({ px: 0, slide: false });
|
||||
const shownRef = useRef(false);
|
||||
|
||||
// How far up the text has to move to clear the bar (or, without it, the video's bottom edge). The
|
||||
// box is measured where it would sit with no lift: its current rect plus the lift it carries now.
|
||||
// outer box is never transformed — the lift moves the inner one — so its rect is where the text sits
|
||||
// with no lift, even mid-slide. The rects are screen pixels and the windowed player is scaled
|
||||
// (PlexPlayer's wrapper), while the lift applies in the box's own pixels: the distance is divided
|
||||
// by that scale.
|
||||
useLayoutEffect(() => {
|
||||
const box = boxRef.current;
|
||||
if (!box || !video || cues.length === 0) return;
|
||||
const bottom = box.getBoundingClientRect().bottom + liftRef.current;
|
||||
if (!box || !video || cues.length === 0) {
|
||||
shownRef.current = false;
|
||||
return;
|
||||
}
|
||||
const rect = box.getBoundingClientRect();
|
||||
const scale = box.offsetHeight > 0 ? rect.height / box.offsetHeight : 1;
|
||||
const bar = avoid ? avoidRef.current?.getBoundingClientRect().top : undefined;
|
||||
const limit = (bar ?? video.getBoundingClientRect().bottom) - CLEARANCE_PX;
|
||||
const next = Math.max(0, Math.round(bottom - limit));
|
||||
liftRef.current = next;
|
||||
setLift(next);
|
||||
const px = Math.max(0, Math.round((rect.bottom - limit) / scale));
|
||||
setLift({ px, slide: shownRef.current });
|
||||
shownRef.current = true;
|
||||
}, [avoid, avoidRef, cues, height, pos, size, video]);
|
||||
|
||||
if (cues.length === 0 || height === 0) return null;
|
||||
@@ -142,16 +152,18 @@ export default function PlexSubtitles({
|
||||
ref={boxRef}
|
||||
data-testid="plex-subtitles"
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-x-0 flex flex-col items-center gap-[0.15em] px-[5%] text-center transition-transform duration-200 ease-out motion-reduce:transition-none"
|
||||
style={{
|
||||
top: `${pos}%`,
|
||||
fontSize: `${(height * CUE_EM_OF_HEIGHT * size) / 100}px`,
|
||||
transform: `translateY(${-lift}px)`,
|
||||
}}
|
||||
className="pointer-events-none absolute inset-x-0 px-[5%] text-center"
|
||||
style={{ top: `${pos}%`, fontSize: `${(height * CUE_EM_OF_HEIGHT * size) / 100}px` }}
|
||||
>
|
||||
{cues.map((cue, i) => (
|
||||
<CueText key={`${cue.startTime}-${i}`} cue={cue} style={text} />
|
||||
))}
|
||||
<div
|
||||
data-testid="plex-subtitles-text"
|
||||
className={`flex flex-col items-center gap-[0.15em] ${lift.slide ? "transition-transform duration-200 ease-out motion-reduce:transition-none" : ""}`}
|
||||
style={{ transform: `translateY(${-lift.px}px)` }}
|
||||
>
|
||||
{cues.map((cue, i) => (
|
||||
<CueText key={`${cue.startTime}-${i}`} cue={cue} style={text} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user