Remotionテンプレートのホワイトラベル販売チェックリスト
執筆: RenderComp チーム 編集方針
Remotionがビデオを「フレーム番号の純粋関数」として扱う設計は、テンプレートを製品として流通させるうえで本質的な利点をもたらします。モーションロジックはブランドに依存しません。useCurrentFrame() が返す数値だけを入力として計算が完結するため、アニメーションのエンジン部分とクライアントのブランドアセットを分離して管理できます。
代理店がこの分離を正しく設計しておけば、同一テンプレートを複数のクライアントブランドへ再販することが技術的に自然な流れになります。しかし「動いた」だけでは不十分です。ライセンス上の権利関係、フォントの扱い、アセットの帰属、クライアントへの成果物の渡し方。これらを体系立てずに進めると、後になってトラブルの火種になります。
本記事では、Remotionテンプレートのホワイトラベル再販を実務レベルで設計するためのチェックリストを、コードを中心に解説します。「動画をコードで作る」という前提から自然に導かれる実装パターンと、その背後にある契約上・ライセンス上の判断基準を合わせて示します。
1. ブランドトークンをPropsとして型定義する
ホワイトラベル設計の出発点は、「何がブランド固有か」を型として明示することです。ブランドカラー、フォントファミリー、ロゴのパス、コピーライト表記。これらをTypeScriptのインターフェースに集約し、テンプレートの他の部分から切り離します。
// src/brand/types.ts
export interface BrandToken {
/** プライマリカラー(例: "#0055FF") */
primary: string;
/** セカンダリカラー */
secondary: string;
/** 背景色 */
background: string;
/**
* staticFile() で解決可能なロゴパス。
* 例: "clients/acme/logo.png"
* public/ 配下のパスを渡すこと。
*/
logoSrc: string;
/** フォントファミリー名。テンプレート同梱フォントのみ指定可能 */
fontFamily: string;
/** 著作権表記(例: "© 2026 ACME Corp.") */
copyright: string;
}
/** 開発・テスト用のデフォルト値 */
export const defaultBrand: BrandToken = {
primary: "#2563EB",
secondary: "#7C3AED",
background: "#0F172A",
logoSrc: "brand/default/logo.svg",
fontFamily: "Inter",
copyright: "© 2026 RenderComp",
};
fontFamily フィールドに注意してください。テンプレートに同梱されていないフォント名を渡しても、ブラウザプレビューでは偶然インストール済みフォントが使われて「動いてしまう」ことがあります。しかしサーバー側の renderMedia() は純粋なヘッドレス環境であり、システムフォントへの依存はクロスプラットフォームで描画の崩れを招きます。フォントの同梱戦略は後述します。
2. Compositionのスキーマで入力を型安全にする
Remotion 4.0以降、<Composition> にZodスキーマを渡すことで、Studio UIとレンダラーの両方で型検証が走ります。ホワイトラベルテンプレートでは、この仕組みを BrandToken の注入経路として活用できます。
// src/Root.tsx
import { Composition } from "remotion";
import { z } from "zod";
import { BrandedIntro } from "./BrandedIntro";
import { defaultBrand } from "./brand/types";
const brandSchema = z.object({
primary: z.string().regex(/^#[0-9A-Fa-f]{6}$/),
secondary: z.string().regex(/^#[0-9A-Fa-f]{6}$/),
background: z.string().regex(/^#[0-9A-Fa-f]{6}$/),
logoSrc: z.string().min(1),
fontFamily: z.string().min(1),
copyright: z.string(),
});
export const RemotionRoot: React.FC = () => (
<Composition
id="BrandedIntro"
component={BrandedIntro}
durationInFrames={150} // 5秒 @ 30fps
fps={30}
width={1920}
height={1080}
schema={brandSchema}
defaultProps={defaultBrand}
/>
);
スキーマを定義しておくと、クライアントから渡された設定ファイルをレンダー前にバリデーションできます。#GGGGGG のような不正なカラーコードや空のロゴパスを受け取った際に、レンダーが途中で失敗するのではなく、入力検証の段階でエラーを返せます。これはCI/CDパイプラインに組み込む際に特に重要です。
3. モーションエンジンとブランドレイヤーを分離する
テンプレートのコアとなるアニメーションロジックは、ブランドトークンを直接参照しないように設計します。BrandToken をPropsとして受け取るコンポーネントを薄いラッパーとし、その内側でモーションエンジンを呼び出します。
// src/BrandedIntro.tsx
import {
AbsoluteFill,
Img,
Sequence,
staticFile,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import { spring, interpolate } from "remotion";
import type { BrandToken } from "./brand/types";
// モーションエンジン: BrandTokenを知らない
const LogoReveal: React.FC<{
frame: number;
fps: number;
src: string;
}> = ({ frame, fps, src }) => {
// stiffness=200, damping=26 は臨界制動に近い設定。
// 約20フレーム(0.67秒 @ 30fps)で安定し、オーバーシュートはほぼ発生しない。
// damping=10 まで下げると明確なバウンスが現れる。
const scale = spring({
frame,
fps,
config: { stiffness: 200, damping: 26, mass: 1 },
from: 0,
to: 1,
});
const opacity = interpolate(frame, [0, 10], [0, 1], {
extrapolateRight: "clamp",
});
return (
<Img
src={staticFile(src)}
style={{
width: 240,
height: "auto",
transform: `scale(${scale})`,
opacity,
}}
/>
);
};
// ブランドレイヤー: BrandTokenをモーションに渡す
export const BrandedIntro: React.FC<BrandToken> = ({
primary,
secondary,
background,
logoSrc,
fontFamily,
copyright,
}) => {
const frame = useCurrentFrame();
const { fps, durationInFrames } = useVideoConfig();
// コピーライトのフェードアウト: 最後の20フレームで消える
const copyrightOpacity = interpolate(
frame,
[durationInFrames - 20, durationInFrames - 5],
[1, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
return (
<AbsoluteFill style={{ background, fontFamily }}>
<Sequence from={0}>
<AbsoluteFill
style={{ justifyContent: "center", alignItems: "center" }}
>
<LogoReveal frame={frame} fps={fps} src={logoSrc} />
</AbsoluteFill>
</Sequence>
{/* グラデーションオーバーレイ */}
<AbsoluteFill
style={{
background: `linear-gradient(135deg, ${primary}22 0%, ${secondary}22 100%)`,
pointerEvents: "none",
}}
/>
{/* コピーライト: 右下に固定 */}
<AbsoluteFill
style={{
justifyContent: "flex-end",
alignItems: "flex-end",
padding: 32,
opacity: copyrightOpacity,
}}
>
<span style={{ color: "#ffffff99", fontSize: 14 }}>{copyright}</span>
</AbsoluteFill>
</AbsoluteFill>
);
};
LogoReveal は src・frame・fps しか受け取りません。このコンポーネントを独立してテストする際、ブランドトークン全体をモックする必要がありません。stiffness と damping の組み合わせはコメントに記録しておき、クライアントから「もっと弾ませてほしい」と要望があった際に根拠を持って変更できる状態にしておきます。
4. staticFile()とフォント同梱の注意点
staticFile("path/to/asset.png") は、Remotionのバンドラーが public/ ディレクトリを基準にパスを解決します。開発時は localhost:3000/path/to/asset.png、バンドル後は相対パスまたはbase64埋め込みになります。
ホワイトラベル運用では、クライアントごとにアセットを差し替える必要があります。推奨するディレクトリ構造は次のとおりです。
public/
brand/
default/
logo.svg
Inter-Regular.woff2
Inter-Bold.woff2
acme/
logo.svg
AcmeSans-Regular.woff2 # クライアント提供フォント(ライセンス確認必須)
globex/
logo.svg
_licenses/
acme/
AcmeSans-LICENSE.pdf # クライアント提供ライセンス書類
default/
OFL.txt # OFLフォントのライセンス
フォントについて重要な点があります。外部CDNからフォントを参照する方法(CSSの @import で外部URLを指す書き方 等)は、ヘッドレスレンダリング環境で確実に失敗します。サーバー側の renderMedia() はブラウザを持たず、外部リソースへのネットワークアクセスは設計上の問題です。フォントは必ず woff2 形式でバンドルし、@font-face でロードします。
// src/load-fonts.ts
import { continueRender, delayRender, staticFile } from "remotion";
/**
* フォントを事前ロードする。
* コンポーネントのuseEffect内、またはwaitForFonts()パターンで呼び出す。
*/
export async function loadBrandFont(
fontFamily: string,
woff2Path: string,
weight: 400 | 700 = 400
) {
const handle = delayRender(`Loading font: ${fontFamily} weight=${weight}`);
try {
const font = new FontFace(
fontFamily,
`url(${staticFile(woff2Path)}) format("woff2")`,
{ weight: String(weight) }
);
await font.load();
document.fonts.add(font);
} finally {
// エラー時もcontinueRenderを呼ばないとフリーズするため finally で呼ぶ
continueRender(handle);
}
}
delayRender() / continueRender() のペアは、Remotionのフレームキャプチャタイミングと非同期処理を同期させる仕組みです。フォントロードが完了する前にフレームがキャプチャされると、フォントフォールバックで描画されたフレームが出力に含まれます。finally ブロックで必ず continueRender を呼ぶことで、ネットワーク系のエラーが発生した場合でもレンダープロセスがフリーズしないようにします。
5. bundle()とrenderMedia()によるクライアント別ビルド
ZodスキーマによるStudio UIの型検証に加えて、CI/CDパイプラインからレンダラーへブランド設定を渡すスクリプトが必要です。bundle() の結果は単一のディレクトリパスで、複数クライアントのレンダーに再利用できます。
// scripts/render-client.ts
import { bundle } from "@remotion/bundler";
import { renderMedia, selectComposition } from "@remotion/renderer";
import path from "path";
import type { BrandToken } from "../src/brand/types";
async function renderForClient(
clientId: string,
brand: BrandToken,
outputPath: string
): Promise<void> {
// バンドルはクライアントをまたいで再利用可能。
// 本番では事前にbundle()した結果をキャッシュしてからループに入る。
const bundleLocation = await bundle({
entryPoint: path.resolve("./src/index.ts"),
});
const composition = await selectComposition({
serveUrl: bundleLocation,
id: "BrandedIntro",
inputProps: brand,
});
await renderMedia({
composition,
serveUrl: bundleLocation,
codec: "h264",
outputLocation: outputPath,
inputProps: brand,
// SNSとWebブラウザの互換性を確保するための標準設定
pixelFormat: "yuv420p",
onProgress: ({ progress }) => {
process.stdout.write(`\r[${clientId}] ${Math.round(progress * 100)}%`);
},
});
console.log(`\nRendered: ${outputPath}`);
}
// 複数クライアントを直列に処理する例
const clients: Array<{ id: string; brand: BrandToken; output: string }> = [
{
id: "acme",
brand: {
primary: "#E63946",
secondary: "#457B9D",
background: "#1D3557",
logoSrc: "brand/acme/logo.svg",
fontFamily: "AcmeSans",
copyright: "© 2026 ACME Corp.",
},
output: "./output/acme-intro.mp4",
},
{
id: "globex",
brand: {
primary: "#00B4D8",
secondary: "#0077B6",
background: "#03045E",
logoSrc: "brand/globex/logo.svg",
fontFamily: "Inter",
copyright: "© 2026 Globex Industries",
},
output: "./output/globex-intro.mp4",
},
];
for (const client of clients) {
await renderForClient(client.id, client.brand, client.output);
}
selectComposition() と renderMedia() の両方に inputProps を渡すのは冗長に見えますが、意味が異なります。selectComposition() はスキーマバリデーションと calculateMetadata の実行に使い、renderMedia() は実際のレンダー時に各コンポーネントへPropsを注入します。両方に渡さないと、スキーマ検証は通るのにレンダー時のPropsが defaultProps に戻るという挙動になります。
6. calculateMetadataを使った動的デュレーション
クライアントのコンテンツ量(テキスト行数、スライド枚数等)によって動画の長さが変わる場合、calculateMetadata を使って durationInFrames を動的に決定できます。この仕組みは selectComposition() 経由でサーバーサイドでも正しく実行されます。
// src/Root.tsx の追加例
import { Composition } from "remotion";
import type { CalculateMetadataFunction } from "remotion";
import { z } from "zod";
const slideSchema = z.object({
brand: z.object({
primary: z.string(),
secondary: z.string(),
background: z.string(),
logoSrc: z.string(),
fontFamily: z.string(),
copyright: z.string(),
}),
slides: z.array(z.string()).min(1).max(20),
secondsPerSlide: z.number().min(2).max(10).default(4),
});
type SlideProps = z.infer<typeof slideSchema>;
const calculateSlideDuration: CalculateMetadataFunction<SlideProps> = async ({
props,
}) => {
const fps = 30;
// スライド間トランジションに0.5秒(15フレーム)を追加
const framesPerSlide = Math.ceil(props.secondsPerSlide * fps);
const transitionFrames = 15;
const totalFrames =
props.slides.length * framesPerSlide +
(props.slides.length - 1) * transitionFrames;
return { durationInFrames: totalFrames, fps };
};
5スライドのクライアントと12スライドのクライアントが同一テンプレートを使いつつ、レンダー長が自動で変わります。calculateMetadata は非同期関数なので、外部APIからスライドデータを取得してデュレーションを計算するような処理も書けますが、その場合はタイムアウトに注意します。
7. ホワイトラベルライセンスチェックリスト
技術設計が整ったところで、契約・権利関係の確認事項を整理します。これらはすべて実装判断に影響します。
7-1. サードパーティライブラリのライセンス確認
# 本番依存のライセンス一覧を出力
npx license-checker --production --json > _licenses/npm-licenses.json
MITライセンスのライブラリはホワイトラベル再販で問題ありません。GPLv3やAGPLを含むパッケージは成果物への組み込みが再販用途に制限される場合があるため、出力JSONを精査します。Remotion本体のライセンスは会社の規模に応じた商用ライセンスが必要で、詳細は公式サイトを参照してください(本記事では価格詳細を記載しません)。
7-2. フォントライセンスの確認方針
| フォント取得元 | ホワイトラベル再販 | 備考 |
|---|---|---|
| クライアント提供(自社所有) | 可 | ライセンス書類を _licenses/ に保管 |
| SIL Open Font License (OFL) | 可 | OFL.txt をバンドルに同梱 |
| システムフォント | 非推奨 | ヘッドレス環境で再現性なし |
| 商用フォントサービス | 条項次第 | サーバーサイドレンダリングの可否を確認 |
OFLフォントを同梱する場合、_licenses/<fontname>/OFL.txt のようにライセンスファイルを必ず保管します。成果物の mp4 自体にフォントは埋め込まれませんが、ソースコードとして配布する場合はライセンス文書の同梱が必要です。
7-3. クライアントへの成果物の引き渡し形式
クライアントに渡す成果物が「レンダー済みmp4のみ」か「Remotionプロジェクトごと」かで、権利関係が大きく変わります。
- mp4のみ渡す場合、モーションロジックはブラックボックスのまま保たれ、知的財産は代理店に留まります。
- Remotionプロジェクトを渡す場合、クライアントが内部実装を参照・改変できるため、ライセンス条項でリバースエンジニアリングや再販を明示的に禁止する必要があります。
- npmパッケージとして配布する場合、
package.jsonのprivate: trueと.npmrcでスコープとレジストリを限定し、意図しない再配布を防ぎます。
8. 納品前の最終確認リスト
実装とライセンス確認が完了したら、クライアント納品前に以下を確認します。
-
BrandToken型のすべてのフィールドにデフォルト値がある(defaultPropsがundefinedにならない) -
staticFile()で参照するすべてのパスがpublic/配下に実際に存在する - フォントは
delayRender()/continueRender()で確実にロード完了してからキャプチャされる -
finallyブロックでcontinueRender()を呼び、エラー時のフリーズを防止している -
npx license-checker --productionを実行し、GPLv3/AGPLパッケージがない - クライアント提供フォントのライセンス書類を
_licenses/に保管している - 渡す成果物の形式(mp4 / ソース)をライセンス条項に明記している
-
renderMedia()を本番環境(CI)で一度実行し、ヘッドレス環境でのフォント描画を確認している -
pixelFormat: "yuv420p"を指定し、SNSプラットフォームとの互換性を確保している -
selectComposition()とrenderMedia()の両方にinputPropsを渡している
Wrapping Up
Remotionがフレームを純粋関数として扱う設計は、ホワイトラベルテンプレートの実装と対応しています。BrandToken 型にブランド固有の値を集約し、モーションエンジンとブランドレイヤーを分離することで、同一のアニメーションロジックを複数クライアントへ安全に展開できます。
ライセンス設計は後付けできません。fontFamily フィールドが指す先のフォントライセンス、bundle() に含まれるサードパーティライブラリの権利関係、クライアントへの引き渡し形式。これらの決定は実装の初期段階で行い、契約書に反映させます。フォントの delayRender() 漏れや inputProps の二重渡し忘れといった細部が、本番レンダー環境で初めて顕在化することも多いため、CIでの自動レンダーテストを早期に整備することを推奨します。
RenderComp カタログのようなテンプレートをホワイトラベルで活用する場合でも、ここで示した BrandToken 型の定義とアセット管理の構造は変わりません。クライアントごとの設定ファイルをJSONで管理し、bundle() → selectComposition() → renderMedia() を叩くパイプラインを整備する。これが再現性の高い代理店向けビデオ制作の基本形です。