feat(transcode): tonemap HDR sources to SDR (zscale-gated)

HDR (HDR10/HLG/Dolby Vision) transcoded to SDR came out washed-out and
desaturated because the filter chain never tonemapped. buildHLSFFmpegArgsAt now
inserts a zscale linearise -> hable tonemap -> BT.709 chain after the scale and
before format=, but only when the source is HDR and the ffmpeg build has zscale
(FFmpegSupportsZscale, cached). Builds without zimg keep the old behaviour
(plays, just desaturated) instead of erroring.

It's a CPU filter, valid for every encoder here: the decode hwaccel deliberately
leaves frames in system memory (no -hwaccel_output_format), so zscale runs ahead
of format=/hwupload exactly like the existing scale filter. Verified on a real
4K HDR10 file — vivid colour and deep blacks vs the washed-out baseline.
This commit is contained in:
Deivid Soto 2026-05-31 23:01:09 +02:00
parent 445da233c0
commit e4373454ba
6 changed files with 211 additions and 5 deletions

View file

@ -1347,16 +1347,23 @@ 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=.
tonemap := ""
if probe.HDR != "" && cfg.Transcode.TonemapHDR {
tonemap = hdrTonemapChain
}
var filterChain string
if maxH > 0 && probe.Height > maxH {
filterChain = fmt.Sprintf(
"scale=-2:%d:force_original_aspect_ratio=decrease,scale=trunc(iw/2)*2:trunc(ih/2)*2,format=%s%s%s",
maxH, pixFormat, colorTail, hwUploadTail,
"scale=-2:%d:force_original_aspect_ratio=decrease,scale=trunc(iw/2)*2:trunc(ih/2)*2,%sformat=%s%s%s",
maxH, tonemap, pixFormat, colorTail, hwUploadTail,
)
} else {
filterChain = fmt.Sprintf(
"scale=trunc(iw/2)*2:trunc(ih/2)*2,format=%s%s%s",
pixFormat, colorTail, hwUploadTail,
"scale=trunc(iw/2)*2:trunc(ih/2)*2,%sformat=%s%s%s",
tonemap, pixFormat, colorTail, hwUploadTail,
)
}
args = append(args, "-vf", filterChain)