Remotion 코드 분석 — renderMedia()와 프레임 렌더링 루프
Puppeteer로 각 프레임을 스크린샷하고 FFmpeg로 합치는 렌더링 핵심 루프를 소스 코드로 분석
GitHub: remotion-dev/remotion/packages/renderer
번들링이 끝나면 실제 렌더링이 시작됩니다. renderMedia()가 모든 것을 조율하는 오케스트레이터입니다.
renderMedia() — 오케스트레이터
// renderer/src/render-media.ts (간략화)
export const renderMedia = async (options: RenderMediaOptions) => {
// 1. 번들 열기
const serveUrl = await prepareServer(options.serveUrl);
// 2. Composition 메타데이터 가져오기
const composition = await selectComposition({
serveUrl,
id: options.composition, // 렌더링할 Composition ID
});
// 3. 프레임 렌더링 (PNG 시퀀스 생성)
const { assetsInfo } = await renderFrames({
config: composition,
concurrency: options.concurrency,
onFrameUpdate: options.onProgress,
});
// 4. FFmpeg로 비디오 인코딩
await stitchFramesToVideo({
fps: composition.fps,
codec: options.codec,
audioBitrate: options.audioBitrate,
});
};
renderFrames() — 프레임 루프의 핵심
// renderer/src/render-frames.ts (핵심 로직)
export const renderFrames = async (options) => {
// concurrency 수만큼 Chrome 페이지를 열어 병렬 처리
const pages = await openPages(options.concurrency);
// 프레임을 풀에 분배
const framePool = createFramePool(totalFrames, pages.length);
// 각 페이지가 프레임을 하나씩 처리
await Promise.all(pages.map(async (page, pageIndex) => {
for (const frame of framePool[pageIndex]) {
// currentFrame을 브라우저에 주입
await page.evaluate((f) => {
window.remotion_setFrame(f);
}, frame);
// delayRender가 있으면 완료될 때까지 대기
await waitForDelayRender(page);
// 스크린샷 → PNG 파일로 저장
await page.screenshot({
path: `frame-${frame}.png`,
type: 'png',
});
}
}));
};
핵심 메커니즘:
window.remotion_setFrame(n): 브라우저 컨텍스트에서 전역 프레임 번호를 변경React가 이를 감지하고 리렌더링 → 새로운 프레임 상태
waitForDelayRender():continueRender()가 호출될 때까지 page.waitForFunction()으로 대기
delayRender / continueRender 구현
// core/src/delay-render.ts
let delayRenderCount = 0;
export const delayRender = (label?: string): number => {
const id = ++delayRenderCount;
// window.__REMOTION_DELAY_RENDER에 등록
window.__REMOTION_DELAY_RENDER.push({ id, label });
return id;
};
export const continueRender = (id: number): void => {
// 해당 ID를 배열에서 제거
window.__REMOTION_DELAY_RENDER =
window.__REMOTION_DELAY_RENDER.filter(d => d.id !== id);
};
// 렌더러가 확인하는 조건:
// window.__REMOTION_DELAY_RENDER.length === 0 이면 스크린샷 진행
설계 포인트: delayRender는 단순한 카운터입니다. 여러 비동기 작업이 각각 delayRender()를 호출하고, 모든 continueRender()가 호출되어 배열이 비워져야 스크린샷이 진행됩니다.
stitchFramesToVideo() — FFmpeg 인코딩
// renderer/src/stitch-frames-to-video.ts (간략화)
export const stitchFramesToVideo = async (options) => {
const ffmpegArgs = [
'-r', String(options.fps), // 프레임레이트
'-i', 'frame-%d.png', // 이미지 시퀀스 입력
'-c:v', 'libx264', // H.264 코덱
'-pix_fmt', 'yuva420p', // 픽셀 포맷
'-y', options.outputPath, // 출력 파일
];
// 오디오가 있으면 -i audio.mp3 추가
if (options.audioFile) {
ffmpegArgs.push('-i', options.audioFile, '-c:a', 'aac');
}
await execa('ffmpeg', ffmpegArgs);
};
PNG 시퀀스를 FFmpeg의 이미지 시퀀스 입력으로 넣어 H.264(MP4) 또는 VP9(WebM)으로 인코딩합니다.
동작 흐름
renderMedia()가 번들 서버를 열고 selectComposition()으로 Composition 메타데이터 조회
renderFrames()가 concurrency 수만큼 Chrome 페이지를 열고 프레임을 분배
각 페이지에서 window.remotion_setFrame(n) → React 리렌더 → waitForDelayRender() → screenshot()
delayRender 배열이 비어질 때까지(= 모든 비동기 완료) 스크린샷 대기
stitchFramesToVideo()가 FFmpeg로 PNG 시퀀스 → MP4/WebM 인코딩 + 오디오 mux
장점
- ✓ remotion_setFrame → React 리렌더 패턴: 기존 React 생태계를 그대로 활용하는 우아한 설계
- ✓ delayRender가 단순 카운터 → 복잡한 비동기 시나리오도 안전하게 처리
단점
- ✗ Chrome 프로세스 관리 복잡: 페이지 크래시, 메모리 누수 등 브라우저 특유 문제