FFmpeg: Fix duplicate hwupload in Vulkan transcoding command #5631

encode.FormatNV12 already ends in hwupload, so appending it again produced
"format=nv12,hwupload,hwupload", which fails because the frames are already on
the GPU. Build the filter from FormatNV12 directly to upload exactly once, and
add a gated Vulkan case to the transcode command tests.
This commit is contained in:
Michael Mayer 2026-05-30 20:14:14 +00:00
parent c5b5362649
commit 5242964330
3 changed files with 32 additions and 4 deletions

View file

@ -175,6 +175,30 @@ func TestTranscodeCmd(t *testing.T) {
RunCommandTest(t, encode.NvidiaAvc, srcName, destName, cmd, true)
}
})
t.Run("Vulkan", func(t *testing.T) {
opt := encode.NewVideoOptions(ffmpegBin, encode.VulkanAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "")
srcName := fs.Abs("./testdata/25fps.vp9")
destName := fs.Abs("./testdata/25fps.vulkan.avc")
cmd, _, err := TranscodeCmd(srcName, destName, opt)
if err != nil {
t.Fatal(err)
}
cmdStr := cmd.String()
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -init_hw_device vulkan=vk -filter_hw_device vk -i SRC -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=nv12,hwupload -c:v h264_vulkan -map 0:v:0 -map 0:a:0? -ignore_unknown -qp 25 -f mp4 -movflags use_metadata_tags+faststart -map_metadata 0 DEST", cmdStr)
// This transcoding test requires a Vulkan device that advertises the video encode
// extensions (VK_KHR_video_encode_queue/h264); select it with PHOTOPRISM_FFMPEG_DEVICE if needed:
if os.Getenv("PHOTOPRISM_FFMPEG_TEST_ENCODER") == "vulkan" {
RunCommandTest(t, encode.VulkanAvc, srcName, destName, cmd, true)
}
})
t.Run("Apple", func(t *testing.T) {
opt := encode.NewVideoOptions("", encode.AppleAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "")
r, _, err := TranscodeCmd("VID123.mov", "VID123.mov.avc", opt)

View file

@ -15,8 +15,10 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
hwDevice = "vulkan=vk:" + opt.Device
}
// Scale + format conversion happens in system memory, then hwupload moves frames into the Vulkan device.
videoFilter := opt.VideoFilter(encode.FormatNV12) + ",hwupload"
// Scale and NV12 conversion happen in system memory, then hwupload moves the frames onto the
// Vulkan device referenced by -filter_hw_device. The hwupload step is already part of
// encode.FormatNV12, so it must not be appended a second time.
videoFilter := opt.VideoFilter(encode.FormatNV12)
// #nosec G204 -- command arguments are built from validated options and paths.
return exec.Command(

View file

@ -15,8 +15,8 @@ func TestVulkan_TranscodeToAvcCmd_WithDevice(t *testing.T) {
s := cmd.String()
assert.True(t, strings.Contains(s, "-init_hw_device vulkan=vk:0"))
assert.True(t, strings.Contains(s, "-filter_hw_device vk"))
assert.True(t, strings.Contains(s, "format=nv12"))
assert.True(t, strings.Contains(s, "hwupload"))
assert.True(t, strings.Contains(s, "format=nv12,hwupload"))
assert.False(t, strings.Contains(s, "hwupload,hwupload"))
assert.True(t, strings.Contains(s, "-c:v h264_vulkan"))
assert.True(t, strings.Contains(s, "-qp 25"))
assert.True(t, strings.Contains(s, "-f mp4"))
@ -29,5 +29,7 @@ func TestVulkan_TranscodeToAvcCmd_NoDevice(t *testing.T) {
assert.True(t, strings.Contains(s, "-init_hw_device vulkan=vk "))
assert.False(t, strings.Contains(s, "vulkan=vk:"))
assert.True(t, strings.Contains(s, "-filter_hw_device vk"))
assert.True(t, strings.Contains(s, "format=nv12,hwupload"))
assert.False(t, strings.Contains(s, "hwupload,hwupload"))
assert.True(t, strings.Contains(s, "-c:v h264_vulkan"))
}