Remotionで動画生成を完全自動化する — Renderer API活用ガイド
Remotionで動画生成を完全自動化する — Renderer API活用ガイド
「動画を量産したい」という要求は、ほとんどの場合、人手の限界に先にぶつかる。商品ページのショートクリップ、顧客ごとにカスタマイズしたお礼動画、週次KPIレポートの自動映像化——これらを人間の編集者が一本ずつ仕上げていたのでは、スケールは永遠に来ない。
Remotionが解決するのはまさにそこだ。Reactでコンポーネントとして動画ロジックを書いておけば、あとはNode.jsのAPIを叩くだけで何百本でも自動生成できる。GUIは不要。エンコーダーソフトも不要。必要なのは@remotion/rendererパッケージと、渡したいデータだけだ。
このガイドでは、renderMedia関数の使い方から始め、Node.jsレンダーサーバーの構築、REST APIエンドポイントからのトリガー、進捗管理、クラウドストレージへの出力まで、実用的なパイプライン全体を解説する。
プログラマティック動画が開く可能性
プログラマティック動画とは、コードとデータから動画を自動生成するアプローチだ。テンプレートを一度作れば、データを差し替えるだけで大量のバリエーションを出力できる。
人手による動画制作との決定的な違いは、追加コストがほぼゼロという点だ。Reactコンポーネントで商品紹介テンプレートを一本書いてしまえば、10本生成しても1万本生成してもサーバーの計算時間しかかからない。
Remotionはこの思想を「Webエンジニアが使いやすい形で」実装したフレームワークだ。動画の各フレームはReactコンポーネントとしてレンダリングされ、そのままヘッドレスChromiumでキャプチャされてMP4やWebMに変換される。特殊なDSLを学ぶ必要はなく、TypeScriptと普通のReactの知識だけで動画制作が始められる。
renderMedia 関数 — サーバーサイドレンダリングの核心
renderMediaは@remotion/rendererパッケージが提供するNode.js関数で、Remotionプロジェクトをバンドルしたディレクトリと各種パラメータを受け取り、動画ファイルを出力する。
インストール
npm install @remotion/renderer @remotion/bundler
# または
pnpm add @remotion/renderer @remotion/bundler
インポート
import { renderMedia, selectComposition } from "@remotion/renderer";
import { bundle } from "@remotion/bundler";
関数シグネチャ
await renderMedia({
composition, // VideoConfig — selectComposition() で取得
serveUrl, // string — バンドルのパスまたはホスト済みURL
codec, // "h264" | "h265" | "vp8" | "vp9" | "gif" | "prores" など
outputLocation, // string — 出力先ファイルパス(絶対パス推奨)
inputProps, // Record<string, unknown> — コンポジションに渡す動的データ
onProgress, // (progress: RenderMediaOnProgress) => void — 進捗コールバック
concurrency, // number — 並列レンダリングスレッド数(デフォルト: CPU数/2)
timeoutInMilliseconds, // number — タイムアウト(デフォルト: 30000)
chromiumOptions, // object — ヘッドレスブラウザ設定(任意)
});
コーデックの選び方
| コーデック | 用途 | 特徴 |
|---|---|---|
"h264" | Web・SNS全般 | 互換性最高。ほぼ全環境で再生可能 |
"h265" | 高品質アーカイブ | h264より圧縮率高いが対応環境に注意 |
"vp9" | WebM配信 | オープンコーデック。Chromiumと相性良好 |
"gif" | SNSループ素材 | 音声なし・色数制限あり。広く使われる |
"prores" | 後編集用中間素材 | 高品質・大容量 |
一般的なWebパイプラインでは"h264"を選べば間違いない。
Node.jsレンダーサーバーを構築する
自動化パイプラインの土台は、常時起動しているレンダーサーバーだ。起動時にプロジェクトをバンドルし、以降はリクエストを受けるたびにレンダリングを実行する構成が基本になる。
ステップ1: 起動時にバンドルを生成する
bundle()はRemotionプロジェクトをWebpackでコンパイルし、ヘッドレスChromiumが読み込めるディレクトリを生成する。10〜30秒かかるためリクエストごとに呼ぶのは厳禁だ。起動時に一度だけ実行し、パスをモジュールスコープの変数に保持する。
import { bundle } from "@remotion/bundler";
import path from "path";
let bundlePath: string;
export async function initBundle(): Promise<void> {
bundlePath = await bundle({
entryPoint: path.resolve("./src/index.ts"),
});
console.log("バンドル完了:", bundlePath);
}
ステップ2: コンポジション情報を取得する
renderMediaに渡すcomposition引数は、selectComposition()で取得したVideoConfigオブジェクトだ。コンポジションのサイズ・FPS・フレーム数などのメタデータが含まれる。
import { selectComposition } from "@remotion/renderer";
async function getComposition(
id: string,
inputProps: Record<string, unknown>
) {
return selectComposition({
serveUrl: bundlePath,
id,
inputProps,
});
}
inputPropsをselectCompositionにも渡すのは重要なポイントだ。コンポジションのdurationInFramesを入力データから動的に計算している場合、ここで渡さないと正しいフレーム数が得られない。
ステップ3: 動画をレンダリングする
import { renderMedia } from "@remotion/renderer";
async function renderVideo(
compositionId: string,
inputProps: Record<string, unknown>,
outputPath: string
): Promise<void> {
const composition = await getComposition(compositionId, inputProps);
await renderMedia({
composition,
serveUrl: bundlePath,
codec: "h264",
outputLocation: outputPath,
inputProps,
onProgress: ({ renderedFrames }) => {
const pct = Math.round(
(renderedFrames / composition.durationInFrames) * 100
);
process.stdout.write(`\rレンダリング中: ${pct}%`);
},
});
console.log("\n完了:", outputPath);
}
REST APIエンドポイントからレンダーをトリガーする
レンダー関数をHTTPサーバーでラップすれば、スタック内の他のサービスからRemotionの知識なしにジョブを投入できるようになる。
import express from "express";
import { v4 as uuidv4 } from "uuid";
import path from "path";
import { renderMedia, selectComposition } from "@remotion/renderer";
const app = express();
app.use(express.json());
// 本番ではRedisやDBに置き換える
const jobs = new Map<string, {
status: "queued" | "rendering" | "done" | "error";
progress: number;
outputPath?: string;
error?: string;
}>();
// レンダリングジョブを投入するエンドポイント
app.post("/render", async (req, res) => {
const { compositionId, props } = req.body;
if (!compositionId || !props) {
return res.status(400).json({ error: "compositionId と props は必須です" });
}
const jobId = uuidv4();
const outputPath = path.resolve(`./output/${jobId}.mp4`);
jobs.set(jobId, { status: "queued", progress: 0 });
res.json({ jobId });
// リクエストハンドラの外で非同期実行
(async () => {
try {
jobs.set(jobId, { status: "rendering", progress: 0 });
const composition = await selectComposition({
serveUrl: bundlePath,
id: compositionId,
inputProps: props,
});
await renderMedia({
composition,
serveUrl: bundlePath,
codec: "h264",
outputLocation: outputPath,
inputProps: props,
onProgress: ({ renderedFrames }) => {
const pct = Math.round(
(renderedFrames / composition.durationInFrames) * 100
);
jobs.set(jobId, { status: "rendering", progress: pct });
},
});
jobs.set(jobId, { status: "done", progress: 100, outputPath });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
jobs.set(jobId, { status: "error", progress: 0, error: message });
}
})();
});
// ジョブ状態を確認するエンドポイント
app.get("/render/:jobId", (req, res) => {
const job = jobs.get(req.params.jobId);
if (!job) return res.status(404).json({ error: "ジョブが見つかりません" });
res.json(job);
});
app.listen(3001, () => console.log("レンダーサーバー起動: http://localhost:3001"));
クライアント側はPOST /renderでジョブIDを受け取り、GET /render/:jobIdをポーリングして完了を待つ。本番環境ではインメモリマップをRedisやPostgreSQLに置き換え、複数サーバーインスタンス間でジョブ状態を共有できるようにする。
inputProps で動的データをコンポジションに渡す
inputPropsはレンダー呼び出し側のデータとRemotionコンポーネントをつなぐインターフェースだ。JSON直列化可能な任意の値を渡せる。
レンダー呼び出し側:
await renderMedia({
composition,
serveUrl: bundlePath,
codec: "h264",
outputLocation: `./output/product-${productId}.mp4`,
inputProps: {
productName: "ノイズキャンセリングイヤホン Pro",
price: "¥24,800",
imageUrl: `https://cdn.example.com/products/${productId}.jpg`,
ctaText: "今すぐ購入",
accentColor: "#ff6b35",
},
});
Remotionコンポーネント側:
import { useCurrentFrame, useVideoConfig, interpolate } from "remotion";
interface ProductProps {
productName: string;
price: string;
imageUrl: string;
ctaText: string;
accentColor: string;
}
export const ProductCard: React.FC<ProductProps> = ({
productName,
price,
imageUrl,
ctaText,
accentColor,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const opacity = interpolate(frame, [0, fps * 0.4], [0, 1], {
extrapolateRight: "clamp",
});
return (
<div style={{ background: "#fff", opacity, padding: 40 }}>
<img src={imageUrl} style={{ width: "100%", borderRadius: 12 }} />
<h1 style={{ color: accentColor }}>{productName}</h1>
<p style={{ fontSize: 32, fontWeight: "bold" }}>{price}</p>
<div style={{ background: accentColor, color: "#fff", padding: "12px 24px" }}>
{ctaText}
</div>
</div>
);
};
defaultPropsは必ず実際のinputPropsの型と形状を一致させること。Studio上でのプレビューだけでなく、レンダー時に一部のプロパティが欠けた際のフォールバックとしても機能する。
進捗追跡とジョブステータス管理
onProgressコールバックが受け取るRenderMediaOnProgressオブジェクトには以下のフィールドが含まれる。
| フィールド | 型 | 説明 |
|---|---|---|
renderedFrames | number | ヘッドレスChromeでレンダー完了したフレーム数 |
encodedFrames | number | エンコード完了したフレーム数 |
progress | number | 全体進捗(0〜1) |
renderedDoneIn | number | null | レンダリング完了時間(ms) |
encodedDoneIn | number | null | エンコード完了時間(ms) |
stitchStage | string | 現在のステージ("encoding" / "muxing") |
Redisを使った進捗保存の例:
onProgress: async ({ progress, renderedFrames, encodedFrames }) => {
await redis.set(
`render:progress:${jobId}`,
JSON.stringify({
pct: Math.round(progress * 100),
renderedFrames,
encodedFrames,
ts: Date.now(),
}),
"EX",
3600
);
},
フロントエンドにServer-Sent Events(SSE)でこの値をストリームすれば、ポーリングなしのリアルタイム進捗バーを実装できる。WebSocketでも同様のことができ、既存のWebSocketインフラがあればそちらを流用するのが手軽だ。
出力先の管理: ローカル・S3・クラウドストレージ
outputLocationに指定したローカルパスへの書き込みがrenderMediaのデフォルト動作だ。本番パイプラインでは、レンダー完了後に即座にクラウドストレージへ転送するのが一般的だ。
S3へのアップロード
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import fs from "fs";
const s3 = new S3Client({ region: "ap-northeast-1" });
async function uploadToS3(
localPath: string,
key: string
): Promise<string> {
const stream = fs.createReadStream(localPath);
await s3.send(
new PutObjectCommand({
Bucket: process.env.OUTPUT_BUCKET!,
Key: key,
Body: stream,
ContentType: "video/mp4",
})
);
fs.unlinkSync(localPath); // ローカルファイルを削除
return `https://${process.env.OUTPUT_BUCKET}.s3.amazonaws.com/${key}`;
}
// renderMedia完了後
const videoUrl = await uploadToS3(outputPath, `renders/${jobId}.mp4`);
await db.jobs.update({ id: jobId }, { status: "done", videoUrl });
CloudflareのR2やGCPのCloud Storageを使う場合も、SDKを差し替えるだけで同じパターンが使える。S3互換APIを持つサービスであればAWS SDKのエンドポイント設定を変更するだけで対応できる。
実際のユースケース
ECサイトの商品動画自動生成 商品が新規登録されるたびにWebhookが発火し、商品画像・タイトル・価格をRemotionコンポジションに渡してショートクリップを自動生成する。編集者が手を動かさなくても、全SKUのアニメーション紹介動画が揃う。
パーソナライズドサンクス動画
会員向けECが購入完了メールにパーソナライズ動画を添付するユースケースだ。注文完了イベントをトリガーに、購入者名・商品名・ブランドカラーをinputPropsとして渡し、10秒のお礼クリップをリアルタイム生成して送付する。
ニュース・データ可視化の自動更新 市場データや気象データを定期取得し、最新値をRemotionのグラフコンポーネントに渡してMP4を生成し、SNSに自動投稿するパイプライン。Cronジョブが30分ごとに動き、常に最新データを映した動画が公開される。
SaaSの週次レポート動画 各顧客のKPIデータを毎週月曜朝に取得し、アニメーションチャートつきのレポート動画を自動生成してメール送信する。顧客はダッシュボードを開かなくても、動画を見るだけで前週の結果を把握できる。
コストとパフォーマンスの考え方
concurrencyパラメータ
renderMediaのconcurrencyは並列で動くヘッドレスブラウザのタブ数を制御する。デフォルトはCPUコア数の半分。増やすほど個別レンダーは速くなるが、メモリ消費が比例して増える。4コアサーバーではconcurrency: 2を基準に実測しながら調整するのが安全だ。
メモリ見積もり
並列タブ1つあたり200〜400MBのメモリを消費する目安で計算する。concurrency: 4なら最低でも2GBの空きメモリが必要だ。メモリ不足はレンダー失敗の最大の原因の一つで、エラーメッセージが分かりにくいことも多い。本番投入前にメモリ使用量を必ず計測すること。
レンダー時間の目安
| 動画尺 | 解像度 | concurrency | 目安レンダー時間 |
|---|---|---|---|
| 15秒 | 1080×1080 | 2 | 30〜60秒 |
| 30秒 | 1920×1080 | 4 | 45〜90秒 |
| 60秒 | 1920×1080 | 8 | 60〜120秒 |
コンポジションの複雑さ・外部アセット取得の有無・サーバースペックで大きく変わる。本番前に必ず自分のコンポジションで計測すること。
コールドスタートの扱い
bundle()は起動時のみ実行し、結果を使い回す。リクエストごとに呼んでいると1リクエストに数十秒の遅延が乗り続ける。常時起動サーバーなら問題ないが、コンテナを動的にスケールする構成では起動スクリプトにバンドル生成を組み込んでおく。
コスト感
t3.medium(2 vCPU・4GB RAM)のオンデマンド料金は約$0.03〜$0.04/時間だ。concurrency: 2で60秒動画を処理すると1時間に120本前後の処理が可能で、1本あたりの計算コストは$0.001未満になる。スポットインスタンスを使えば60〜80%のコスト削減が見込める。
よくある質問
Q: AWS Lambdaなどサーバーレス環境でrenderMediaは動きますか?
標準のLambdaでも短い動画なら動作しますが、実行時間15分・エフェメラルストレージ10GBの制約があります。Remotionは@remotion/lambdaパッケージを公式提供しており、複数Lambda関数への分散レンダリングが可能です。長尺動画や高スループットが必要な場合はこちらを検討してください。
Q: @remotion/rendererが対応しているNode.jsのバージョンは? Node.js 16以上が必要です。本番環境ではNode 18 LTSまたはNode 20 LTSが推奨されます。奇数バージョン(非LTS)は本番で避けてください。
Q: 複数の動画を並列生成するときにメモリ不足にならないようにするには? ジョブレベルの同時実行数をBullMQやp-queueなどで制御してください。各renderMediaジョブ内部ですでに並列処理が行われているため、ジョブ自体を多重起動するとメモリ消費が急増します。サーバースペックに合わせてジョブ同時実行数は2〜4本を目安にしてください。
Q: inputPropsに画像URLを渡した場合、レンダー時に実際にダウンロードされますか?
はい、ヘッドレスChromiumがレンダリング中に外部URLを取得します。遅いホストからの取得はレンダー時間増加やタイムアウトの原因になります。本番では高速なCDNに素材を置くか、あらかじめローカルにダウンロードしてパスを渡すことを推奨します。プロジェクトにバンドルする素材にはstaticFile()を使ってください。
Q: cronジョブからレンダーを定期実行するにはどうすればいいですか? node-cronやサーバーのcrontabでレンダー関数を定期呼び出しするだけです。ジョブが完了したらS3などに出力し、完了後の通知やDB更新も同じスクリプト内で行うのが最もシンプルです。
Q: レンダーが失敗したときの再試行はどう実装しますか? renderMediaをtry/catchで囲み、失敗時はジョブレコードにエラー内容と試行回数を記録します。最大リトライ数(例: 3回)を設けてトランジェントエラー(一時的なネットワーク障害・Chromiumのクラッシュなど)は自動リトライし、上限を超えたジョブはデッドレターキューに移してアラートを送る構成が実運用で安定しています。
RenderCompのテンプレートで自動化を加速する
レンダーサーバーの構築はこのガイドの通りに進められる。残る課題は「何を自動化するか」——つまり、どんなコンポジションを量産するかだ。
RenderCompのアニメーションテンプレートライブラリは、inputPropsを前提に設計された本番品質のRemotionコンポジション集だ。ローワーサード、キネティックテキスト、データ可視化、商品ショーケース、SNS向けアニメーションなど多数のテンプレートが揃っており、すべてTypeScriptの型定義とdefaultProps、サンプルレンダースクリプト付きで提供される。
パイプラインにドロップインして、データソースを接続するだけで自動動画生成が動き出す。テンプレートを一から書く時間を、ビジネスロジックの構築に使ってほしい。
rendercomp.com でテンプレートを確認する。