Merge f71d5558c7 into 393441faa1
This commit is contained in:
commit
adccfed81b
3 changed files with 420 additions and 17 deletions
68
README.md
68
README.md
|
|
@ -79,6 +79,45 @@ or
|
|||
docker run -p 3000:3000 -v ./data:/app/data ghcr.io/c4illin/convertx
|
||||
```
|
||||
|
||||
### NVIDIA GPU Hardware Acceleration
|
||||
|
||||
For improved performance on NVIDIA GPUs with NVENC/NVDEC support, use this enhanced docker-compose configuration:
|
||||
|
||||
```yml
|
||||
# docker-compose.yml (with NVIDIA GPU support)
|
||||
services:
|
||||
convertx:
|
||||
image: ghcr.io/c4illin/convertx
|
||||
container_name: convertx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
runtime: nvidia
|
||||
group_add:
|
||||
- 226 # The render group GID on the host (check with: getent group render | cut -d: -f3)
|
||||
environment:
|
||||
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234
|
||||
# - HTTP_ALLOWED=true # uncomment this if accessing it over a non-https connection
|
||||
- FFMPEG_PREFER_HARDWARE=true # Enable hardware acceleration for video encoding/decoding
|
||||
- NVIDIA_VISIBLE_DEVICES=all # Use 'all' for all GPUs, or '0,1' for specific GPUs (get IDs with: nvidia-smi -L)
|
||||
- NVIDIA_DRIVER_CAPABILITIES=all # Comma-separated list (e.g., 'compute,video,utility'), 'all' for everything
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- /usr/bin/nvidia-smi:/usr/bin/nvidia-smi:ro # Mount nvidia-smi for GPU detection
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
|
||||
- NVIDIA drivers with NVENC/NVDEC support
|
||||
- nvidia-docker runtime
|
||||
- The render group GID (226) may differ on your system
|
||||
|
||||
**Notes:**
|
||||
|
||||
- `group_add` may not be needed in Unraid
|
||||
- `nvidia-smi` volume mount may not be needed in Unraid
|
||||
- Hardware acceleration requires: `FFMPEG_PREFER_HARDWARE=true`, NVIDIA GPU detection, and supported video codecs (H.264, H.265, VP9, VP8, MPEG-2, MPEG-4, AV1)
|
||||
|
||||
Then visit `http://localhost:3000` in your browser and create your account. Don't leave it unconfigured and open, as anyone can register the first account.
|
||||
|
||||
If you get unable to open database file run `chown -R $USER:$USER path` on the path you choose.
|
||||
|
|
@ -87,20 +126,21 @@ If you get unable to open database file run `chown -R $USER:$USER path` on the p
|
|||
|
||||
All are optional, JWT_SECRET is recommended to be set.
|
||||
|
||||
| Name | Default | Description |
|
||||
| ---------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| JWT_SECRET | when unset it will use the value from randomUUID() | A long and secret string used to sign the JSON Web Token |
|
||||
| ACCOUNT_REGISTRATION | false | Allow users to register accounts |
|
||||
| HTTP_ALLOWED | false | Allow HTTP connections, only set this to true locally |
|
||||
| ALLOW_UNAUTHENTICATED | false | Allow unauthenticated users to use the service, only set this to true locally |
|
||||
| AUTO_DELETE_EVERY_N_HOURS | 24 | Checks every n hours for files older then n hours and deletes them, set to 0 to disable |
|
||||
| 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` |
|
||||
| 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 |
|
||||
| MAX_CONVERT_PROCESS | 0 | Maximum number of concurrent conversion processes allowed. Set to 0 for unlimited. |
|
||||
| Name | Default | Description |
|
||||
| ---------------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| JWT_SECRET | when unset it will use the value from randomUUID() | A long and secret string used to sign the JSON Web Token |
|
||||
| ACCOUNT_REGISTRATION | false | Allow users to register accounts |
|
||||
| HTTP_ALLOWED | false | Allow HTTP connections, only set this to true locally |
|
||||
| ALLOW_UNAUTHENTICATED | false | Allow unauthenticated users to use the service, only set this to true locally |
|
||||
| AUTO_DELETE_EVERY_N_HOURS | 24 | Checks every n hours for files older then n hours and deletes them, set to 0 to disable |
|
||||
| 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. Also enables CUDA hardware acceleration for video input decoding (not applied to image 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 |
|
||||
| MAX_CONVERT_PROCESS | 0 | Maximum number of concurrent conversion processes allowed. Set to 0 for unlimited. |
|
||||
|
||||
### Docker images
|
||||
|
||||
|
|
|
|||
|
|
@ -688,6 +688,144 @@ export const properties = {
|
|||
},
|
||||
};
|
||||
|
||||
// CUDA-supported codec names (as detected by ffprobe)
|
||||
const cudaSupportedCodecs = new Set(["h264", "hevc", "vp9", "vp8", "mpeg2video", "mpeg4", "av1"]);
|
||||
|
||||
// Container formats that can contain video streams with unknown codecs (run ffprobe)
|
||||
const containerFormats = new Set([
|
||||
// Most common/popular video containers
|
||||
"avi",
|
||||
"mkv",
|
||||
"mov",
|
||||
"mp4",
|
||||
"m4v", // MPEG-4 video
|
||||
"m4a", // MPEG-4 audio
|
||||
"webm",
|
||||
"flv",
|
||||
"wmv",
|
||||
"vob", // DVD video
|
||||
// MPEG transport streams
|
||||
"m2ts",
|
||||
"ts",
|
||||
"mts",
|
||||
// MPEG containers
|
||||
"mpeg",
|
||||
"mpg",
|
||||
"m2v", // MPEG-2 video
|
||||
// Mobile/legacy formats
|
||||
"3gp",
|
||||
"3g2",
|
||||
// Other containers
|
||||
"nut",
|
||||
"ogv", // OGG video
|
||||
"asf",
|
||||
"mxf", // Professional video
|
||||
"f4v", // Flash video
|
||||
]);
|
||||
|
||||
// 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.
|
||||
* Only probes container formats that can contain video streams with unknown codecs.
|
||||
* Falls back to false if probing fails (safe default).
|
||||
*/
|
||||
async function isCudaSupportedCodec(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
execFile: ExecFileFn = execFileOriginal,
|
||||
): Promise<boolean> {
|
||||
// Only run ffprobe on container formats that can contain video streams
|
||||
if (!containerFormats.has(fileType.toLowerCase())) {
|
||||
console.log(`Skipping CUDA detection for non-container 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 +837,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 +871,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 +892,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+/)
|
||||
: [];
|
||||
|
|
|
|||
|
|
@ -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,58 @@ 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
|
||||
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", "");
|
||||
}
|
||||
|
|
@ -19,6 +71,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 () => {
|
||||
|
|
@ -121,6 +176,138 @@ 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;
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
||||
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("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 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;
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
// calls[0] is nvidia-smi, calls[1] is ffprobe, calls[2] is ffmpeg
|
||||
expect(calls[2]).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;
|
||||
|
||||
// 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[1]).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";
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue