Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@open-motion/cli",
"version": "0.1.6",
"version": "0.2.0",
"bin": {
"open-motion": "dist/bin.js"
},
Expand Down
66 changes: 54 additions & 12 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ import path from 'path';
import fs from 'fs';
import { Command } from 'commander';
import cliProgress from 'cli-progress';
import { execSync } from 'child_process';

const getPackageManager = () => {
try {
execSync('pnpm -v', { stdio: 'ignore' });
return 'pnpm';
} catch (e) {
return 'npm';
}
};

export const runInit = async (projectName: string) => {
const targetDir = path.join(process.cwd(), projectName);
Expand All @@ -13,7 +23,8 @@ export const runInit = async (projectName: string) => {
process.exit(1);
}

console.log(`Initializing OpenMotion project: ${projectName}...`);
const pm = getPackageManager();
console.log(`Initializing OpenMotion project: ${projectName} using ${pm}...`);

// Basic template structure
const dirs = ['', 'src'];
Expand All @@ -32,7 +43,7 @@ export const runInit = async (projectName: string) => {
dev: 'vite',
build: 'vite build',
preview: 'vite preview',
render: 'npm run build && (npx http-server dist -p 5173 -a 127.0.0.1 > /dev/null 2>&1 & sleep 2 && open-motion render -u http://127.0.0.1:5173 --composition main -o ./out.mp4 --concurrency 4 && pkill -f http-server)'
render: `${pm} run build && npx http-server dist -p 5173 -a 127.0.0.1 > /dev/null 2>&1 & sleep 2 && open-motion render -u http://127.0.0.1:5173 --composition main -o ./out.mp4 --concurrency 4; pkill -f http-server`
},
dependencies: {
'react': '^18.2.0',
Expand Down Expand Up @@ -80,6 +91,12 @@ const Root = () => {
const config = { width: 1920, height: 1080, fps: 30, durationInFrames: 120 };
const isRendering = typeof (window as any).__OPEN_MOTION_FRAME__ === 'number';

React.useEffect(() => {
if (isRendering) {
(window as any).__OPEN_MOTION_READY__ = true;
}
}, [isRendering]);

if (isRendering) {
return (
<CompositionProvider config={config} frame={(window as any).__OPEN_MOTION_FRAME__}>
Expand Down Expand Up @@ -137,11 +154,11 @@ export const App = () => {
fs.writeFileSync(path.join(targetDir, name), content);
}

console.log(`Success! Project \${projectName} initialized.`);
console.log(`Success! Project ${projectName} initialized.`);
console.log(`Next steps:`);
console.log(` cd \${projectName}`);
console.log(` npm install (or pnpm install)`);
console.log(` npm run dev`);
console.log(` cd ${projectName}`);
console.log(` ${pm} install`);
console.log(` ${pm} run dev`);
};

export const runRender = async (options: {
Expand All @@ -157,7 +174,10 @@ export const runRender = async (options: {
publicDir?: string;
format?: 'mp4' | 'gif' | 'webp' | 'webm' | 'auto';
chromiumPath?: string;
timeout?: number;
}) => {
const timeout = options.timeout || parseInt(process.env.OPEN_MOTION_RENDER_TIMEOUT || '300000', 10);

if (options.chromiumPath) {
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH = options.chromiumPath;
}
Expand Down Expand Up @@ -187,7 +207,7 @@ export const runRender = async (options: {
if (options.compositionId) {
// If ID is provided, we can skip the heavy discovery if we want,
// but for now let's just make it non-fatal if discovery fails but ID is present
const compositions = await getCompositions(options.url, { inputProps }).catch(() => []);
const compositions = await getCompositions(options.url, { inputProps, timeout }).catch(() => []);
selectedComp = compositions.find((c: any) => c.id === options.compositionId);

if (!selectedComp) {
Expand All @@ -201,7 +221,7 @@ export const runRender = async (options: {
};
}
} else {
const compositions = await getCompositions(options.url, { inputProps });
const compositions = await getCompositions(options.url, { inputProps, timeout });
if (compositions.length === 0) {
console.error('No compositions found in the provided URL.');
process.exit(1);
Expand Down Expand Up @@ -234,7 +254,8 @@ export const runRender = async (options: {
inputProps,
concurrency: options.concurrency || 1,
publicDir: options.publicDir ? path.join(process.cwd(), options.publicDir) : undefined,
onProgress: (frame) => renderBar.update(frame)
onProgress: (frame) => renderBar.update(frame),
timeout
});

renderBar.update(config.durationInFrames);
Expand Down Expand Up @@ -343,7 +364,19 @@ export const main = () => {
program
.name('open-motion')
.description('CLI for OpenMotion')
.version(pkg.version);
.version(pkg.version)
.addHelpText('after', `
Quick Start:
1. Initialize project: $ open-motion init my-video
2. Enter directory: $ cd my-video
3. Install deps: $ pnpm install
4. Start dev server: $ pnpm dev
5. Render video: $ pnpm render

Example Usage:
$ open-motion init my-project
$ open-motion render -u http://localhost:5173 -o output.mp4 --composition main
`);

program
.command('init <name>')
Expand All @@ -357,7 +390,7 @@ export const main = () => {
}
});

program
const renderCommand = program
.command('render')
.description('Render a video')
.requiredOption('-u, --url <url>', 'URL of the OpenMotion app')
Expand All @@ -372,6 +405,7 @@ export const main = () => {
.option('--public-dir <path>', 'Public directory path for static assets (default: "./public")')
.option('--format <format>', 'Output format (mp4, webm, gif, webp, auto)', 'auto')
.option('--chromium-path <path>', 'Custom path to Chromium executable')
.option('--timeout <number>', 'Timeout for browser operations in milliseconds', parseInt)
.action(async (options) => {
try {
await runRender({
Expand All @@ -386,13 +420,21 @@ export const main = () => {
duration: options.duration,
publicDir: options.publicDir,
format: options.format,
chromiumPath: options.chromiumPath
chromiumPath: options.chromiumPath,
timeout: options.timeout
});
} catch (err) {
console.error('Render failed:', err);
process.exit(1);
}
});

renderCommand.addHelpText('after', `
Examples:
$ open-motion render -u http://localhost:5173 -o out.mp4
$ open-motion render -u http://localhost:5173 -o out.mp4 --composition main --concurrency 4
$ open-motion render -u http://localhost:3000 -o banner.gif --format gif --width 1200 --height 630
`);

program.parse(process.argv);
};
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@open-motion/core",
"version": "0.1.6",
"version": "0.1.9",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/encoder/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@open-motion/encoder",
"version": "0.1.6",
"version": "0.1.9",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/renderer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@open-motion/renderer",
"version": "0.1.6",
"version": "0.1.9",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
Expand Down
27 changes: 22 additions & 5 deletions packages/renderer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,25 +72,33 @@ export interface RenderOptions {
concurrency?: number;
onProgress?: (frame: number) => void;
publicDir?: string;
timeout?: number;
}

export interface GetCompositionsOptions {
inputProps?: any;
chromiumOptions?: any;
timeout?: number;
}

export const getCompositions = async (url: string, options: GetCompositionsOptions = {}) => {
const { inputProps = {} } = options;
const { inputProps = {}, timeout = 30000 } = options;
const browser = await chromium.launch({
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || undefined,
});
const page = await browser.newPage();

if (timeout) {
page.setDefaultTimeout(timeout);
page.setDefaultNavigationTimeout(timeout);
}

await page.goto(url);
await page.waitForLoadState('networkidle');

// Wait for React to mount and all compositions to register
// We wait for the variable to exist AND for a small stabilization period
await page.waitForFunction(() => (window as any).__OPEN_MOTION_COMPOSITIONS__ !== undefined, { timeout: 10000 }).catch(() => {});
await page.waitForFunction(() => (window as any).__OPEN_MOTION_COMPOSITIONS__ !== undefined, { timeout }).catch(() => {});
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 500)));

const compositions = await page.evaluate(() => {
Expand All @@ -116,7 +124,7 @@ export const getCompositions = async (url: string, options: GetCompositionsOptio
return processedCompositions;
};

export const renderFrames = async ({ url, config, outputDir, compositionId, inputProps = {}, concurrency = 1, publicDir, onProgress }: RenderOptions) => {
export const renderFrames = async ({ url, config, outputDir, compositionId, inputProps = {}, concurrency = 1, publicDir, onProgress, timeout = 300000 }: RenderOptions) => {
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
Expand All @@ -133,6 +141,11 @@ export const renderFrames = async ({ url, config, outputDir, compositionId, inpu
viewport: { width: config.width, height: config.height }
});

if (timeout) {
page.setDefaultTimeout(timeout);
page.setDefaultNavigationTimeout(timeout);
}

const workerAudioAssets: any[] = [];
const videoCache = new Map<string, string>(); // Path to local resolved path

Expand Down Expand Up @@ -184,8 +197,12 @@ export const renderFrames = async ({ url, config, outputDir, compositionId, inpu
const ready = (window as any).__OPEN_MOTION_READY__ === true;
const delayCount = (window as any).__OPEN_MOTION_DELAY_RENDER_COUNT__ || 0;
return ready && delayCount === 0;
}, { timeout: 120000 }); // Increased from 60s to 120s for complex scenes
await page.waitForLoadState('networkidle');
}, { timeout });

// Only wait for networkidle on the first frame to avoid hanging on persistent requests
if (i === startFrame) {
await page.waitForLoadState('networkidle');
}

// Check for OffthreadVideo assets
const videoAssets = await page.evaluate(() => (window as any).__OPEN_MOTION_VIDEO_ASSETS__ || []);
Expand Down