551bff56a2
- 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.
111 lines
2.8 KiB
TypeScript
111 lines
2.8 KiB
TypeScript
/**
|
|
* 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>
|
|
);
|
|
};
|