Setting Up Rendering
How to build the API endpoints your editor needs to export video
The RVE SDK handles everything in the browser - editing, previewing, timeline management. When a user clicks Export, the SDK needs a way to render the final video file.
You have two options:
- Client-side rendering - render directly in the browser using WebCodecs (no server needed)
- Server rendering - send the job to your own server or AWS Lambda via API endpoints
For server rendering, you provide two API endpoints. The SDK's HttpRenderer calls them automatically.
How It Works
User clicks "Export"
│
▼
SDK calls POST /api/render/ssr/render
→ Your server starts a Remotion render job
→ Returns { renderId: "uuid" }
│
▼
SDK polls POST /api/render/ssr/progress
→ Your server returns progress, done, or error
→ SDK shows progress bar to user
│
▼
Render complete → SDK receives download URLThe API Contract
Your server needs to implement two endpoints. The base path is whatever you pass to HttpRenderer:
// If you configure this:
new HttpRenderer('/api/render/ssr', { type: 'ssr', entryPoint: '/api/render/ssr' })
// The SDK will call:
// POST /api/render/ssr/render
// POST /api/render/ssr/progressPOST {base}/render
Request body:
{
"id": "composition-id",
"inputProps": {
"overlays": [...],
"durationInFrames": 900,
"width": 1920,
"height": 1080,
"fps": 30
}
}Expected response:
{
"renderId": "unique-render-id"
}POST {base}/progress
Request body:
{
"id": "the-renderId-from-above"
}Expected response (one of three shapes):
{
"type": "progress",
"progress": 0.45
}{
"type": "done",
"url": "/rendered-videos/abc123.mp4",
"size": 12345678
}{
"type": "error",
"message": "Render failed: out of memory"
}The SDK polls the progress endpoint every second until it receives a done or error response.
Next.js Implementation
Here's a working implementation using Next.js App Router and Remotion SSR.
Install Dependencies
npm install @remotion/bundler @remotion/renderer remotion uuidRender State Helper
A simple file-based state store so the render and progress endpoints can share state:
import fs from 'fs';
import path from 'path';
const RENDER_STATE_DIR = path.join(process.cwd(), 'tmp', 'render-state');
if (!fs.existsSync(RENDER_STATE_DIR)) {
fs.mkdirSync(RENDER_STATE_DIR, { recursive: true });
}
export const saveRenderState = (renderId: string, state: unknown) => {
fs.writeFileSync(
path.join(RENDER_STATE_DIR, `${renderId}.json`),
JSON.stringify(state)
);
};
export const getRenderState = (renderId: string) => {
const filePath = path.join(RENDER_STATE_DIR, `${renderId}.json`);
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
};
export const updateRenderProgress = (renderId: string, progress: number) => {
const state = getRenderState(renderId) || {};
state.progress = progress;
state.status = 'rendering';
saveRenderState(renderId, state);
};
export const completeRender = (renderId: string, url: string, size: number) => {
const state = getRenderState(renderId) || {};
state.status = 'done';
state.url = url;
state.size = size;
saveRenderState(renderId, state);
};
export const failRender = (renderId: string, error: string) => {
const state = getRenderState(renderId) || {};
state.status = 'error';
state.error = error;
saveRenderState(renderId, state);
};Render Endpoint
import { NextRequest, NextResponse } from 'next/server';
import { bundle } from '@remotion/bundler';
import { renderMedia, selectComposition } from '@remotion/renderer';
import { v4 as uuidv4 } from 'uuid';
import path from 'path';
import fs from 'fs';
import {
saveRenderState,
updateRenderProgress,
completeRender,
failRender,
} from '../lib/render-state';
const VIDEOS_DIR = path.join(process.cwd(), 'public', 'rendered-videos');
if (!fs.existsSync(VIDEOS_DIR)) {
fs.mkdirSync(VIDEOS_DIR, { recursive: true });
}
export async function POST(request: NextRequest) {
try {
const { id, inputProps } = await request.json();
const renderId = uuidv4();
// Save initial state
saveRenderState(renderId, {
status: 'rendering',
progress: 0,
timestamp: Date.now(),
});
// Start rendering asynchronously (don't await)
(async () => {
try {
// Bundle your Remotion composition
const bundleLocation = await bundle(
path.join(process.cwd(), 'remotion', 'index.ts')
);
// Select the composition
const composition = await selectComposition({
serveUrl: bundleLocation,
id,
inputProps,
});
// Render the video
await renderMedia({
codec: 'h264',
composition: {
...composition,
durationInFrames: inputProps.durationInFrames || composition.durationInFrames,
width: inputProps.width || composition.width,
height: inputProps.height || composition.height,
},
serveUrl: bundleLocation,
outputLocation: path.join(VIDEOS_DIR, `${renderId}.mp4`),
inputProps,
onProgress: ({ progress }) => {
updateRenderProgress(renderId, progress);
},
});
const stats = fs.statSync(path.join(VIDEOS_DIR, `${renderId}.mp4`));
completeRender(renderId, `/rendered-videos/${renderId}.mp4`, stats.size);
} catch (error) {
failRender(renderId, (error as Error).message);
}
})();
return NextResponse.json({ renderId });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to start render' },
{ status: 500 }
);
}
}Progress Endpoint
import { NextRequest, NextResponse } from 'next/server';
import { getRenderState } from '../lib/render-state';
export async function POST(request: NextRequest) {
try {
const { id } = await request.json();
const renderState = getRenderState(id);
if (!renderState) {
return NextResponse.json({
type: 'error',
message: `No render found with ID: ${id}`,
});
}
switch (renderState.status) {
case 'error':
return NextResponse.json({
type: 'error',
message: renderState.error || 'Unknown error',
});
case 'done':
return NextResponse.json({
type: 'done',
url: renderState.url,
size: renderState.size,
});
default:
return NextResponse.json({
type: 'progress',
progress: renderState.progress || 0,
});
}
} catch (error) {
return NextResponse.json(
{ error: 'Failed to get render progress' },
{ status: 500 }
);
}
}Remotion Composition
Your render endpoint needs a Remotion composition that knows how to render the SDK's overlay data. The SDK exports layer components you can use in your composition:
import { registerRoot } from 'remotion';
import { MyComposition } from './Composition';
registerRoot(MyComposition);Remotion setup
Setting up the Remotion composition is covered in detail in the Remotion documentation. The SDK provides layer rendering components (text-layer-content, video-layer-content, image-layer-content, etc.) that you can use inside your composition to render each overlay type.
Rendering Modes
| CSR (Browser) | SSR | Lambda | |
|---|---|---|---|
| Where it runs | User's browser | Your server | AWS Lambda |
| Best for | Quick start, light use | Development, full control | Production, high volume |
| Server setup | None | API routes required | AWS + API routes |
| Scaling | N/A (client device) | Manual | Automatic |
| Browser support | Chrome/Edge only | Any | Any |
| Dependencies | @remotion/web-renderer, @remotion/media | @remotion/bundler, @remotion/renderer | @remotion/lambda |
- Client-Side Rendering - render in the browser, no server needed
- SSR Rendering - render on your own server
- Lambda Rendering - render on AWS Lambda for production scale