@vibeo/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +173 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +101 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/preview.d.ts +5 -0
- package/dist/commands/preview.d.ts.map +1 -0
- package/dist/commands/preview.js +74 -0
- package/dist/commands/preview.js.map +1 -0
- package/dist/commands/render.d.ts +5 -0
- package/dist/commands/render.d.ts.map +1 -0
- package/dist/commands/render.js +187 -0
- package/dist/commands/render.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
- package/src/commands/create.ts +197 -0
- package/src/commands/list.ts +136 -0
- package/src/commands/preview.ts +88 -0
- package/src/commands/render.ts +206 -0
- package/src/index.ts +65 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { availableParallelism } from "node:os";
|
|
3
|
+
import { parseFrameRange } from "@vibeo/renderer";
|
|
4
|
+
import { renderComposition } from "@vibeo/renderer";
|
|
5
|
+
import type { Codec, ImageFormat, RenderProgress } from "@vibeo/renderer";
|
|
6
|
+
|
|
7
|
+
interface RenderArgs {
|
|
8
|
+
entry: string;
|
|
9
|
+
composition: string;
|
|
10
|
+
output: string | null;
|
|
11
|
+
fps: number | null;
|
|
12
|
+
frames: string | null;
|
|
13
|
+
codec: Codec;
|
|
14
|
+
concurrency: number;
|
|
15
|
+
imageFormat: ImageFormat;
|
|
16
|
+
quality: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseArgs(args: string[]): RenderArgs {
|
|
20
|
+
const result: RenderArgs = {
|
|
21
|
+
entry: "",
|
|
22
|
+
composition: "",
|
|
23
|
+
output: null,
|
|
24
|
+
fps: null,
|
|
25
|
+
frames: null,
|
|
26
|
+
codec: "h264",
|
|
27
|
+
concurrency: Math.max(1, Math.floor(availableParallelism() / 2)),
|
|
28
|
+
imageFormat: "png",
|
|
29
|
+
quality: 80,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < args.length; i++) {
|
|
33
|
+
const arg = args[i]!;
|
|
34
|
+
const next = args[i + 1];
|
|
35
|
+
|
|
36
|
+
if (arg === "--entry" && next) {
|
|
37
|
+
result.entry = next;
|
|
38
|
+
i++;
|
|
39
|
+
} else if (arg.startsWith("--entry=")) {
|
|
40
|
+
result.entry = arg.slice("--entry=".length);
|
|
41
|
+
} else if (arg === "--composition" && next) {
|
|
42
|
+
result.composition = next;
|
|
43
|
+
i++;
|
|
44
|
+
} else if (arg.startsWith("--composition=")) {
|
|
45
|
+
result.composition = arg.slice("--composition=".length);
|
|
46
|
+
} else if (arg === "--output" && next) {
|
|
47
|
+
result.output = next;
|
|
48
|
+
i++;
|
|
49
|
+
} else if (arg.startsWith("--output=")) {
|
|
50
|
+
result.output = arg.slice("--output=".length);
|
|
51
|
+
} else if (arg === "--fps" && next) {
|
|
52
|
+
result.fps = parseInt(next, 10);
|
|
53
|
+
i++;
|
|
54
|
+
} else if (arg.startsWith("--fps=")) {
|
|
55
|
+
result.fps = parseInt(arg.slice("--fps=".length), 10);
|
|
56
|
+
} else if (arg === "--frames" && next) {
|
|
57
|
+
result.frames = next;
|
|
58
|
+
i++;
|
|
59
|
+
} else if (arg.startsWith("--frames=")) {
|
|
60
|
+
result.frames = arg.slice("--frames=".length);
|
|
61
|
+
} else if (arg === "--codec" && next) {
|
|
62
|
+
result.codec = next as Codec;
|
|
63
|
+
i++;
|
|
64
|
+
} else if (arg.startsWith("--codec=")) {
|
|
65
|
+
result.codec = arg.slice("--codec=".length) as Codec;
|
|
66
|
+
} else if (arg === "--concurrency" && next) {
|
|
67
|
+
result.concurrency = parseInt(next, 10);
|
|
68
|
+
i++;
|
|
69
|
+
} else if (arg.startsWith("--concurrency=")) {
|
|
70
|
+
result.concurrency = parseInt(arg.slice("--concurrency=".length), 10);
|
|
71
|
+
} else if (arg === "--image-format" && next) {
|
|
72
|
+
result.imageFormat = next as ImageFormat;
|
|
73
|
+
i++;
|
|
74
|
+
} else if (arg.startsWith("--image-format=")) {
|
|
75
|
+
result.imageFormat = arg.slice("--image-format=".length) as ImageFormat;
|
|
76
|
+
} else if (arg === "--quality" && next) {
|
|
77
|
+
result.quality = parseInt(next, 10);
|
|
78
|
+
i++;
|
|
79
|
+
} else if (arg.startsWith("--quality=")) {
|
|
80
|
+
result.quality = parseInt(arg.slice("--quality=".length), 10);
|
|
81
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
82
|
+
printHelp();
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function printHelp(): void {
|
|
91
|
+
console.log(`
|
|
92
|
+
vibeo render - Render a composition to video
|
|
93
|
+
|
|
94
|
+
Usage:
|
|
95
|
+
vibeo render --entry <path> --composition <id> [options]
|
|
96
|
+
|
|
97
|
+
Required:
|
|
98
|
+
--entry <path> Path to the root file with compositions
|
|
99
|
+
--composition <id> Composition ID to render
|
|
100
|
+
|
|
101
|
+
Options:
|
|
102
|
+
--output <path> Output file path (default: out/<compositionId>.mp4)
|
|
103
|
+
--fps <number> Override fps
|
|
104
|
+
--frames <range> Frame range "start-end" (e.g., "0-100")
|
|
105
|
+
--codec <codec> h264 | h265 | vp9 | prores (default: h264)
|
|
106
|
+
--concurrency <number> Parallel browser tabs (default: cpu count / 2)
|
|
107
|
+
--image-format <format> png | jpeg (default: png)
|
|
108
|
+
--quality <number> 0-100 for jpeg quality / crf (default: 80)
|
|
109
|
+
--help Show this help
|
|
110
|
+
`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function formatTime(ms: number): string {
|
|
114
|
+
const seconds = Math.floor(ms / 1000);
|
|
115
|
+
const minutes = Math.floor(seconds / 60);
|
|
116
|
+
const s = seconds % 60;
|
|
117
|
+
return `${minutes}:${String(s).padStart(2, "0")}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderProgressBar(progress: RenderProgress): void {
|
|
121
|
+
const { framesRendered, totalFrames, percent, etaMs } = progress;
|
|
122
|
+
const barWidth = 30;
|
|
123
|
+
const filled = Math.round(barWidth * percent);
|
|
124
|
+
const empty = barWidth - filled;
|
|
125
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
126
|
+
const pct = (percent * 100).toFixed(1);
|
|
127
|
+
const eta = etaMs !== null ? ` ETA ${formatTime(etaMs)}` : "";
|
|
128
|
+
|
|
129
|
+
process.stdout.write(
|
|
130
|
+
`\r [${bar}] ${pct}% (${framesRendered}/${totalFrames})${eta} `,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Execute the render command.
|
|
136
|
+
*/
|
|
137
|
+
export async function renderCommand(args: string[]): Promise<void> {
|
|
138
|
+
const parsed = parseArgs(args);
|
|
139
|
+
|
|
140
|
+
if (!parsed.entry) {
|
|
141
|
+
console.error("Error: --entry is required");
|
|
142
|
+
printHelp();
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!parsed.composition) {
|
|
147
|
+
console.error("Error: --composition is required");
|
|
148
|
+
printHelp();
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const entry = resolve(parsed.entry);
|
|
153
|
+
const compositionId = parsed.composition;
|
|
154
|
+
|
|
155
|
+
// TODO: In a full implementation, we would bundle the entry, extract
|
|
156
|
+
// composition metadata, and use it here. For now we require the user
|
|
157
|
+
// to have the composition info available.
|
|
158
|
+
// This is a simplified flow that demonstrates the pipeline.
|
|
159
|
+
|
|
160
|
+
const ext = parsed.codec === "vp9" ? "webm" : parsed.codec === "prores" ? "mov" : "mp4";
|
|
161
|
+
const output = parsed.output
|
|
162
|
+
? resolve(parsed.output)
|
|
163
|
+
: resolve(`out/${compositionId}.${ext}`);
|
|
164
|
+
|
|
165
|
+
console.log(`\nRendering composition "${compositionId}"`);
|
|
166
|
+
console.log(` Entry: ${entry}`);
|
|
167
|
+
console.log(` Output: ${output}`);
|
|
168
|
+
console.log(` Codec: ${parsed.codec}`);
|
|
169
|
+
console.log(` Image format: ${parsed.imageFormat}`);
|
|
170
|
+
console.log(` Concurrency: ${parsed.concurrency}`);
|
|
171
|
+
console.log();
|
|
172
|
+
|
|
173
|
+
// For a full render, we need composition info from the bundle.
|
|
174
|
+
// This would normally be extracted by bundling and evaluating the entry.
|
|
175
|
+
// Here we set up the render config and delegate to renderComposition.
|
|
176
|
+
const compositionInfo = {
|
|
177
|
+
width: 1920,
|
|
178
|
+
height: 1080,
|
|
179
|
+
fps: parsed.fps ?? 30,
|
|
180
|
+
durationInFrames: 300,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const frameRange = parseFrameRange(parsed.frames, compositionInfo.durationInFrames);
|
|
184
|
+
|
|
185
|
+
const startTime = Date.now();
|
|
186
|
+
|
|
187
|
+
await renderComposition(
|
|
188
|
+
{
|
|
189
|
+
entry,
|
|
190
|
+
compositionId,
|
|
191
|
+
outputPath: output,
|
|
192
|
+
codec: parsed.codec,
|
|
193
|
+
imageFormat: parsed.imageFormat,
|
|
194
|
+
quality: parsed.quality,
|
|
195
|
+
fps: parsed.fps,
|
|
196
|
+
frameRange,
|
|
197
|
+
concurrency: parsed.concurrency,
|
|
198
|
+
pixelFormat: "yuv420p",
|
|
199
|
+
onProgress: renderProgressBar,
|
|
200
|
+
},
|
|
201
|
+
compositionInfo,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const elapsed = Date.now() - startTime;
|
|
205
|
+
console.log(`\n\nDone in ${formatTime(elapsed)}. Output: ${output}`);
|
|
206
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { renderCommand } from "./commands/render.js";
|
|
4
|
+
import { previewCommand } from "./commands/preview.js";
|
|
5
|
+
import { listCommand } from "./commands/list.js";
|
|
6
|
+
import { createCommand } from "./commands/create.js";
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const command = args[0];
|
|
10
|
+
|
|
11
|
+
function printUsage(): void {
|
|
12
|
+
console.log(`
|
|
13
|
+
vibeo - React video framework CLI
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
vibeo <command> [options]
|
|
17
|
+
|
|
18
|
+
Commands:
|
|
19
|
+
create Create a new project from a template
|
|
20
|
+
render Render a composition to video
|
|
21
|
+
preview Start a dev server with live preview
|
|
22
|
+
list List registered compositions
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--help Show help for a command
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
vibeo create my-video
|
|
29
|
+
vibeo create music-viz --template audio-reactive
|
|
30
|
+
vibeo render --entry src/index.tsx --composition MyComp --output out.mp4
|
|
31
|
+
vibeo preview --entry src/index.tsx
|
|
32
|
+
vibeo list --entry src/index.tsx
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function main(): Promise<void> {
|
|
37
|
+
if (!command || command === "--help" || command === "-h") {
|
|
38
|
+
printUsage();
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
switch (command) {
|
|
43
|
+
case "create":
|
|
44
|
+
await createCommand(args.slice(1));
|
|
45
|
+
break;
|
|
46
|
+
case "render":
|
|
47
|
+
await renderCommand(args.slice(1));
|
|
48
|
+
break;
|
|
49
|
+
case "preview":
|
|
50
|
+
await previewCommand(args.slice(1));
|
|
51
|
+
break;
|
|
52
|
+
case "list":
|
|
53
|
+
await listCommand(args.slice(1));
|
|
54
|
+
break;
|
|
55
|
+
default:
|
|
56
|
+
console.error(`Unknown command: ${command}`);
|
|
57
|
+
printUsage();
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main().catch((err) => {
|
|
63
|
+
console.error(err);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|