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:
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user