Skip to content

Commit c6cb3b6

Browse files
authored
feat: add --compute flag for gpu encoding (#57)
1 parent fa613a4 commit c6cb3b6

2 files changed

Lines changed: 67 additions & 2 deletions

File tree

src/__tests__/ffmpeg-command.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,47 @@ describe("ffmpeg command flow", () => {
128128
expect(commandString).toBe("ffmpeg -i https://cdn.rendobar.com/video.mp4 -c:v libx264 -preset fast -vf scale=1920:1080 output.mp4");
129129
});
130130
});
131+
132+
// ── --compute flag → submit params ─────────────────────────────
133+
//
134+
// Mirrors how `rb ffmpeg` constructs the job params object: `compute` is only
135+
// included when the user passed a value, and that value is validated against
136+
// auto|cpu|gpu before submission.
137+
138+
type Compute = "auto" | "cpu" | "gpu";
139+
140+
function isCompute(value: string): value is Compute {
141+
return value === "auto" || value === "cpu" || value === "gpu";
142+
}
143+
144+
function buildParams(command: string, timeout: number, compute: Compute | null): Record<string, unknown> {
145+
return { command, timeout, ...(compute ? { compute } : {}) };
146+
}
147+
148+
describe("ffmpeg --compute flag", () => {
149+
it("includes compute in params when --compute gpu is passed", () => {
150+
const compute = "gpu";
151+
expect(isCompute(compute)).toBe(true);
152+
const params = buildParams("ffmpeg -i in.mp4 out.mp4", 120, compute);
153+
expect(params.compute).toBe("gpu");
154+
});
155+
156+
it("accepts auto and cpu as valid compute modes", () => {
157+
expect(isCompute("auto")).toBe(true);
158+
expect(isCompute("cpu")).toBe(true);
159+
expect(buildParams("ffmpeg -i in.mp4 out.mp4", 120, "cpu").compute).toBe("cpu");
160+
});
161+
162+
it("rejects an invalid compute value", () => {
163+
expect(isCompute("turbo")).toBe(false);
164+
expect(isCompute("GPU")).toBe(false);
165+
expect(isCompute("")).toBe(false);
166+
});
167+
168+
it("omits compute from params when no --compute flag is passed", () => {
169+
const params = buildParams("ffmpeg -i in.mp4 out.mp4", 120, null);
170+
expect("compute" in params).toBe(false);
171+
expect(params.command).toBe("ffmpeg -i in.mp4 out.mp4");
172+
expect(params.timeout).toBe(120);
173+
});
174+
});

src/commands/ffmpeg.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ function localManifestPath(written: string[], manifestRemotePath: string): strin
5353

5454
// ── Flags ──────────────────────────────────────────────────────
5555

56+
type Compute = "auto" | "cpu" | "gpu";
57+
const COMPUTE_MODES: readonly Compute[] = ["auto", "cpu", "gpu"];
58+
59+
function isCompute(value: string): value is Compute {
60+
return (COMPUTE_MODES as readonly string[]).includes(value);
61+
}
62+
5663
interface GlobalFlags {
5764
json: boolean;
5865
urlOnly: boolean;
@@ -62,6 +69,7 @@ interface GlobalFlags {
6269
output: string | null;
6370
outputDir: string | null;
6471
timeout: number;
72+
compute: Compute | null;
6573
}
6674

6775
function extractGlobalFlags(): GlobalFlags {
@@ -75,6 +83,7 @@ function extractGlobalFlags(): GlobalFlags {
7583
output: null,
7684
outputDir: null,
7785
timeout: 120,
86+
compute: null,
7887
};
7988
for (let i = 0; i < argv.length; i++) {
8089
const arg = argv[i];
@@ -94,6 +103,14 @@ function extractGlobalFlags(): GlobalFlags {
94103
const val = parseInt(argv[i + 1]!, 10);
95104
if (!Number.isNaN(val) && val > 0) flags.timeout = Math.min(val, 900);
96105
i++;
106+
} else if (arg === "--compute" && i + 1 < argv.length) {
107+
const val = argv[i + 1]!; // Guarded by i + 1 < argv.length
108+
if (!isCompute(val)) {
109+
process.stderr.write(pc.red(` ✗ Invalid --compute value "${val}". Expected one of: auto, cpu, gpu.\n`));
110+
process.exit(2);
111+
}
112+
flags.compute = val;
113+
i++;
97114
}
98115
}
99116
return flags;
@@ -104,7 +121,7 @@ function extractFfmpegArgs(): string[] {
104121
const ffmpegIdx = argv.indexOf("ffmpeg");
105122
if (ffmpegIdx === -1) return [];
106123
const globalFlags = new Set(["--json", "--url-only", "--quiet", "--no-wait", "--no-download"]);
107-
const globalFlagsWithValue = new Set(["--timeout", "--output", "--output-dir"]);
124+
const globalFlagsWithValue = new Set(["--timeout", "--output", "--output-dir", "--compute"]);
108125
const result: string[] = [];
109126
for (let i = ffmpegIdx + 1; i < argv.length; i++) {
110127
const arg = argv[i]!;
@@ -135,6 +152,7 @@ ${pc.bold("Flags:")}
135152
--quiet No output, exit code only
136153
--no-wait Submit and exit immediately (prints job ID)
137154
--timeout N Max execution time in seconds (default: 120, max: 900)
155+
--compute <mode> Run on cpu or gpu hardware (auto, cpu, gpu; gpu needs Pro)
138156
139157
${pc.dim("Outputs download to your folder by default — like running ffmpeg locally.")}
140158
${pc.dim("Local files are auto-uploaded before job submission.")}
@@ -224,7 +242,10 @@ export default defineCommand({
224242

225243
const job = await steps.step("Submitting", async () => {
226244
return client.jobs.create(
227-
{ type: "ffmpeg", params: { command, timeout: flags.timeout } },
245+
{
246+
type: "ffmpeg",
247+
params: { command, timeout: flags.timeout, ...(flags.compute ? { compute: flags.compute } : {}) },
248+
},
228249
{ signal: controller.signal },
229250
);
230251
});

0 commit comments

Comments
 (0)