DojoClip

始める

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

By: Pansa LegrandDate: 2025-02-18Category: Engineering
DojoClip内のFFmpeg WebAssemblyパイプライン図

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側で未圧縮フレームを抱えない。

再現可能なベンチマーク(公開前に計測を)

クリーンな本番ビルドで実行し、デバイスとブラウザのバージョンを記録しましょう。

  1. 音声抽出(リマックス)

    • 入力:MP4(H.264+AAC)、1〜2分。
    • コマンド:-i in.mp4 -vn -acodec copy out.m4a
    • 指標:処理時間(ミリ秒)、JSヒープ最大値(MB)、WASMヒープ(MB)、出力サイズ。
  2. プロキシ変換(4K→1080p)

    • コマンド:-vf scale=-2:1080 -c:v libx264 -preset veryfast -crf 23 -c:a aac -b:a 160k
    • 比較:FFmpeg WASM vs WebCodecs(同等のビットレート・フレームレート)。処理時間、CPU使用率、ドロップフレームを記録。
  3. キャンバスタイムライン書き出し

    • 方法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でこのスタックを体験できます。開発者の方は次回公開予定のベンチマークハーネスをクローンして計測し、結果を共有してください。集めたデータは公開テーブルにまとめ、クリエイターが最適なワークフローを選べるようにします。