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:
2026-11-07 18:39:15 +01:00
parent 2b7620ded9
commit 9778bdf351
6 changed files with 385 additions and 87 deletions
@@ -0,0 +1,65 @@
"""plex_items.transcode_hw: the facet says whether the GPU ENCODES, under the measured codec sets
Measured on prod (2026-11-07): the iGPU has no VC-1 Advanced decode, so a VC-1 film never ran on the
GPU although the facet said it did; VC-1, XviD and DivX now take the CPU-decode → GPU-encode path; and an
HDR film runs on the GPU through the OpenCL tonemap although the facet said software. The rule is
`plex.sync.transcode_hw_capable`; the two codec sets below are pinned to `plex.sync` by
tests/test_plex_playback_class.py.
Rewritten from the stored codec and bit depth. HDR is not stored, so a 10-bit HW-decodable item is set to
software here and its probe watermark cleared: the bit-depth backfill (`plex.sync._probe_media_batch`,
a bounded batch per Plex sync) reads its HDR flag again and moves an HDR one back to the GPU.
"""
from typing import Sequence, Union
from alembic import op
revision: str = "0119_plex_transcode_gpu"
down_revision: Union[str, None] = "0118_videos_shuffle_key"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
HW_DECODABLE_CODECS = ("h264", "hevc", "h265", "vp9", "mpeg2video")
CPU_DECODE_CODECS = ("vc1", "mpeg4", "msmpeg4v3")
def _in(codecs: tuple[str, ...]) -> str:
return ", ".join(f"'{c}'" for c in codecs)
def upgrade() -> None:
op.execute(
f"""
UPDATE plex_items
SET transcode_hw = CASE
WHEN lower(codec_video) IN ({_in(HW_DECODABLE_CODECS)})
THEN coalesce(video_bit_depth, 8) <= 8
WHEN lower(codec_video) IN ({_in(CPU_DECODE_CODECS)})
THEN coalesce(video_bit_depth, 8) <= 8
ELSE false
END
WHERE playable = 'transcode'
"""
)
op.execute(
f"""
UPDATE plex_items
SET media_probed_at = NULL
WHERE playable = 'transcode'
AND lower(codec_video) IN ({_in(HW_DECODABLE_CODECS)})
AND video_bit_depth > 8
"""
)
def downgrade() -> None:
# The old rule: a decodable codec (VC-1 counted) at 8 bits; HDR and every other codec software.
op.execute(
f"""
UPDATE plex_items
SET transcode_hw = lower(codec_video) IN ({_in(HW_DECODABLE_CODECS + ("vc1",))})
AND coalesce(video_bit_depth, 8) <= 8
WHERE playable = 'transcode'
"""
)