
はじめに
個人開発者がSNSに投稿するデモ動画を作るとき、選択肢は限られている。Mac専用のデスクトップアプリは高機能だが、Windowsユーザーは使えない。サブスク課金に移行するツールも増えている。
「ブラウザだけで、映えるデモ動画を無料で作れるツールがあれば」——この課題意識から、ZERONOVA LABの89番目の無料ツールとしてデモ動画作成ツールを開発した。
この記事では、ツールの中核となる3層アーキテクチャの設計判断を記録する。特に「装飾処理をFFmpegでやるか、Canvasでやるか」という最大の分岐点について、なぜCanvas方式を選んだのかを詳しく解説する。開発後にドッグフーディングで見つけたUI/UX改善については別記事で書いている。
課題: FFmpegフィルターグラフの複雑さ
当初の設計では、FFmpeg WASMにすべてを任せる方針だった。カット・早送り・結合だけでなく、角丸やドロップシャドウといった装飾もFFmpegのフィルターグラフで処理する想定だ。
2026年2月23日の開発日記にはこう書いている。
FFmpeg で角丸(
alphamergeマスク)やドロップシャドウを再現するには巨大なフィルターグラフが必要。Canvas API ならroundRect・shadowBlurがネイティブで使える。
FFmpegで角丸を実現するには、アルファマスクを生成して alphamerge フィルターで合成し、さらにドロップシャドウを drawbox や独自フィルターチェーンで重ねる必要がある。フィルターグラフが10行、20行と膨らんでいく。しかもこの複雑なフィルターグラフのデバッグは、エラーメッセージが不親切なFFmpegでは苦行に近い。
もう一つの問題は「プレビューとエクスポートの乖離」だった。CSSでプレビューを描画し、FFmpegフィルターでエクスポートする方式では、両者の見た目が微妙にずれるリスクがある。角丸の半径、シャドウの広がり、色の再現——すべてが二重管理になる。
設計判断: 3層アーキテクチャへの転換
roundRect と shadowBlur がネイティブで使えるから、そっちに切り替えたい。captureStream() でMediaRecorderに渡せば、「プレビュー画面 = エクスポート結果」になります。FFmpegは前処理(カット・早送り・結合)に専念させる分担が最適です。この議論を経て、以下の3層アーキテクチャに落ち着いた。
第1層: FFmpeg WASM(前処理)
FFmpeg WASMが担当するのは、動画ファイルの「素材加工」だ。
- カット: タイムラインで選択した区間を切り出す
- 早送り: 指定倍速(2x, 5x)でセグメントを加速
- 結合: 複数セグメントを
concatdemuxerで1本に結合
これらはFFmpegが最も得意とする処理であり、Canvas APIでは実現が困難な領域だ。既にセルフホスト済みのFFmpeg WASM LGPLビルド(/public/ffmpeg/)をそのまま共用している。
第2層: Canvas API(装飾・描画)
装飾処理はすべてCanvas APIが担当する。
- 角丸:
roundRect()でクリッピング - ドロップシャドウ:
shadowBlur/shadowColor/shadowOffsetX,Y - ズーム演出:
scale()+translate()でキーフレームベースのズームイン・アウト - 背景: グラデーションやウォールペーパー風の背景をCanvasに描画
ブラウザネイティブのAPIだけで、CSSと同等の装飾がすべて実現できる。追加ライブラリは不要だ。
第3層: MediaRecorder(キャプチャ・出力)
Canvas上の描画を動画ファイルとして出力する層だ。
// Canvas → MediaRecorder → Blob の基本フロー
const stream = canvas.captureStream(fps);
const recorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
captureStream() でCanvasの出力をストリーム化し、MediaRecorderで録画する。プレビュー画面で見ている映像がそのまま動画ファイルになる。
実装で直面した3つの問題
1. エクスポート解像度の罠
最初の実装では、プレビュー用Canvasのサイズをそのまま録画していた。画面上では綺麗に見えるが、出力される動画は480pや720p相当の低解像度になる。
解決策として、エクスポート時には動画の元解像度に基づいて別の高解像度Canvasを作成する方式に変更した。プレビュー用と出力用でCanvasを分離することで、表示の軽さと出力品質を両立できた。
2. ObjectURLのメモリリーク
動画ファイルのインポートやエクスポート結果の生成で URL.createObjectURL() を使うが、コンポーネントのアンマウント時に revokeObjectURL() を呼ばないとメモリが蓄積する。
// useRef で最新のURLを追跡し、unmount時に一括revoke
const videoSrcUrlRef = useRef<string | null>(null);
useEffect(() => {
return () => {
if (videoSrcUrlRef.current) {
URL.revokeObjectURL(videoSrcUrlRef.current);
}
};
}, []);
開発日記には「ObjectURL アンマウント時 revoke(videoSrcUrlRef / exportResultUrlRef)」とガイドライン準拠修正として記録している。
3. Canvasピクセル上限
ブラウザにはCanvasの最大ピクセル数に制限がある。Chromeは約16MP、Firefoxは約11MP。安全マージンとして MAX_EXPORT_PIXELS = 8,000,000 を設定した。
ブラウザごとの制限(Chrome 16MP、Firefox 11MP)を考慮して 8MP を安全マージンとして設定
この上限を超えるとCanvasの描画が無効になり、真っ黒な動画が出力される。エラーメッセージも出ないため、防御的なバリデーションが必須だった。
MP4エクスポート: WebCodecs APIの採用
3層アーキテクチャのMediaRecorder層はWebM(VP9)での出力に対応するが、SNS投稿にはMP4(H.264)が必須だ。X(Twitter)はWebMの埋め込み再生をサポートしておらず、ダウンロードリンクになってしまう。
当初の仕様では「FFmpeg WASMでWebM→MP4変換」を想定していたが、別のアプローチを検討した。
prefer-hardware)を指定すればGPUで高速エンコードが可能です。mp4-muxer(MITライセンス)と組み合わせてMP4コンテナにマルチプレクスする方式なら、FFmpegを経由するよりも高速・省メモリで済みます。この議論を経て、WebCodecs API + mp4-muxerの組み合わせを採用した。
WebCodecs API が H.264 ハードウェアエンコードをネイティブサポートしており、FFmpeg.wasm を経由するよりも高速・省メモリ
VideoEncoder でCanvasからH.264フレームを直接生成し、mp4-muxer でMP4コンテナにマルチプレクスする。ハードウェアアクセラレーション(prefer-hardware)指定により、エンコード速度が大幅に向上した。
ただし、WebCodecs APIには注意点がある。isConfigSupported() が "supported" を返しても、実際のハードウェアエンコーダーが解像度上限を超えるとクラッシュする。APIの「対応」と「実動作」の乖離は防御的プログラミングで補う必要がある。
学んだこと
「役割分担」が複雑さを制御する
3層アーキテクチャの本質は「各層が得意なことだけをやる」という分担だ。FFmpegは動画の前処理、Canvasは描画と装飾、MediaRecorderはキャプチャ。この分離により、どの層で問題が起きているかの切り分けが容易になった。
ブラウザAPIだけで十分な装飾力がある
Canvas APIの roundRect、shadowBlur、scale + translate で、デスクトップアプリと遜色ない装飾が実現できる。「ブラウザでは無理」という先入観を捨てると、選択肢が広がる。
プレビュー ≠ エクスポートのリスクは設計で排除する
二重管理が発生する設計は、いずれ差異が問題になる。Canvas方式は「プレビューで見ている画面をそのまま録画する」ことで、このリスクを構造的に排除した。
この3層アーキテクチャは、デモ動画に限らず「ブラウザで映像を加工・出力する」用途に広く応用できる。ゲームのリプレイ録画、アニメーション生成、インタラクティブな動画エディターなど、Canvas + MediaRecorderの組み合わせを知っているとブラウザでの動画生成の可能性が大きく広がる。
Zeronova(ゼロノバ)
Product Manager / AI-Native Builder
Web/IT業界19年以上・20以上のWebサービスを担当したPdM。東証プライム上場企業の子会社代表として事業経営を経験。現在はAIを駆使して企画から実装まで完結させる個人開発を実践中。