refactor(player): the Plex session machine is a tested engine, one request per item in flight
PlexPlayer's session logic (generation, pending play intent, clock ownership, session offset, reclaim
budget, listener teardown) moves into lib/hlsEngine.ts, a plain class with hls.js behind lib/hlsPort.ts
and its own unit tests.
- A seek, track change or reclaim asked for while a session request is out replaces the queued
target; it is sent once the in-flight answer settles. A burst of 20 presses past the buffer was 20
requests and 20 ffmpeg spawns; it is now 2-3, and plays again ~2 s sooner.
- The player stops its server session when it leaves the item, closes or the page unloads
(POST /api/plex/stream/{rk}/session/{sid}/stop, identity-aware). A left transcode used to run to
the end of the film first.
- A restart asked for after a new session attached but before it played no longer reads the emptied
media as the viewer's pause.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { type RefObject, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type Hls from "hls.js";
|
||||
import { X } from "lucide-react";
|
||||
import { buildStatRows, type FragMeter, type HlsLevelInfo, type StatRow } from "../lib/plexStats";
|
||||
import type { HlsEngine } from "../lib/hlsEngine";
|
||||
import { buildStatRows, type FragMeter, type StatRow } from "../lib/plexStats";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
|
||||
// "Stats for nerds" debug overlay. Samples the live HTML5 <video> + hls.js state once a second
|
||||
@@ -23,29 +23,24 @@ function frameCounts(v: HTMLVideoElement | null): { dropped: number; total: numb
|
||||
|
||||
export function PlexStatsOverlay({
|
||||
videoRef,
|
||||
hlsRef,
|
||||
engine,
|
||||
fragMeterRef,
|
||||
audioLabel,
|
||||
subLabel,
|
||||
playable,
|
||||
hwRef,
|
||||
tonemapRef,
|
||||
sessionStartRef,
|
||||
absRef,
|
||||
durationRef,
|
||||
onClose,
|
||||
}: {
|
||||
videoRef: RefObject<HTMLVideoElement | null>;
|
||||
hlsRef: RefObject<Hls | null>;
|
||||
// The session machine: the hls.js level, the transcode encoder and the session's start offset.
|
||||
engine: Pick<HlsEngine, "levelInfo" | "hw" | "tonemap" | "sessionStart">;
|
||||
// Rolling FRAG_LOADED meter owned by the player (it lives as long as the hls.js instance, not as
|
||||
// long as this panel) — the source of the MEASURED bitrate row.
|
||||
fragMeterRef: RefObject<FragMeter>;
|
||||
audioLabel?: string; // selected audio track label, as the tracks menu names it
|
||||
subLabel?: string; // selected subtitle track label (or the menu's "Off" entry)
|
||||
playable: string;
|
||||
hwRef: RefObject<boolean | null>; // active transcode encoder: true=GPU, false=CPU, null=not transcode
|
||||
tonemapRef: RefObject<boolean | null>; // HW HDR→SDR tonemap engaged (adds a "· HDR→SDR" mode tag)
|
||||
sessionStartRef: RefObject<number>;
|
||||
absRef: RefObject<number>;
|
||||
durationRef: RefObject<number>;
|
||||
onClose: () => void;
|
||||
@@ -57,7 +52,6 @@ export function PlexStatsOverlay({
|
||||
useEffect(() => {
|
||||
const sample = () => {
|
||||
const v = videoRef.current;
|
||||
const hls = hlsRef.current;
|
||||
|
||||
const { dropped, total } = frameCounts(v);
|
||||
|
||||
@@ -72,29 +66,15 @@ export function PlexStatsOverlay({
|
||||
}
|
||||
}
|
||||
|
||||
let level: HlsLevelInfo | null = null;
|
||||
if (hls) {
|
||||
// currentLevel is -1 while auto-ABR hasn't settled; fall back to loadLevel for the res/bitrate.
|
||||
const idx = hls.currentLevel >= 0 ? hls.currentLevel : hls.loadLevel;
|
||||
const lv = idx >= 0 ? hls.levels?.[idx] : undefined;
|
||||
level = {
|
||||
index: hls.currentLevel,
|
||||
total: hls.levels?.length ?? 0,
|
||||
width: lv?.width,
|
||||
height: lv?.height,
|
||||
bitrate: lv?.bitrate,
|
||||
videoCodec: lv?.videoCodec,
|
||||
audioCodec: lv?.audioCodec,
|
||||
};
|
||||
}
|
||||
const level = engine.levelInfo();
|
||||
|
||||
setRows(
|
||||
buildStatRows({
|
||||
lang,
|
||||
playable,
|
||||
hwEncoder: hwRef.current,
|
||||
tonemap: tonemapRef.current,
|
||||
transport: hls ? "hls" : "direct",
|
||||
hwEncoder: engine.hw,
|
||||
tonemap: engine.tonemap,
|
||||
transport: level ? "hls" : "direct",
|
||||
videoWidth: v?.videoWidth ?? 0,
|
||||
videoHeight: v?.videoHeight ?? 0,
|
||||
droppedFrames: dropped,
|
||||
@@ -104,7 +84,7 @@ export function PlexStatsOverlay({
|
||||
measuredBps: fragMeterRef.current?.bitrate(),
|
||||
audioLabel,
|
||||
subLabel,
|
||||
sessionStartS: sessionStartRef.current ?? 0,
|
||||
sessionStartS: engine.sessionStart.current,
|
||||
abs: absRef.current ?? 0,
|
||||
duration: durationRef.current ?? 0,
|
||||
}),
|
||||
@@ -113,20 +93,7 @@ export function PlexStatsOverlay({
|
||||
sample();
|
||||
const iv = setInterval(sample, 1000);
|
||||
return () => clearInterval(iv);
|
||||
}, [
|
||||
lang,
|
||||
videoRef,
|
||||
hlsRef,
|
||||
fragMeterRef,
|
||||
audioLabel,
|
||||
subLabel,
|
||||
playable,
|
||||
hwRef,
|
||||
tonemapRef,
|
||||
sessionStartRef,
|
||||
absRef,
|
||||
durationRef,
|
||||
]);
|
||||
}, [lang, videoRef, engine, fragMeterRef, audioLabel, subLabel, playable, absRef, durationRef]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user