WebCodecs + WebRTC Insertable Streams で Scalable live w/ P2P を実現してみた

# これは何？

[前回記事](https://qiita.com/komasshu/items/13b0225ed839ac20548d) の続き。これまでは、現実的ではなかった WebRTC P2P での live 配信。WebCodecs と WebRTC Insertable Streams 使ったら「お、これは使えるかも！？」だったので、それの紹介記事。

# WebCodecs + WebRTC Insertable Streams 使った Scalable live w/ P2P のアーキテクチャ説明

まず、[前回](https://qiita.com/komasshu/items/13b0225ed839ac20548d)を振り返ると、WebRTC Insertable Streams を使い、以下のアーキテクチャで動作させてみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83466/f7fdd6ff-b612-870b-c2a5-16f4d2916d14.png)

要は、P2Pで対抗ごとにまともにエンコーダ回すと、その処理で CPU なりが厳しくなっちゃうんで、2本目以降はダミー映像エンコードにして、 Insertable Streams で 1本目のまともにエンコードした映像データに差し替える・・・・というもの。

詳細は、前回記事を参照いただければですが、こちらの問題は、「差し替えるオリジナルのデコード済み映像データの keyframe 制御ができないため、受信側でまともに映像がみれない」というものでした。結局エンコーダは、WebRTCまかせでアンタッチャブルなんでなんともできない（涙

そこで、今回は下図に示す WebCodecs を用いたアーキテクチャに変更しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83466/ebf49454-72b5-3aca-c126-a9e2aa7911af.png)

一番の違いは、ソースの映像を WebCodecs を使って、コントローラブルにエンコード可能としたこと。なので、WebRTCにはすべてダミー映像 (videoの `track.enabled = false` に設定したストリームオブジェクト） をくわせています。

そんで、 Insertable Streams で、ダミー映像をエンコードしたものが毎フレームごとに取得できるので、それを WebCodecs でエンコードしたものに差し替えるという戦略です。考え方としては、非常にシンプル。

エンコードの際、映像データの場合、 keyframe 制御が非常に重要になります。ここで、WebRTCでは、新規に視聴参加者が来たり、映像データのロスが検出されると、同図中の「エンコーダ by WebRTC」にその情報が渡され、keyframe としてエンコードがなされます（あくまで、ダミー映像ですが）。すると、 Insertable Streams ではこのとき `chunk.type` の値が `key` となるため 

```js
if( chunk.type === "key" ) {
 // WebCodecs に keyframe 要求を送る
}
```

と、そのプロパティをチェックし、 WebCodecs の keyframe 制御を行うことで、いい感じで映像配信ができる・・・という形です。

# コードの説明

ソースコード全体は https://github.com/kokutele/kokutele-live で公開しています。
以下では、あくまで、ポイントだけ。動作原理は、先に示したとおりとなっており、詳細はスニペット中のコメントを参照ください。

## 送信側

まず、 WebCodecs によるエンコードの部分を抜粋したのが以下のコード

```js
  start():void {
    // MediaStream オブジェクトから Video Track を取り出す
    const [track]:Array<MediaStreamTrack> = this._stream.getVideoTracks()

    // WebCodecs の定義。エンコードが行われる都度、 'chunk' イベントを発生する。
    const videoEncoder = new window.VideoEncoder({
      output: chunk => {
        this.emit('chunk', chunk)
      },
      error: err => {
        this.emit('error', err)
      }
    })
    videoEncoder.configure({
      codec: 'vp8',
      width: 640,
      height: 480,
      framerate: 30
    })

    // 映像トラックの読み込みを開始し、逐次 `encode()` によりエンコード処理を
    // 行う
    const videoReader = new window.VideoTrackReader(track)

    let idx = 0
    const interval = 10 * 30 // 10 sec
    videoReader.start( frame => {
      // keyframe 制御の部分。10秒に一回の periodical 制御とともに、
      // プロパティ `_reqKeyFrame` が `true` に設定されたときも、
      // keyframe 生成を行う
      const _reqKeyFrame = this._reqKeyFrame || !(idx++ % interval)
      videoEncoder.encode(frame, {keyFrame: _reqKeyFrame})
      this._reqKeyFrame = false
    })
  }
```

これを、 Insertable Streams でダミー映像のものと差し替えます。

```js
      transform: (chunk, controller) => {
        const kind = chunk instanceof window.RTCEncodedVideoFrame ? 'video' : 'audio'
        // ダミー映像の chunk.type が `key` だったら、WebCodecsで、keyframe 生成
        if( kind === "video" && chunk.type === "key") {
          this._encoder.reqKeyFrame = true
        }

　　　　　// エンコードデータの差し替え処理
        if( kind === "video") {
          // replace media data
          const _chunks = chunksMap.get(this._receiver)
          const _chunk = (_chunks && _chunks.length > 0) && _chunks[idx++]

          if( _chunk ) {
            if( typeof _chunk === 'object' && _chunk.data ) {
              chunk.data = _chunk.data
            }
          } 
          controller.enqueue( chunk )
        } else if( kind==="audio" ) {
          controller.enqueue( chunk )
        }
      });
```

## 受信側

普通に、WebRTCで受信したものを処理するだけで、Insertable Streams や WebCodecs 的な特別な処理は不要です。

# パフォーマンステスト

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83466/ad6e3ccc-13f3-0969-90fe-93cc5a71ee60.png)

後述のサンプル Web app を使い、簡易的なパフォーマンステストを行いました。結果が上図の通り。同一端末内で複数ブラウザを立ち上げ、一つが送信側、それ以外が受信側・・・という簡易なもの。あくまで、参考として御覧ください。

同時視聴数の増加に伴い、送信側 Chrome の CPU 使用率があがっていきますが、通常のP2P配信（赤線）と、今回のスケーラブル配信（青色）とで明らかにパフォーマンスメリットが得られています。

ここで、同時接続 6 で通常の P2P 配信で CPU 使用率が下がっていますが、こちら、CPU処理的に厳しくなったためか、送信エンコードのレートを下げているためで、あくまで目視ですが 1.6Mbps -> 600kbps 程度に下げていました。なので、通常の P2P 配信だと配信限界は 5 ぐらいかな・・・ということになります（さすがに、この数では使えない）

一方、WebCodecsの場合は、通常時で 1Mbps の値となっていたのですが、こちら全く変わらず。データとしてきちんととっていないですが、20弱に試しにあげても、問題なく配信できていました。たぶん、もっと行くんじゃないかな。100のオーダーは厳しそうだけど（端末性能に依存するところですが）

## サンプル Web app

https://live.kokutele.com/

で、スケーラブル P2P live 配信試せます。配信側では

https://live.kokutele.com/sender

にアクセスいただき、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83466/d1047073-2a8a-f7d6-ed7d-04edb7527b0d.png)

"start sender" をクリックすると、下部のほうに視聴側のURLが払い出されるため、こちらにアクセスすることでリモート視聴できます。

予想に難くなく、 "enable scalable" のチェックを外せば、通常 P2P 配信モードになります（Chrome M87以降で、 `chrome://flags/#enable-experimental-web-platform-features` の enable を忘れずに）

## 終わりに

WebCodecs + WebRTC Insertable Streams で、「完全サーバレス」で小規模ライブ配信が可能であることを示しました。仲間内とか、社内とかだと数十ぐらいの配信で十分だったりするケース結構多いので、割と使い勝手あるのかな？と思います（かなり荒く作ってるんで、もーちっとコードの改善必要ですがｗｗ

こちらのサンプルApp、「単なる配信以上の何か」も入っているため、謎のモノクロ映像も表示されていますが、とりあえず今は気にしないでくださいｗ（この辺については、近日中に僕の個人ブログで記事を書く予定）