diff --git a/src/converters/ffmpeg.ts b/src/converters/ffmpeg.ts index 8edf7f1..a33acc0 100644 --- a/src/converters/ffmpeg.ts +++ b/src/converters/ffmpeg.ts @@ -719,6 +719,51 @@ const imageFormats = new Set([ "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 { + // Cache the result to avoid repeated checks + if (nvidiaGpuAvailable !== null) { + return nvidiaGpuAvailable; + } + + try { + await new Promise((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). @@ -731,6 +776,7 @@ async function isCudaSupportedCodec( ): Promise { // 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; } @@ -760,7 +806,15 @@ async function isCudaSupportedCodec( } const codecName = videoStream.codec_name.toLowerCase(); - return cudaSupportedCodecs.has(codecName); + 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); @@ -783,6 +837,17 @@ export async function convert( 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 = [ @@ -832,6 +897,9 @@ export async function convert( 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"); } } diff --git a/tests/converters/ffmpeg.test.ts b/tests/converters/ffmpeg.test.ts index 73ced1b..f4aac26 100644 --- a/tests/converters/ffmpeg.test.ts +++ b/tests/converters/ffmpeg.test.ts @@ -1,5 +1,5 @@ import { beforeEach, expect, test } from "bun:test"; -import { convert } from "../../src/converters/ffmpeg"; +import { convert, resetNvidiaGpuCache } from "../../src/converters/ffmpeg"; let calls: string[][] = []; @@ -11,6 +11,9 @@ function mockExecFile( calls.push(args); if (args.includes("fail.mov")) { callback(new Error("mock failure"), "", "Fake stderr: fail"); + } else if (_cmd === "nvidia-smi") { + // Mock nvidia-smi - assume GPU is available for tests + callback(null, "GPU 0: NVIDIA GeForce RTX 3080 (UUID: GPU-12345678-1234-1234-1234-123456789012)\n", ""); } else if (_cmd === "ffprobe") { // Mock ffprobe responses for codec detection // Return H.264 codec for .mp4 files, no video stream for images @@ -46,6 +49,9 @@ function mockExecFile( beforeEach(() => { calls = []; delete process.env.FFMPEG_ARGS; + delete process.env.FFMPEG_PREFER_HARDWARE; + // Reset the GPU availability cache between tests + resetNvidiaGpuCache(); }); test("converts a normal file", async () => { @@ -162,8 +168,8 @@ test("uses h264_nvenc for h264.mp4 when hardware preferred", async () => { console.log = originalConsoleLog; - // calls[0] is ffprobe, calls[1] is ffmpeg - expect(calls[1]).toEqual(expect.arrayContaining(["-c:v", "h264_nvenc"])); + // calls[0] is nvidia-smi, calls[1] is ffprobe, calls[2] is ffmpeg + expect(calls[2]).toEqual(expect.arrayContaining(["-c:v", "h264_nvenc"])); expect(loggedMessage).toBe("stdout: Fake stdout"); delete process.env.FFMPEG_PREFER_HARDWARE; @@ -183,8 +189,8 @@ test("uses hevc_nvenc for h265.mp4 when hardware preferred", async () => { console.log = originalConsoleLog; - // calls[0] is ffprobe, calls[1] is ffmpeg - expect(calls[1]).toEqual(expect.arrayContaining(["-c:v", "hevc_nvenc"])); + // calls[0] is nvidia-smi, calls[1] is ffprobe, calls[2] is ffmpeg + expect(calls[2]).toEqual(expect.arrayContaining(["-c:v", "hevc_nvenc"])); expect(loggedMessage).toBe("stdout: Fake stdout"); delete process.env.FFMPEG_PREFER_HARDWARE; @@ -225,8 +231,8 @@ test("adds CUDA hwaccel for video input when hardware preferred", async () => { console.log = originalConsoleLog; - // calls[0] is ffprobe, calls[1] is ffmpeg - expect(calls[1]).toEqual(expect.arrayContaining(["-hwaccel", "cuda"])); + // calls[0] is nvidia-smi, calls[1] is ffprobe, calls[2] is ffmpeg + expect(calls[2]).toEqual(expect.arrayContaining(["-hwaccel", "cuda"])); expect(loggedMessage).toBe("stdout: Fake stdout"); delete process.env.FFMPEG_PREFER_HARDWARE; @@ -268,8 +274,10 @@ test("does not add CUDA hwaccel if FFMPEG_ARGS already specifies hwaccel", async console.log = originalConsoleLog; + // When FFMPEG_ARGS already has hwaccel, no ffprobe call is made + // calls[0] is nvidia-smi, calls[1] is ffmpeg // Should use vaapi from FFMPEG_ARGS, not add cuda - expect(calls[0]).toEqual(expect.arrayContaining(["-hwaccel", "vaapi"])); + expect(calls[1]).toEqual(expect.arrayContaining(["-hwaccel", "vaapi"])); expect(calls[0]).not.toEqual(expect.arrayContaining(["-hwaccel", "cuda"])); expect(loggedMessage).toBe("stdout: Fake stdout");