ffmpeg.wasmをブラウザ上で動かしてみた

はじめに

こんにちは、主にWebフロントエンドエンジニアをしているきーくん(komura-c)です。今回は業務とは関係なく、ffmpeg.wasmに興味を持ったため、ブラウザ上で動かしてみました。主にフロントエンド側の処理について追って、書いてみたのでぜひ読んでみてください!

ffmpegとは

ffmpegはオーディオ、ビデオなどを変換、処理するためのライブラリ、ツール群です。 https://github.com/FFmpeg/FFmpeg

ffmpegのコードは主にLGPLライセンスで公開されていますが、依存ライブラリにより多様なライセンスが適用されています。 https://github.com/FFmpeg/FFmpeg/blob/master/LICENSE.md

歴史のあるソフトウェアのため、オーディオや動画コンテンツを変換、圧縮するために使ったことがある方も多いのではないでしょうか。

ちなみに、ffmpegのコードサンプルが弊ブログの記事にあります。良かったらこちらも読んでみてください。 tech-blog.voicy.jp

ffmpeg.wasmとは

ffmpeg.wasmは、ffmpegをWebassembly / Javascriptコンパイルし、ブラウザ上で動くようにするためのOSSです。 https://github.com/ffmpegwasm/ffmpeg.wasm

ffmpeg.wasmは、次の2つのライブラリによって構成されています。

@ffmpeg/coreは、Emscriptenを使用してffmpegのC / C++コードをWebassembly / Javascriptコードにコンパイルするものです。ライセンスはffmpegやそれに依存する外部ライブラリが適用されます。

@ffmpeg/ffmpegは、@ffmpeg/coreを使用しやすくするためのラッパーライブラリです。MITライセンスが適用されています。 https://github.com/ffmpegwasm/ffmpeg.wasm#what-is-the-license-of-ffmpegwasm

今回は、@ffmpeg/ffmpegの方を見ていきます。

ちなみに@ffmpeg/coreの部分である、ffmpegコンパイルする手順もメンテナーの方が公開しています。 https://github.com/ffmpegwasm/ffmpeg.wasm#how-can-i-build-my-own-ffmpegwasm

ffmpeg.wasmをブラウザ上で動かす

今回は、趣味で作成したmusic-waves-visualizerというサイトで試しました。webm形式で書き出した動画をmp4形式に変換する処理で使用しました。 music-waves-visualizer.vercel.app

ffmpeg.wasmの導入手順としては、次の通りになります。

まず、一般的なnodeパッケージを使用するのと同じように、npmを使ってinstallします。(npmの説明は省略)

npm install @ffmpeg/ffmpeg

その後、次のようにコードを書きます。

// この辺りはffmpegとは関係ありませんが、使用例として記します
...
// ファイル名を定義
const movieName = "movie_" + Math.random().toString(36).slice(-8);
const webmName = movieName + ".webm";
const mp4Name = movieName + ".mp4";
...
// 動画を生成するMediaRecorder(ffmpegと関係ないため説明は省略)から
// 取得したrecordedBlobsをwebm形式のBlobに変換します
const webmBlob = new Blob(recordedBlobs, { type: "video/webm" });
// webmBlobを8ビット符号なし整数値の配列であるUint8Arrayに変換します
const binaryData = new Uint8Array(await webmBlob.arrayBuffer());
// ffmpeg.wasmでwebm形式の動画データをmp4形式に変換します
const video = await generateMp4Video(binaryData, webmName, mp4Name);
// 返ってきたvideo(Uint8Array)をmp4形式のBlobに変換します
const mp4Blob = new Blob([video], { type: "video/mp4" });

https://github.com/komura-c/music-waves-visualizer/blob/master/pages/index.tsx#L167-L171

import { createFFmpeg } from "@ffmpeg/ffmpeg";

const ffmpegCoreVersion = "0.10.0";
const corePath = `https://unpkg.com/@ffmpeg/core@${ffmpegCoreVersion}/dist/ffmpeg-core.js`;

export async function generateMp4Video(
  binaryData: Uint8Array,
  webmName: string,
  mp4Name: string
) {
// ffmpegのインスタンスを生成します、読み込むjsのファイルパスをcorePathに渡します
  const ffmpeg = createFFmpeg({ corePath, log: true });
// ffmpegを読み込みます
  await ffmpeg.load();
// Emscriptenが提供するFSにより、Node.jsのインメモリファイルシステムのライブラリである
// memfsにwebm動画のバイナリデータを保存します
  ffmpeg.FS("writeFile", webmName, binaryData);
// runでコマンドラインツールと同じようにffmpegにオプションを渡し、webm動画をmp4動画に変換します
  await ffmpeg.run("-i", webmName, "-vcodec", "copy", mp4Name);
// 変換したmp4動画のバイナリをmemfsから読み込みます
  const videoUint8Array = ffmpeg.FS("readFile", mp4Name);
  try {
// WebWorkerスレッドで行われている、ffmpegの実行を強制終了し、memfsを削除してメモリを解放します
    ffmpeg.exit();
  } catch (error) {
// 今のところ確実にprocess.exit(1)の例外が投げられてメインスレッドの処理が止まってしまうため、catchしています
//(検証ツールで見る限り正常にWebWorkerが削除されているのでメモリは解放されているっぽいです)
}
  return videoUint8Array;
}

https://github.com/komura-c/music-waves-visualizer/blob/23893b79c556fed3965d50cd2143d1a6d634e5ce/scripts/Ffmpeg.ts

親切にもライブラリの提供するAPI一覧は、リポジトリにまとめてありdocs/api.mdを見れば、何をしているかが大体わかります。 https://github.com/ffmpegwasm/ffmpeg.wasm/blob/master/docs/api.md

ここで、実装する際の注意点が3つあります。

1つ目は、デフォルトでローカル環境のcorePathが/node_modules/@ffmpeg/core/dist/になっており、必要なファイルが読み込まれない場合があることです。 https://github.com/ffmpegwasm/ffmpeg.wasm#why-it-doesnt-work-in-my-local-environment

@ffmpeg/ffmpegffmpeg.load()時に、createFFmpegのcorePathに指定したファイルパスから、次のファイルを読み込みます。

先述したコードでは、corePathにhttps://unpkg.com/というnpmのライブラリを非公式で有志がCDNとして公開しているURLを使用しました。 ローカル環境以外では、corePathを指定しなければ同様にデフォルトで、unpkgのCDNのURLで読み込まれます。 https://github.com/ffmpegwasm/ffmpeg.wasm/blob/master/src/browser/defaultOptions.js

2つ目は、ffmpeg.wasmはSharedArrayBufferを使用しているため、対応ブラウザが限られることです。 自分はChromeでしか確認していません。 https://caniuse.com/sharedarraybuffer

SharedArrayBufferを使用するにはホストしているサーバーに次のHTTPヘッダーを含める必要があります。

Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer

3つ目は、読み込むファイルには2GBの制限があることです。 これはWebAssemblyの制限ですが、将来的には4GBになる可能性があるそうです。 https://github.com/ffmpegwasm/ffmpeg.wasm#what-is-the-maximum-size-of-input-file

おわりに

ffmpeg.wasmのフロントエンド側の処理について、少しではありますが紹介しました。 @ffmpeg/ffmpegが読み込むcorePathに関しては、実プロダクトで使うのであれば@ffmpeg/coreをビルドしたファイルか、CDNのファイルを静的に置いてセルフホストし、そのパスにした方が良いと思いました。 ただ、前述したffmpegのライセンス周りがややこしいことや、wasmバイナリのライセンス表示などの問題があるため、デモで試しに使ったものであるということもあり、今回はそうしませんでした。 ffmpeg.wasmはまだ実験的なプロジェクトのため、実プロダクトで使うのは厳しいのではないかなと思いつつ、ffmpegがフロントエンドで動かせるのはとてもロマンのあることだと感じました。 この記事が少しでも参考になれば嬉しいです!