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.
154 lines
5.3 KiB
Go
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")
|
|
}
|
|
}
|