はじめに
「無料のオンライン変換ツール」の多くは、だいたい同じ構成をしています。アップロードフォームがあって、キューがあって、裏で ffmpeg や LibreOffice が動くワーカー群があり、最後に「1〜24時間で削除されます」という期限付きのダウンロードリンクが出てくる。動くことは動きますが、アップロード待ち、「1時間に2回まで」といった制限、ファイルサイズ上限、そして何より「パスポートのスキャンがしばらく他人のディスクに置かれる」というコストがあります。
いまのブラウザなら、この処理の大半を自分でこなせます。本記事では、ブラウザだけで完結する変換処理をどう組み立てるか、そしてどこに限界があるかを整理します。題材として、筆者らが開発している Nibbo(動画・画像・音声・PDF を端末内で処理する無料ツール集)の設計を例に使いますが、内容自体はどのプロジェクトにも応用できるはずです。
全体構成:静的サイト+ユーザーのハードウェア
クライアントサイドの変換ツールは、ネットワークから見るとただの静的サイトです。サーバーが配るのは HTML・JS・WebAssembly モジュールだけ。ユーザーがドロップしたファイルは File API で読み込まれ、タブ内(できれば Worker 内)で処理され、Blob としてダウンロードに渡されます。どこにも送信されません。
この構成には副次的なメリットがあります。
- キューがない:ユーザーが各自の CPU/GPU を持ち込むので、同時利用者が増えてもかかるのは CDN 代くらいです。
- 初回ロード後はオフラインで動く:ツールのページを開いてから機内モードにしても変換できれば、ファイルの送り先はそもそも存在しません。
-
プライバシーを検証できる:DevTools の Network タブで
method:POSTに絞って変換を実行すると、サーバー型ならファイルとほぼ同サイズのリクエストが見えます。クライアント型ならページのアセットと数KBの解析リクエスト程度です。
難しいのはすべてクライアント側、つまりコーデック、メモリ、そして制御できないハードウェア上での性能です。
2つのエンジン:WebCodecs と ffmpeg.wasm
ブラウザで動画をトランスコードする現実的な方法は、いまのところ2つです。性格はかなり違います。
WebCodecs:ハードウェアの道
WebCodecs は、ブラウザが動画再生や WebRTC で使っているデコーダ/エンコーダを VideoDecoder / VideoEncoder として公開する API です。多くの環境では GPU やメディアエンジンによるハードウェアエンコードになります。
長所
- 速い。一般的なノートPCでも H.264 のハードウェアエンコードは実時間よりずっと速く進みます。
- フレームは
VideoFrameオブジェクトとして GPU メモリに置いたままにでき、毎フレーム WASM ヒープへコピーする必要がありません。 - CPU 負荷が低く、ファンが唸りにくい。
短所
- 扱うのはコーデックだけで、コンテナは対象外です。MP4/WebM/MKV のデマルチプレクサと、出力用のマルチプレクサは別途必要です。
- 対応コーデックはブラウザと OS 次第。
isConfigSupported()での事前確認とフォールバックが必須です。
const config = {
codec: 'avc1.640028',
width: 1280,
height: 720,
bitrate: 2_000_000,
framerate: 30,
};
const { supported } = await VideoEncoder.isConfigSupported(config);
if (!supported) {
// ffmpeg.wasm 側にフォールバック
}
ffmpeg.wasm:互換性の道
ffmpeg.wasm は FFmpeg を WebAssembly にコンパイルしたものです。FFmpeg が扱えるコンテナやコーデックはほぼそのまま扱えるので、古い AVI や珍しい音声形式など、ブラウザが自力でデコードできない入力の受け皿になります。
トレードオフは WebCodecs のちょうど裏返しです。
- ソフトウェアエンコードなので、特に動画ではかなり遅い。
- WASM の線形メモリ内で動くため、ここからサイズの問題が始まります(後述)。
組み合わせ方:ルーター設計
実用的なのは「まずハードウェア経路を試し、ブラウザがデコードできない入力だけ WASM に回す」というルーターです。Nibbo の動画圧縮ページにも、GPU が WebCodecs でエンコードし、ブラウザがデコードできない形式は WebAssembly 版 FFmpeg で処理する、と書いています。
async function transcode(file, opts) {
if (await canUseWebCodecs(file, opts)) {
return transcodeWithWebCodecs(file, opts); // 高速・低メモリ
}
return transcodeWithFFmpegWasm(file, opts); // 遅いが何でも読める
}
メモリ:「ブラウザで3GB」が簡単ではない理由
ffmpeg.wasm の素朴な使い方は「入力ファイルをメモリ上の仮想ファイルシステムに書き込む → コマンド実行 → 出力ファイルを読み出す」です。3GB の動画なら、入力3GB+出力が丸ごと WASM ヒープに載ることになります。32bit の WebAssembly メモリは最大4GBで、実際のブラウザはそれよりずっと手前で大きな確保を拒否することが多く、モバイル Safari ならもっと早くタブが落ちます。
数GBの入力を扱うには、「ファイル全体をメモリに持たない」設計にする必要があります。
-
Fileから少しずつ読む:FileはBlobなので、file.slice(start, end)で必要な範囲だけ読めます。デマルチプレクサには必要なバイト範囲だけを渡します。 -
フレームをヒープに入れない:WebCodecs ならデコードした
VideoFrameをそのままエンコーダへ。使い終わったら即frame.close()。閉じ忘れは GPU メモリを食い潰す一番よくある原因です。 -
バックプレッシャーをかける:
encoder.encodeQueueSizeとdecoder.decodeQueueSizeを監視し、増えたら供給を止めます。放っておくと速いデコーダは何千フレームでも積み上げます。 -
出力は逐次書き出す:エンコード済みチャンクはマルチプレクサ経由で OPFS やストリーミングダウンロードなどディスク側へ流し、膨らみ続ける
ArrayBufferは作らない。 - Worker で動かす:UI を固めず、重い確保をメインスレッドから隔離できます。
for await (const chunk of demux(file)) { // file.slice() で逐次読み込み
while (decoder.decodeQueueSize > 8 || encoder.encodeQueueSize > 8) {
await new Promise(r => setTimeout(r, 0)); // バックプレッシャー
}
decoder.decode(chunk);
}
// decoder の output コールバック
function onFrame(frame) {
encoder.encode(frame);
frame.close(); // 忘れると GPU メモリがリークする
}
こうすると、ピークメモリはファイルサイズではなく「処理中のフレーム数」で決まるようになります。Nibbo が無料で3GBまでの動画を受け付けているのは、この前提があるからです。
おまけ:目標サイズぴったりに圧縮する
Discord の無料枠 20MB、Gmail の添付 25MB など、「◯MB以下」に収めたい場面はよくあります。Nibbo の動画圧縮では目標サイズを MB で指定すると、そこから逆算します。クリップの細かさ(空間情報 SI)と動きの量(時間情報 TI)を測り、目標サイズと長さからビットレート予算を出し、その予算で一番きれいに見える解像度とフレームレートを選ぶ、という流れです。「ビットが足りないフル解像度より、ビットが足りている低解像度のほうが良い」という考え方です。
計算自体は単純で、おおむね bitrate ≈ 目標ビット数 / 長さ から音声とコンテナのオーバーヘッドを引くだけです。厄介なのはハードウェアエンコーダが指定ビットレートを正確に守らないことで、そのため結果がわずかでも上限を超えたら補正したビットレートで再エンコードしています。
クライアントサイドが今でも負けるところ
正直に限界も書いておきます。
- ハードウェアの差:古いスマホは遅いし、特定のエンコーダがないブラウザもあります。フォールバックと分かりやすいメッセージが必要です。
- ソフトウェアフォールバックは遅い:ffmpeg.wasm でエンコードする場合、ネイティブの FFmpeg よりかなり遅くなると考えてください。
- 上限はある:ストリーミングにしても実用上の天井は残ります。
- コールドスタート:WASM モジュールは初回にダウンロードが必要です。キャッシュで緩和はできますが、初回は帯域を使います。
動画以外にも同じ考え方が使える
サーバーが本当は要らない CPU バウンドな処理なら、同じアプローチが使えます。Nibbo では、画像変換(HEIC/WebP/AVIF/PNG/JPG)、PDF の結合・分割・圧縮、25言語対応の OCR、EXIF 削除、署名をローカルで検証する JWT デコーダなどを同じ方針で作っています。JWT デコーダや個人情報の墨消しツールのように、「入力をアップロードした時点で意味がなくなる」ツールもあります。
まとめ
ユーザーのファイルを扱うもの(メディア処理、ドキュメント処理、開発者ツール)を作るなら、「そのサーバー、本当に必要?」と一度問い直す価値があります。
- 速度が欲しいなら WebCodecs(ハードウェア)
- 形式の網羅性が欲しいなら ffmpeg.wasm(フォールバック)
- メモリは「ファイルを持たない」ストリーミング設計で抑える
運用コストは下がり、プライバシーはポリシーの約束ではなく DevTools で検証できるものになります。実際の挙動は、Network タブを開いた状態で nibbo.org/ja/ で変換してみるのが一番わかりやすいと思います。何もアップロードされないのを眺めるのが、いちばんのデモです。