ブラウザで動画編集! ffmpeg.wasmの活用方法紹介

こんにちは! テリーです。コロナのワクチンができたかも!?といううれしいニュースが話題になりましたね。自宅引きこもり生活もそろそろ終わりが見えてきたでしょうか。多くの芸能人がYouTubeにチャンネルを開設してくれたおかげで、入浴中に10分程度のお気に入りチャンネルを毎日見る習慣ができてしまいました。自分もヒカキンのようなYouTuberになって一儲けしてやろうと思い立ち、スマホで撮影してみましたが、編集の大変さでギブアップしてしまいました。オンラインで動画編集して、誰かに作業を手伝ってもらったり、アドバイスしてほしいですよね。そこで今回は、ブラウザで動画編集するのに役立つ最新技術をご紹介します。

今回紹介する技術

wasmとは

wasmはWebAssemblyの略称または拡張子で、ワズムまたはワスムと読みます。WebAssemblyとは、ブラウザ上で動作する実行プログラムです。JavaScriptと異なり、コンパイルされた形式で配布され、処理速度は少し速いです。速度よりも重要なことは、JavaScript以外の言語が使えるようになり、C言語やGo、Rust、などで書かれた複雑で大規模な計算処理プログラムを、全面的な書き直しをすることなく、ブラウザで動作させることができます。世界中で数年間稼働実績のあるプログラムを、ブラウザ上で安心して使えることに価値があります。

rnnoise-wasmとは

rnnoise-wasmはrnnoiseをwasm形式にしたものです。rnnoiseとは、マイクで収録した音声データの短い区間が、人間の声か雑音かどうかを機械学習で判定するプログラムです。WebRTCのゲイン調整やノイズ削除に使われています。C言語で書かれており、大量のバイナリデータを扱うことから、wasmに適した事例です。

ffmpeg.wasmとは

ffmpeg.wasmはffmpegをwasm形式にしたものです。ffmpegはさまざまな動画形式のトランスコードをするためのプログラムです。あまりにも巨大すぎるプログラムですので、JavaScriptで同等の処理を書くことは誰も挑戦しないでしょう。C言語で書かれており、大量のバイナリデータを扱うことから、wasmに適した事例です。こんな歴史のあるデカいプログラムを、誰も想定していなかった新しいプラットフォームで動かせるなんて、WebAssemblyのコンパイラはおそろしい完成度です。

ffmpeg.wasmの弱点

ffmpegはご存知の通りすばらしいプログラムです。ffmpeg.wasmではffmpegのほとんどの機能が使えますが、ブラウザで動作するという特性上、できないことがあります。それはハードウェアをフルに使った計算処理です。SIMDとGPUが使えません。映像のトランスコードをする場合、GPUやSIMDが使われずに、加算・乗算を配列ループ処理することになり、処理時間が大幅に遅くなります。音声の場合は映像に比べてデータ量が少ないため、大きな問題になりませんが、映像をffmpeg.wasmでトランスコードした場合に、wasm版でないffmpegと比べてとても遅いことに気づきます。またメモリをフルに使えないため、メモリ不足に落ちることもよくあります。

ffmpeg.wasmのオススメの使い方

私の考えるffmpeg.wasmのオススメの使い方は、切り出しと、コンテナ変換です。映像コーデックや解像度を変更すると処理に時間がかかりますが、トランスコードが必要のない処理はとても速いです。切り出しとは、カット編集とも呼ばれ、映像の不要な区間を捨て、必要な場所だけを前に詰めて残す処理です。コンテナ変換とは、ファイルフォーマットであるMP4, WebM, MPEG2-TS, AVI, FLV, MOVなどを変更することです。

デモアプリケーション

今回ご紹介するデモアプリは、カメラの映像とマイクの音声を録画し、rnnoiseで無音区間を抽出し、声のある区間のみを出力するジャンプカットまたはジェットカットと呼ばれる編集ツールです。一部のYouTuberは視聴のテンポをよくするために、無音区間をカットし、短い視聴時間でトーク番組を楽しめるように工夫しています。

  • 編集用動画ファイルの取得
  • 無音区間と有声音区間の区別
  • 映像の切り出しと結合

の順に個別のサンプルとして説明し、最後にこれらを一つにまとめます。

デモアプリケーション動作環境

  • OS:mac OSもしくはWindows
  • ブラウザ:Chrome

編集用動画ファイルの取得

recordボタンを押し、YouTuberになった気持ちで、何かしら言葉を話しながら、動画を撮影してください。stopボタンを押す、または20秒経過すると、自動的に動画がダウンロードされます。


うまく動かない場合は、こちらのページで試してください。スマートフォンで自撮りした動画をコピーしても構いません。撮影が難しい方はこちらの動画をダウンロードしてお試しください。

無音区間と有声音区間の区別

次に無音区間と有声音区間を取得する処理です。「ファイルを選択」ボタンを押し、上で撮影した動画等を選択してください。人が話している区間では、ビデオのフチが赤くなり、話をしていない区間では黒になるのが確認できると思います。

See the Pen
rnnoise
by terry (@terry_a)
on CodePen.

うまく動かない場合は、こちらのページで試してください。

映像の切り出しと結合

映像の中のある区間を切り出すためにffmpeg.wasmを使用します。「ファイルを選択」ボタンを押し、動画ファイルを選択してください。2秒ごとに映像が飛んでいるのが確認できると思います。

See the Pen
cut_and_cat
by terry (@terry_a)
on CodePen.

うまく動かない場合は、こちらのページで試してください。

使用した動画ファイルによっては、音声は再生されるが、映像が映らない場合があります。映像も音声も再生されない場合もあります。特にh.264のmp4ファイルを指定した場合に顕著に現れます。映像のカット編集をトランスコードなしで行うためにはキーフレームがこまめに入っている必要があります。

ジャンプカット編集

rnnoiseで有声音区間を抽出し、その区間のみを動画ファイルに出力します。「ファイルを選択」ボタンを押し、動画ファイルを選択してください。すべての区間で何かしら声を出しているのが確認できると思います。(アップロードした動画を一度再生した後、有声音区間のみの動画が再生されます)

See the Pen
jumpcut
by terry (@terry_a)
on CodePen.

うまく動かない場合は、こちらのページで試してください。

コードの説明

サンプルプログラムがとても長いため、部分的な解説をします。

行番号 関数またはコード 解説
3-9 changeFile関数 ファイルを選択した場合に呼ばれる関数
拡張子を取得している
15 const voice_ranges = await FullRNNoise(); FullRNNoise関数を呼び、動画の有声音区間配列を取得している
17 const blobBuffer = await blob.arrayBuffer(); BlobをArrayBufferに変換している
19-20 const inputfilename = 'input' + ext;
await ffmpeg.FS('writeFile', inputfilename, new Uint8Array(blobBuffer));
入力動画ファイルのバイト配列を、ffmpeg.wasmがアクセス可能なメモリストレージに保存している
22 for(let i=0;i<voice_ranges.length;i++){ 有声音区間ごとのループ処理
25 await ffmpeg.run('-i', inputfilename, '-c', 'copy', '-ss', sec2time(voice_ranges[i][0]), '-t', sec2time(voice_ranges[i][1] - voice_ranges[i][0]), splitfilename); 有声音の一区間を個別の動画ファイルに出力している
26 filelist += 'file ' + splitfilename + "\n"; 書き出したファイル名を、のちの結合用に文字列で保管している
30 await ffmpeg.FS('writeFile', listfilename, filelist); 結合するファイル名のリストをffmpeg.wasmがアクセス可能なメモリストレージに保存している
32 await ffmpeg.run('-f', 'concat', '-i', listfilename, '-c', 'copy', outputfilename); リストファイルに書かれた動画ファイルを結合し、1動画ファイルとして出力している
33 const data = ffmpeg.FS('readFile', outputfilename); ffmpeg.wasmが出力したメモリストレージを、JavaScript上の変数に読み込み
34-35 ffmpeg.FS('unlink', inputfilename);
ffmpeg.FS('unlink', outputfilename);
使い終わった大きいファイルを削除
36 video.src = URL.createObjectURL(new Blob([data.buffer])); ffmpeg.wasmから受け取った動画データをBlobに変換し、videoタグで表示
40-42 zeroPadding関数 指定した桁数でゼロ詰めする
44-52 sec2time関数 ミリ秒単位の時刻をffmpegのコマンドライン引数にするため、秒の値を"hh:mm:ss.SSS"形式に変換する
54-59 await ffmpeg.load();他 ffmpeg.wasmをロード。初回は1〜3秒程度かかるため非同期に読み込みしている
61-166 FullRNNoise関数 rnnoise.wasmのロードと使用。
10ミリ秒ごとのデータが人間の声を含むかどうか0〜1のスコアを取得。
110-117 score_avg50 = (score_avg50 * 49 + score * 1) / 50;他 スコアの50回(直近0.5秒)の移動平均を求めている。
スコアが0.9以上ならば即人間の声とすることで、一言目が脱落することを防いでいる。
移動平均が0.5以上ならば有声音区間、0.5未満ならば無音区間としている。
148 if (video.currentTime >= 30) { 30秒でrnnoiseを強制停止(サンプル)
168-181 rnnoise.wasmのモジュールロード関数 CORSの関係でインライン化しているが、本来は別ファイルで、htmlと同じフォルダに配置する

トランスコードによる速度低下

ffmpeg.wasmを使用して、動画のトランスコードをすることは可能です。先にも述べたように、とても遅いためオススメできませんが、どうしてもブラウザで処理しなければならない事情がある場合には価値があるでしょう。念のため、どのくらい遅いか体感してください。「ファイルを選択」ボタンを押し、動画ファイルを選択してください。アップロードした動画を一度再生した後、しばらく待つと(PCによっては数分)、有声音区間のみの動画が再生されます。最初は短めの10秒くらいの動画でお試しください。上のコードと違うのは、24行目と25行目です。ffmpeg.runの引数に「'-c', 'copy',」が含まれているかいないの違いです。これが含まれている場合はトランスコードなし、含まれていない場合はトランスコードありになります。

トランスコードなし

await ffmpeg.run('-i', inputfilename, '-c', 'copy', '-ss', sec2time(voice_ranges[i][0]), '-t', sec2time(voice_ranges[i][1] - voice_ranges[i][0]), splitfilename);

トランスコードあり

await ffmpeg.run('-i', inputfilename, '-ss', sec2time(voice_ranges[i][0]), '-t', sec2time(voice_ranges[i][1] - voice_ranges[i][0]), splitfilename);

See the Pen
jumpcut_transcode
by terry (@terry_a)
on CodePen.

うまく動かない場合は、こちらのページで試してください。

さらに挑戦したい方へ

自分のトークを録画してデモアプリを使用すると思っていた以上に無音の区間が多かったことに気づくでしょう。ジャンプカットはカット編集したつなぎ目で少し違和感が出てしまうため、モーフカットという補完処理を入れるとより自然な映像になります。また動画ファイルや編集作業をクラウドに保存すれば、複数人による共同編集作業サービスも実現可能です。ぜひ挑戦してみてください。

有料サービスのご紹介

WebRTCを利用した会員制ライブ配信サービスを短期間で導入したい方、大規模配信のためのサーバ構築・インフラ運用を専門家に任せたい方は「ImageFlux Live Streaming」をご検討下さい。ポスプロ会社様向けに動画編集のコラボレーションシステムとして利用された実績があります。WebRTC SFUという技術を用いて、配信映像をサーバに中継させる仕組みです。1対多の会員制映像配信に特に強みがあり、初期の利用コストが低価格に抑えられます。