WebRTC Insertable Streams: P2P でのスケーラブルWeb会議に try してみた

## これは何？

E2EE（End to End Encryption）目途で導入が加速した [WebRTC Insertable Streams](https://w3c.github.io/webrtc-insertable-streams/)。これ使うと、WebRTC P2Pでは実現難しかった、スケーラブル Web 会議実現するのまじつらい。みんな SFU 使おうよっていう現実を、多少なりとも変えれるかも？？？？と思い、まずは try してみた。という記事です。

## Insertable Streams って？

@massie_g さんの、https://qiita.com/massie_g/items/2b0b6d4f61f1865b4da5 が良記事なので、そちら確認いただきたいのですが（ `createEncodedVideo/AudioStreams()` が deprecated となり、`createEncodedStreams()` になる点だけ、ご注意下さい）、僕なりに軽く説明。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83466/5e6e608a-29ce-7a8a-529a-92ed597a727d.png)

上にあげたのが、WebRTC で映像をやりとりするときの模式図。カメラから生データを取得 -> エンコーディングして、WebRTCで対向に届ける。んで、デコーダで生データを取り出し、映像視聴ができる。って寸法です。

で、単純なビデオチャットだとブラウザにお任せ楽ちんだね！でいいのですが、慣れてくると映像データなりを色々いじりたくなります。ってことで、WebRTCもその後進化し、これまでは青のポイント **エンコーダに送る生映像** を色々いじることが出来るようになっています。例えば、zoom のようなバーチャル背景やりたいなら、ここでいじるのが王道。

んじゃぁ、 Insetable Streams は何ができるの？って話ですが、ここで追加されたのが赤い矢印の部分 **エンコーディングされたデータをネットワークに送信する直前 / 受信直後** に、色々と操作することができるようになりました。

これによって、何が嬉しいの？というのが、この記事の本題。現状主なターゲットは、 @massie_g さんの記事に記載されている、End-to-End Encryption。つまり暗号化。これにより、Zoomによって取り沙汰された、SFUでデータ抜かれて悪用されるんじゃ・・・・を回避しようというものです。

でも、それだけではつまんないですよね。ってのが、この記事です。

## Insertable Streams で、P2PスケーラブルWeb会議が実現できるんじゃ？

[このMedium記事](https://medium.com/kokutele/kokutele-%E3%81%AF-%E3%81%AA%E3%81%9C4%E4%BA%BA%E8%B6%85%E3%81%88%E3%81%A7%E9%9F%B3%E5%A3%B0%E3%81%AE%E3%81%BF%E3%83%A2%E3%83%BC%E3%83%89-%E3%81%9D%E3%81%AE1-68bd07cab254) にあるように、最近 P2P での Web会議実現にこだわっているのですが、ここで問題となるのがスケーラビリティの無さ。P2Pで Web 会議を実現するのは中々のイバラの道です。

P2P方式だと、盗聴リスク無茶苦茶少ないし、トラフィックの効率性やレスポンス性の良さとかメリットあるんですけどねーーー。残念。

その理由を軽く模式的に書くと、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83466/5651518a-aca7-5783-2aa1-24a724bd66d9.png)

Web会議で3人の参加者に映像を送るよ！っていうのを模式的に書いたものなんですが、ここにあるように P2P 方式だと、参加者分エンコーダ回してやらなきゃならないんで、参加者が増えるとここの負荷がはんぱないことになってしまう。せいぜい、3,4 人が限界だなってのが僕の経験です（普通の映像でやろうとすると）。

冒頭であげたように、従来の WebRTC だとエンコード後の映像はいじれません。なんで、「音声会議だけにするかーーー。P2Pの場合は・・・。やっぱ会議作るならSFU使わないとね！」となるんですが、Insertable Streams により、これ変わるんじゃん？ってのが、今回のアイディア

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83466/adff3b38-d4b5-afae-e602-60c5d94ddcf5.png)

そのアイディアを模式的に描いたものが上図。まともにエンコードするのは、最初の参加者の分だけ、それ以外はブランク映像をエンコードします。そんで、エンコード後のデータに Insertable Streams 使って、1人目用にエンコードしたデータに差し替えちゃう。そしたら、エンコード負荷を下げることができるんで、スケーラブルな多人数Web会議を P2P でもできるんじゃないの？というアイディアです。

## ってことで試してみた。

まずは、結果から。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83466/5a00abca-3685-f837-7427-aa88821afecd.png)

https://conf.kokutele.com/survey/insertable-streams/ で実際に試せます。（Chrome M83以降。chrome://flags/#enable-experimental-web-platform-features　を `enable` にして下さい）

こちら、ローカルループバックのデモで、同時に4人に配信してる時を模したものです。画面中央にある、 `add`  ボタンを押せば、どんどん増やせるのですが、

* 一人目は、上段のソース映像左側をエンコードして、そのまま配信。
* 二人目以降は、上段のソース映像右側のブランク映像をエンコードした後、Insetable Streams で一人目に配ったのに差し替えて配信。

って感じ。それを WebRTC でローカルループバック受信して表示しているのが下段です。実際に試すと分かりますが、綺麗に揃って差し替え配信されることがわかります。

気になる、CPU使用率の低減効果ですが、僕の手持ちの環境で4人接続の場合

* 通常方式: 300% 程度
* 本方式: 200% 程度

と、CPUの石、約1個分の低減効果が得られました。結構な効果があることが分かります。

## どうコーディングするの？

js のコードは https://github.com/kokutele/kokutele/blob/master/public/survey/insertable-streams/script.js に置いてあるんで、そこをチェックしてね。ということになるわけですが、ポイントだけ解説します。vanilla JSで書いてるんで、若干長ったらしいですが、利活用はしやすいと思います。

### ブランク映像の作り方

みんな大好き `geUserMedia()` で MediaStream オブジェクト生成後

```js
stream.getVideoTracks().forEach( t => t.enabled = false)
```

と、videoTrack の `enabled` を `false` にするだけです。簡単

### Insertable Streams 使って差し替える方法

まず、差し替えるデータを管理する `Map<Array>` を定義します。

```js
const queue = {
  audio: new Map(), // <Array>
  video: new Map(), // <Array>
}
```

次に差し替え処理の部分。Insertable Streamsでは、WebRTCの Sender/Receiver を取得後、 `createEncodedStreams()` をコールすると、エンコード済みのチャンクデータを `pipeThrogh()` でストリーム処理できるよって流れになるのですが、そこの中で以下の処理を行うことで差し替えています。

```js
if( this.idx === 0 ) {
  const size = queue[kind].size
  if( size > 0 ) {
    for( let [idx, q] of queue[kind] ) {
      q.push( chunk.data )
    }
  }
} else {
  const dataArr = queue[kind].get( this.idx )
  if( !dataArr ) {
    queue[kind].set( this.idx, [])
  } else {
    const d = dataArr.shift()
    if( !!d ) { chunk.data = d }
  }
}
```

`idx` が、参加者番号となっていて、`0` だと一人目になっています。 `chunk.data` にエンコード後のデータが入っているので、それを他の参加者用に、Map<Array> にストアしています。

2人目以降は、 `else` 以下の処理になる訳ですが、なんてことはない、 `chunk.data` に Map<Array> にストアしたデータをFIFOで取り出し、上書きしているだけです。こんなんで、あくまで初期実装ですが CPU 使用率を激減させるのに成功しました。いやーめでたい。

## Disadvantage は？

### keyframe 制御に課題

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83466/3ccc6688-82d6-515b-9b45-8adc0b5b4350.png)

デモサイトで遊んでみると、すぐに気づくのですが、 `add` した直後、上図のような妙なブロックが踊る映像になります。これは、映像の keyframe が取れていないためで、Chromeさんが自動で定期的に送ってくれる keyframe を待っている状態です。

通常だと、参加時には自動的に PLI と呼ばれるメッセージが RTCP で飛んできて、エンコーダサイドではそれを検知して keyframe 生成してくれるのですが、 Insertable Streams では映像データしかケアしてないので、そこをどうすることもできません。

最近の Chrome だとハードウェアアクセラレーションが効かせられる環境だと、数秒インターバルで keyframe 送ってくれるみたいなんですが、ソフトウェアエンコードに陥ると、とたんに数分に一回送ってくれるモードに突入します。たぶん、その変が関与してるのだと思いますが、上述のデモでは参加者を増やすと映像がおかしくなります。

### トラフィック制御に課題

そもそも、P2Pでそれぞれ用にエンコーダが回るのは、各回線毎にトラフィック制御（回線状態に応じ、エンコードパラメータを自動で変更し、映像品質を落としつつも視聴には耐えれるようにする）をするた めです。

詳細は https://qiita.com/komasshu/items/1cb5d4469595a635c689 （僕が以前書いた記事）を参照いただきたいのですが、本方式ではそれも叶いません。ローカルループバックなので、まぁ割とよさげに動いているように見えますが、このまま外海にだすと大変なことになりますｗ

## WebCodecs が来たら、解決可能？

てな感じで、Insertable Streams 単体では、ちと片手落ち。「いい線いってるし、可能性はみえるんだけどなーーーーーー」という残念状況です。

しかし、まだ諦めるのは早い。最近の Web のリアルタイムメディア対応シリーズ注目株。 [WebCodecs](https://wicg.github.io/web-codecs/) が、この状況を一変してくれる可能性が高いです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83466/6e633f3c-9e5c-28fe-c3e6-33e09f8a7ae5.png)

WebCodecsの詳細は、ここでは割愛しますが、先の図に追記すると、緑色の `エンコーダ/デコーダ` の部分をゴニョゴニョ出来るようになります。確証は現状取れてないので、詳細割愛しますが、ドラフト仕様見る限り、上に列挙した課題は WebCodecs でかなり解決可能だなと僕は見ています。

ちなみに、WebCodecs は ChromeのM86以降(今だとβチャンネルかな？)で experimental featureとして利用可能です。実は結構すぐそこにいたりします。

## 最後に

P2Pは、それ自体非常に可能性を秘めています。今回の記事では、Web会議というユースケースで、それを紹介し、WebRTC Insertable Streams + WebCodecs で、それの明るい未来図を技術的に示してみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83466/fa4c47ab-f2f6-dd11-a143-8189224cc9e6.png)
https://twitter.com/komasshu/status/1300084656063168513
につぶやいたように、これ以外の API 含め、今まさに革新が Web の世界では起きようとしています（と勝手に思っているｗ）。楽しみ楽しみ

この辺りは、またおって記事にしていきたいと思います。