feat: add CUDA hardware acceleration with ffprobe codec detection

- Detect video codec using ffprobe instead of file extensions
- Auto-enable CUDA hwaccel for supported codecs when FFMPEG_PREFER_HARDWARE=true
- Skip probing for image formats to optimize performance
- Add comprehensive tests for hardware acceleration logic
This commit is contained in:
rob_otman 2025-12-17 14:53:20 -07:00
parent 6cca431a76
commit f2504519f0
3 changed files with 210 additions and 21 deletions

View file

@ -11,6 +11,33 @@ function mockExecFile(
calls.push(args);
if (args.includes("fail.mov")) {
callback(new Error("mock failure"), "", "Fake stderr: fail");
} else if (_cmd === "ffprobe") {
// Mock ffprobe responses for codec detection
// Return H.264 codec for .mp4 files, no video stream for images
if (args.includes("in.mp4") || args.includes("in.mkv") || args.includes("in.avi")) {
callback(null, JSON.stringify({
streams: [{
codec_type: "video",
codec_name: "h264",
}],
}), "");
} else if (args.includes("in.jpg") || args.includes("in.png")) {
// Image files have no video stream
callback(null, JSON.stringify({
streams: [{
codec_type: "audio",
codec_name: "pcm",
}],
}), "");
} else {
// Default: assume H.264 for video files
callback(null, JSON.stringify({
streams: [{
codec_type: "video",
codec_name: "h264",
}],
}), "");
}
} else {
callback(null, "Fake stdout", "");
}
@ -135,7 +162,8 @@ test("uses h264_nvenc for h264.mp4 when hardware preferred", async () => {
console.log = originalConsoleLog;
expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "h264_nvenc"]));
// calls[0] is ffprobe, calls[1] is ffmpeg
expect(calls[1]).toEqual(expect.arrayContaining(["-c:v", "h264_nvenc"]));
expect(loggedMessage).toBe("stdout: Fake stdout");
delete process.env.FFMPEG_PREFER_HARDWARE;
@ -155,7 +183,8 @@ test("uses hevc_nvenc for h265.mp4 when hardware preferred", async () => {
console.log = originalConsoleLog;
expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "hevc_nvenc"]));
// calls[0] is ffprobe, calls[1] is ffmpeg
expect(calls[1]).toEqual(expect.arrayContaining(["-c:v", "hevc_nvenc"]));
expect(loggedMessage).toBe("stdout: Fake stdout");
delete process.env.FFMPEG_PREFER_HARDWARE;
@ -181,6 +210,73 @@ test("uses libx264 for h264.mp4 when hardware not preferred", async () => {
delete process.env.FFMPEG_PREFER_HARDWARE;
});
test("adds CUDA hwaccel for video input when hardware preferred", async () => {
process.env.FFMPEG_PREFER_HARDWARE = "true";
delete process.env.FFMPEG_ARGS;
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
await convert("in.mp4", "mp4", "avi", "out.avi", undefined, mockExecFile);
console.log = originalConsoleLog;
// calls[0] is ffprobe, calls[1] is ffmpeg
expect(calls[1]).toEqual(expect.arrayContaining(["-hwaccel", "cuda"]));
expect(loggedMessage).toBe("stdout: Fake stdout");
delete process.env.FFMPEG_PREFER_HARDWARE;
});
test("does not add CUDA hwaccel for image input when hardware preferred", async () => {
process.env.FFMPEG_PREFER_HARDWARE = "true";
delete process.env.FFMPEG_ARGS;
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
await convert("in.jpg", "jpg", "png", "out.png", undefined, mockExecFile);
console.log = originalConsoleLog;
expect(calls[0]).not.toEqual(expect.arrayContaining(["-hwaccel", "cuda"]));
expect(loggedMessage).toBe("stdout: Fake stdout");
delete process.env.FFMPEG_PREFER_HARDWARE;
});
test("does not add CUDA hwaccel if FFMPEG_ARGS already specifies hwaccel", async () => {
process.env.FFMPEG_PREFER_HARDWARE = "true";
process.env.FFMPEG_ARGS = "-hwaccel vaapi";
const originalConsoleLog = console.log;
let loggedMessage = "";
console.log = (msg) => {
loggedMessage = msg;
};
await convert("in.mp4", "mp4", "avi", "out.avi", undefined, mockExecFile);
console.log = originalConsoleLog;
// Should use vaapi from FFMPEG_ARGS, not add cuda
expect(calls[0]).toEqual(expect.arrayContaining(["-hwaccel", "vaapi"]));
expect(calls[0]).not.toEqual(expect.arrayContaining(["-hwaccel", "cuda"]));
expect(loggedMessage).toBe("stdout: Fake stdout");
delete process.env.FFMPEG_PREFER_HARDWARE;
delete process.env.FFMPEG_ARGS;
});
test("respects FFMPEG_ARGS", async () => {
process.env.FFMPEG_ARGS = "-hide_banner -y";