created: 2022-06-16T03:16:00.272Z

ffmpeg.wasm を使う

今は ffmpeg をブラウザ上で動かすことができるようになっている。 つまり、クライアント側で動画の編集をしてもらうことができる。

ffmpeg は巨大な歴史あるソフトウェアで脆弱性もいろいろあり、これをサーバ側で動作させないで済むならかなりうれしい。

ブラウザ側で ffmpeg を走らせるには ffmpeg.wasm を利用する。

ただし、いくつか超える壁がある。(それぞれ後述)

  • ffmpeg.wasm は SharedArrayBuffer をつかっている
    • 現状だとChrome以外では動かなかった
    • Chromeでもサーバ側からクロスオリジン系のヘッダをつける必要がある
      • このヘッダが有効に扱われるのにはページが https な必要がある
  • ffmpeg.wasm は設計としてランタイムで wasm ファイルを遅延ロードするつくりになっている
    • webpack などと組み合わせる方法は公式のドキュメントには見当たらなかった
    • wasm ファイルを自前でビルドするのはけっこう面倒
      • ffmpeg のビルドも必要になるため
    • wasm ファイルは CDN においてあったりはする

使い始める

wasm を自前でビルドしないことにしたので、@ffmpeg/core は入れていない。

$ npm install @ffmpeg/ffmpeg 

最低限で使うならこうなる。

import { createFFmpeg, fetchFile, FFmpeg } from "@ffmpeg/ffmpeg";

const corePath = "/static/ffmpeg/ffmpeg-core.js";
const ffmpeg: FFmpeg = createFFmpeg({ corePath, log: true });
await ffmpeg.load();
const binaryData = await fetchFile(src);
ffmpeg.FS("writeFile", inputFilename, binaryData);
await ffmpeg.run(...args);
const transcoded = ffmpeg.FS("readFile", outputFilename);
const blob = new Blob([transcoded.buffer]);

ffmpeg.load

ffmpeg.wasm は20MBくらいある wasm ファイルなどを遅延ロードする設計になっている。 遅延ロードは ffmpeg.load() を呼んだタイミングで行われる。

どこから wasm ファイルをダウンロードするかは corePath 引数で指定する。 3つのファイルが遅延ロードされる必要があるので、これらをホスティングしておく。

  • ffmpeg-core.js
  • ffmpeg-core.wasm
  • ffmpeg-core.worker.js

今回は自前で ffmpeg をビルドするのが面倒だったので以下のスクリプトで CDN からダウンロードしてホスティングすることにした。

readonly FFMPEG_CORE_VERSION="0.10.0"
readonly dirpath=$(pwd)/static/ffmpeg
readonly cdn="unpkg.com/@ffmpeg/core@${FFMPEG_CORE_VERSION}/dist/"

mkdir -p $dirpath
curl "https://${cdn}/ffmpeg-core.js" -o $dirpath/ffmpeg-core.js
curl "https://${cdn}/ffmpeg-core.wasm" -o $dirpath/ffmpeg-core.wasm
curl "https://${cdn}/ffmpeg-core.worker.js" -o $dirpath/ffmpeg-core.worker.js

これのうち ffmpeg-core.jscorePath として指定すれば残りの2ファイルも同じディレクトリから読み込んでくれる。

再掲

const corePath = "/static/ffmpeg/ffmpeg-core.js";
const ffmpeg: FFmpeg = createFFmpeg({ corePath, log: true });

ffmpeg.run

面倒な ffmpeg.load が片付いたら、あとはドキュメントに書いてある通りに run を呼ぶ。

// URL文字列やFileオブジェクトをUint8Arrayにしてくれるヘルパー
const binaryData = await fetchFile(src);
// ffmpeg が扱えるように memfs 管理下に書き込む
ffmpeg.FS("writeFile", inputFilename, binaryData);
// ffmpeg と同様の引数で実行
await ffmpeg.run("-i", inputFilename, "-s", "1280x720", outputFilename);
// memfs 配下に書き出されたデータ(Uint8Array)を読み込み
const transcoded = ffmpeg.FS("readFile", outputFilename);
// blob にして、あとはvideoタグのsrcに入れたりFileとしてアップロードしたり
const blob = new Blob([transcoded.buffer]);

この辺はドキュメントも親切。

ブラウザ上でも動作してくれる memfs というのがあって、ffmpeg とのファイルのやりとりはそこで行う。

なお、createFFmpeg({ log: true }) としておくと ffmpeg をターミナルで走らせた時と同等のログがコンソールに出てくれるので、エラーなどはそこから確認できる。

SharedArrayBuffer

ffmpeg.wasm は localhost だと問題なく動くが、それ以外のホストだと以下のエラーがでて動かない。

Uncaught (in promise) ReferenceError: SharedArrayBuffer is not defined

これは ffmpeg.wasm が内部で SharedArrayBuffer を使っているため。

クロスオリジン系のヘッダをつけてあげると SharedArrayBuffer が使えるようになってちゃんと動く。

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