-
-
Notifications
You must be signed in to change notification settings - Fork 869
Add nvidia cuda hardware accelerated decoding/encoding support #480
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 9 commits
6cca431
f250451
db3acff
0093109
53d576d
947be70
7119ff3
d141551
572bac9
c9b43e1
5ec86d6
f71d555
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -688,6 +688,141 @@ export const properties = { | |
| }, | ||
| }; | ||
|
|
||
| // CUDA-supported codec names (as detected by ffprobe) | ||
| const cudaSupportedCodecs = new Set(["h264", "hevc", "vp9", "vp8", "mpeg2video", "mpeg4", "av1"]); | ||
|
|
||
| // Known image formats that should skip ffprobe (no video codec to detect) | ||
| const imageFormats = new Set([ | ||
| "jpg", | ||
| "jpeg", | ||
| "png", | ||
| "gif", | ||
| "bmp", | ||
| "webp", | ||
| "ico", | ||
| "tiff", | ||
| "tif", | ||
| "svg", | ||
| "avif", | ||
| "jxl", | ||
| "heic", | ||
| "heif", | ||
| "raw", | ||
| "cr2", | ||
| "nef", | ||
| "orf", | ||
| "sr2", | ||
| "arw", | ||
| "dng", | ||
| "psd", | ||
| "xcf", | ||
| "exr", | ||
| "hdr", | ||
| ]); | ||
|
|
||
| // Cache NVIDIA GPU availability to avoid repeated checks | ||
| let nvidiaGpuAvailable: boolean | null = null; | ||
|
|
||
| // Export for testing (allows resetting cache between tests) | ||
| export const resetNvidiaGpuCache = () => { | ||
| nvidiaGpuAvailable = null; | ||
| }; | ||
|
|
||
| /** | ||
| * Checks if an NVIDIA GPU is available using nvidia-smi. | ||
| * Returns false if no GPU is available or nvidia-smi fails. | ||
| */ | ||
| async function checkNvidiaGpuAvailable(execFile: ExecFileFn = execFileOriginal): Promise<boolean> { | ||
| // Cache the result to avoid repeated checks | ||
| if (nvidiaGpuAvailable !== null) { | ||
| return nvidiaGpuAvailable; | ||
| } | ||
|
|
||
| try { | ||
| await new Promise<void>((resolve, reject) => { | ||
| execFile( | ||
| "nvidia-smi", | ||
| ["-L"], // List GPUs (simple check that succeeds if GPU is available) | ||
| (error) => { | ||
| if (error) { | ||
| reject(error); | ||
| return; | ||
| } | ||
| resolve(); | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| // If nvidia-smi succeeds, GPU is available | ||
| nvidiaGpuAvailable = true; | ||
| console.log("NVIDIA GPU detected - hardware acceleration available"); | ||
| return true; | ||
| } catch (error) { | ||
| // nvidia-smi failed - no GPU available or not installed | ||
| console.warn("NVIDIA GPU not available - using software encoding/decoding:", error); | ||
| nvidiaGpuAvailable = false; | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Uses ffprobe to detect if the video codec in a file is supported by CUDA hardware acceleration. | ||
| * Returns false for image formats without probing (performance optimization). | ||
| * Falls back to false if probing fails (safe default). | ||
| */ | ||
| async function isCudaSupportedCodec( | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Couldn't we have a list of all formats that potentially supports instead. It should only be all video container formats right?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried that at first but file types like .mkv can contain virtually any codec and .mp4 can contain H.264, H.265, MPEG-2, AV1 etc. so many formats we can not know without ffprobe if has a cuda supported codec.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes and then instead of skipping all image formats we skip everything that isn't a container. And we use ffprobe on mkv, mp4 etc.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think my current approach is preferable because it only skips definitively non-video formats (images) and lets ffprobe handle codec detection for all video files. File extensions can be changed/wrong but ffprobe reading the file headers is very accurate assurance that the file is supported. I'm open to change to your approach if you want, this is just my thinking.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But FFmpeg support almost 500 file extensions and of them only a handful is a container format. I don't mean that we should remove FFprobe just changing it from an image blacklist to a container whitelist :) |
||
| filePath: string, | ||
| fileType: string, | ||
| execFile: ExecFileFn = execFileOriginal, | ||
| ): Promise<boolean> { | ||
| // Skip ffprobe for known image formats (no video codec to detect) | ||
| if (imageFormats.has(fileType.toLowerCase())) { | ||
| console.log(`Skipping CUDA detection for image format: ${fileType}`); | ||
| return false; | ||
| } | ||
|
|
||
| try { | ||
| // Wrap execFile callback in a Promise for async/await | ||
| const stdout = await new Promise<string>((resolve, reject) => { | ||
| execFile( | ||
| "ffprobe", | ||
| ["-v", "quiet", "-print_format", "json", "-show_streams", filePath], | ||
| (error, stdout) => { | ||
| if (error) { | ||
| reject(error); | ||
| return; | ||
| } | ||
| resolve(stdout); | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| const probeData = JSON.parse(stdout); | ||
| const videoStream = probeData.streams?.find( | ||
| (s: { codec_type?: string }) => s.codec_type === "video", | ||
| ); | ||
|
|
||
| if (!videoStream || !videoStream.codec_name) { | ||
| return false; | ||
| } | ||
|
|
||
| const codecName = videoStream.codec_name.toLowerCase(); | ||
| const isSupported = cudaSupportedCodecs.has(codecName); | ||
| if (isSupported) { | ||
| console.log(`CUDA-supported codec detected: ${codecName} in ${filePath}`); | ||
| } else { | ||
| console.log( | ||
| `Codec not CUDA-supported: ${codecName} in ${filePath} - using software decoding`, | ||
| ); | ||
| } | ||
| return isSupported; | ||
| } catch (error) { | ||
| // If probing fails, fall back to conservative approach (no CUDA) | ||
| console.warn(`Failed to probe codec for ${filePath}:`, error); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| export async function convert( | ||
| filePath: string, | ||
| fileType: string, | ||
|
|
@@ -699,6 +834,21 @@ export async function convert( | |
| let extraArgs: string[] = []; | ||
| let message = "Done"; | ||
|
|
||
| // Check if hardware encoding is preferred (NVENC, VAAPI, etc.) | ||
| const preferHardware = | ||
| process.env.FFMPEG_PREFER_HARDWARE === "true" || process.env.FFMPEG_PREFER_HARDWARE === "1"; | ||
|
|
||
| // Check GPU availability if hardware is preferred | ||
| const gpuAvailable = preferHardware ? await checkNvidiaGpuAvailable(execFile) : false; | ||
|
|
||
| if (preferHardware && gpuAvailable) { | ||
| console.log("Hardware acceleration enabled for conversion"); | ||
| } else if (preferHardware && !gpuAvailable) { | ||
| console.log("Hardware acceleration requested but GPU not available - using software"); | ||
| } else { | ||
| console.log("Using software encoding/decoding (hardware not preferred)"); | ||
| } | ||
|
|
||
| if (convertTo === "ico") { | ||
| // Make sure image is 256x256 or smaller | ||
| extraArgs = [ | ||
|
|
@@ -718,10 +868,18 @@ export async function convert( | |
| extraArgs.push("-c:v", "libaom-av1"); | ||
| break; | ||
| case "h264": | ||
| extraArgs.push("-c:v", "libx264"); | ||
| if (preferHardware && gpuAvailable) { | ||
| extraArgs.push("-c:v", "h264_nvenc"); | ||
| } else { | ||
| extraArgs.push("-c:v", "libx264"); | ||
| } | ||
| break; | ||
| case "h265": | ||
| extraArgs.push("-c:v", "libx265"); | ||
| if (preferHardware && gpuAvailable) { | ||
| extraArgs.push("-c:v", "hevc_nvenc"); | ||
| } else { | ||
| extraArgs.push("-c:v", "libx265"); | ||
| } | ||
| break; | ||
| case "h266": | ||
| extraArgs.push("-c:v", "libx266"); | ||
|
|
@@ -731,6 +889,21 @@ export async function convert( | |
|
|
||
| // Parse FFMPEG_ARGS environment variable into array | ||
| const ffmpegArgs = process.env.FFMPEG_ARGS ? process.env.FFMPEG_ARGS.split(/\s+/) : []; | ||
|
|
||
| // If hardware is preferred, check if the codec supports CUDA hardware acceleration | ||
| // This only applies if FFMPEG_ARGS doesn't already specify a hardware accelerator | ||
| const hasHardwareAccel = ffmpegArgs.includes("-hwaccel"); | ||
|
|
||
| if (preferHardware && gpuAvailable && !hasHardwareAccel) { | ||
| const supportsCuda = await isCudaSupportedCodec(filePath, fileType, execFile); | ||
| if (supportsCuda) { | ||
| ffmpegArgs.push("-hwaccel", "cuda"); | ||
| console.log("Added CUDA hardware acceleration for input decoding"); | ||
| } else { | ||
| console.log("CUDA not supported for input file - using software decoding"); | ||
| } | ||
| } | ||
|
|
||
| const ffmpegOutputArgs = process.env.FFMPEG_OUTPUT_ARGS | ||
| ? process.env.FFMPEG_OUTPUT_ARGS.split(/\s+/) | ||
| : []; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it would be better to add this under a separate header? I want to keep the example compose as general as possible to make it look easy. What do you think?