From 6cca431a76a4a4ccf558ef4ade462e53983e3760 Mon Sep 17 00:00:00 2001 From: rob_otman <11605437+Rob-Otman@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:12:13 -0700 Subject: [PATCH] feat: add NVENC hardware encoding support for FFmpeg Add FFMPEG_PREFER_HARDWARE env var to enable hardware acceleration Use h264_nvenc and hevc_nvenc encoders when hardware preferred Fall back to software encoders (libx264/libx265) when disabled Add comprehensive tests for hardware/software encoding modes Update README with new environment variable documentation This enables GPU-accelerated video encoding for better performance on systems with NVIDIA GPUs. --- README.md | 1 + src/converters/ffmpeg.ts | 16 +++++++-- tests/converters/ffmpeg.test.ts | 60 +++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0d1fa9e..7a04fe9 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ All are optional, JWT_SECRET is recommended to be set. | WEBROOT | | The address to the root path setting this to "/convert" will serve the website on "example.com/convert/" | | FFMPEG_ARGS | | Arguments to pass to the input file of ffmpeg, e.g. `-hwaccel vaapi`. See https://github.com/C4illin/ConvertX/issues/190 for more info about hw-acceleration. | | FFMPEG_OUTPUT_ARGS | | Arguments to pass to the output of ffmpeg, e.g. `-preset veryfast` | +| FFMPEG_PREFER_HARDWARE | false | Use hardware encoders (NVENC, VAAPI, etc.) when available instead of software encoders for h264/h265 formats | | HIDE_HISTORY | false | Hide the history page | | LANGUAGE | en | Language to format date strings in, specified as a [BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) | | UNAUTHENTICATED_USER_SHARING | false | Shares conversion history between all unauthenticated users | diff --git a/src/converters/ffmpeg.ts b/src/converters/ffmpeg.ts index 8418e2d..454c633 100644 --- a/src/converters/ffmpeg.ts +++ b/src/converters/ffmpeg.ts @@ -712,15 +712,27 @@ export async function convert( const split = convertTo.split("."); const codec_short = split[0]; + // Check if hardware encoding is preferred (NVENC, VAAPI, etc.) + const preferHardware = process.env.FFMPEG_PREFER_HARDWARE === "true" || + process.env.FFMPEG_PREFER_HARDWARE === "1"; + switch (codec_short) { case "av1": extraArgs.push("-c:v", "libaom-av1"); break; case "h264": - extraArgs.push("-c:v", "libx264"); + if (preferHardware) { + extraArgs.push("-c:v", "h264_nvenc"); + } else { + extraArgs.push("-c:v", "libx264"); + } break; case "h265": - extraArgs.push("-c:v", "libx265"); + if (preferHardware) { + extraArgs.push("-c:v", "hevc_nvenc"); + } else { + extraArgs.push("-c:v", "libx265"); + } break; case "h266": extraArgs.push("-c:v", "libx266"); diff --git a/tests/converters/ffmpeg.test.ts b/tests/converters/ffmpeg.test.ts index 6f0afc6..e190892 100644 --- a/tests/converters/ffmpeg.test.ts +++ b/tests/converters/ffmpeg.test.ts @@ -121,6 +121,66 @@ test("uses libx266 for h266.mp4", async () => { expect(loggedMessage).toBe("stdout: Fake stdout"); }); +test("uses h264_nvenc for h264.mp4 when hardware preferred", async () => { + process.env.FFMPEG_PREFER_HARDWARE = "true"; + + const originalConsoleLog = console.log; + + let loggedMessage = ""; + console.log = (msg) => { + loggedMessage = msg; + }; + + await convert("in.mkv", "mkv", "h264.mp4", "out.mp4", undefined, mockExecFile); + + console.log = originalConsoleLog; + + expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "h264_nvenc"])); + expect(loggedMessage).toBe("stdout: Fake stdout"); + + delete process.env.FFMPEG_PREFER_HARDWARE; +}); + +test("uses hevc_nvenc for h265.mp4 when hardware preferred", async () => { + process.env.FFMPEG_PREFER_HARDWARE = "true"; + + const originalConsoleLog = console.log; + + let loggedMessage = ""; + console.log = (msg) => { + loggedMessage = msg; + }; + + await convert("in.mkv", "mkv", "h265.mp4", "out.mp4", undefined, mockExecFile); + + console.log = originalConsoleLog; + + expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "hevc_nvenc"])); + expect(loggedMessage).toBe("stdout: Fake stdout"); + + delete process.env.FFMPEG_PREFER_HARDWARE; +}); + +test("uses libx264 for h264.mp4 when hardware not preferred", async () => { + process.env.FFMPEG_PREFER_HARDWARE = "false"; + + const originalConsoleLog = console.log; + + let loggedMessage = ""; + console.log = (msg) => { + loggedMessage = msg; + }; + + await convert("in.mkv", "mkv", "h264.mp4", "out.mp4", undefined, mockExecFile); + + console.log = originalConsoleLog; + + expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx264"])); + expect(loggedMessage).toBe("stdout: Fake stdout"); + + delete process.env.FFMPEG_PREFER_HARDWARE; +}); + test("respects FFMPEG_ARGS", async () => { process.env.FFMPEG_ARGS = "-hide_banner -y";