ブラウザの中だけで動画を変換したいという話になると、まず名前が挙がるのは ffmpeg.wasm です。検索して出てくる実装例もほとんどがこれを使っていて、事実上の「定番」と言っていい位置にありますし、ffmpeg のコマンドの知識がそのまま活きるので選びたくなる気持ちも分かります。
私は画像・音声・動画をブラウザ内で変換するツールを作るときにこれを採用せず、WebCodecs と mediabunny という組み合わせで組んでいます。
この記事では、ffmpeg.wasm を外した理由と、代わりに何をどう使ったか、実装でつまずいた点、そして実際に測った処理時間を書きます。速いほうを選んだという話ではありません。選んだ理由はライセンスで、速度の差は後から測って分かったことです。
先に断っておくと、この判断が効くのはソースを公開していないアプリで、処理をクライアント側に置くという条件のもとになります。サーバー側で変換する構成なら ffmpeg をそのまま使えますし、ソースを公開しているプロジェクトであれば GPL も障害になりません。条件が違えば結論も変わりますので、その前提で読み進めていただければと思います。
- 対象: ブラウザ内で動画を変換する実装を検討している人
- 前提: TypeScript と、WebCodecs に対応したブラウザ(Chrome / Edge)
ffmpeg.wasm を採用しなかった理由
クライアントに配るなら GPL が効く
いちばん大きい理由がライセンスなのですが、ここは「Web サービスなら GPL は関係ない」と誤解されやすいところなので、先に切り分けておきます。
構成 | GPL の義務 |
|---|---|
サーバー側で ffmpeg を実行する Web アプリ | 発生しない(利用者にバイナリを渡していない) |
ffmpeg.wasm をブラウザへ配信する | 発生する(wasm を利用者の端末へ渡している) |
FFmpeg 自体は既定では LGPL 2.1 以降ですが、--enable-gpl と --enable-libx264 を付けてビルドすると全体が GPL になります。そして npm で配られている @ffmpeg/core は、まさにその構成でビルドされています。package.json の license も GPL-2.0-or-later と書かれています。ラッパーの @ffmpeg/ffmpeg が MIT なので混同しやすいのですが、実際にコーデックが入っているのは core のほうになります。
つまり同じ機能でも、サーバー側で処理する構成にしていれば ffmpeg は問題なく使えたわけで、ブラウザ内で完結させるという選択そのものが、GPL を踏む側へ回る原因になっています。私が作っているのはソースを公開していないツールなので、ここで外す判断をしました。
逃げ道として libx264 を外した LGPL ビルドを自分で作る手はありますが、それをやると H.264 が書き出せなくなります。MP4 を扱えない動画変換ツールは成立しないので、この道は最初から閉じていました。
メモリの上限
ffmpeg.wasm は入力ファイルも出力ファイルも wasm の仮想ファイルシステムに置きます。つまり変換するファイルのサイズがそのままメモリを圧迫することになり、扱える大きさに現実的な上限が生まれます。
私が作っているのは「時間制限も回数制限もなし」を掲げたツールなので、入力の大きさで頭打ちになる構成は前提と噛み合わず、ここも外す理由になりました。
マルチスレッド版はサイト全体に影響する
ffmpeg.wasm にはマルチスレッド版があり、そちらを使えば処理は速くなります。ただし SharedArrayBuffer が必要になるため、サイト全体に COOP と COEP のヘッダーを設定することになります。これはページに埋め込んでいる外部サービスの動作にまで影響が及ぶ設定なので、既存のサイトへ後から足すのは簡単ではありません。
代わりに使ったもの
WebCodecs と mediabunny の 2 つで組んでいて、役割ははっきり分かれています。
担当 | 提供元 | |
|---|---|---|
WebCodecs | コーデック本体(エンコード・デコード) | ブラウザ内蔵 |
mediabunny | コンテナの読み書き、WebCodecs の抽象化 | npm(MPL-2.0) |
WebCodecs はブラウザが持っているエンコーダとデコーダを JavaScript から直接叩く API で、コーデックを自前で持ち込まないので GPL の問題はここで消えます。ただし WebCodecs が扱うのは映像と音声のフレームだけで、MP4 や WebM といった「コンテナ」の読み書きは守備範囲の外にあり、そこを埋めるのが mediabunny です。
mediabunny を選んだ理由は消去法に近いもので、同じ作者が以前に公開していた mp4-muxer と webm-muxer がどちらも mediabunny へ統合されて非推奨になっているため、この用途では実質これ一択という状況になっています。
導入と最小構成
pnpm add mediabunnyMP4 へ変換するだけなら、これだけで動きます。
import {
ALL_FORMATS, BlobSource, BufferTarget, Conversion,
Input, Mp4OutputFormat, Output, Quality, canEncodeVideo,
} from "mediabunny";
export async function convertToMp4(file: File): Promise<Blob> {
// エンコーダの対応はブラウザごとに違うので、処理を始める前に問い合わせる
if (!(await canEncodeVideo("avc"))) {
throw new Error("この環境では H.264 を書き出せません");
}
const input = new Input({ source: new BlobSource(file), formats: ALL_FORMATS });
const output = new Output({ format: new Mp4OutputFormat(), target: new BufferTarget() });
const conversion = await Conversion.init({
input,
output,
video: { codec: "avc", quality: new Quality("medium") },
});
await conversion.execute();
return new Blob([output.target.buffer], { type: "video/mp4" });
}Conversion が「分離」「デコード」「エンコード」「多重化」をまとめて面倒を見てくれるので、フレームを 1 枚ずつ扱うコードは書かずに済みますし、解像度やフレームレートを変えたい場合も video に height や frameRate を足すだけで足ります。
BlobSource を渡している点が要で、ファイル全体をメモリに読み込まずに、必要な範囲だけを順に読みながら変換が進みます。 2 時間の動画を渡してもメモリが張り付かないのはこの構造のおかげです。
実装でつまずいた点
ここからは、ドキュメントを読んだだけでは避けられなかったものを挙げます。
音声のデコードは mediabunny を先に試す
音声を PCM に展開する処理では、Web Audio の decodeAudioData を使いたくなります。ところがこの API は、AudioContext のサンプリングレート(多くの環境で 48kHz)へ勝手にリサンプルして返します。 44.1kHz の音源を読んだつもりが 48kHz になって戻ってくるので、元のレートを保ったまま処理したい場面では使えません。
そこで私は mediabunny のデコーダを先に試し、それが失敗したときだけ Web Audio へ落ちる形にしていて、この順序は今も変えていません。
エンコーダの対応はブラウザと OS で違う
WebCodecs で一番はまったのがここです。同じ Chrome でも、OS によって使えるコーデックが違います。 具体的には、Linux 版の Chromium には AAC のエンコーダが入っていません。
そのため canEncodeVideo() と canEncodeAudio() で事前に問い合わせ、書き出せない形式なら処理を始める前に弾く作りにしています。動画で音声だけエンコードできない場合は、映像だけを書き出したうえで「音声を落とした」と画面に表示します。黙って落とすと、利用者は無音の動画をダウンロードして初めて気づくことになるからです。
canEncodeAudio() の結果はメモ化される
これは仕様を読んでも書いていない挙動です。mediabunny の canEncodeAudio() は結果をメモ化するため、エンコーダを登録する前に一度呼んでしまうと、その false が固定されます。
MP3 のエンコーダは @mediabunny/mp3-encoder を別途登録して使うのですが、登録の前に対応可否を調べる処理を挟んでいたせいで、登録後もずっと「MP3 は書き出せない」と判定され続けました。いまは WebCodecs へ直接問い合わせる形に変えています。
React の開発モードでは useEffect が 2 回走るので登録処理が二重に動く点にも注意が要り、私は登録の Promise を共有して待つようにしています。
ここには続きがあって、公開したあとに実際に踏みました。 メモ化されるのは解決済みの結果だけではなく、判定中の Promise そのものが共有されます(mediabunny の encode.ts)。つまり判定が 1 つ詰まると、同じ問い合わせを待っている別の処理まで道連れで止まります。
私の環境では AudioEncoder.isConfigSupported({ codec: "mp3" }) が約 43 秒返ってこないという事象が起き、その間ページ内の WebCodecs 判定がすべて止まって、動画の変換まで巻き込まれて待たされました。CPU も帯域も使っていない純粋な待ちだったので、処理が重いのだと勘違いして原因の特定に時間がかかっています。
いまは判定に上限時間を設けて、返ってこなくても先へ進むようにしました。本当に未対応であればエンコーダの初期化で落ちるので、判定の失敗を待ち続ける必要はありません。
// 能力判定が返ってこないことがあるので、待ち時間に上限を設ける
async function withProbeTimeout<T>(probe: Promise<T>, fallback: T, ms = 5000): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
probe,
new Promise<T>((resolve) => {
timer = setTimeout(() => resolve(fallback), ms);
}),
]);
} finally {
clearTimeout(timer);
}
}実測:ffmpeg.wasm とどれだけ違うか
速度を根拠に選んだわけではありませんが、実際に測ってみると差ははっきり出ました。
素材には ffmpeg で生成した 20 秒 / 1280×720 / 30fps / H.264 + AAC / 8.2 MB のファイルを使っています。マシンは Ryzen 8000 系(16 コア・内蔵 GPU)を積んだ Ubuntu サーバーで、ブラウザは Chromium を使い、どれも同じ条件で動かしています。
方式 | 所要時間 | 出力サイズ |
|---|---|---|
ffmpeg.wasm シングルスレッド(-preset veryfast -crf 23) | 20.1 秒 | 6.3 MB(約 2.5 Mbps) |
ffmpeg.wasm シングルスレッド(-preset medium -crf 23) | 10 分を超えたため打ち切り | — |
WebCodecs + mediabunny(この記事のコード) | 0.82 秒 | 4.05 MB(1.49 Mbps) |
20 秒の動画が 0.82 秒で終わっているので、実時間のおよそ 24 倍の速さになります。書き出したファイルを ffprobe で確かめると映像のビットレートが 3.13 Mbps から 1.49 Mbps へ変わっていたので、コンテナを詰め替えただけではなくきちんと再エンコードされたうえでこの時間でした。
ここまで開くのは、WebCodecs がブラウザの内蔵エンコーダをそのまま呼ぶのに対して、ffmpeg.wasm は x264 のコードを wasm 上で回すからです。前者はハードウェアの支援を受けられますが、後者はどこまでいっても JavaScript エンジンの上で動く計算になります。
ただし ffmpeg.wasm 側の数字は preset の選び方に強く依存する点には注意が必要です。veryfast なら 20 秒で終わったものが、medium では 10 分を超えても終わりませんでした。どこかで速度比較を見かけたときは、どの preset で測ったのかまで確かめるようにしてください。
よくある質問
ffmpeg.wasm は使ってはいけないのですか?
条件が合えば使えます。ソースを公開しているプロジェクトや、社内向けで「配布」に当たらない使い方であれば、GPL の義務は問題になりにくいはずです。私の場合はソースを公開していないツールで、かつ wasm を利用者へ配信する構成だったので外しました。
WebCodecs はどのブラウザで使えますか?
Chrome と Edge では問題なく動きます。Safari も対応が進んでいますが、扱えるコーデックがブラウザと OS の組み合わせで変わるので、canEncodeVideo() による事前の問い合わせは必須だと考えてください。
mediabunny は無料で使えますか?
MPL-2.0 で公開されているので、商用のプロジェクトでも使えます。とはいえライセンス条文の解釈は用途によって変わるため、自分の構成に当てはめて必ず確認してください。
サーバーで変換したほうが速いのでは?
処理そのものは速くなります。ただしファイルをアップロードする時間と、サーバーの費用と、利用者のファイルを預かる責任がそれぞれ発生しますし、私はその 3 つを避けたかったのでブラウザ内に寄せています。
H.264 以外も書き出せますか?
書き出せます。WebM(VP8 / VP9)、MKV、MOV などに対応していて、音声も MP3・AAC・Opus・FLAC・PCM を扱えます。ただし実際にどれが使えるかは、繰り返しになりますが動かすブラウザと OS の組み合わせ次第になります。
2 時間の動画でもメモリは大丈夫ですか?
ストリームで処理する構成なら最後まで動きます。ファイル全体をメモリへ載せる実装にすると、そこで頭打ちになります。
まとめ
ブラウザ内で動画を変換する実装として、私は ffmpeg.wasm ではなく WebCodecs と mediabunny を選びました。速度で選んだのではなく、クライアントへ配る以上 GPL が効くという一点が決め手になっています。
- サーバー側で ffmpeg を回すなら GPL の配布義務は発生しないが、wasm をブラウザへ配ると発生する
- WebCodecs はブラウザ内蔵のコーデックを使うので、コーデックを持ち込まずに済む
- コンテナの読み書きは mediabunny に任せる。同種のライブラリはここへ統合されている
- エンコーダの対応はブラウザと OS で違うので、処理前に必ず問い合わせる
- 速度でも差は出た。20 秒の動画で ffmpeg.wasm が 20.1 秒、WebCodecs が 0.82 秒

画像の変換・圧縮・切り抜き・AI 高画質化・背景除去、音声の変換・無音除去・音量調整、動画の変換・圧縮・トリム・字幕焼き込み・GIF 化を、アップロードせずブラウザ内で処理する無料ツール集。サイズ制限なし、登録不要。

A JavaScript library for reading, writing, and converting media files. Directly in the browser, and faster than anybunny else.
動画に字幕を入れるのに、ソフトのインストールもファイルのアップロードも要りません。ブラウザーだけで文字起こしから SRT の作成、字幕の焼き込み(ハードサブ)まで済ませる手順を、無料・アカウント登録不要で説明します。
