feat(stream): GPU HDR tonemap via libplacebo

Prefer the single-pass Vulkan libplacebo filter over the CPU zscale chain
for HDR->SDR tonemapping when the agent ffmpeg has it. One GPU pass does
tonemap + BT.709 primaries/transfer/matrix + 8-bit yuv420p, replacing the
four-stage zscale chain and its trailing format=/setparams. Higher quality,
far cheaper than the CPU path, and present on builds that lack zscale.

- FFmpegSupportsLibplacebo probe (cached, mirrors FFmpegSupportsZscale)
- HasLibplacebo on TranscodeRuntime, wired from buildTranscodeRuntime
- hls.go: videoTail picks libplacebo when present (not h264_vaapi), else
  keeps the zscale tonemap + format chain
- test: libplacebo replaces the zscale chain, never runs alongside it
This commit is contained in:
Deivid Soto 2026-06-03 09:29:55 +02:00
parent 325c11c1eb
commit 005a4380dd
5 changed files with 90 additions and 11 deletions

View file

@ -1353,26 +1353,37 @@ func buildHLSFFmpegArgsAt(cfg HLSSessionConfig, probe *StreamProbe, tmpDir strin
hwUploadTail = ",hwupload"
colorTail = ""
}
// HDR→SDR tonemap, inserted after the scale (downscale-first = fewer pixels
// to tonemap) and before format=. Only for an HDR source on a zscale-capable
// ffmpeg; the trailing comma in hdrTonemapChain slots it in front of format=.
// HDR→SDR tonemap, after the scale (downscale-first = fewer pixels to map).
// Prefer libplacebo (GPU, ONE pass — it also emits the BT.709 colorspace +
// 8-bit format, so it REPLACES the format/setparams tail); else the zscale
// CPU chain; else play untonemapped (desaturated, last resort). Skip
// libplacebo on VAAPI: its Vulkan surface flow doesn't compose with our
// nv12+hwupload path, so VAAPI keeps the zscale-or-none behaviour.
useLibplacebo := probe.HDR != "" && cfg.Transcode.HasLibplacebo && codec != "h264_vaapi"
tonemap := ""
if probe.HDR != "" && cfg.Transcode.TonemapHDR {
if probe.HDR != "" && cfg.Transcode.TonemapHDR && !useLibplacebo {
tonemap = hdrTonemapChain
}
// Core video chain (scale + optional tonemap + pixel format + color metadata),
// WITHOUT the optional hwUploadTail — that has to run last, after any subtitle
// overlay, so it's appended separately below.
// videoTail = everything after the scale: either libplacebo (tonemap +
// colorspace + format in one) or the (optional zscale) tonemap then the
// format + color-metadata tail. No leading comma — the scale chain ends in one.
videoTail := tonemap + "format=" + pixFormat + colorTail
if useLibplacebo {
videoTail = libplaceboTonemapFilter
}
// Core video chain (scale + tonemap/format tail), WITHOUT the optional
// hwUploadTail — that has to run last, after any subtitle overlay, so it's
// appended separately below.
var vchain string
if maxH > 0 && probe.Height > maxH {
vchain = fmt.Sprintf(
"scale=-2:%d:force_original_aspect_ratio=decrease,scale=trunc(iw/2)*2:trunc(ih/2)*2,%sformat=%s%s",
maxH, tonemap, pixFormat, colorTail,
"scale=-2:%d:force_original_aspect_ratio=decrease,scale=trunc(iw/2)*2:trunc(ih/2)*2,%s",
maxH, videoTail,
)
} else {
vchain = fmt.Sprintf(
"scale=trunc(iw/2)*2:trunc(ih/2)*2,%sformat=%s%s",
tonemap, pixFormat, colorTail,
"scale=trunc(iw/2)*2:trunc(ih/2)*2,%s",
videoTail,
)
}
if burnIdx >= 0 {