Addresses items raised by the multi-agent code review of the 0.9.9 HW accel + first-start work: - EncoderProfile now carries DecodeHwAccel so the demuxer `-hwaccel` flag and the encoder argv derive from a single resolved profile. Adding a new backend can no longer leave the two switches out of sync. - VAAPI no longer passes `-hwaccel_output_format vaapi`. That option pinned decoded frames to GPU memory, but the filter chain (scale, format, setparams) runs on CPU and would fail with "impossible to convert between formats". Frames now decode HW + flow on CPU; the encoder uploads back to GPU. Pre-existing bug, never reported because no one had VAAPI auto-detected in practice. - readyMax field comment + name: documented that it's a COUNT (segments ready), not an index. The semantics were correct but the comment read "highest index" which made `idx < readyMax` look like an off-by-one to reviewers. - probe_cache background janitor: 5-minute sweeper that drops expired entries even when no lookup retouches the key. Lookup-only eviction was fine for small libraries but unbounded for users who browse and abandon thousands of files within a TTL window. Lazy + sync.Once. - probe_cache TTL eviction now re-checks under the write lock so a concurrent re-insert isn't accidentally evicted. - probe_cache size-change test now Chtimes the file back to its original mtime so only `size` differs between store and lookup keys — properly exercises the size-check path. - New TestProbeCache_SweepDropsExpired covers the janitor sweep. - CHANGELOG: backfilled missing compare links 0.6.4 → 0.9.9. - Stale "line ~1119" reference in VideoToolbox comment dropped; the bitrate block moved a few lines and the comment was already wrong.
156 lines
5.3 KiB
Go
156 lines
5.3 KiB
Go
package engine
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestHWAccelFFmpegVideoCodec(t *testing.T) {
|
|
cases := []struct {
|
|
hw HWAccel
|
|
target string
|
|
want string
|
|
}{
|
|
{HWAccelNone, "h264", "libx264"},
|
|
{HWAccelNone, "hevc", "libx264"},
|
|
{HWAccelNVENC, "h264", "h264_nvenc"},
|
|
{HWAccelNVENC, "hevc", "hevc_nvenc"},
|
|
{HWAccelQSV, "h264", "h264_qsv"},
|
|
{HWAccelQSV, "hevc", "hevc_qsv"},
|
|
{HWAccelVAAPI, "h264", "h264_vaapi"},
|
|
{HWAccelVAAPI, "hevc", "hevc_vaapi"},
|
|
{HWAccelVideoToolbox, "h264", "h264_videotoolbox"},
|
|
{HWAccelVideoToolbox, "hevc", "hevc_videotoolbox"},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := tc.hw.FFmpegVideoCodec(tc.target); got != tc.want {
|
|
t.Errorf("%s.FFmpegVideoCodec(%q) = %q want %q", tc.hw, tc.target, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDetectHWAccelEmptyPathReturnsNone(t *testing.T) {
|
|
ResetHWAccelCache()
|
|
if got := detectHWAccelFresh(t.Context(), ""); got != HWAccelNone {
|
|
t.Errorf("got %s, want %s", got, HWAccelNone)
|
|
}
|
|
}
|
|
|
|
func TestResolveEncoderProfileDefaults(t *testing.T) {
|
|
cases := []struct {
|
|
hw HWAccel
|
|
configured string
|
|
wantCodec string
|
|
wantPreset string
|
|
wantHint string
|
|
}{
|
|
// Empty configured preset → pick latency-biased default per backend.
|
|
// DecodeHwAccel matches the encoder family for HW encoders; libx264 +
|
|
// VideoToolbox have no demuxer hint.
|
|
{HWAccelNone, "", "libx264", "superfast", ""},
|
|
{HWAccelNVENC, "", "h264_nvenc", "p3", "cuda"},
|
|
{HWAccelQSV, "", "h264_qsv", "veryfast", "qsv"},
|
|
// VAAPI: decoder hint set, no preset, no `-hwaccel_output_format vaapi`
|
|
// (so the CPU filter chain can consume the decoded frames).
|
|
{HWAccelVAAPI, "", "h264_vaapi", "", "vaapi"},
|
|
// VideoToolbox has no preset knob — Preset should be "" regardless of input.
|
|
// VideoToolbox uses per-encoder flags, not a demuxer `-hwaccel` hint.
|
|
{HWAccelVideoToolbox, "p4", "h264_videotoolbox", "", ""},
|
|
{HWAccelVideoToolbox, "", "h264_videotoolbox", "", ""},
|
|
}
|
|
for _, tc := range cases {
|
|
got := ResolveEncoderProfile(tc.hw, tc.configured)
|
|
if got.Codec != tc.wantCodec || got.Preset != tc.wantPreset || got.DecodeHwAccel != tc.wantHint {
|
|
t.Errorf("ResolveEncoderProfile(%s, %q) = {codec=%s preset=%s hint=%s}, want {codec=%s preset=%s hint=%s}",
|
|
tc.hw, tc.configured,
|
|
got.Codec, got.Preset, got.DecodeHwAccel,
|
|
tc.wantCodec, tc.wantPreset, tc.wantHint)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResolveEncoderProfileHonoursConfiguredPreset(t *testing.T) {
|
|
// Only libx264 honours the configured preset — the libx264 vocabulary
|
|
// (ultrafast…veryslow) doesn't apply to vendor encoders. NVENC has its
|
|
// own p1-p7 scale; QSV uses a different subset; VideoToolbox has no
|
|
// preset knob. Passing a libx264 preset to them would have ffmpeg reject
|
|
// the argv, so ResolveEncoderProfile always falls back to the hardcoded
|
|
// vendor preset for non-libx264 codecs.
|
|
cases := []struct {
|
|
hw HWAccel
|
|
configured string
|
|
wantPreset string
|
|
}{
|
|
{HWAccelNone, "ultrafast", "ultrafast"}, // libx264 honours
|
|
{HWAccelNone, "medium", "medium"}, // libx264 honours
|
|
{HWAccelNVENC, "p1", "p3"}, // NVENC ignores, sticks to p3
|
|
{HWAccelNVENC, "veryfast", "p3"}, // NVENC ignores libx264 vocab
|
|
{HWAccelQSV, "veryslow", "veryfast"}, // QSV ignores, sticks to veryfast
|
|
{HWAccelVideoToolbox, "veryfast", ""}, // VideoToolbox has no preset
|
|
}
|
|
for _, tc := range cases {
|
|
got := ResolveEncoderProfile(tc.hw, tc.configured)
|
|
if got.Preset != tc.wantPreset {
|
|
t.Errorf("ResolveEncoderProfile(%s, %q).Preset = %q, want %q",
|
|
tc.hw, tc.configured, got.Preset, tc.wantPreset)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHWAccelDiagnosticLogLineNone(t *testing.T) {
|
|
d := HWAccelDiagnostic{
|
|
Pick: HWAccelNone,
|
|
FFmpegPath: "/usr/local/bin/ffmpeg",
|
|
FFmpegVersion: "ffmpeg version 6.1.1",
|
|
Encoders: nil,
|
|
Devices: nil,
|
|
}
|
|
line := d.LogLine()
|
|
wantSubstrings := []string{
|
|
"ffmpeg version 6.1.1",
|
|
"/usr/local/bin/ffmpeg",
|
|
"HW=none",
|
|
"software libx264",
|
|
"no HW encoders compiled in",
|
|
}
|
|
for _, want := range wantSubstrings {
|
|
if !strings.Contains(line, want) {
|
|
t.Errorf("expected substring %q in log line; got %q", want, line)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHWAccelDiagnosticLogLineNVENCWithDevices(t *testing.T) {
|
|
d := HWAccelDiagnostic{
|
|
Pick: HWAccelNVENC,
|
|
FFmpegPath: "/usr/bin/ffmpeg",
|
|
FFmpegVersion: "ffmpeg version 6.0",
|
|
Encoders: []string{"h264_nvenc", "hevc_nvenc", "h264_qsv"},
|
|
Devices: []string{"/dev/nvidia0", "nvidia-smi"},
|
|
}
|
|
line := d.LogLine()
|
|
for _, want := range []string{"HW=nvenc", "h264_nvenc", "/dev/nvidia0", "nvidia-smi"} {
|
|
if !strings.Contains(line, want) {
|
|
t.Errorf("expected substring %q in log line; got %q", want, line)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHWAccelDiagnosticLogLineSoftwareButEncodersFound(t *testing.T) {
|
|
// Edge case: ffmpeg compiled WITH nvenc but no /dev/nvidia0 (container w/o GPU).
|
|
// LogLine should flag the encoders so the user knows where the gap is.
|
|
d := HWAccelDiagnostic{
|
|
Pick: HWAccelNone,
|
|
FFmpegPath: "/usr/bin/ffmpeg",
|
|
FFmpegVersion: "ffmpeg version 6.0",
|
|
Encoders: []string{"h264_nvenc"},
|
|
Devices: nil,
|
|
}
|
|
line := d.LogLine()
|
|
for _, want := range []string{"HW=none", "encoders found but no matching device", "h264_nvenc"} {
|
|
if !strings.Contains(line, want) {
|
|
t.Errorf("expected substring %q in log line; got %q", want, line)
|
|
}
|
|
}
|
|
}
|
|
|