RVE LogoReact Video EditorDOCS
RVE SDK/Rendering/Setting Up Rendering

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. But when a user clicks Export, the SDK needs a server to actually render the video file.

You provide this by implementing 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 URL

The 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/progress

POST {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 uuid

Render State Helper

A simple file-based state store so the render and progress endpoints can share state:

app/api/render/ssr/lib/render-state.ts
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

app/api/render/ssr/render/route.ts
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

app/api/render/ssr/progress/route.ts
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:

remotion/index.ts
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

SSRLambda
Where it runsYour serverAWS Lambda
Best forDevelopment, small scaleProduction, high volume
ScalingManualAutomatic
SetupSimpleRequires AWS configuration
CostYour server costsPay per render