From 9d4ece9ec4905a07167253a498d98bc89a89d296 Mon Sep 17 00:00:00 2001 From: Abdelrahman Essawy Date: Sat, 13 Jun 2026 08:59:32 +0300 Subject: [PATCH 1/2] feat: add --compute flag for gpu encoding --- src/__tests__/ffmpeg-command.test.ts | 44 ++++++++++++++++++++++++++++ src/commands/ffmpeg.ts | 25 ++++++++++++++-- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/__tests__/ffmpeg-command.test.ts b/src/__tests__/ffmpeg-command.test.ts index bff3ca7..cc5357f 100644 --- a/src/__tests__/ffmpeg-command.test.ts +++ b/src/__tests__/ffmpeg-command.test.ts @@ -128,3 +128,47 @@ describe("ffmpeg command flow", () => { expect(commandString).toBe("ffmpeg -i https://cdn.rendobar.com/video.mp4 -c:v libx264 -preset fast -vf scale=1920:1080 output.mp4"); }); }); + +// ── --compute flag → submit params ───────────────────────────── +// +// Mirrors how `rb ffmpeg` constructs the job params object: `compute` is only +// included when the user passed a value, and that value is validated against +// auto|cpu|gpu before submission. + +type Compute = "auto" | "cpu" | "gpu"; + +function isCompute(value: string): value is Compute { + return value === "auto" || value === "cpu" || value === "gpu"; +} + +function buildParams(command: string, timeout: number, compute: Compute | null): Record { + return { command, timeout, ...(compute ? { compute } : {}) }; +} + +describe("ffmpeg --compute flag", () => { + it("includes compute in params when --compute gpu is passed", () => { + const compute = "gpu"; + expect(isCompute(compute)).toBe(true); + const params = buildParams("ffmpeg -i in.mp4 out.mp4", 120, compute); + expect(params.compute).toBe("gpu"); + }); + + it("accepts auto and cpu as valid compute modes", () => { + expect(isCompute("auto")).toBe(true); + expect(isCompute("cpu")).toBe(true); + expect(buildParams("ffmpeg -i in.mp4 out.mp4", 120, "cpu").compute).toBe("cpu"); + }); + + it("rejects an invalid compute value", () => { + expect(isCompute("turbo")).toBe(false); + expect(isCompute("GPU")).toBe(false); + expect(isCompute("")).toBe(false); + }); + + it("omits compute from params when no --compute flag is passed", () => { + const params = buildParams("ffmpeg -i in.mp4 out.mp4", 120, null); + expect("compute" in params).toBe(false); + expect(params.command).toBe("ffmpeg -i in.mp4 out.mp4"); + expect(params.timeout).toBe(120); + }); +}); diff --git a/src/commands/ffmpeg.ts b/src/commands/ffmpeg.ts index eac09b4..c0c111e 100644 --- a/src/commands/ffmpeg.ts +++ b/src/commands/ffmpeg.ts @@ -53,6 +53,13 @@ function localManifestPath(written: string[], manifestRemotePath: string): strin // ── Flags ────────────────────────────────────────────────────── +type Compute = "auto" | "cpu" | "gpu"; +const COMPUTE_MODES: readonly Compute[] = ["auto", "cpu", "gpu"]; + +function isCompute(value: string): value is Compute { + return (COMPUTE_MODES as readonly string[]).includes(value); +} + interface GlobalFlags { json: boolean; urlOnly: boolean; @@ -62,6 +69,7 @@ interface GlobalFlags { output: string | null; outputDir: string | null; timeout: number; + compute: Compute | null; } function extractGlobalFlags(): GlobalFlags { @@ -75,6 +83,7 @@ function extractGlobalFlags(): GlobalFlags { output: null, outputDir: null, timeout: 120, + compute: null, }; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; @@ -94,6 +103,14 @@ function extractGlobalFlags(): GlobalFlags { const val = parseInt(argv[i + 1]!, 10); if (!Number.isNaN(val) && val > 0) flags.timeout = Math.min(val, 900); i++; + } else if (arg === "--compute" && i + 1 < argv.length) { + const val = argv[i + 1]!; // Guarded by i + 1 < argv.length + if (!isCompute(val)) { + process.stderr.write(pc.red(` ✗ Invalid --compute value "${val}". Expected one of: auto, cpu, gpu.\n`)); + process.exit(2); + } + flags.compute = val; + i++; } } return flags; @@ -104,7 +121,7 @@ function extractFfmpegArgs(): string[] { const ffmpegIdx = argv.indexOf("ffmpeg"); if (ffmpegIdx === -1) return []; const globalFlags = new Set(["--json", "--url-only", "--quiet", "--no-wait", "--no-download"]); - const globalFlagsWithValue = new Set(["--timeout", "--output", "--output-dir"]); + const globalFlagsWithValue = new Set(["--timeout", "--output", "--output-dir", "--compute"]); const result: string[] = []; for (let i = ffmpegIdx + 1; i < argv.length; i++) { const arg = argv[i]!; @@ -135,6 +152,7 @@ ${pc.bold("Flags:")} --quiet No output, exit code only --no-wait Submit and exit immediately (prints job ID) --timeout N Max execution time in seconds (default: 120, max: 900) + --compute Run on cpu or gpu hardware (auto, cpu, gpu; gpu needs Pro) ${pc.dim("Outputs download to your folder by default — like running ffmpeg locally.")} ${pc.dim("Local files are auto-uploaded before job submission.")} @@ -224,7 +242,10 @@ export default defineCommand({ const job = await steps.step("Submitting", async () => { return client.jobs.create( - { type: "ffmpeg", params: { command, timeout: flags.timeout } }, + { + type: "ffmpeg", + params: { command, timeout: flags.timeout, ...(flags.compute ? { compute: flags.compute } : {}) }, + }, { signal: controller.signal }, ); }); From f43a902d5a3ae836b8db22b6050364eae43afe18 Mon Sep 17 00:00:00 2001 From: Abdelrahman Essawy Date: Mon, 22 Jun 2026 09:01:43 +0300 Subject: [PATCH 2/2] chore: rename executor to runner --- src/__tests__/progress.test.ts | 4 ++-- src/commands/ffmpeg.ts | 6 +++--- src/lib/progress.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/__tests__/progress.test.ts b/src/__tests__/progress.test.ts index 72c4f20..e89b05e 100644 --- a/src/__tests__/progress.test.ts +++ b/src/__tests__/progress.test.ts @@ -106,13 +106,13 @@ describe("buildResult — unified output", () => { it("surfaces a structured error (code + message + detail) on failure", () => { const r = buildResult("failed", undefined, { error: { - code: "PROVIDER_ERROR", + code: "RUNNER_ERROR", message: "Job failed", detail: "frame= 100\n[error] Conversion failed!", retryable: false, }, }); - expect(r.error?.code).toBe("PROVIDER_ERROR"); + expect(r.error?.code).toBe("RUNNER_ERROR"); expect(r.error?.message).toBe("Job failed"); expect(r.error?.detail).toContain("Conversion failed!"); expect(r.error?.retryable).toBe(false); diff --git a/src/commands/ffmpeg.ts b/src/commands/ffmpeg.ts index c0c111e..69e14b1 100644 --- a/src/commands/ffmpeg.ts +++ b/src/commands/ffmpeg.ts @@ -156,7 +156,7 @@ ${pc.bold("Flags:")} ${pc.dim("Outputs download to your folder by default — like running ffmpeg locally.")} ${pc.dim("Local files are auto-uploaded before job submission.")} -${pc.dim("All FFmpeg flags are passed through to the cloud executor.")} +${pc.dim("All FFmpeg flags are passed through to the cloud runner.")} `); } @@ -259,7 +259,7 @@ export default defineCommand({ } // ── 3. Wait for cloud execution ────────────────────── - // Phase 1: "Queued" spinner until job.context arrives (executor started) + // Phase 1: "Queued" spinner until job.context arrives (runner started) // Phase 2: "Executing" spinner with machine specs until completion // Final: replace spinner with server-timed "Executed" line let machine: MachineContext | undefined; @@ -276,7 +276,7 @@ export default defineCommand({ signal: controller.signal, onContext(ctx) { machine = ctx; - // job.context = executor started = queue phase over + // job.context = runner started = queue phase over // Print "Queued ✓" with elapsed time, start "Executing" spinner const queuedElapsed = Date.now() - queuedStart; steps.stopSpinnerRaw(); diff --git a/src/lib/progress.ts b/src/lib/progress.ts index be74d38..ee4ce55 100644 --- a/src/lib/progress.ts +++ b/src/lib/progress.ts @@ -24,7 +24,7 @@ export interface ProgressResult { duration: number; /** Created → Dispatched (API processing + queue dispatch) */ dispatchMs: number; - /** Dispatched → Started (waiting for executor machine) */ + /** Dispatched → Started (waiting for runner machine) */ queueMs: number; /** Started → Completed (actual execution) */ execMs: number; @@ -253,7 +253,7 @@ export function buildResult(status: string, machine: MachineContext | undefined, // Dispatch time: Created → Dispatched (API processing + queue dispatch) const dispatchMs = dispatchedAt && createdAt ? dispatchedAt - createdAt : 0; - // Queue time: Dispatched → Started (waiting for executor machine) + // Queue time: Dispatched → Started (waiting for runner machine) const queueMs = startedAt && dispatchedAt ? startedAt - dispatchedAt : 0; // Execution time: Started → Completed (FFmpeg running) const execMs = completedAt && startedAt ? completedAt - startedAt : 0;