RTCPeerConnection
インターフェースの解説
- MediaプレーンとSignalingプレーンを分けて考えてる
- そしてMediaプレーンに焦点をあててる
- 既存の仕組みとの互換性などを考えて、Signalingプレーンはお任せにしてる
- JSEP = セッション確立のための実装
- SDPの受け渡しAPI(
create{Offer|Answer}()
/set{Local|Remote}Description()
) - 内部的なICEの状態管理
- SDPの受け渡しAPI(
- ICEの状態管理とSignalingの状態も分離して考えてる
- そうすることでTrickleICEとかにも対応できる
- もっと細かいAPIを提供することも検討していたけどやめた
- コードが煩雑になる
- グレア状態など理解すべきことが多くなる
getCapabilities()
をcreate{Offer|Answer}()
の代わりに公開することも検討していたけどやめた- オファーアンサーの生成を手動でやるのは手間がかかりすぎる
- この仕様を考えた当時はこれが最高の落としどころだと思ってた
- いつもの
- 本文なし
- JSEPはオファーとアンサーのSDPを生成する
- しかしそれをピア同士がどのように交換するかは関与しない
- Signalingプレーンとはこの部分のこと
- 再送もグレア制御も全ておまかせ
- JSEPにとってはSDPがすべて
- 使えるコーデックなど
- しかしオファー・アンサーが全て受け入れられるとは限らない
m=
行は同じ数でないといけないなど制約もある- localかremoteか、offerかanswerかは全て独立している
- アンサーは送り直すことが仕様としては想定されてる
- ただし最後のものだけが使われる
- 途中のやつが
pranswer
というやつ(Provisional) - しかしJavaScriptのAPIに
createPrAnswer()
とかはない
- オファーも送り直せる
- もちろんこれも最後のものだけが使われる
- SDPの扱いにくさは仕方ない
- いちおう枯れた仕様なのでそっとしてある
SessionDescription
クラスでラップしてある- 将来的にはコイツをいじればいい感じにSDPを修正できるかも
- まぁSDPをアプリ側で直接修正することはないはず
- 後述するAPIがあるので
- ここで紹介するAPIを介することで、
create{Offer|Answer}()
時に生成されるSDPを変えられる
- 単一の
m=
セクションに対応するクラスRTPSender
とRTCReceiver
をペアで持つ- 手動で追加した場合は、もちろんSDPのそれと数は合わない
- 各
m=
セクションのmid
を一意に持つ- ただし何らかの拍子で同じ
m=
セクションが再利用されると、違うmid
を持つ新たなインスタンスができることもある
- ただし何らかの拍子で同じ
- APIを使って手動でインスタンスを作るか、
setRemoteDescription()
の際に自動で用意される
- その名の通りRTPを送るためのクラス
MediaStreamTrack
を接続するのも仕事- あとはRTCPとの兼ね合いも
- その名の通りRTPを受け取るためのクラス
RTPSender
と同じ責務
- 本文なし
- ICEのcandidate収集はきっかけで行われる
m=
行が追加された- リスタートによりクレデンシャルが変更された
- その場合は
needs-ice-restart
というフラグが内部で立つ- このときに
createOffer()
すると新しいクレデンシャルが得られる - そのクレデンシャルを
setLocalDescription()
するとフラグが降りる
- このときに
- 収集の様子は随時イベントとして知らされる
- TrickleICEの話
- オファーを送ったらもうcandidateも送る
- オファー側の全候補収集を待ってからオファーをもらう必要がない
- 対応してないなら全候補が揃ってからオファーを送ればいい
- candidateは
IceCandidate
クラスで内包されてる- 基本的にSDPの1行と同じ
- ただし
a=
はついてない
- 各candidateはICEの
ufrag
を含む- これでICEがリスタートしたときなど判別できる
- どの
m=
セクションと紐付いているかも書いてある- 何番目かをあらわすインデックス(0はじまり)
- or
mid
mid
を優先して使うことで、RtpTransceiver
と紐付けできる
- candidateのtypeを制限できるようにしている
- host / srflx / relay
- 制限したものは絶対に露出させてはいけない
- その指定が変わったらICEをリスタートする
setLocalDescription()
でICEは動き出す- SDPを見ればcomponentの数がわかるから
- ただ実はそれより前にcandidateを集めてプールしておいてもいい
- そうすればより早くICEの仕事を終えられる
- ICEのRFCは5245と8445がある
- 世の中のだいたいは5245をベースに実装してるはず
ice2
という属性があるなら8445対応
- 5245との互換性は保つ必要がある
- SDPの
a=imageattr
で動画のフレームサイズを設定できる - 受け取っても再生できないデカさの動画とかあるかも
- JSEP的には、正方形ではないピクセルを送信しない
- 受信はする
- 特に制約がない場合、
a=imageattr
は省略されるsendonly
の場合も必ず省略する
- 基本的にdirectionが
recv
のときに使うもの - 指定の例
a=imageattr:* recv [x=[48:1280],y=[48:720],q=1.0]
- 48x48から1280x720まで、フォーマットは不問の場合
q
は1.0
が推奨値
- そもそもSDPでもadvisoryな項目なので、無視されることもある
- JSEP的にはフォーマットごとに1つしか指定しない
- けどJSEPじゃないやつからは複数送られてくるかも
- その場合は後勝ち
- この属性のパースに失敗した場合は、そのフォーマットを送れない
- 単一の
m=
セクションに複数のMediaStreamTrack
を含むSimulcastができる- 複数を送信できるけど、受信は1つ
RTPSender
で指定する- SDPにそれ用の記述(後述)をする
- 解釈されなかった場合は最初の設定だけ使う
- Simulcastで受信したい旨を設定する方法は今のところない
- オファーで提示されても、それに応える術がない
- このあたりの仕様は将来変わるかも
- 今どうにかするためのあれこれは以下を参照
- draft-ietf-mmusic-sdp-simulcast
- draft-ietf-mmusic-rid
- いくつかのSignalingシステムは、複数のエンドポイントに同じオファーを送る
- これをFolkingという
- Folkingには直列か並列かの種類がある
- JSEPとしてはSignalingはスコープ外ではあるけど、影響もあるので触れる
- メディアをどの時点で送受信すればいいかとか
- アクティブなセッションは常に1つ
- 先着1名様か、後勝ちか
- 先着1名の場合、他のアンサーは拒否する
- SIP用語でACK+BYE
- 後勝ちの場合は、いったんすべて
pranswer
として扱う- 最後のを正式な
answer
とする
- 最後のを正式な
- できるけど推奨してないし、ほとんどのSIPでもやってない
- JSEPとしては、そういうことしたいなら複数の
PeerConnection
を作ってね- RFC3960でいう
UPDATE
をやってもいい
- RFC3960でいう
- JSEPが実装するAPIについて
- 実装されてる変数名・関数名は微妙に違うかもしれないので注意
- 本文なし
- ICE/STUN/TURNの設定などグローバルなパラメータを指定できる
iceCandidatePolicy
:all
orrelay
all
: デフォルト、特に制限されず全部を対象にするrelay
:relay
だけを使う
iceCandidatePoolSize
: 事前に候補収集する数- STUN/TURNサーバーのリソースを食うので、要求があるまで普通はやらない
- なのでデフォルトは
0
bundlePolicy
:balanced
ormax-compat
ormax-bundle
- draft-ietf-mmusic-sdp-bundle-negotiation にあるやつ
- メディア・データなどストリームをBUNDLEするかどうか
balanced
: バランスを取るmax-compat
: バンドルしないmax-bundle
: 最初のm=
セクションにすべてバンドルする- デフォルトは
balanced
(だが挙動はmax-bundle
がほとんどなはず)
rtcpMuxPolicy
:negotiate
orrequire
- RTP/RTCPをmultiprexするかどうか
negotiate
: 別々に候補を集めるけど、a=rtcp-mux
もつけるrequire
:a=rtcp-mux-only
をつける- デフォルトは
require
MediaStreamTrack
を追加するMediaStream
をあわせて渡すことで、同じLS
(LipSync)グループにできる- RFC5888
signalingState
がhave-remote-offer
のときは、最初のRtpTransceiver
に- それ以外のときは、新しい
RtpTransceiver
を作成する
- それ以外のときは、新しい
MediaStreamTrack
を削除する- どの
RTPSender
かをあわせて指定する
- どの
- 削除されたら、その
m=
セクションはrecevonly
かinactive
になる- 次に
createOffer()
されたとき - SDPから消えるわけではないので
- 次に
RtpTransceiver
を追加するMediaStreamTrack
をあわせて渡すことでセットもできる
recvonly
なRtpTransceiver
を作りたいときに便利direction
とsendEncodings
を設定できる
- Data Channelを作る
- はじめて作った場合は、ネゴシエーションが必要
- 作られたすべてのDCは、同じSCTP/DTLSアソシエーションを使う
- なので同じ
m=
セクションに入る - そういうわけで、一度ネゴシエーションできたなら、後から増やしてもJSEPには関係ない
- なので同じ
- その他指定できるオプションもいろいろあるが、それもJSEPには関係ない
- オファーSDPを生成する
- そこには既に設定されたメディアやシステムのコーデックなどが載る
- 既に集められたICEのcandidateがあればそれも
iceRestart
のためのオプションも渡せる
- セッション確立後に呼ぶと、それまでの変更点が更新される
- できたSDPは
setLocalDescription()
できるものでないといけない - これを呼んだ後は、ICEのクレデンシャルを生成するなどしてよい
- ただ実際に候補を収集したりはしない
- もちろんメディアを送ったりもしない
- アンサーSDPを生成する
- 直近の
setRemoteDescription()
を反映したもの
- 直近の
- 基本的には
createOffer()
のときと同じ
- SDPのタイプは4つ
offer
,pranswer
,answer
,rollback
offer
: オファーpranswer
: Provisionalなアンサーanswer
: アンサーrollback
: 最後にstable
だった状態に戻す用
- 最終的な
answer
が届くまでの一時的なもの
have-local-offfer
とかになったのをキャンセルできる- 中身は空っぽ
- 自身のSDPを設定する
- ICEの候補収集がはじまる
- プールされてるものがあればそれを使う
- 既にオファーが
setRemoteDescription()
されていれば、メディアの送受信がはじまる- メディアの準備ができてれば
- メディアのエンコードと送信をはじめられる
- 既にアンサーを
setLocalDescription()
されていれば、メディアの送受信がはじまる- メディアの準備ができてれば
localDescription
というGetterがコレ- 何もなければ
null
pendingLocalDescription
というGetterがコレstable
かhave-remote-offer
のときはnull
remoteDescription
というGetterがコレ- 何もなければ
null
pendingRemoteDescription
というGetterがコレstable
かhave-local-offer
のときはnull
- リモート側が、TrickleICEできるかどうかを示すプロパティ
- たぶん実装されてない
- 3つの値がある
null
: SDPがなくてわからない、初期状態ture
: 利用できるfalse
: 利用できない
- グローバルな設定を変えられる
- Construcotrで指定してたやつ
iceServers
、iceTransportPolicy
を変更すると、次の候補収集の挙動が変わるneeds-ice-restart
フラグが立つことがある- ICEクレデンシャルが更新される
iceCandidatePoolSize
は変更できないsetLocalDescription()
してないならできる
rtcpMuxPolicy
とbundlePolicy
は変更できない
- ICE Agentに
IceCandidate
を渡すことで更新を伝えるcandidate
プロパティがあれば、新たな候補として扱う- それがないなら、候補の終わりとして扱われる
- それぞれどの
m=
セクション、mid
に属するか決められる - 新たな候補を受け取ったら、接続確認が行われる
- 本文なし
RTPTransceiver
を止める- 一度止めると、それに紐づく
m=
セクションに影響がある
- 状態を表すフラグ
stopped
がtrue
なら、RTP/RTCPを送受信しない
direction
を変更するメソッドrecvonly
,sendrecv
,sendonly
,inactive
のいずれか
- Offer側では指定したものがそのままSDPに載る
- 変えたら即そうなるのではなく、Answerが受理されてやっと変わる
direction
を表す
- 現状の
direction
を表すsetDirection()
しても即変わらないので
- リモート側で
recv
なら、こっちではsend
のように反転される
- コーデックの設定ができる
- あくまで希望の表明である
- 実装がないかもしれないので
- SDPを作る・パースする方法について
- オファー側もアンサー側もここにある手順に従ってね
- 以下の仕様を満たしていなければエラーになる
- ICE: RFC84445
- もしかしたらICE-Liteがいるかもしれないがよしなにせよ
- DTLS: RFC6347 or DTLS-SRTP: RFC5763
- ICE: RFC84445
- SDESは使わない、DTLS-SRTPを使う
- プロファイルが決まってる
- Media:
UDP/DTLS/RTP/SAVPF
ORTCP/DTLS/RTP/SAVPF
- Data:
UDP/DTLS/SCTP
ORTCP/DTLS/SCTP
- どれになるかは、ICEのcandidateで決定する
- Media:
- でも実際はどれか欠けたりするのでよしなにせよ
TCP
とか使ったりするし
createOffer()
時にSDPをつくる- その手順についての詳細
- 最初の1回とそれ以外でアップデートする内容が違う
- 最初なのでセッション全体に関する記述が必要
v=0
とかo=
とか
- 詳細は割愛
- 2回目以降、既に
localDescription
がある場合など setLocalDescription()
してる・してないで微妙に違う- 詳細は割愛
createOffer()
の引数であるRTCOfferOptions
について
iceRestart
:boolean
- 新しい
ufrag
とpwd
を生成しなおす - もちろん初オファーの場合には意味がない
voiceActivityDetection
:boolean
- いわゆるDTX(DiscontinuousTransmission): 話してないときに帯域を節約する
- CN(ComfortNoise)対応のコーデックのとき
- 実装されてなさそう
createAnswer()
時にSDPをつくる- その手順についての詳細
- こちらも初回と以降で微妙に違う
remoteDescription
がない状態では、createAnswer()
できない- 詳細は割愛
setLocalDescription()
してる・してないで微妙に違う- 詳細は割愛
createAnswer()
の引数であるRTCAnswerOptions
について
voiceActivityDetection
:boolean
- 実装されてなさそう
createOffer()
やcreateAnswer()
で得たSDPは、修正してはいけない- そのまま
setLocalDescription()
に渡す必要がある - 修正したいものは
RtpTransceiver
のAPIや、それ用のオプションを使う
- そのまま
setLocalDescription()
後、リモートに送るSDPは修正されるかもしれない- いろいろな理由で
- 解釈される保証はないし、なんかあっても自己責任
setLocalDescription()
すると何が起きるか- 詳細は後述
setRemoteDescription()
すると何が起きるか- 詳細は後述
- SDPのtype:
rollback
signalingState
をstable
に戻すRtpTransceiver
とm=
セクションの紐付けなど注意が必要
- SDPは言うなればただのテキスト
- これを内部的なオブジェクトに変換していく
- なにかしらよくわからない行があればエラーにする
- セッション全体のところ
- 並びも固定になってる
a=
行は順不同
- SDPの仕様にはあるけど、JSEPとしては見てないフィールドもたくさんある
- 詳細は割愛
m=
セクションのこと- 詳細は割愛
- パースが終わったSDPオブジェクトを検証するステップがある
- 何かあったらエラーにする
- 詳細は割愛
localDescription
を反映する- ICEの
ufrag
とpwd
が変わってたらICEをリスタートするとか
- ICEの
- 詳細は割愛
remoteDescription
を反映するcanTrickleIceCandidates
をアップデートしたり
- 詳細は割愛
- SDPが
pranswer
とanswer
だった場合、加えてこのステップが必要 - 詳細は割愛
- bundleするときは、その紐付けを
m=
セクションにマークする - そうすると、
RtpTransceiver
がRTP/RTCPを扱えるようになる
- シチュエーション別のSDPの解説
- SDPのサンプルがもっと欲しい場合: draft-ietf-rtcweb-sdp
- 最低限の動画・音声通話の例
- Vanilla ICE
- より踏み込んだ例
- Full Trickle ICE
bundlePolicy: max-bundle
,rtcpMuxPolicy: require
- TURN利用
- 最初は音声とデータだけ、後から2本のビデオを一方通行で追加
- 動画と音声を送るが、最初は
sendonly
で応答する- あとから
sendrecv
にする - こうすることで先に疎通を済ませておくことができるテクニック
- あとから
iceTransportPolicy
:relay
- セキュリティに関しては2つの文書がある
- draft-ietf-rtcweb-security-arch
- draft-ietf-rtcweb-security
- JSの実行はユーザーに任されるので、間で何をされるかわからない
createOffer()
したSDPがそのままsetLocalDescription()
されないかも- なので固めの実装方針にして、エラーで落とすようにしてる
- JSのAPIから触れない部分があることも知っておく