フレクトのクラウドblog re:newal

http://blog.flect.co.jp/cloud/からさらに引っ越しています

ffmpeg.wasmをGitHub Pagesで動かすよ

こんにちは。研究開発室の岡田です。

Heroku 無料枠終了のお知らせの衝撃からだいぶ月日がたち、終了まで残り1か月強となりました。皆様におかれましては、有料プランへの移行準備あるいは、別サービスへの移行作業は進められておられますでしょうか。どうすべきか迷っておられる方は、識者の方々がいろいろと比較検討してくださっていますので、ご参考にされるといいと思います。(参考, 参考, 参考, 参考)

私も例にもれず、実験的なアプリはHerokuにデプロイするのが常であり、いろいろとお世話になっていました。慣れもあるかもしれませんが、Herokuの開発UXは非常に優れていると感じており、サーバ側で処理が必要となるようなアプリについては、引き続きHerokuを使ってもいいかなと考えています。一方で、サーバ側の処理がない、いわゆる静的なアプリについては、ソースコードと一緒にリポジトリで管理できるので、GitHub Pagesがお手軽でよさそうだなと考えています1

さて、私がHerokuにデプロイしていたアプリにはffmpeg.wasmを利用したものが幾つかあるのですが、実はこれをGitHub Pagesにデプロイする際には問題が発生します。ffmpeg.wasmを使用したことがある方にはおなじみの話だと思いますが、例のCOOP, COEP制限に引っかかってしまいブラウザで処理が行えなくなってしまうのです。今回は、これについて、簡単な説明と回避方法についてご紹介したいと思います。

GitHub Pages

GitHub Pagesの正確な説明は公式のページを見ていただくのが良いと思いますが、ここでざっくりと説明すると次のようなものです。

GitHub Pagesは、公開リポジトリの特定のフォルダに格納したHTMLをウェブページとして公開することができる機能です。このHTMLからJavascriptもロードできますので、ウェブアプリとしても公開することができます。予めGitHub Pagesの公開設定をしておく必要がありますが、GitHubの公開リポジトリの設定画面において簡単に設定可能です2

ffmpeg.wasmとSharedArrayBuffer

ffmpegは、説明する必要はないと思いますが、主に動画・音声を変換するために使用されるオープンソースのソフトウェアです。ffmpeg.wasmは、ffmpegをブラウザで利用できるようにWebAssembly(wasm)にコンパイルしたモジュールです。

ffmpeg.wasmは内部的にSharedArrayBufferという機能を使用しています。SharedArrayBufferは、複数のスレッド(複数の実行コンテキスト)でメモリを共有するための機能です。少し古いですがこの解説がわかりやすいです。

SharedArrayBufferとCOOPとCOEP

さて、ここからが問題が一気に難しくなります。正確に説明する自信がないので詳細はリンク先等で確認していただきたいですが、すこし頑張ってここでも説明してみたいと思います。

かつてSpectreという攻撃手法が発見され大きな問題になったことがありました。Spectreが発見されてからSharedArrayBufferって安全なのか?ということになったようです。Spectreは、CPUの投機的実行による残骸から情報を推測する攻撃手法です。ハードウェアレベルの脆弱性であり、アプリケーションレベルで対策は難しいです。SharedArrayBufferもこの攻撃手法にさらされるリスクがありますが、ブラウザでの対策は難しいため、ChromeではSharedArrayBufferを厳密にcross-origin isolationされている環境においてのみ使用できるようにしています(参考)。

このcross-origin isolationを実現するには、HTTPヘッダでCOOPとCOEPを設定する必要があります。

COOPはCross Origin Opener Policyの略です。同じブラウジングコンテキストグループ内に存在する、Cross Originのオブジェクト間での参照関係を設定することができます。この値をsame-originに設定することで、同じブラウジングコンテキストグループ内に存在していたとしても、Cross Originのオブジェクト間では相互に参照できないようにできます。

COEPは、Cross Origin Embedder Policyの略です。Cross Originのリソースを読み込む場合に、読み込むリソースにCORSが設定されていることを強制することができます。CORSが設定されていることを強制する場合には、require-corpを設定します3

具体的には、メインドキュメントで、次のようなHTTPヘッダを送信する必要があります。

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

この辺の説明は、この動画とこの動画がわかりやすいです。

問題:GitHub PagesではHTTPヘッダを制御できない

これまで説明してきたとおり、ffmpeg.wasmはSharedArrayBufferを使用しています。SharedArrayBufferは、cross-origin isolation設定されている環境でのみ利用可能です。cross-origin isolationは、HTTPヘッダで設定します。

しかし、「GitHub PagesではHTTPヘッダを制御できない」のです。これは仕様です。つまり、cross-origin isolationをGitHub Pagesで実現することはできません。ということで残念ですが、他のサービスを探しましょう。。。と、いう話ではなく、何とか回避策を考えたいと思います。

解決策:Service WorkerでHTTPヘッダ書き換え

実は、HTTPヘッダをブラウザ内で追加する方法があります。それはService Workerを使用する方法です。Service Workerは、ネットワークに繋がっていない状態(オフライン)でWebアプリを動かしたいときに使う透過的なプロキシと説明されることが多いと思います。Service Workerを使うとブラウザのfetch処理をhookしてそこに処理を追加することができます。オフラインの時には、fetch処理でキャッシュデータを返すように処理を追加することでWebアプリを動かし続けることができます。

ということで、察しの良い方はお気づきと思います。Service Workerを用いて、fetch処理に対してHTTPヘッダにCOOPとCOEPのヘッダを追加する処理を追加すれば、cross-origin isolationの設定ができそうということです。そして、これは実際にできます。

具体的な実現方法は、自分で実装してもたいして難しくはないのですが、Githubでライブラリを公開されている方がおられるので、今回はそれを使用してみたいと思います。

https://github.com/gzuidhof/coi-serviceworker

Service Workerの本体はcoi-serviceworker.jsで100行程度のスクリプトです(2022/10/14時点)。 中盤あたりに記載されている下記の部分がheaderを追加しているところです。

event.respondWith(
            fetch(request)
                .then((response) => {
                    if (response.status === 0) {
                        return response;
                    }

                    const newHeaders = new Headers(response.headers);
                    newHeaders.set("Cross-Origin-Embedder-Policy",
                        coepCredentialless ? "credentialless" : "require-corp"
                    );
                    newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");

                    return new Response(response.body, {
                        status: response.status,
                        statusText: response.statusText,
                        headers: newHeaders,
                    });
                })
                .catch((e) => console.error(e))
        );

このJavascirptをメインのHTMLから読み込むことで、無事cross-origin isolationの設定が行えて、ffmpeg.wasmも利用可能となります。

以上の方法でGitHub Pagesにおいてffmpeg.wasmを使用するWebアプリを公開することができます。 なお、cross-origin isolationは各自の責任でオプトインするものですので、その点はご注意ください。

GitHub Pagesで公開しているアプリのご紹介

最後に、今回GitHub Pagesで公開することができたアプリをいくつか紹介したいと思います。

Screen Recorder

PCの画面を動画としてキャプチャするアプリです。 ブラウザ上で動くので新しいアプリケーションをインストールする必要はありません。 Windows, Mac, LinuxChromeで動きます。Safariでは動きません。

https://w-okada.github.io/screen-recorder-ts/

ezgif-1-e406039666

Simple Movie Editor and Ffmpeg CLI Generator

動画を加工するアプリです。 動画の開始時間と終了時間を編集できます。 指定したエリアを切り出したり、ぼかしを入れたりできます。 また、ffmpegCLIも生成します。ブラウザ上だと処理が遅い場合にCLIをコピペして実行することができます。

https://w-okada.github.io/ffmpeg-cli-gen-js/

ezgif-1-eedd9a0541

Offline Transcribe

音声書き起こしをするアプリです。 日本語、英語等複数の言語に対応しています。

https://w-okada.github.io/vosk-browser-ts/

ezgif-1-4af8f8ab04

まとめ

GitHub Pagesでffmpeg.wasmを用いたアプリの動かしかたをご紹介しました。 デプロイ先に困っている方の一助になれれば幸いです。

Disclaimer

本ブログのソフトウェアの使用または使用不能により生じたいかなる直接損害・間接損害・波及的損害・結果的損害 または特別損害についても、一切責任を負いません。


  1. 最初からHerokuではなく、GitHub Pagesで動かしておけよ、という突込みはごもっともですが、開発環境を流用していた関係で、「まぁHerokuでいいか」ということになってました。そういう方、結構おられますよね?

  2. 裏側ではGitHub Actionsが動いてどうのこうの、というのがありますが細かいので割愛します。

  3. 実は、この設定がなぜ必要なのかがわかっていない。Cross Originのリソースを読み込む側に悪意がある場合に、読み込まれる側のリソース上で投機実行された残骸から情報を推測されてしまうのを防ぐためかな?となんとなくの理解。ただ、COOPだけでも十分な気もするが、、。