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.js
を corePath
として指定すれば残りの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