diff --git a/internal/playback/gpudetect.go b/internal/playback/gpudetect.go index 7196589f..4d1c81b1 100644 --- a/internal/playback/gpudetect.go +++ b/internal/playback/gpudetect.go @@ -202,9 +202,12 @@ func ffmpegSupportsVideoToolbox(ffmpegPath string) (bool, string) { } // probeFFmpegVideoToolbox verifies the configured FFmpeg exposes VideoToolbox -// decode plus both encoders, then smoke-encodes one frame. No filter probes: -// the videotoolbox pipeline keeps decoded frames in system memory, so the -// regular software filter graph applies (see appendHWAccelArgs). +// decode plus both encoders, then smoke-encodes one frame per encoder in the +// same constant-quality mode the uncapped transcode path emits (-q:v needs an +// Apple Silicon compression session; Intel Macs must fail here so auto +// resolves to software instead of approving commands that cannot run). No +// filter probes: the videotoolbox pipeline keeps decoded frames in system +// memory, so the regular software filter graph applies (see appendHWAccelArgs). func probeFFmpegVideoToolbox(ffmpegPath string) nvencProbeResult { if output, err := runFFmpegProbe(ffmpegPath, "-hide_banner", "-hwaccels"); err != nil { return nvencProbeResult{reason: "hwaccels probe failed: " + probeFailure(err, output)} @@ -220,18 +223,27 @@ func probeFFmpegVideoToolbox(ffmpegPath string) nvencProbeResult { return nvencProbeResult{reason: "hevc_videotoolbox encoder unavailable"} } - if output, err := runFFmpegProbe(ffmpegPath, - "-hide_banner", - "-loglevel", "error", - "-f", "lavfi", - "-i", "testsrc2=size=640x360:rate=1", - "-frames:v", "1", - "-an", - "-c:v", "h264_videotoolbox", - "-f", "null", - "-", - ); err != nil { - return nvencProbeResult{reason: "h264_videotoolbox smoke encode failed: " + probeFailure(err, output)} + for _, smoke := range []struct { + encoder string + quality string + }{ + {"h264_videotoolbox", "65"}, + {"hevc_videotoolbox", "60"}, + } { + if output, err := runFFmpegProbe(ffmpegPath, + "-hide_banner", + "-loglevel", "error", + "-f", "lavfi", + "-i", "testsrc2=size=640x360:rate=1", + "-frames:v", "1", + "-an", + "-c:v", smoke.encoder, + "-q:v", smoke.quality, + "-f", "null", + "-", + ); err != nil { + return nvencProbeResult{reason: smoke.encoder + " smoke encode failed: " + probeFailure(err, output)} + } } return nvencProbeResult{available: true} diff --git a/internal/playback/gpudetect_test.go b/internal/playback/gpudetect_test.go index 88dee02f..f3c8a596 100644 --- a/internal/playback/gpudetect_test.go +++ b/internal/playback/gpudetect_test.go @@ -376,6 +376,38 @@ func TestResolveHWAccelWithFFmpegDarwinRequiresBothVTEncoders(t *testing.T) { } } +func TestResolveHWAccelWithFFmpegDarwinFallsBackToNoneWhenSmokeEncodeFails(t *testing.T) { + setupHWAccelTest(t) + currentGOOS = "darwin" + ffmpeg := writeFakeFFmpeg(t, fakeFFmpegProbe{videotoolbox: true, h264VT: true, hevcVT: true}) + + if got := ResolveHWAccelWithFFmpeg("auto", ffmpeg.path); got != "none" { + t.Fatalf("ResolveHWAccelWithFFmpeg() = %q, want none when smoke encode fails", got) + } +} + +func TestVideoToolboxProbeSmokesBothEncodersInConstantQualityMode(t *testing.T) { + setupHWAccelTest(t) + currentGOOS = "darwin" + ffmpeg := writeFakeFFmpeg(t, successfulVideoToolboxProbe()) + + if got := ResolveHWAccelWithFFmpeg("auto", ffmpeg.path); got != "videotoolbox" { + t.Fatalf("ResolveHWAccelWithFFmpeg() = %q, want videotoolbox", got) + } + log, err := os.ReadFile(ffmpeg.logPath) + if err != nil { + t.Fatalf("read probe log: %v", err) + } + // The smoke encodes must exercise the exact uncapped (-q:v) mode the + // transcode arg builder emits — Intel Macs fail that mode at session + // creation, so probing without it would approve unrunnable commands. + for _, want := range []string{"-c:v h264_videotoolbox -q:v 65", "-c:v hevc_videotoolbox -q:v 60"} { + if !strings.Contains(string(log), want) { + t.Fatalf("probe log missing %q:\n%s", want, log) + } + } +} + func TestExplicitVideoToolboxBypassesFFmpegProbe(t *testing.T) { setupHWAccelTest(t)