* fix(chapterthumbs): add software HDR tone-map fallback * fix(chapterthumbs): tighten filter capability detection * fix(chapterthumbs): harden tone-map fallback retries * fix(chapterthumbs): coalesce filter probes * fix(chapterthumbs): name disabled accelerator * feat(chapterthumbs): add CPU tone-map toggle * fix(config): reuse CPU tone-map setting key --------- Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com>
329 lines
9.1 KiB
Go
329 lines
9.1 KiB
Go
package playback
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type hwAccelTestEnv struct {
|
|
devDir string
|
|
driDir string
|
|
sysDir string
|
|
}
|
|
|
|
type fakeFFmpegProbe struct {
|
|
cuda bool
|
|
h264NVENC bool
|
|
hevcNVENC bool
|
|
scaleCUDA bool
|
|
uploadCUDA bool
|
|
smokeOK bool
|
|
hang bool
|
|
}
|
|
|
|
type fakeFFmpegBinary struct {
|
|
path string
|
|
logPath string
|
|
}
|
|
|
|
func TestResolveHWAccelWithFFmpegAutoPrefersNVENCOverIntel(t *testing.T) {
|
|
env := setupHWAccelTest(t)
|
|
env.addRenderDevice(t, "renderD128", "0x8086")
|
|
env.addRenderDevice(t, "renderD129", "0x10de")
|
|
ffmpeg := writeFakeFFmpeg(t, successfulNVENCProbe())
|
|
|
|
if got := ResolveHWAccelWithFFmpeg("auto", ffmpeg.path); got != "nvenc" {
|
|
t.Fatalf("ResolveHWAccelWithFFmpeg() = %q, want nvenc", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveHWAccelWithFFmpegFallsBackToIntelWhenNVENCProbeFails(t *testing.T) {
|
|
env := setupHWAccelTest(t)
|
|
env.addRenderDevice(t, "renderD128", "0x8086")
|
|
env.addRenderDevice(t, "renderD129", "0x10de")
|
|
ffmpeg := writeFakeFFmpeg(t, fakeFFmpegProbe{})
|
|
|
|
if got := ResolveHWAccelWithFFmpeg("auto", ffmpeg.path); got != "qsv" {
|
|
t.Fatalf("ResolveHWAccelWithFFmpeg() = %q, want qsv", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveHWAccelWithFFmpegFallsBackToVAAPIWhenNVENCProbeFails(t *testing.T) {
|
|
env := setupHWAccelTest(t)
|
|
env.addRenderDevice(t, "renderD128", "0x10de")
|
|
env.addRenderDevice(t, "renderD129", "0x1002")
|
|
ffmpeg := writeFakeFFmpeg(t, fakeFFmpegProbe{})
|
|
|
|
if got := ResolveHWAccelWithFFmpeg("auto", ffmpeg.path); got != "vaapi" {
|
|
t.Fatalf("ResolveHWAccelWithFFmpeg() = %q, want vaapi", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveHWAccelWithFFmpegReturnsNoneWhenNVENCProbeFailsWithoutFallback(t *testing.T) {
|
|
env := setupHWAccelTest(t)
|
|
env.addRenderDevice(t, "renderD128", "0x10de")
|
|
ffmpeg := writeFakeFFmpeg(t, fakeFFmpegProbe{})
|
|
|
|
if got := ResolveHWAccelWithFFmpeg("auto", ffmpeg.path); got != "none" {
|
|
t.Fatalf("ResolveHWAccelWithFFmpeg() = %q, want none", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveHWAccelWithFFmpegUsesNVIDIADeviceNodesWithoutDRM(t *testing.T) {
|
|
env := setupHWAccelTest(t)
|
|
env.addNVIDIADevice(t, "nvidia0")
|
|
ffmpeg := writeFakeFFmpeg(t, successfulNVENCProbe())
|
|
|
|
if got := ResolveHWAccelWithFFmpeg("auto", ffmpeg.path); got != "nvenc" {
|
|
t.Fatalf("ResolveHWAccelWithFFmpeg() = %q, want nvenc", got)
|
|
}
|
|
}
|
|
|
|
func TestExplicitNVENCBypassesFFmpegProbe(t *testing.T) {
|
|
setupHWAccelTest(t)
|
|
|
|
if got := ResolveHWAccelWithFFmpeg("nvenc", "/does/not/exist/ffmpeg"); got != "nvenc" {
|
|
t.Fatalf("ResolveHWAccelWithFFmpeg() = %q, want nvenc", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveFFmpegPathTrimsWithoutCleaningRelativeExecutable(t *testing.T) {
|
|
if got := ResolveFFmpegPath(" ./ffmpeg "); got != "./ffmpeg" {
|
|
t.Fatalf("ResolveFFmpegPath() = %q, want ./ffmpeg", got)
|
|
}
|
|
}
|
|
|
|
func TestFFmpegSupportsNVENCRequiresCUDAEncodersFiltersAndSmoke(t *testing.T) {
|
|
setupHWAccelTest(t)
|
|
tests := []struct {
|
|
name string
|
|
probe fakeFFmpegProbe
|
|
}{
|
|
{
|
|
name: "missing cuda hwaccel",
|
|
probe: fakeFFmpegProbe{
|
|
h264NVENC: true, hevcNVENC: true, scaleCUDA: true, uploadCUDA: true, smokeOK: true,
|
|
},
|
|
},
|
|
{
|
|
name: "missing h264 nvenc encoder",
|
|
probe: fakeFFmpegProbe{
|
|
cuda: true, hevcNVENC: true, scaleCUDA: true, uploadCUDA: true, smokeOK: true,
|
|
},
|
|
},
|
|
{
|
|
name: "missing hevc nvenc encoder",
|
|
probe: fakeFFmpegProbe{
|
|
cuda: true, h264NVENC: true, scaleCUDA: true, uploadCUDA: true, smokeOK: true,
|
|
},
|
|
},
|
|
{
|
|
name: "missing scale cuda filter",
|
|
probe: fakeFFmpegProbe{
|
|
cuda: true, h264NVENC: true, hevcNVENC: true, uploadCUDA: true, smokeOK: true,
|
|
},
|
|
},
|
|
{
|
|
name: "missing hwupload cuda filter",
|
|
probe: fakeFFmpegProbe{
|
|
cuda: true, h264NVENC: true, hevcNVENC: true, scaleCUDA: true, smokeOK: true,
|
|
},
|
|
},
|
|
{
|
|
name: "smoke encode failure",
|
|
probe: fakeFFmpegProbe{
|
|
cuda: true, h264NVENC: true, hevcNVENC: true, scaleCUDA: true, uploadCUDA: true,
|
|
},
|
|
},
|
|
{
|
|
name: "probe timeout",
|
|
probe: fakeFFmpegProbe{hang: true},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
resetNVENCProbeCacheForTest()
|
|
ffmpeg := writeFakeFFmpeg(t, tt.probe)
|
|
if ok, reason := ffmpegSupportsNVENC(ffmpeg.path); ok {
|
|
t.Fatalf("ffmpegSupportsNVENC() = true, want false")
|
|
} else if reason == "" {
|
|
t.Fatalf("ffmpegSupportsNVENC() reason is empty")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFFmpegSupportsNVENCCachesByFFmpegPath(t *testing.T) {
|
|
env := setupHWAccelTest(t)
|
|
env.addRenderDevice(t, "renderD128", "0x10de")
|
|
ffmpeg := writeFakeFFmpeg(t, successfulNVENCProbe())
|
|
|
|
for i := 0; i < 2; i++ {
|
|
if got := ResolveHWAccelWithFFmpeg("auto", ffmpeg.path); got != "nvenc" {
|
|
t.Fatalf("ResolveHWAccelWithFFmpeg() call %d = %q, want nvenc", i+1, got)
|
|
}
|
|
}
|
|
|
|
logData, err := os.ReadFile(ffmpeg.logPath)
|
|
if err != nil {
|
|
t.Fatalf("read ffmpeg probe log: %v", err)
|
|
}
|
|
if got := strings.Count(string(logData), "\n"); got != 4 {
|
|
t.Fatalf("probe command count = %d, want 4; log:\n%s", got, logData)
|
|
}
|
|
}
|
|
|
|
func TestFFmpegSupportsNVENCSmokeProbeUsesSafeFrameDimensions(t *testing.T) {
|
|
setupHWAccelTest(t)
|
|
ffmpeg := writeFakeFFmpeg(t, successfulNVENCProbe())
|
|
|
|
if ok, reason := ffmpegSupportsNVENC(ffmpeg.path); !ok {
|
|
t.Fatalf("ffmpegSupportsNVENC() = false, want true (reason=%q)", reason)
|
|
}
|
|
|
|
logData, err := os.ReadFile(ffmpeg.logPath)
|
|
if err != nil {
|
|
t.Fatalf("read ffmpeg probe log: %v", err)
|
|
}
|
|
logText := string(logData)
|
|
if !strings.Contains(logText, "testsrc2=size=640x360:rate=1") {
|
|
t.Fatalf("smoke probe should use 640x360 input; log:\n%s", logText)
|
|
}
|
|
}
|
|
|
|
func successfulNVENCProbe() fakeFFmpegProbe {
|
|
return fakeFFmpegProbe{
|
|
cuda: true,
|
|
h264NVENC: true,
|
|
hevcNVENC: true,
|
|
scaleCUDA: true,
|
|
uploadCUDA: true,
|
|
smokeOK: true,
|
|
}
|
|
}
|
|
|
|
func setupHWAccelTest(t *testing.T) *hwAccelTestEnv {
|
|
t.Helper()
|
|
|
|
oldDRIDir := defaultDRIDir
|
|
oldNVIDIAControlDevice := defaultNVIDIAControlDevice
|
|
oldNVIDIADeviceGlob := defaultNVIDIADeviceGlob
|
|
oldSysClassDRMDir := sysClassDRMDir
|
|
oldGOOS := currentGOOS
|
|
oldProbeTimeout := nvencProbeCommandTimeout
|
|
resetNVENCProbeCacheForTest()
|
|
|
|
tmp := t.TempDir()
|
|
env := &hwAccelTestEnv{
|
|
devDir: filepath.Join(tmp, "dev"),
|
|
driDir: filepath.Join(tmp, "dev", "dri"),
|
|
sysDir: filepath.Join(tmp, "sys", "class", "drm"),
|
|
}
|
|
defaultDRIDir = env.driDir
|
|
defaultNVIDIAControlDevice = filepath.Join(env.devDir, "nvidiactl")
|
|
defaultNVIDIADeviceGlob = filepath.Join(env.devDir, "nvidia[0-9]*")
|
|
sysClassDRMDir = env.sysDir
|
|
currentGOOS = "linux"
|
|
nvencProbeCommandTimeout = 200 * time.Millisecond
|
|
|
|
if err := os.MkdirAll(env.driDir, 0o755); err != nil {
|
|
t.Fatalf("create test dri dir: %v", err)
|
|
}
|
|
if err := os.MkdirAll(env.devDir, 0o755); err != nil {
|
|
t.Fatalf("create test dev dir: %v", err)
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
defaultDRIDir = oldDRIDir
|
|
defaultNVIDIAControlDevice = oldNVIDIAControlDevice
|
|
defaultNVIDIADeviceGlob = oldNVIDIADeviceGlob
|
|
sysClassDRMDir = oldSysClassDRMDir
|
|
currentGOOS = oldGOOS
|
|
nvencProbeCommandTimeout = oldProbeTimeout
|
|
resetNVENCProbeCacheForTest()
|
|
})
|
|
|
|
return env
|
|
}
|
|
|
|
func (e *hwAccelTestEnv) addRenderDevice(t *testing.T, name string, vendor string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(filepath.Join(e.driDir, name), []byte{}, 0o600); err != nil {
|
|
t.Fatalf("create render device: %v", err)
|
|
}
|
|
vendorPath := filepath.Join(e.sysDir, name, "device", "vendor")
|
|
if err := os.MkdirAll(filepath.Dir(vendorPath), 0o755); err != nil {
|
|
t.Fatalf("create vendor dir: %v", err)
|
|
}
|
|
if err := os.WriteFile(vendorPath, []byte(vendor+"\n"), 0o644); err != nil {
|
|
t.Fatalf("write vendor file: %v", err)
|
|
}
|
|
}
|
|
|
|
func (e *hwAccelTestEnv) addNVIDIADevice(t *testing.T, name string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(filepath.Join(e.devDir, name), []byte{}, 0o600); err != nil {
|
|
t.Fatalf("create nvidia device: %v", err)
|
|
}
|
|
}
|
|
|
|
func writeFakeFFmpeg(t *testing.T, probe fakeFFmpegProbe) fakeFFmpegBinary {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "ffmpeg")
|
|
logPath := filepath.Join(dir, "probe.log")
|
|
|
|
script := "#!/bin/sh\n"
|
|
script += fmt.Sprintf("printf '%%s\\n' \"$*\" >> %q\n", logPath)
|
|
if probe.hang {
|
|
script += "sleep 5\n"
|
|
}
|
|
script += "case \"$*\" in\n"
|
|
script += " *-hwaccels*)\n"
|
|
script += " echo 'Hardware acceleration methods:'\n"
|
|
if probe.cuda {
|
|
script += " echo 'cuda'\n"
|
|
}
|
|
script += " exit 0 ;;\n"
|
|
script += " *-encoders*)\n"
|
|
if probe.h264NVENC {
|
|
script += " echo ' V..... h264_nvenc NVIDIA NVENC H.264 encoder'\n"
|
|
}
|
|
if probe.hevcNVENC {
|
|
script += " echo ' V..... hevc_nvenc NVIDIA NVENC hevc encoder'\n"
|
|
}
|
|
script += " exit 0 ;;\n"
|
|
script += " *-filters*)\n"
|
|
if probe.scaleCUDA {
|
|
script += " echo ' ... scale_cuda V->V GPU video scaling'\n"
|
|
}
|
|
if probe.uploadCUDA {
|
|
script += " echo ' ... hwupload_cuda V->V upload CUDA frames'\n"
|
|
}
|
|
script += " exit 0 ;;\n"
|
|
script += " *)\n"
|
|
if probe.smokeOK {
|
|
script += " exit 0 ;;\n"
|
|
} else {
|
|
script += " echo 'no capable devices found' >&2\n"
|
|
script += " exit 1 ;;\n"
|
|
}
|
|
script += "esac\n"
|
|
|
|
if err := os.WriteFile(path, []byte(script), 0o755); err != nil {
|
|
t.Fatalf("write fake ffmpeg: %v", err)
|
|
}
|
|
return fakeFFmpegBinary{path: path, logPath: logPath}
|
|
}
|
|
|
|
func resetNVENCProbeCacheForTest() {
|
|
nvencProbeCache.Lock()
|
|
defer nvencProbeCache.Unlock()
|
|
nvencProbeCache.byPath = make(map[string]nvencProbeResult)
|
|
}
|