webRTC における icecandidate の取り扱いについて

まえがき

webRTC を用いた、ブラウザ間での p2p 映像相互配信機能を以下のサイトを参考にしつつ開発していたのですが、
1対1での相互配信から多人数での相互配信に拡張した際に、経路情報を表す icecandidate の交換が失敗するようになってしまいました。

具体的には、ブラウザから ICE failed, add a STUN server and see about:webrtc for more details と怒られるようになってしまいました。
調査したところ、 onicecandidate() の中で icecandidate を送信していたのですが、この icecandidate の送信タイミングが悪く、
icecandidate を受信する側の peer の準備が完了していないために発生しているもの(peer が undefined になる)だということが分かったため、
その詳細について説明します。

多人数を想定した offer/answer の交換

上記の URL にもあるように p2p を用いた多人数での相互配信を想定した場合、以下の図のようなシーケンスで offer/answer を交換することになると思います。

  1. まず、新たな参加者は映像を共有する各メンバーに対して "call me" のような、自分に offer を送ってもらうためのメッセージを送信します
  2. "call me" を受け取った各メンバーは、新たな参加者に対して、 offer を送信します
  3. 各メンバーから offer を受け取った新たな参加者は、各メンバーに answer を返します

以下の図では browser A が新たな参加者、 browser B がメンバーのうちの一人を表しています。

1

icecandidate の発見とその交換

上図には icecandidate の交換について書いてはいませんが、icecandidate はこの offer/answer のシーケンスとは別のシーケンスで処理が行われます。
icecandidate は、 RTCPeerConnection が経路情報を見つけた際に、コールバックである onicecandidate() を通して取得することができます。

onicecandidate()ドキュメントによると、

… specifies a function to be called when the icecandidate event occurs on an RTCPeerConnection instance. This happens whenever the local ICE agent needs to deliver a message to the other peer through the signaling server.

とあり、要するに RTCPeerConnection の準備ができたらいつでも onicecandidate() が呼ばれる可能性がある、とあります。
では、 onicecandidate() は具体的に上図のどのタイミングから呼ばれる可能性があるのでしょうか。

手元でいくつか試してみたところ、 onicecandidate()setLocalDescription() で offer を登録すると呼ばれるようになる、ということがわかりました。
すなわち、上図の browser B について、 offer を送信する前に経路情報を発見し onicecandidate() が呼ばれる可能性があることになります。

実装において、 onicecandidate() の中で icecandidate を送信するようにコードを書くと、下図のように peer が生成される前に icecandidate が送信される可能性があることがわかります。

2

このとき、 browser A での addIceCandidate() は peer が存在しないので undefined となり失敗し、
ブラウザからは ICE failed, add a STUN server and see about:webrtc for more details と怒られることになります。

対策方法

原因がわかりましたので対策ですが、これについてはいくつかの方法が考えられます。

  1. icecandidate 受信側(上図でいうと browser A)で peer の準備が整っていない場合、 queue や stack に積んでおく
    • ただし、各メンバー毎にバッファを用意する必要があり、やや面倒です
  2. icecandidate 送信側(上図でいうと browser B)での、 icecandidate の送信を遅らせる
    • https://qiita.com/massie_g/items/f5baf316652bbc6fcef1 にあるように、ある程度 icecandidate が出そろってから一気に送る、ということが可能なようです
    • ただし、 offer 送信前にある程度 icecandidate が出そろう可能性があり、確実ではありません
  3. offer だけ送信し peer の準備ができたら、お互いに合図を送る
    • これも新たなメッセージが増えるため、面倒そうです

一番実装が簡単そうなのは 1. の方法ですが、よりよい方法があるのでしょうか。

まとめ

onicecandidate()setLocalDescription() 後に実行される可能性があり、 onicecandidate() の中で icecandidate を送信するコードを書くと、
送信相手の peer の準備が整っていない可能性があり、それに対応する処理が必要になります。