diff --git a/packages/cli/package.json b/packages/cli/package.json index 7a86faa..c2a5fff 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@open-motion/cli", - "version": "0.1.6", + "version": "0.2.0", "bin": { "open-motion": "dist/bin.js" }, diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 28f0c3a..b6c966b 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -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); @@ -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']; @@ -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', @@ -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 ( @@ -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: { @@ -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; } @@ -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) { @@ -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); @@ -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); @@ -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 ') @@ -357,7 +390,7 @@ export const main = () => { } }); - program + const renderCommand = program .command('render') .description('Render a video') .requiredOption('-u, --url ', 'URL of the OpenMotion app') @@ -372,6 +405,7 @@ export const main = () => { .option('--public-dir ', 'Public directory path for static assets (default: "./public")') .option('--format ', 'Output format (mp4, webm, gif, webp, auto)', 'auto') .option('--chromium-path ', 'Custom path to Chromium executable') + .option('--timeout ', 'Timeout for browser operations in milliseconds', parseInt) .action(async (options) => { try { await runRender({ @@ -386,7 +420,8 @@ 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); @@ -394,5 +429,12 @@ export const main = () => { } }); + 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); }; diff --git a/packages/core/package.json b/packages/core/package.json index 215fe3f..067deb7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/encoder/package.json b/packages/encoder/package.json index 74c0686..a558f69 100644 --- a/packages/encoder/package.json +++ b/packages/encoder/package.json @@ -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", diff --git a/packages/renderer/package.json b/packages/renderer/package.json index f88d8a6..010336b 100644 --- a/packages/renderer/package.json +++ b/packages/renderer/package.json @@ -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", diff --git a/packages/renderer/src/index.ts b/packages/renderer/src/index.ts index ffce6f4..5fbec6e 100644 --- a/packages/renderer/src/index.ts +++ b/packages/renderer/src/index.ts @@ -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(() => { @@ -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 }); } @@ -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(); // Path to local resolved path @@ -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__ || []);