ブラウザで動くFFmpeg:DojoClip Render Lab 技術解説

Render Labへようこそ。ここではDojoClipを高速・プライベート・クリエイター向けに保つ技術を掘り下げます。今回は概要ではなく実装に踏み込み、FFmpegをWebAssembly(WASM)にコンパイルした仕組み、その得意・不得意、そしてMediaRecorderやWebCodecsとハイブリッドで連携させる低レイテンシーワークフローを紹介します。
TL;DR
- FFmpeg WASMはローカル処理と幅広い機能(フィルタ、トリム、リマックス)を両立。
- WebCodecsはハードウェア支援による最小レイテンシーのエンコード・デコードを提供するが、マルチプレクサが必要。
- MediaRecorderはリアルタイムキャプチャに最適。プレビュー用途には十分だが制御の幅は狭い。
- DojoClipはハイブリッド構成:精密な変換はWASM、スピード重視のプレビューと書き出しはWebCodecsとMediaRecorder。
コンセプト(2分でおさらい)
- コンテナ vs コーデック:MP4、MKV、WebMはコンテナ。H.264、H.265、VP9、AV1、AAC、Opusはコーデック。編集はデコード→フィルタ→エンコード→マルチプレックスの流れ。
- トランスコード vs リマックス:トランスコードは再エンコード(品質とサイズのトレードオフ)。リマックスは圧縮済みストリームに触れずコンテナだけ変える(高速・無劣化)。
- CFR vs VFR:Constant / Variable Frame Rate。Web録画はVFRになりがち。編集ではフレーム単位で扱いやすいCFRが好まれることも。
- キーフレーム(IDR):キーフレームに合わせないと無劣化でのカットは難しい。再エンコードやスマートレンダリングが必要。
- CRFとビットレート:品質調整ノブ。CRF値が低いほど高品質&大容量。ビットレートは配信ターゲット向けに上限を指定。
アーキテクチャ概要
[File Input]
└─▶ Browser FS (OPFS / RAM)
├─▶ FFmpeg WASM worker (precision filters, remux, waveform)
├─▶ WebCodecs (fast decode/encode; preview/export)
└─▶ MediaRecorder (real-time canvas/tab capture)
▼
[Muxer] → MP4/WebM → Download / OPFS / Upload
なぜWASM?
FFmpeg CLIの大半をブラウザに持ち込めるからです。フレーム精度のトリム、複雑なフィルタグラフ、オーディオチャンネル操作などをローカルで実行でき、プライバシーに敏感なメディアでも安心です。
なぜWASM“だけ”ではないのか?
長時間のエンコードはメモリとCPUを圧迫し、初回ロードも数MB。スレッドやSIMDを使うにはクロスオリジン分離ヘッダーが必要です。
WebCodecsとMediaRecorderを併用する理由
WebCodecsはプラットフォームのハードウェアエンコーダ/デコーダを直接活用でき、超低レイテンシー。MediaRecorderはライブキャプチャや簡易プロキシ作成がワンクリックで済みます。
FFmpeg WASMを“正しく”配備する
1. ThreadsとSIMDを有効化(現実的な速度向上)
ワークロードによって1.5〜3倍のスピードアップが見込めます。COOP/COEPなどのヘッダー設定が前提です。
// next.config.js など
const securityHeaders = [
{ key: 'Cross-Origin-Opener-Policy', value: 'same-origin' },
{ key: 'Cross-Origin-Embedder-Policy', value: 'require-corp' },
{ key: 'Cross-Origin-Resource-Policy', value: 'same-site' },
];
module.exports = {
async headers() {
return [{ source: '/:path*', headers: securityHeaders }];
},
};
2. Workerでコアを読み込む
UIスレッドのブロッキングを避け、FFmpegのメモリを隔離します。
// ffmpeg.worker.ts
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
const ffmpeg = createFFmpeg({ log: true, corePath: '/wasm/ffmpeg-core.js' });
self.onmessage = async (event) => {
const { name, file, args } = event.data;
if (!ffmpeg.isLoaded()) {
await ffmpeg.load();
}
ffmpeg.FS('writeFile', name, await fetchFile(file));
await ffmpeg.run(...args);
const out = ffmpeg.FS('readFile', 'out.bin');
self.postMessage({ ok: true, data: out.buffer }, [out.buffer]);
};
// UI側
const worker = new Worker(new URL('./ffmpeg.worker.ts', import.meta.url));
worker.postMessage({
name: 'in.mp4',
file,
args: ['-i', 'in.mp4', '-vn', '-acodec', 'copy', 'out.bin'],
});
worker.onmessage = ({ data }) => {
const blob = new Blob([data.data], { type: 'audio/mp4' });
download(blob, 'audio.m4a');
};
3. 大きな中間ファイルはOPFSへ保存
Origin Private File Systemを使えばRAMを圧迫せずに済みます。
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('clip.m4a', { create: true });
const writer = await fileHandle.createWritable();
await writer.write(blob);
await writer.close();
日常タスク向けFFmpegレシピ
音声抽出(AACならビット同一)
ffmpeg -i input.mp4 -vn -acodec copy audio.m4a
ダウンスケール+CRFトランスコード(H.264プロキシ)
ffmpeg -i input_4k.mp4 -vf scale=-2:1080 -c:v libx264 -preset veryfast -crf 23 -c:a aac -b:a 160k out_1080p.mp4
フレーム精度の区間抽出(短時間のみ再エンコード)
ffmpeg -ss 00:00:12.300 -to 00:00:19.000 -i input.mp4 -c:v libx264 -crf 20 -pix_fmt yuv420p -c:a aac slice.mp4
タイムライン用の波形PNG
ffmpeg -i audio.m4a -lavfi showwavespic=s=1200x200:colors=white waveform.png
WASMでも ffmpeg.run(...)
に同じ引数を渡します。リマックス(無再エンコード)の場合は -c copy
を維持すると品質も速度も◎。
WebCodecs:最速レーン
WebCodecsはプラットフォームのデコーダ・エンコーダに直接アクセスできます。エンコード済みチャンクをMP4やWebMにまとめるマルチプレクサが別途必要です。
const fps = 30;
const encoder = new VideoEncoder({
output: handleChunk,
error: console.error,
});
encoder.configure({
codec: 'avc1.42E01E',
width: canvas.width,
height: canvas.height,
bitrate: 3_000_000,
framerate: fps,
});
let t0 = performance.now();
let frameIndex = 0;
const track = canvas.captureStream(fps).getVideoTracks()[0];
const reader = new MediaStreamTrackProcessor({ track }).readable.getReader();
async function pump() {
const { value: frame, done } = await reader.read();
if (done) {
await encoder.flush();
muxer.finalize();
return;
}
const timestamp = Math.floor((performance.now() - t0) * 1000);
const videoFrame = new VideoFrame(frame, { timestamp });
encoder.encode(videoFrame, { keyFrame: frameIndex % (fps * 2) === 0 });
frameIndex += 1;
videoFrame.close();
frame.close();
pump();
}
function handleChunk(chunk) {
muxer.addVideoChunk(chunk);
}
ポイント:WebCodecsはエレメンタリーストリームを出力します。ブラウザ向けのマルチプレクサライブラリでダウンロード用ファイルを生成しましょう。プレビューならMedia Source Extensionsで video
要素にストリーミングも可能です。
FFmpeg WASMと比べて、ハードウェア支援が効く環境ではエンコードレイテンシーが短く、CPU使用率も低めです。
MediaRecorder:ゼロ手間のプロキシ
const stream = canvas.captureStream(30);
const recorder = new MediaRecorder(stream, { mimeType: 'video/webm;codecs=vp9' });
const chunks = [];
recorder.ondataavailable = (event) => {
if (event.data.size) {
chunks.push(event.data);
}
};
recorder.onstop = () => {
const blob = new Blob(chunks, { type: recorder.mimeType });
download(blob, 'preview.webm');
};
recorder.start();
// ... フレームを描画 ...
recorder.stop();
メリット:圧倒的に簡単。プレビューやSNS向けプロキシに最適。デメリット:GOPやCRFなど細かい制御は不可。
最適なパイプラインの選び方
タスク | 精度 | レイテンシー目標 | 推奨 |
---|---|---|---|
音声抽出・リマックス | 高 | 低 | FFmpeg WASM (-c copy ) |
波形・サムネ生成 | 高 | 低 | FFmpeg WASM (filters) |
フレーム精度カット | 高 | 中 | FFmpeg WASM(必要箇所だけ再エンコード) |
ライブプレビュー/画面キャプチャ | 中 | 超低 | MediaRecorder |
キャンバスタイムラインの最終書き出し | 中 | 低 | WebCodecs+マルチプレクサ |
長時間のオフライン変換 | 中 | 高 | ネイティブ or サーバー処理 |
メモリ計算(描画前に計画を)
- デコード済みフレームサイズ ≒ 幅 × 高さ × 1.5 バイト(YUV420p)。例:1920×1080 ≒ 3.1MB。120フレーム保持すると約372MB。
- WASMヒープはリサンプルやスケーラーなどで急増。中間結果は可能な限りOPFSへ退避し、ストリーミング処理を意識。
- 音声:48kHzステレオ16bit PCMは約192KB/秒。5分で57MB程度。メモリ保持し続けるなら要注意。
実践ヒント
- 可能な限りリマックス(
-c copy
)を選び品質と時間を節約。 - 長いジョブはセグメントに分割し、OPFSへ保存→FFmpegで連結。
- プレビューはWebCodecsで処理し、JavaScript側で未圧縮フレームを抱えない。
再現可能なベンチマーク(公開前に計測を)
クリーンな本番ビルドで実行し、デバイスとブラウザのバージョンを記録しましょう。
-
音声抽出(リマックス)
- 入力:MP4(H.264+AAC)、1〜2分。
- コマンド:
-i in.mp4 -vn -acodec copy out.m4a
- 指標:処理時間(ミリ秒)、JSヒープ最大値(MB)、WASMヒープ(MB)、出力サイズ。
-
プロキシ変換(4K→1080p)
- コマンド:
-vf scale=-2:1080 -c:v libx264 -preset veryfast -crf 23 -c:a aac -b:a 160k
- 比較:FFmpeg WASM vs WebCodecs(同等のビットレート・フレームレート)。処理時間、CPU使用率、ドロップフレームを記録。
- コマンド:
-
キャンバスタイムライン書き出し
- 方法A:WebCodecsエンコード+MP4マルチプレクサ。
- 方法B:MediaRecorderで30fps録画。
- 指標:エンコードFPS、総時間、出力ビットレート/サイズ、可能なら基準データとのSSIM/PSNR。
テンプレート(N/Aは実測値に置き換え)
Device / Browser | Test | Pipeline | Time (s) | CPU avg (%) | Peak Mem (MB) | Notes |
---|---|---|---|---|---|---|
M2 Pro / Chrome 128 | Audio extract | FFmpeg WASM | Threads + SIMD | |||
M2 Pro / Chrome 128 | 4K to 1080p | FFmpeg WASM | Threads + SIMD | |||
M2 Pro / Chrome 128 | 4K to 1080p | WebCodecs | H.264 HW | |||
Pixel 8 / Chrome | Canvas export | MediaRecorder | VP9 |
計測補助スニペット
const perf = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, entry.duration);
}
});
perf.observe({ entryTypes: ['measure'] });
const memory = performance.memory; // Chrome限定。usedJSHeapSize / totalJSHeapSize
よくあるハマりどころ
- スローモーション書き出し:WebCodecsには単調増加のタイムスタンプを渡す。固定fpsでCFRエンコードするのも有効。
requestAnimationFrame
頼みは危険。 - 音ズレ:音声を基準クロックにし、映像PTSをそのタイムラインに合わせる。必要なら音声をプロジェクトレートにリサンプル。
- 緑フレーム/色ズレ:ピクセルフォーマット(例:RGBA→I420)や色空間情報をマルチプレクサでも一致させる。
- Safariの隙間:WebCodecs対応が部分的。MediaRecorderまたはWASM H.264へフォールバック。
- スレッド無効:
SharedArrayBuffer
が使えない環境(COOP/COEPなし)では、シングルスレッド版WASMコアをロードしてエラー回避。
現行DojoClipのハイブリッド構成
- FFmpeg WASM:音声抽出、波形・サムネ生成、精密トリム、リマックス、字幕準備。
- WebCodecs:キャンバスタイムラインのプレビューと対応環境での最終書き出し。
- MediaRecorder:即席プロキシ、共有用クリップ、タブキャプチャデモ。
- ストレージ:中間データはOPFS、軽いジョブはメモリ。
現在チューニング中のエンコード設定が固まり次第、複数デバイス/ブラウザの実測ベンチマークを続編で公開する予定です。
Appendix A — 安心のデフォルト値
- H.264(Web向け):
-preset veryfast -crf 23 -maxrate 4M -bufsize 8M -pix_fmt yuv420p
- 音声:
-c:a aac -b:a 160k
(音楽)、-b:a 96k
(音声主体) - 高速シーク:フレーム精度不要なら
-ss
を-i input ...
より前へ。必要なら-i input -ss
後ろで。 - 字幕:可能な限り
.srt
や.vtt
のサイドカーファイルで管理。焼き付けは最終納品時に限定。
Appendix B — 機能マップ(抜粋)
機能 | FFmpeg WASM | WebCodecs | MediaRecorder |
---|---|---|---|
精密なフィルタグラフ | ○ | × | × |
ハードウェアアクセラレーション | ×(CPU) | ○ | ○ |
CFR/VFR制御 | ○ | ○ | △ |
リアルタイムキャプチャ | × | ○(デコード/エンコード) | ○ |
MP4マルチプレクス内蔵 | ○ | ×(別途必要) | ×(WebMが一般的) |
導入の易しさ | 中 | 中 | 高 |
触ってみたい方へ
DojoClipのVideo Compressor、Audio Extractor、Subtitle Studioでこのスタックを体験できます。開発者の方は次回公開予定のベンチマークハーネスをクローンして計測し、結果を共有してください。集めたデータは公開テーブルにまとめ、クリエイターが最適なワークフローを選べるようにします。