unarr/internal/engine/tonemap_test.go
Deivid Soto cfaedb7f3b fix(stream): functional libplacebo probe + benchmark hardening
Review (critico) caught a regression: the prod agent image ships a BtbN GPL
ffmpeg with libplacebo COMPILED IN but no Vulkan runtime (debian-slim, no
libvulkan1/mesa-vulkan-drivers/nvidia ICD). The presence probe (ffmpeg
-filters) would flip HasLibplacebo on, the filter's Vulkan device creation
would fail at runtime, and HDR sources that previously tonemapped via zscale
would break.

- FFmpegSupportsLibplacebo now RUNS the filter on one synthetic frame and
  requires a clean exit (forces Vulkan device init + filtergraph negotiation),
  so it is honest about THIS host: works on Vulkan-capable hosts, falls back to
  zscale where Vulkan is absent. Logs the real ffmpeg error on failure.
- Warm the libplacebo (Vulkan init ~1.7s) + zscale caches in a background
  goroutine at startup so the first stream session doesn't pay the probe and
  risk its setup timeout.
- Benchmark: margin 1.5x -> 2.0x (the probe measures encode only; real decode
  of HEVC/10-bit + busier content needs more headroom), per-probe timeout
  12s -> 6s + overall 45s -> 20s (it blocks registration on software hosts),
  and a 'no rung measured' case (missing lavfi/wedged ffmpeg) now keeps the
  1080 default instead of flooring at 480 — an infra failure isn't a slow host.

Verified e2e on the fixed binary: LOTR Two Towers (HEVC 3840x1608 10-bit
HDR10/PQ, 12GB) on desktop-Chrome caps -> hls, ffmpeg runs h264_nvenc with
-vf ...,libplacebo=...:format=yuv420p:tonemapping=bt.2390 (zscale chain
replaced), 45 fMP4 segments, ffprobe confirms output h264 yuv420p bt709
(tonemapped from bt2020/smpte2084), no ffmpeg errors.
2026-06-03 09:57:48 +02:00

154 lines
5.3 KiB
Go

package engine
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func hlsArgsFor(hdr string, tonemap bool, hw HWAccel) string {
cfg := HLSSessionConfig{
SessionID: "test",
SourcePath: "/movies/x.mkv",
Quality: "720p",
Transcode: TranscodeRuntime{
FFmpegPath: "/usr/bin/ffmpeg",
FFprobePath: "/usr/bin/ffprobe",
HWAccel: hw,
TonemapHDR: tonemap,
},
}
probe := &StreamProbe{Width: 3840, Height: 2160, BitDepth: 10, HDR: hdr, DurationSec: 100}
return strings.Join(buildHLSFFmpegArgsAt(cfg, probe, "/tmp/t", 0, 0), " ")
}
func vfChain(joined string) string {
parts := strings.Split(joined, " ")
for i, p := range parts {
if p == "-vf" && i+1 < len(parts) {
return parts[i+1]
}
}
return ""
}
func TestTonemap_AppliedForHDRWhenSupported(t *testing.T) {
vf := vfChain(hlsArgsFor("HDR10", true, HWAccelNone))
if !strings.Contains(vf, "zscale=t=linear") || !strings.Contains(vf, "tonemap=tonemap=hable") {
t.Fatalf("HDR + zscale-capable: expected tonemap in -vf, got %q", vf)
}
// Order: a scale filter, then tonemap (zscale), then format=.
scaleIdx := strings.Index(vf, "scale=")
zIdx := strings.Index(vf, "zscale=t=linear")
fmtIdx := strings.Index(vf, "format=")
if !(scaleIdx >= 0 && scaleIdx < zIdx && zIdx < fmtIdx) {
t.Errorf("filter order wrong (scale < tonemap < format): %q", vf)
}
}
func TestTonemap_AppliedInNoDownscaleBranch(t *testing.T) {
// Source already within the quality cap → no downscale; tonemap must still
// be inserted before format=.
cfg := HLSSessionConfig{
SessionID: "test",
SourcePath: "/movies/x.mkv",
Quality: "2160p",
Transcode: TranscodeRuntime{FFmpegPath: "/usr/bin/ffmpeg", HWAccel: HWAccelNone, TonemapHDR: true},
}
probe := &StreamProbe{Width: 3840, Height: 2160, HDR: "HDR10", DurationSec: 100}
vf := vfChain(strings.Join(buildHLSFFmpegArgsAt(cfg, probe, "/tmp/t", 0, 0), " "))
if !strings.Contains(vf, "tonemap=tonemap=hable") {
t.Errorf("no-downscale branch: expected tonemap, got %q", vf)
}
if z, f := strings.Index(vf, "zscale=t=linear"), strings.Index(vf, "format="); !(z >= 0 && z < f) {
t.Errorf("tonemap must precede format=: %q", vf)
}
}
func TestTonemap_LibplaceboPreferredOverZscale(t *testing.T) {
// HDR source + an ffmpeg with libplacebo → the single GPU filter replaces
// the whole CPU zscale chain (and the trailing format=/setparams it folds in).
cfg := HLSSessionConfig{
SessionID: "test",
SourcePath: "/movies/x.mkv",
Quality: "720p",
Transcode: TranscodeRuntime{FFmpegPath: "/usr/bin/ffmpeg", HWAccel: HWAccelNone, TonemapHDR: true, HasLibplacebo: true},
}
probe := &StreamProbe{Width: 3840, Height: 2160, BitDepth: 10, HDR: "HDR10", DurationSec: 100}
vf := vfChain(strings.Join(buildHLSFFmpegArgsAt(cfg, probe, "/tmp/t", 0, 0), " "))
if !strings.Contains(vf, "libplacebo") {
t.Fatalf("libplacebo-capable ffmpeg: expected libplacebo filter, got %q", vf)
}
if strings.Contains(vf, "zscale=t=linear") || strings.Contains(vf, "tonemap=tonemap=hable") {
t.Errorf("libplacebo must replace the zscale chain, not run alongside it: %q", vf)
}
}
func TestTonemap_SkippedWhenFFmpegLacksZscale(t *testing.T) {
vf := vfChain(hlsArgsFor("HDR10", false, HWAccelNone))
if strings.Contains(vf, "zscale") || strings.Contains(vf, "tonemap") {
t.Errorf("ffmpeg without zscale: tonemap must be skipped, got %q", vf)
}
}
func TestTonemap_SkippedForSDR(t *testing.T) {
vf := vfChain(hlsArgsFor("", true, HWAccelNone))
if strings.Contains(vf, "zscale") || strings.Contains(vf, "tonemap") {
t.Errorf("SDR source: no tonemap expected, got %q", vf)
}
}
func TestTonemap_VAAPIInsertsBeforeHwupload(t *testing.T) {
vf := vfChain(hlsArgsFor("HDR10", true, HWAccelVAAPI))
if !strings.Contains(vf, "tonemap=tonemap=hable") {
t.Fatalf("VAAPI HDR: expected tonemap, got %q", vf)
}
// Tonemap is a CPU filter — must run before the GPU upload.
if up := strings.Index(vf, "hwupload"); up >= 0 {
if strings.Index(vf, "zscale=t=linear") > up {
t.Errorf("tonemap must precede hwupload: %q", vf)
}
}
}
func TestFFmpegSupportsLibplacebo_FunctionalProbe(t *testing.T) {
if FFmpegSupportsLibplacebo("") {
t.Error("empty path must be false")
}
// A bogus path can't run → false (no panic, no hang).
if FFmpegSupportsLibplacebo("/nonexistent/ffmpeg") {
t.Error("nonexistent ffmpeg must be false")
}
// With a real ffmpeg the result is environment-dependent (true only when a
// Vulkan runtime is present), so we only assert the probe completes and
// returns a bool — its whole purpose is to be honest about THIS host.
if _, err := exec.LookPath("ffmpeg"); err == nil {
_ = FFmpegSupportsLibplacebo("ffmpeg") // must not hang or panic
}
}
func TestFFmpegSupportsZscale_Stub(t *testing.T) {
dir := t.TempDir()
withZ := filepath.Join(dir, "ffmpeg-with.sh")
if err := os.WriteFile(withZ, []byte("#!/bin/sh\necho ' .SC zscale V->V'\n"), 0o755); err != nil {
t.Fatal(err)
}
if !FFmpegSupportsZscale(withZ) {
t.Error("expected true for an ffmpeg whose -filters lists zscale")
}
noZ := filepath.Join(dir, "ffmpeg-without.sh")
if err := os.WriteFile(noZ, []byte("#!/bin/sh\necho ' ... scale V->V'\n"), 0o755); err != nil {
t.Fatal(err)
}
if FFmpegSupportsZscale(noZ) {
t.Error("expected false for an ffmpeg whose -filters omits zscale")
}
if FFmpegSupportsZscale("") {
t.Error("empty path must be false")
}
}