fix(plex): a codec the GPU cannot decode is decoded on the CPU and still encoded on the GPU
The prod iGPU has no VC-1 Advanced decode ("No support for codec vc1 profile 3"), so every VC-1
session died on the GPU and fell back to libx264, which needs ~17 s for the three warm-up segments:
a start or a seek took ~19.5 s. VC-1, XviD and DivX now take CPU decode -> hwupload -> h264_vaapi
(three segments in 5.5 s for a 1080p VC-1 remux, measured on prod hardware with this argv).
The transcode_hw facet now says whether the GPU encodes: VC-1/XviD/DivX count, and so does an HDR
film (the OpenCL tonemap runs on the GPU; the facet said software). Migration 0119 rewrites the
stored facet and re-queues 10-bit items for the HDR probe.
This commit is contained in:
+34
-21
@@ -60,14 +60,20 @@ def classify_playable(vcodec: str | None, acodec: str | None, container: str | N
|
||||
|
||||
# Video codecs the prod iGPU (UHD 630 / Quick Sync Gen 9.5) can hardware-DECODE — the precondition for
|
||||
# the full-GPU VAAPI transcode pipeline. THE single source of this set: plex.stream imports it here so
|
||||
# the runtime encoder choice (`_want_hw`) and this catalog facet can never disagree by codec. A source
|
||||
# outside it (AV1, MPEG-4 ASP/XviD…) is software-only even on the iGPU (it can't decode it to feed its
|
||||
# own encoder).
|
||||
HW_DECODABLE_CODECS = frozenset({"h264", "hevc", "h265", "vp9", "vc1", "mpeg2video"})
|
||||
# the runtime encoder choice (`_select_encode`) and this catalog facet can never disagree by codec. VC-1
|
||||
# is NOT in it: measured on prod, the iHD driver answers "No support for codec vc1 profile 3" (VC-1
|
||||
# Advanced, what a Blu-ray remux carries), so every session that asked the GPU to decode one died and
|
||||
# fell back to libx264.
|
||||
HW_DECODABLE_CODECS = frozenset({"h264", "hevc", "h265", "vp9", "mpeg2video"})
|
||||
# Codecs the iGPU cannot decode but whose picture it can still ENCODE: the CPU decodes, the frames go up
|
||||
# to the GPU as 8-bit nv12, and h264_vaapi encodes (plex.stream's `cpu_decode` path). Measured on prod:
|
||||
# three segments in ~5 s for a 1080p VC-1 remux (libx264: ~17 s) and ~1 s for an SD XviD or DivX
|
||||
# episode. Only codecs that are always 8-bit SDR, and only ones measured; any other codec stays software.
|
||||
CPU_DECODE_CODECS = frozenset({"vc1", "mpeg4", "msmpeg4v3"})
|
||||
# Color-transfer values marking an HDR source (PQ / HLG). THE single source of this set: plex.stream
|
||||
# (the tonemap choice) and downloads.clip (a clip's tonemap) import it here, so a transfer added for
|
||||
# one is tonemapped by all. A 10-bit/HDR source breaks the VAAPI scale=nv12 path on Gen 9.5 (see
|
||||
# plex.stream) → software transcode, so it counts as NOT HW-capable.
|
||||
# one is tonemapped by all. An HDR source breaks the plain VAAPI scale=nv12 path on Gen 9.5, so it takes
|
||||
# the OpenCL tonemap graph (see plex.stream) — still the GPU encoder.
|
||||
HDR_TRANSFERS = frozenset({"smpte2084", "arib-std-b67"})
|
||||
|
||||
|
||||
@@ -78,18 +84,24 @@ def transcode_hw_capable(
|
||||
hdr: bool = False,
|
||||
) -> bool | None:
|
||||
"""The stored `PlexItem.transcode_hw` facet: for a `playable="transcode"` item, whether the prod
|
||||
iGPU can HARDWARE-transcode it (else software libx264 only); None for a non-transcode item. HW needs
|
||||
a HW-decodable codec AND an 8-bit SDR source — a 10-bit/HDR source (bit_depth>8 or an HDR transfer)
|
||||
breaks the VAAPI nv12 pipeline and falls back to software. `bit_depth`/`hdr` are unknown at sync
|
||||
time (the cheap listing carries no Stream), so they default to 8-bit SDR (codec-only) and the
|
||||
bit-depth backfill refines a 10-bit/HDR HEVC to False later."""
|
||||
iGPU ENCODES it ("Transcoded on the hardware encoder"), else software libx264; None for a
|
||||
non-transcode item. The same answer plex.stream's `_select_encode` gives on prod (render node and
|
||||
OpenCL runtime present, policy "auto"):
|
||||
* a HW-decodable codec: 8-bit SDR → the full-GPU path; HDR → the OpenCL tonemap, also on the GPU;
|
||||
10-bit SDR → software (an exact downconvert, never the HDR tone curve);
|
||||
* a CPU-decode codec (`CPU_DECODE_CODECS`) at 8 bits → CPU decode, GPU encode;
|
||||
* any other codec → software.
|
||||
`bit_depth`/`hdr` are unknown at sync time (the cheap listing carries no Stream), so they default to
|
||||
8-bit SDR and the bit-depth backfill refines a 10-bit SDR HEVC to False later."""
|
||||
if playable != "transcode":
|
||||
return None
|
||||
if (vcodec or "").lower() not in HW_DECODABLE_CODECS:
|
||||
return False
|
||||
if bit_depth is not None and bit_depth > 8:
|
||||
return False
|
||||
return not hdr
|
||||
codec = (vcodec or "").lower()
|
||||
deep = bit_depth is not None and bit_depth > 8
|
||||
if codec in HW_DECODABLE_CODECS:
|
||||
return hdr or not deep
|
||||
if codec in CPU_DECODE_CODECS:
|
||||
return not deep and not hdr
|
||||
return False
|
||||
|
||||
|
||||
def _video_stream_props(meta: dict) -> tuple[int | None, bool]:
|
||||
@@ -266,8 +278,9 @@ def sync(db: Session) -> dict:
|
||||
# a Plex shape change, a DB error or our own bug, and `%s` told them apart badly.
|
||||
log.exception("the Plex people-enrich batch failed")
|
||||
# Probe a bounded batch of HW-decodable transcode items for bit-depth/HDR, so the
|
||||
# playback-class filter's HW/SW split is precise (a 10-bit/HDR HEVC → software) over
|
||||
# successive runs. Only HW-decodable-codec transcodes are probed — the rest are SW by codec.
|
||||
# playback-class filter's HW/SW split is precise (a 10-bit SDR HEVC → software) over
|
||||
# successive runs. Only HW-decodable-codec transcodes are probed — the rest are classed
|
||||
# by codec alone.
|
||||
try:
|
||||
stats["media_probed"] = _probe_media_batch(db, plex)
|
||||
except Exception: # best-effort; never fail the sync over a backfill
|
||||
@@ -294,15 +307,15 @@ def sync(db: Session) -> dict:
|
||||
# whole library is covered over successive runs (each fetched item is stamped people_enriched_at once).
|
||||
_ENRICH_BATCH = 60
|
||||
# Same bounded-per-run pattern for the bit-depth probe. Only HW-decodable-codec transcode items are
|
||||
# probed (typically a small minority — the bulk of transcodes are old codecs that are SW by codec), so
|
||||
# probed (typically a small minority — the bulk of transcodes are old codecs classed by codec), so
|
||||
# a modest batch covers the work-list in a run or two; a stuck item retries next run (watermark unset).
|
||||
_PROBE_BATCH = 60
|
||||
|
||||
|
||||
def _probe_media_batch(db: Session, plex: PlexClient, limit: int = _PROBE_BATCH) -> int:
|
||||
"""Rich-fetch up to `limit` not-yet-probed HW-decodable transcode items, read the video Stream's
|
||||
bit-depth + HDR, and store `video_bit_depth` + the refined `transcode_hw` (a 10-bit/HDR HEVC →
|
||||
False/software). Stamps `media_probed_at` on every attempt (success OR a Plex miss with no Stream),
|
||||
bit-depth + HDR, and store `video_bit_depth` + the refined `transcode_hw` (a 10-bit SDR HEVC →
|
||||
False/software; an HDR one stays on the GPU through the tonemap). Stamps `media_probed_at` on every attempt (success OR a Plex miss with no Stream),
|
||||
so an item is probed once and not re-fetched every sync; a transient fetch FAILURE leaves the
|
||||
watermark unset to retry. Oldest-id first. Returns how many items were probed this run."""
|
||||
rows = (
|
||||
|
||||
Reference in New Issue
Block a user