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.
This commit is contained in:
@@ -392,6 +392,7 @@ export interface PlexPlaySession {
|
||||
start_s: number;
|
||||
transcode?: boolean; // hls only: video re-encoded (non-h264 source) vs stream-copied (remux)
|
||||
hw?: boolean; // hls transcode only: ran on the VAAPI hardware encoder (else libx264 software)
|
||||
tonemap?: boolean; // hls HW transcode only: 10-bit/HDR source tone-mapped HDR→SDR on the GPU (OpenCL)
|
||||
}
|
||||
|
||||
// Discover surface (E5 S4): the consolidated rail payload. Every rail is a plain PlexCard list the
|
||||
|
||||
@@ -118,6 +118,11 @@ describe("buildStatRows", () => {
|
||||
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,
|
||||
@@ -133,5 +138,9 @@ describe("fmtMode", () => {
|
||||
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
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,9 @@ export interface PlexStatsInput {
|
||||
// software, null/undefined = not a transcode (or unknown) — so the mode row can read "transcode · GPU"
|
||||
// vs "transcode · CPU", the visible confirmation of which S1 path engaged.
|
||||
hwEncoder?: boolean | null;
|
||||
// For a HW transcode of a 10-bit/HDR source: true = the GPU OpenCL tonemap path (HDR→SDR) engaged, so
|
||||
// the mode row can read "transcode · GPU · HDR→SDR". Absent/false for an 8-bit HW or software session.
|
||||
tonemap?: boolean | null;
|
||||
videoWidth: number; // intrinsic decoded dims (0 while the browser can't decode / pre-metadata)
|
||||
videoHeight: number;
|
||||
droppedFrames: number; // getVideoPlaybackQuality().droppedVideoFrames
|
||||
@@ -94,20 +97,30 @@ export function fmtCodec(video: string | undefined, audio: string | undefined):
|
||||
}
|
||||
|
||||
// Active mode, with the transcode encoder appended when known: "transcode · GPU" (VAAPI hardware) or
|
||||
// "transcode · CPU" (libx264 software) — the at-a-glance confirmation of which S1 path engaged. Non-
|
||||
// transcode modes (direct/remux) and an unknown encoder read as the bare mode. Raw tokens (not i18n'd),
|
||||
// consistent with the other technical values in this overlay (e.g. transport "hls").
|
||||
export function fmtMode(playable: string, hwEncoder?: boolean | null): string {
|
||||
// "transcode · CPU" (libx264 software) — the at-a-glance confirmation of which S1 path engaged — and a
|
||||
// "· HDR→SDR" tag when the GPU OpenCL tonemap path handled a 10-bit/HDR source ("transcode · GPU ·
|
||||
// HDR→SDR"). Non-transcode modes (direct/remux) and an unknown encoder read as the bare mode. Raw tokens
|
||||
// (not i18n'd), consistent with the other technical values in this overlay (e.g. transport "hls").
|
||||
export function fmtMode(
|
||||
playable: string,
|
||||
hwEncoder?: boolean | null,
|
||||
tonemap?: boolean | null,
|
||||
): string {
|
||||
const mode = playable || DASH;
|
||||
if (playable !== "transcode" || hwEncoder == null) return mode;
|
||||
return `${mode} · ${hwEncoder ? "GPU" : "CPU"}`;
|
||||
const base = `${mode} · ${hwEncoder ? "GPU" : "CPU"}`;
|
||||
return tonemap ? `${base} · HDR→SDR` : base;
|
||||
}
|
||||
|
||||
// Build the ordered, i18n-keyed rows the overlay renders. Rows whose data is absent still show a dash
|
||||
// (honest "not available" beats a missing row) — except HLS-only rows, dropped entirely for direct play.
|
||||
export function buildStatRows(s: PlexStatsInput): StatRow[] {
|
||||
const rows: StatRow[] = [
|
||||
{ key: "mode", labelKey: "plex.player.stats.mode", value: fmtMode(s.playable, s.hwEncoder) },
|
||||
{
|
||||
key: "mode",
|
||||
labelKey: "plex.player.stats.mode",
|
||||
value: fmtMode(s.playable, s.hwEncoder, s.tonemap),
|
||||
},
|
||||
{ key: "transport", labelKey: "plex.player.stats.transport", value: s.transport },
|
||||
{
|
||||
key: "res",
|
||||
|
||||
Reference in New Issue
Block a user