unarr/internal/engine/vaapi_args_test.go

98 lines
2.8 KiB
Go
Raw Permalink Normal View History

feat(vaapi): hybrid CPU-scale + hwupload encode path (QW2, 0.9.14) Closes QW2. Validated against the dev box's AMD Raphael iGPU (/dev/dri/renderD128, radeonsi/mesa 25.2.8). The "proper" full-GPU path via scale_vaapi triggers a known mesa 25 + Raphael bug ("Cannot allocate memory" per session start, encode still succeeds but logs are spammy) — hybrid CPU scale → format=nv12 → hwupload → h264_vaapi encode delivers GPU surfaces to the encoder without poking the broken scaler. Three concrete changes in buildHLSFFmpegArgsAt: 1. New `case "h264_vaapi"` adds `-vaapi_device /dev/dri/renderD128`. Multi-GPU hosts (this dev box has NVIDIA on renderD129 + AMD on renderD128) need it so the encoder doesn't bind to a non-VAAPI render node — without it the encoder fell back to NULL device in manual smoke testing. 2. Filter chain branches on codec: VAAPI uses `scale=…,format=nv12,hwupload` while libx264 / NVENC / QSV keep the existing `scale=…,format=yuv420p,setparams=…` shape. The setparams color metadata block is dropped on VAAPI because VAAPI surfaces don't expose VUI fields and the encoder writes its own. 3. Two new unit tests lock the argv shape so a future refactor doesn't accidentally merge the paths back together: TestBuildHLSFFmpegArgsVAAPI asserts the new flags + the ABSENCE of scale_vaapi; TestBuildHLSFFmpegArgsLibx264NoRegression verifies the software path keeps yuv420p + setparams + has none of the VAAPI extras. Manual ffmpeg validation on the dev box: hybrid encode of 5 s 4K → 720p: 0.66 s wall, 472 % CPU, 268 KB output — no errors logged. scale_vaapi variant in comparison spammed "Cannot allocate memory" while emitting valid output.
2026-05-27 15:45:55 +02:00
package engine
import (
"strings"
"testing"
)
func TestBuildHLSFFmpegArgsVAAPI(t *testing.T) {
cfg := HLSSessionConfig{
SessionID: "test",
SourcePath: "/tmp/test.mkv",
Quality: "720p",
AudioIndex: 0,
Transcode: TranscodeRuntime{
FFmpegPath: "/usr/bin/ffmpeg",
FFprobePath: "/usr/bin/ffprobe",
HWAccel: HWAccelVAAPI,
},
}
probe := &StreamProbe{Width: 1920, Height: 1080, DurationSec: 100}
args := buildHLSFFmpegArgsAt(cfg, probe, "/tmp/tmpdir", 0, 0)
got := strings.Join(args, " ")
wants := []string{
"-hwaccel vaapi",
"-vaapi_device /dev/dri/renderD128",
"-c:v h264_vaapi",
"format=nv12",
"hwupload",
}
for _, want := range wants {
if !strings.Contains(got, want) {
t.Errorf("argv missing %q\n%s", want, got)
}
}
if strings.Contains(got, "scale_vaapi") {
t.Errorf("argv unexpectedly contains scale_vaapi (mesa bug): %s", got)
}
if strings.Contains(got, "format=yuv420p") {
t.Errorf("argv contains format=yuv420p (libx264 path) for VAAPI codec: %s", got)
}
}
func TestBuildHLSFFmpegArgsLibx264NoRegression(t *testing.T) {
cfg := HLSSessionConfig{
SessionID: "test",
SourcePath: "/tmp/test.mkv",
Quality: "720p",
AudioIndex: 0,
Transcode: TranscodeRuntime{
FFmpegPath: "/usr/bin/ffmpeg",
FFprobePath: "/usr/bin/ffprobe",
HWAccel: HWAccelNone,
},
}
probe := &StreamProbe{Width: 1920, Height: 1080, DurationSec: 100}
args := buildHLSFFmpegArgsAt(cfg, probe, "/tmp/tmpdir", 0, 0)
got := strings.Join(args, " ")
for _, want := range []string{"-c:v libx264", "format=yuv420p", "setparams=colorspace=bt709"} {
if !strings.Contains(got, want) {
t.Errorf("libx264 argv missing %q: %s", want, got)
}
}
for _, bad := range []string{"-vaapi_device", "format=nv12", "hwupload"} {
if strings.Contains(got, bad) {
t.Errorf("libx264 argv unexpectedly contains %q: %s", bad, got)
}
}
}
// TestBuildHLSFFmpegArgsVAAPIDump prints the full argv buildHLSFFmpegArgsAt
// emits for a typical VAAPI session. Mimics the daemon spawn step so the
// operator can verify the ffmpeg command-line shape without booting the
// stack — equivalent to `journalctl --user -u unarr-dev | grep ffmpeg`
// but without waiting for a real player session.
func TestBuildHLSFFmpegArgsVAAPIDump(t *testing.T) {
cfg := HLSSessionConfig{
SessionID: "vaapi-smoke",
SourcePath: "/mnt/nas/peliculas/sample.mkv",
Quality: "720p",
AudioIndex: -1,
Transcode: TranscodeRuntime{
FFmpegPath: "/usr/bin/ffmpeg",
FFprobePath: "/usr/bin/ffprobe",
HWAccel: HWAccelVAAPI,
},
}
probe := &StreamProbe{
VideoCodec: "hevc",
Width: 3840,
Height: 2160,
DurationSec: 5400,
AudioTracks: []ProbeAudioTrack{{Index: 0, Lang: "en", Codec: "ac3"}},
}
args := buildHLSFFmpegArgsAt(cfg, probe, "/tmp/smoke-tmpdir", 0, 0)
t.Logf("ffmpeg %s", strings.Join(args, " "))
}