feat: Phase 4 — replace @remotion/bundler + @remotion/renderer with Puppeteer + FFmpeg

- Created src/engine/renderer/puppeteerRenderer.ts (frame capture via headless Chrome)
- Created src/engine/renderer/videoEncoder.ts (FFmpeg CLI wrapper for MP4/WebM)
- Created src/pages/RenderPage.tsx (headless render page with __BRADLY_RENDER__ API)
- Rewrote src/server/renderQueue.ts — zero Remotion imports
- Deleted scripts/bundle-remotion.js
- Replaced @remotion/bundler + @remotion/renderer with puppeteer-core
- Added renderMode detection in main.tsx entry point

Zero Remotion dependencies remain. Fully independent.
This commit is contained in:
2026-06-02 05:33:17 -05:00
parent 3e3e23b6b7
commit 551bff56a2
9 changed files with 1166 additions and 1652 deletions
+110
View File
@@ -0,0 +1,110 @@
/**
* RenderPage — Headless rendering page for Puppeteer capture.
*
* When the app loads with ?renderMode=true, this component mounts instead
* of the full editor UI. It renders a BradlyPlayer at exact pixel dimensions
* and exposes window.__BRADLY_RENDER__ API for external control.
*
* API:
* __BRADLY_RENDER__.init({ inputProps, width, height, fps, durationInFrames })
* __BRADLY_RENDER__.seekTo(frame)
* __BRADLY_RENDER__.ready // boolean
*/
import React, { useState, useRef, useCallback, useEffect } from 'react';
import { BradlyPlayer, BradlyPlayerRef } from '../engine/player';
import { BrandComposition } from '../components/BrandComposition';
interface RenderConfig {
inputProps: Record<string, any>;
width: number;
height: number;
fps: number;
durationInFrames: number;
}
export const RenderPage: React.FC = () => {
const [config, setConfig] = useState<RenderConfig | null>(null);
const playerRef = useRef<BradlyPlayerRef>(null);
const seekTo = useCallback((frame: number) => {
playerRef.current?.seekTo(frame);
}, []);
// Expose global API for Puppeteer
useEffect(() => {
const api = {
ready: false,
init: (cfg: RenderConfig) => {
setConfig(cfg);
// Mark ready after React renders
requestAnimationFrame(() => {
requestAnimationFrame(() => {
api.ready = true;
});
});
},
seekTo,
};
(window as any).__BRADLY_RENDER__ = api;
return () => {
delete (window as any).__BRADLY_RENDER__;
};
}, [seekTo]);
// Update ready state when config changes
useEffect(() => {
if (config && (window as any).__BRADLY_RENDER__) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
(window as any).__BRADLY_RENDER__.ready = true;
});
});
}
}, [config]);
if (!config) {
return (
<div style={{
width: '100vw',
height: '100vh',
background: '#000',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#666',
fontFamily: 'monospace',
fontSize: 12,
}}>
Waiting for render init...
</div>
);
}
return (
<div style={{
width: config.width,
height: config.height,
overflow: 'hidden',
background: '#000',
}}>
<BradlyPlayer
ref={playerRef}
component={BrandComposition}
inputProps={config.inputProps}
durationInFrames={config.durationInFrames}
compositionWidth={config.width}
compositionHeight={config.height}
fps={config.fps}
controls={false}
autoPlay={false}
clickToPlay={false}
style={{
width: config.width,
height: config.height,
}}
/>
</div>
);
};