MediaStreamへのPNGグリッチの適用
TL;DR
- MediaStreamに対してPNGグリッチを適用しました
- PNGグリッチは、Rustで実装しWasm化したものを利用しています
- グリッチを行うコンテキストをリソースとして定義することで、グリッチのための処理をJavaScriptで柔軟に記述できるようになります
作成したもの
navigator.mediaDevices.getDisplayMedia()
で取得したMediaStream
のビデオトラックに対してPNGグリッチを適用しています。
図1. ZennのトップページにPNGグリッチを適用している様子
ストリーム処理
MediaStream
をストリーム処理することで、全体を実現しています。PNGグリッチはMediaDevies
オブジェクトとHTMLVideElement
の間のGlitchFilter
として実装されています(図2)。
図2. ストリーム処理の様子
GlitchFilter
はMediaStreamTrackProcessor
とMediaStreamTrackGenerator
を利用して実装されています。これらはInsertable Stream For MediaStream APIという仕様に定義されています。
MediaStreamTrackProcessor
を利用して、入力のMediaStream
から動画像の流れるMediaStreamTrack
を抽出し、各フレームの流れるReadableStream
を作成します。またMediaStreamTrackGenerator
は指定した種類のデータを書き込めるWritableStream
を持っています。GlitchFilter
はこの2つのストリームの間に挟まるTransformStream
として実現されています。
なお、GlitchFilter
はWebWorker
上で実行されています。
GlitchFilter
の処理
GlitchFilter
は次の手順でVideoFrame
にPNGグリッチを適用します:
-
OffScreenCanvas
を利用しVideFrame
からPNG画像を作成します - 作成したPNG画像のバイト列からPNGグリッチを適用するためのコンテキストを作成します
- 操作後のPNG画像のデータからビットマップデータを作成します
- 作成したビットマップデータと、元の
VideoFrame
のタイムスタンプから新しいVideoFrame
を作成します - 作成した
VideoFrame
をTransformStreamDefaultController
に渡して、ストリームに送出します
コードは次のようになっています。実際にはステップ1と2の間で、ランダムにVideoFrame
の部分をコピーすることでTransposeも適用しています。
ステップ2で作成されているコンテキストは、chikoski:glitch-art/png-glitchable
にpng
リソースとして定義されています。これをRustで実装し、Wasmコンポーネントとしてビルドしたものを利用しています。
ビットマップからPNGへの変換をどこで行うべきか
PNGグリッチはPNGファイルの書式を保ちつつデータを破壊することで、グリッチアートを作成する手法です。一方、VideoFrame
から直接作成できるのははビットマップ画像です。つまりPNGグリッチをVideoFrame
に対して適用するには、ビットマップ画像からPNG画像を作成する必要があります。これには以下の2通りの方針があるように思います:
- ブラウザーの提供するAPIを利用してPNG画像の作成を行う
- PNG画像への変換をWasm内で行う
最初は後者の手法を取っていました。これはメインスレッド内で処理を行っていた場合、OffScreenCanvas
からのBlob
オブジェクト作成と、Blob
オブジェクトからArrayBuffer
オブジェクトの作成に概ね255ミリ秒(16フレーム弱)かかっていたためです。この時は体感できるほどの遅延がありました。
これをWebWorker
に移したところ、大きな遅延は無くなりました。よって現在は前者の手法をとっています。図3はキャプチャーのウィンドウ(上)とグリッチさせたもの(下)との比較を行なっています。体感できるほどの大きな遅延はなくなっています。
図3. キャプチャーのウィンドウ(上)とグリッチさせたもの(下)との比較
なおOffScreenCanvas
を利用したPNG画像の作成は、次のように行なっています:
コンテキストをWIT上のリソースとして定義することの利点
グリッチ対象のデータは、Wasm内部で管理されているオブジェクトとして表現されています。またグリッチのための操作によって値がどんどん変更されていきます。つまりWITのリソースとして表現することができます。
WIT上のリソースとして表現することで、グリッチのための操作はコンテキストへのメソッド呼び出しとして実現できます。これによってTypeScriptやJavaScritのコードを変更することで、作成されるグリッチアートを変更することができます。つまり、気軽に試行錯誤ができるようになりました。
実際Rust内部でグリッチアートの作成自体も実装したこともありました。このとき、作成されたグリッチアートが気に入らない場合、次のような修正作業が必要となりました:
- Rust側のコードの変更
- Wasmへのビルドとトランスパイル
- トランスパイルされたコードの配置と、TypeScriptのビルド
- 結果の確認
Wasmのビルドとトランスパイルが特に面倒でした。実際、ビルドしたファイルを取り違えたり、トランスパイルした結果を適切な位置に配置しなかったといったミスも頻発したため、処理を自動化するためのビルドスクリプトを作成したこともあります。このビルドスクリプトのメンテナンスも面倒でした。
グリッチ処理をWIT上のリソースとして定義し直すことで、Wasmはグリッチ処理のためのAPIを提供し、実際の処理はTypeScriptで定義するといった形に責任分解点を変更することができました。これによりビルドスクリプトの削除を含むビルド手順の簡略化を行うことができ、TypeScript側のコードを変更するだけでグリッチアートの作成方法の変更ができるようになりました。TypeScriptビルドツールの提供するホットリロード機能と組み合わせることによって、試行錯誤は次のように簡単にできるようになりました:
- TypeScriptのコードの変更
- ブラウザーでの確認
まとめと感想
この記事ではMediaStream
に対してPNGグリッチを適用しました:
- Insertable Stream For MediaStream APIはビデオフレームの処理をWebWorkerで行うことを可能にします
-
OffScreenCanvas
を利用することで、VideoFrame
からPNGデータの作成を高速に行えます - WITのリソースを適切に使うことで、WasmはAPIの提供、TypeScriptはAPIの利用側といった形で責任分解点を設定できます
3つ目の点は試行錯誤を容易にする以外に、コードの見通しもよくする効果があったようにも思います。Wasmはライブラリーであり、アプリはあくまでTypeScriptで書かれているというような理解が可能になりました。Wasmの内容に踏み込む必要もなくなり、抽象度の高く理解しておけば良くなったようにも思います。
Discussion