Files
siftlode/frontend/src/lib/plexStats.test.ts
T
peter 4827aebb7f feat(plex): HW HDR→SDR tonemap for 10-bit/HDR transcode (OpenCL on the iGPU)
Upgrade mode-3's 10-bit/HDR branch from "route to software libx264" (BUG 1's
behaviour) to a full-GPU pipeline: VAAPI decode → OpenCL tonemap (hable,
HDR10/HLG → bt709 SDR nv12) → h264_vaapi. Gen 9.5 (UHD 630) can't tonemap in
the VAAPI VPP (tonemap_vaapi fails), so OpenCL is the only HW route — proven on
the prod iGPU at ~4.2x realtime, ~1 CPU core.

- stream.py: `_select_encode` returns (hw, tonemap); a 10-bit/HDR HW-decodable
  source picks the tonemap graph when the OpenCL runtime is present, else drops
  to software. `_opencl_present` gates on a registered ICD. `_venc_opts` /
  `_hwaccel_in_opts` gain the tonemap variant (named vaapi+opencl devices,
  `-filter_hw_device ocl`, hwmap→tonemap_opencl→hwmap reverse). `_want_hw` kept
  as the hw-half wrapper. The runtime HW→SW fallback still backstops a session
  that emits no segment.
- Dockerfile: add the Intel NEO OpenCL runtime (intel-opencl-icd). It was removed
  from Debian trixie, so it's pulled from bookworm (22.43 still supports Gen 9.5;
  the .deb runs on the trixie base — verified live). Harmless without /dev/dri.
- route + FE: thread `tonemap` through the session response; the stats overlay
  mode row reads "transcode · GPU · HDR→SDR" for the tonemap path.
- tests: tonemap command branch, the (hw, tonemap) selection matrix (auto/on ×
  8-bit/10-bit × opencl), start_session tonemap wiring, and the fmtMode tag.
2026-08-30 06:02:45 +02:00

147 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "vitest";
import {
buildStatRows,
fmtBitrate,
fmtBufferSecs,
fmtClock,
fmtCodec,
fmtFrames,
fmtLevel,
fmtMode,
fmtRes,
type PlexStatsInput,
} from "./plexStats";
const DASH = "—";
describe("plexStats formatters", () => {
it("fmtBitrate scales kbps/Mbps and dashes zero/undefined", () => {
expect(fmtBitrate(undefined)).toBe(DASH);
expect(fmtBitrate(0)).toBe(DASH);
expect(fmtBitrate(820_000)).toBe("820 kbps");
expect(fmtBitrate(4_200_000)).toBe("4.2 Mbps");
expect(fmtBitrate(1_000_000)).toBe("1.0 Mbps");
});
it("fmtBufferSecs shows one decimal, dashes negatives/non-finite", () => {
expect(fmtBufferSecs(12.34)).toBe("12.3 s");
expect(fmtBufferSecs(0)).toBe("0.0 s");
expect(fmtBufferSecs(-1)).toBe(DASH);
expect(fmtBufferSecs(Infinity)).toBe(DASH);
});
it("fmtFrames shows ratio + drop %, dashes when nothing decoded", () => {
expect(fmtFrames(0, 0)).toBe(DASH);
expect(fmtFrames(0, 1000)).toBe("0 / 1000 (0.00%)");
expect(fmtFrames(5, 1000)).toBe("5 / 1000 (0.50%)");
expect(fmtFrames(30, 1000)).toBe("30 / 1000 (3.0%)");
});
it("fmtRes dashes an undecoded 0×0 frame", () => {
expect(fmtRes(1920, 1080)).toBe("1920×1080");
expect(fmtRes(0, 0)).toBe(DASH);
expect(fmtRes(1920, 0)).toBe(DASH);
});
it("fmtClock renders m:ss and dashes bad input", () => {
expect(fmtClock(0)).toBe("0:00");
expect(fmtClock(9)).toBe("0:09");
expect(fmtClock(75)).toBe("1:15");
expect(fmtClock(3661)).toBe("61:01");
expect(fmtClock(-1)).toBe(DASH);
});
it("fmtLevel is 1-based with resolution, handles auto and empty", () => {
expect(fmtLevel(null)).toBe(DASH);
expect(fmtLevel({ index: 1, total: 4, width: 1280, height: 720 })).toBe("2 / 4 · 1280×720");
expect(fmtLevel({ index: -1, total: 3 })).toBe("auto / 3");
expect(fmtLevel({ index: 0, total: 0 })).toBe(DASH);
});
it("fmtCodec strips profile suffixes and dashes when absent", () => {
expect(fmtCodec("avc1.640028", "mp4a.40.2")).toBe("avc1 / mp4a");
expect(fmtCodec("hvc1.1.6.L120", undefined)).toBe("hvc1");
expect(fmtCodec(undefined, undefined)).toBe(DASH);
});
});
describe("buildStatRows", () => {
const base: PlexStatsInput = {
playable: "remux",
transport: "hls",
videoWidth: 1280,
videoHeight: 720,
droppedFrames: 2,
totalFrames: 500,
bufferAheadS: 8.2,
hlsLevel: { index: 0, total: 1, width: 1280, height: 720, bitrate: 3_000_000 },
sessionStartS: 65,
abs: 130,
duration: 5214,
};
it("includes the HLS-only rows (codec/bitrate/level) for hls transport", () => {
const keys = buildStatRows(base).map((r) => r.key);
expect(keys).toEqual([
"mode",
"transport",
"res",
"codec",
"bitrate",
"level",
"frames",
"buffer",
"offset",
"position",
]);
});
it("drops the HLS-only rows for direct play", () => {
const keys = buildStatRows({ ...base, transport: "direct", hlsLevel: null }).map((r) => r.key);
expect(keys).not.toContain("codec");
expect(keys).not.toContain("bitrate");
expect(keys).not.toContain("level");
expect(keys).toContain("res");
});
it("surfaces the classification as the mode value and formats position", () => {
const rows = buildStatRows({ ...base, playable: "transcode" });
expect(rows.find((r) => r.key === "mode")?.value).toBe("transcode");
expect(rows.find((r) => r.key === "offset")?.value).toBe("1:05");
expect(rows.find((r) => r.key === "position")?.value).toBe("2:10 / 86:54");
});
it("appends the transcode encoder to the mode row when known", () => {
const gpu = buildStatRows({ ...base, playable: "transcode", hwEncoder: true });
expect(gpu.find((r) => r.key === "mode")?.value).toBe("transcode · GPU");
const cpu = buildStatRows({ ...base, playable: "transcode", hwEncoder: false });
expect(cpu.find((r) => r.key === "mode")?.value).toBe("transcode · CPU");
});
it("tags the mode row with HDR→SDR for a HW tonemap session", () => {
const rows = buildStatRows({ ...base, playable: "transcode", hwEncoder: true, tonemap: true });
expect(rows.find((r) => r.key === "mode")?.value).toBe("transcode · GPU · HDR→SDR");
});
it("shows an empty classification honestly as a dash", () => {
expect(buildStatRows({ ...base, playable: "" }).find((r) => r.key === "mode")?.value).toBe(
DASH,
);
});
});
describe("fmtMode", () => {
it("appends the encoder only for a transcode with a known encoder", () => {
expect(fmtMode("transcode", true)).toBe("transcode · GPU");
expect(fmtMode("transcode", false)).toBe("transcode · CPU");
expect(fmtMode("transcode", null)).toBe("transcode"); // encoder unknown → bare mode
expect(fmtMode("transcode")).toBe("transcode");
expect(fmtMode("remux", true)).toBe("remux"); // not a transcode → no encoder suffix
expect(fmtMode("")).toBe(DASH);
// The HDR→SDR tag rides on the GPU encoder token for a tonemap session.
expect(fmtMode("transcode", true, true)).toBe("transcode · GPU · HDR→SDR");
expect(fmtMode("transcode", true, false)).toBe("transcode · GPU");
expect(fmtMode("transcode", null, true)).toBe("transcode"); // no encoder → no tag either
});
});