wasm版FFmpegが登場!
と言ってもちょっと前の話ですが…
FFmpeg君とは腐れ縁なので、wasmの姿になっても仲良くしていきたいと思います。
今回は何を作るのか?
今回はみんな大好き動画ファイルからGIFアニメーションへの変換を行います。
最近GitHubのREADMEに動画を載せることができることになりましたが、その前まではPhotoshopを使って機能ごとのデモGIFアニメーションを載せていました。動画が載せるようになった今もGIFアニメーションの方が見やすいので今後もGIFアニメーションは必要ですが…
今回はwasm版FFmpegを用いて、わざわざPhotoshopみたいな重いソフトを使うのも面倒なのでWeb画面でサクッと変換できるようにしました!
尚、今回のブログではFFmpegの部分を中心的に解説していますが、JavaScriptの基本的な部分やサンプルコード内のFFmpegに関係のない部分以外は特に解説をしませんのでコード内のコメントを元に理解していただければと思います(そんな複雑な処理はしていないです。)
サンプルコードとデモページ
今回のコードはGitHubへ、デモ用にGitHubPagesにてデモページを公開しています。
ブラウザでの読み込み
npm でのインストールも勿論できるのですが、今回は簡単にscriptタグを使って読み込んでみます。
1 |
<script src="https://unpkg.com/@ffmpeg/ffmpeg@0.9.5/dist/ffmpeg.min.js"></script> |
これで読み込みは完了です。
因みに今回は使わないですが、npmの場合はこんな感じ。
1 |
$ npm install @ffmpeg/ffmpeg @ffmpeg/core |
インスタンスの作成
FFmpegのインスタンスを作成します。
1 2 |
const { createFFmpeg, fetchFile } = FFmpeg const ffmpeg = createFFmpeg({ log: true, progress: p => displayProgress(p)}) |
今回は log と progress のみを有効にしていますが、他のオプションもオブジェクトでまとめて渡すことが出来ます。
- corePath :ffmpeg-core.jsスクリプトのパス
- log :ログメッセージのコンソール出力の有効 / 無効(デフォルト: false )
- logger :ログメッセージの取得関数
- <span>progress :進行状況の取得
と言った感じです。
ちなみにここでのログメッセージとはコマンドライン等でFFmpegを実行した際の標準出力に出力される内容と同じかと思います。横に並べて見比べたわけでは無いので、正確には分かりませんが…
コマンドのFFmpegではオプションでログメッセージのレベルを指定できるのですが、この場合のログレベルがいくつに定まっているか謎です。
スクリプトのロード
1 2 3 4 |
// FFmpegの読み込み if (!ffmpeg.isLoaded()) { await ffmpeg.load() } |
ffmpeg.wasm-core というwasm版FFmpegのスクリプトを読み込みます。
ffmpeg.isLoaded() でFFmpegのスクリプトが既に読み込まれているかを確認できます。
重複して読み込む場合はエラーになるので気を付けましょう。
尚、読み込み自体に数秒から数分の時間がかかるとされているため、極力早めに読み込むことが推奨されています。
FS操作
wasm版のFFmpegで扱うためには、MEMFS(メモリ内ファイルシステム)に保存する必要があります。
そのための関数が ffmpeg.FS() です。
ffmpeg.FS() は第一引数にメソッド名(文字列)が入ります。
- 保存: 'writeFile'
- 取得: 'readFile'
- リンク解除: 'writeFile'
実際は以下のように書きます。
尚、入力できるファイルサイズに関しては2GBまでという制限があります。
保存
1 2 |
// MEMFSへ保存 ffmpeg.FS('writeFile', 'input.mp4', await fetchFile(videoFile[0])) |
- 第二引数:保存時の名称(文字列)
- 第三引数:保存するファイル
取得
1 2 |
// MEMFSからファイルを取得 const data = ffmpeg.FS('readFile', 'output.gif') |
- 第一引数:呼び出すファイルの名称(文字列)
リンク解除
1 2 3 |
// データのリンク解除 ffmpeg.FS('unlink', 'input.mp4') ffmpeg.FS('unlink', 'output.gif') |
- 第一引数:リンク解除するファイルの名称(文字列)
FFmpegの実行
ffmpeg.run() でFFmpegコマンドを実行します。
1 2 |
// コマンドの実行 await ffmpeg.run('-i', 'input.mp4', '-r', '10', 'output.gif') |
コマンドライン上での実行とあまり大差ないのですが、コマンドライン引数はスペース区切りで渡す部分を、JavaScriptの引数の渡し方であるカンマ区切りでオプションやパラメータを文字列として渡しています。
オプションに関しては、通常のFFmpegのドキュメントや記事を参考にしても問題ないです。
因みに上の例は
1 |
ffmpeg -i input.mp4 -r 10 output.gif |
このコマンドをFFmpegのコマンドとしてコマンドラインで実行するのと同じ処理になります。
尚、個々での出力結果もMEMFSとしてメモリファイルシステムに保存されるため、先程の ffmpeg.FS() を用いて結果を取得します。
実際にGIFへ変換
それでは、実際にGIFへ変換する過程を見ていきます。
基本的にFFmpeg周りに関しては先程解説した内容になります。
流れとしては、
- FFmpegの読み込み( ffmpeg.load() の実行)
- MEMFSへの対象動画ファイルの保存( ffmpeg.FS('writeFile', ...) の実行)
- GIFファイルへの変換コマンドの実行( ffmpeg.run() の実行)
- 実行結果のファイルをMEMFSから取得( ffmpeg.FS('readFile', ...) の実行)
- 画像として出力
- リンク解除( ffmpeg.FS('unlink', ...) の実行)
の順に処理を行います。
実際のコードはこちら。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// GIFへの変換処理 async function convertGIF () { // FFmpegの読み込み if (!ffmpeg.isLoaded()) { await ffmpeg.load() } const videoName = videoFile[0].name // MEMFSへ保存 ffmpeg.FS('writeFile', videoName, await fetchFile(videoFile[0])) // コマンドの実行 await ffmpeg.run('-i', videoName, '-r', '10', 'output.gif') // MEMFSからファイルを取得 const data = ffmpeg.FS('readFile', 'output.gif') // 変換結果を表示 imagePreview.src = URL.createObjectURL(new Blob([data.buffer], { type: 'image/gif' })) imagePreview.style.display = 'block' // データのリンク解除 ffmpeg.FS('unlink', videoName) ffmpeg.FS('unlink', 'output.gif') } |
因みに読み込み部分はこちら
1 2 |
const { createFFmpeg, fetchFile } = FFmpeg const ffmpeg = createFFmpeg({ log: true, progress: p => displayProgress(p)}) |
progress オプションの p には進行状況が少数で入ります。
指定している displayProgress(p) の内容はこちらです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 進行情報の表示 function displayProgress (progress) { console.log(progress) convertButton.innerText = Math.round(100 * progress.ratio) + '%' if (progress.ratio >= 1) { setTimeout( function () { convertButton.innerText = 'GIF画像に変換' }, 1000 ) } } |
進行中に変換ボタン内に進行状況を出力するようにしています。
実際の画面はこんな感じです。
ちなみにこのGIFアニメーションもサンプルで作りました!
変換時の問題なのか、表示サイズによっては画像上に四角い模様が見えますね。
mp4でしかまだ試していませんが、FFmpegで対応している動画形式なら大半ができるのかな?と思います。
最後に
簡単な内容ですが、今回の記事はここで終わりたいと思います。
FFmpeg自体のコード部分はとても短いながらに色々な機能が実装できると思いました。
処理の速度に関しても10秒程度の動画であれば特にストレスない速度で結果が返ってきました。
これからも色々と作ってみたいと思います!
尚、今回紹介していない機能なども公式ドキュメントに記載されているのでこちらも確認してみてください。
参考にした記事
- ffmpegwasm/ffmpeg.wasm – GitHub
- alt9800/ffmpegsample – GitHub
- FFmpegで動画をGIFに変換 – Qiita
- streamich/memfs – GitHub
今回のブログ曲
今回投稿中に聴いていた曲はこちら