🧊

webrtc/adapterのコードを読んだメモ

GitHub - webrtc/adapter: Shim to insulate apps from spec changes and prefix differences. Latest adapter.js release:

  • 界隈で脊髄反射的に必要って言われてる気がする
  • 個人的にはお世話になったことはない
    • SkyWayのJS-SDKでもサンプルでも使ってないし(いちおうメンテナしてるけど)
    • なので余計にフロントエンドでいうjQueryと同じノリを感じる
  • なので、実際に何やってるのか読んで調べてみて、その確証を得たい

ちなみに、2018/04/23時点での`master`ブランチの内容。

ディレクトリ構造

`/src/js`が本丸。

.
├── adapter_core.js
├── adapter_factory.js
├── chrome
│   ├── chrome_shim.js
│   └── getusermedia.js
├── common_shim.js
├── edge
│   ├── edge_shim.js
│   └── getusermedia.js
├── firefox
│   ├── firefox_shim.js
│   └── getusermedia.js
├── safari
│   └── safari_shim.js
└── utils.js

特に言うことなし。

ちなみに、行数はこんな感じ。

$ wc -l src/js/**/*.js
      13 src/js/adapter_core.js
     149 src/js/adapter_factory.js
     745 src/js/chrome/chrome_shim.js
     244 src/js/chrome/getusermedia.js
     283 src/js/common_shim.js
      80 src/js/edge/edge_shim.js
      34 src/js/edge/getusermedia.js
     217 src/js/firefox/firefox_shim.js
     209 src/js/firefox/getusermedia.js
     308 src/js/safari/safari_shim.js
     174 src/js/utils.js
    2456 total

依存抜きで2500行もあんのかよ・・重いな・・。

ちなみにコードは、ES2015ですらない懐かしい感じ。

以下詳細。

adapter_core

すべてのはじまり、エントリーポイント。

このプロジェクトはBrowserifyを使ってて、そのエントリーにもなってる。

`window`を引数に`adapter_factory`を呼んでるだけ。

adapter_factory

やってることは、

  • 各ブラウザごとのアダプターのロード
  • UAのパースと、各アダプターの適用

各アダプターってのは、この5つ。

詳細は後述。

ちなみに`RTCXxx`だけではなく、`getUserMedia()`とか`srcObject`とか、いわゆる関連APIももれなく対象になってる。

utils

各アダプターを読み進める前に、外堀を埋めておく。

その名の通り、汎用的なメソッドが諸々。

  • ログを出す・出さないのフラグ管理
  • ワーニング出す・出さないの管理
  • UAからブラウザ判定
  • `RTCPeerConnection`でのイベントをラップするやつ

読んで気になった点は2つ。

wrapPeerConnectionEvent()

`RTCPeerConnection`で起こるイベント(`icecandidate`とか)にフックして、得られるイベントオブジェクトを操作する、いわゆるミドルウェア的な処理をしてるやつ。

使われ方としてはこんな感じ。

utils.wrapPeerConnectionEvent(window, 'icecandidate', function(e) {
  if (e.candidate) {
    Object.defineProperty(e, 'candidate', {
      value: new window.RTCIceCandidate(e.candidate),
      writable: 'false'
    });
  }
  return e;
});

つまり、何もしなくても以下と同じようになるようにしてる。

pc.addEventListener('icecandidate', ev => {
  ev.candidate = new RTCIceCandidate(ev.candidate);
});

この関数わざわざ必要か・・?
まあ確かに`RTCSessionDescription`とかインスタンスにしないと読んでくれないブラウザとかもあったけども。

きっとまた後で必要性がわかるんでしょう・・きっと・・。

detectBrowser()

UA判定のロジック。

if (navigator.mozGetUserMedia) { // Firefox.
  result.browser = 'firefox';
  result.version = extractVersion(navigator.userAgent,
      /Firefox\/(\d+)\./, 1);
} else if (navigator.webkitGetUserMedia) {
  // Chrome, Chromium, Webview, Opera.
  // Version matches Chrome/WebRTC version.
  result.browser = 'chrome';
  result.version = extractVersion(navigator.userAgent,
      /Chrom(e|ium)\/(\d+)\./, 2);
} else if (navigator.mediaDevices &&
    navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { // Edge.
  result.browser = 'edge';
  result.version = extractVersion(navigator.userAgent,
      /Edge\/(\d+).(\d+)$/, 2);
} else if (window.RTCPeerConnection &&
    navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) { // Safari.
  result.browser = 'safari';
  result.version = extractVersion(navigator.userAgent,
      /AppleWebKit\/(\d+)\./, 1);
} else { // Default fallthrough: not supported.
  result.browser = 'Not a supported browser.';
  return result;
}

HIDOI。
機能で判別したいのか、UA文字列で判別したいのか不明。

案の定、関連Issueもある。

Chrome will be identified as Safari if webkitGetUserMedia is removed · Issue #764 · webrtc/adapter · GitHub

うーん、なんというか・・、うーん・・。

common_shim

最後にコレ。

こいつがパッチする対象となるものは以下。

  • RTCIceCandidate
  • URL.createObjectURL()
  • RTCPeerConnection.prototype.sctp(と、maxMessageSize)
  • RTCDataChannel.prototype.send

RTCIceCandidate

`RTCIceCandidate.prototype.foundation`が生えた`RTCIceCandidate`にするためにパッチ。
あとは`toJSON()`時、`usernameFragment`も取れるようになる。

なんでこれはグローバルにパッチしないんだ・・。

ちなみにこれは、さっきの`utils.wrapPeerConnectionEvent()`で返すようにしてる。
というかふと思ったけど、`ev.candidate`でなんやかんやしたいケースってあんのかな?

URL.createObjectURL()

`createObjectURL()`した時に、URLとBlobへの`Map`を用意しておく。
そして`HTMLMediaElement.prototype.srcObject`が使える環境で、そのマップを見て同`.src`への代入を`.srcObject`への代入にすり替えてる。

いやいや余計なお世話じゃない?ブラウザに任せてエラーにすればよくない・・?

RTCPeerConnection.prototype.sctp(と、maxMessageSize)

なにやらFirefoxに対して、DataChannelで送信できるデータ量に制限があるっぽい。

なので`setRemoteDescription()`をパッチして、相手のブラウザを判別し、後にあるかもしれないDataChannelの通信に備えて`maxMessageSize`を取得してる。

つまり、DataChannel使わないなら不要。

RTCDataChannel.prototype.send()

通常の`send()`に対して、さっきの`maxMessageSize`の制限を適用するだけ。
そのために、`createDataChannel()`をパッチしてる。

つまり、以下略。

Chrome

一番行数多い・・。

getusermedia

  • 各種エラーの`name`のマッピング
  • `mandatory`とか`advanced`とか色々ある`constraints`記法のコンバート
  • `noiseSuppression`と`autoGainControl`に`webkit`プレフィックスをつけたり
  • `navigator.getUserMedia()`を動くようにしたり
  • `navigator.mediaDevices.xxx`がない場合に埋めたり

`constraints`まわりですごいごちゃごちゃやってるけど、今でも必要なんだろうか・・?いらんくね?

chrome_shim

  • `RTCPeerConnection.prototype.ontrack`がない場合、Stream系のAPIを使ってイベントを再現
  • `track`イベントで`RTCTrackEvent.prototype.receiver`が取れるように
  • `getSenders()`および`createDTMFSender()`の補完
    • StreamやTrackの増減時に、`DTMFSender`も増減するように
  • `srcObject`がない場合、`src`と`createObjectURL()`を使ってそれを使えるように
  • v65以上で`addTrack()`がある場合、`getLocalStreams()`の挙動をあわせる
  • ない場合は、`(add|remove)Track()`を実装して同様に挙動をあわせる
    • `create(Offer|Answer)()`で得たSDPに含まれる`streamId`を置換
  • `iceTransportPolicy`を`iceTransports`に
  • `RTCIceServer.url`を`RTCIceServer.urls`に
  • `getStats()`の返り値を、バージョンによって`Map`にしたりプロパティ名を整理したり
  • v51未満の`setXxxDescription()`などのPromiseベース化
    • `addIceCandidate()`も同様
  • v52未満の`createOffer()`などのPromiseベース化
  • `setXxxDescription()`で与えられる引数を、`RTCSessionDescription()`でラップ
    • `addIceCandidate()`も同様
  • `addIceCandidate(null OR undefined)`できるように

`srcObject`のやつのココでやるな感がすごい + またも余計なお世話感もすごい。

後はまあですよねという感じで、Stream系のAPIとTrack系のAPIの辻褄あわせがコードのほとんどを占める。
もうすぐ不要になるけど。

てか、v51なんてもう2年前のブラウザやし、今となっては不要なコードがほとんどに見える。

Firefox

getusermedia

うーん、いらない・・。

firefox_shim

  • `RTCPeerConnection.prototype.ontrack`がない場合に、`addEventListener()`を使って補完
  • `RTCTrackEvent.prototype.transceiver`で、`RTCTrackEvent.prototype.receiver`が取れるように
  • `HTMLMediaElement.prototype.srcObject`がない場合、`mozSrcObject`を使うように
  • v37以下の`iceServers`の指定の調整
  • `mozRTCIceCandidate` -> `RTCIceCandidate`のようなリネーム諸々
  • `setXxxDescription()`で与えられる引数を、`RTCSessionDescription()`でラップ
    • `addIceCandidate()`も同様
  • `getStats()`の返り値を、バージョンによって`Map`にしたりプロパティ名を整理したり
  • `removeStream()`を`removeTrack()`を使って動くように
  • `addIceCandidate(null OR undefined)`できるように

この後方互換推しはいったいなんなんだ・・ほんと誰のためのadapterなんだ・・って感じ。
ブラウザのバージョンを上げてはいけない世界線があるんだろうか・・。

Safari

getusermedia

`navigator.webkitGetUserMedia`で叩いても通るように。

もはやこんなん拾う必要ないやろ・・。

safari_shim

  • Stream関連APIを、`addTrack`やら`removeTrack`を使って動くように
    • RTCPeerConnection.prototype.getLocalStreams()
    • RTCPeerConnection.prototype.getStreamById()
    • RTCPeerConnection.prototype.addStream()
    • RTCPeerConnection.prototype.removeStream()
    • RTCPeerConnection.prototype.getRemoteStreams()
    • RTCPeerConnection.prototype.onaddstream
  • `createOffer()`みたいなPromiseベースのメソッドを、あえてコールバックスタイルでも動くように
  • `createOffer({ offerToReceiveAudio, offerToReceiveVideo })`のフォールバック
    • `setDirection()`やらを使って`sendonly`とか`inactive`とかに
  • `RTCIceServer.url`を`RTCIceServer.urls`に
  • `RTCTrackEvent.prototype.transceiver`で、`RTCTrackEvent.prototype.receiver`が取れるように

うーん、いったいいつのバージョンのSafariを対象としてるの・・?大人しく新しい仕様のAPI使えばよくない・・?なんのためのSafariなの?

ProimseになってるAPIはそのまま使ったらいいやん・・なんで・・。

Edge

他に比べてコードの行数が少ない。DataChannelがないから・・ってのもあるやろうけど、実際は次のライブラリに丸投げしてるから。

GitHub - otalk/rtcpeerconnection-shim: Implementation of the RTCPeerConnection API ontop of ORTC

EdgeのORTCなAPI上でWebRTCをいい感じにつなぐためのあれこれ・・的なやつらしい。
詳細は今回は割愛。

Microsoft Edge での WebRTC 1.0 および相互運用可能なリアルタイム通信の紹介 | Microsoft Edge Japan

WebRTC 1.0とはなんだったのか!

getusermedia

  • 各種エラーの`name`のマッピング
    • `PermissionDeniedError`を`NotAllowedError`として返すようにしてるだけ

edge_shim

`otalk/rtcpeerconnection-shim`の他には何やってるか。

  • v15025以下の`addStream()`のバグの対応
    • `MediaStreamTrack.prototype.enabled`が変更された時に、`enabled`イベントを発火
  • `RTCRtpSender.prototype.dtmf`を生やす
  • `RTCDTMFSender`ではなく`RTCDtmfSender`なら生えてるらしいのでそれをエイリアス
  • `RTCRtpSender.prototype.replaceTrack()`がない場合に、`RTCRtpSender.prototype.setTrack()`を呼ぶように

なんでこれだけは独自にやってるんやろう・・?全部あっちのライブラリに寄せたらいいのに。

おわりに

個人的な考えの結論としては、「いらないし、依存したくない」ですね。

そもそもライブラリとして、

  • 古いブラウザで新しいコードを動かしたいのか
  • 新しいブラウザで古いコードを動かしたいのか

そのあたりの立ち位置が不明瞭だなーと読んでて思った。
ずるずる過去の遺産を引きずってるようにしか見えないというのが個人的な感想。

このパッチがなんのためのものなのか、いつまで必要なのかも探せないし、そのサポートポリシーも明確になってない。

BabelとかLodashとか見習って、もっとAPIごとに砕いたりすればいいのに。

  • 対象を最新ブラウザの2−3バージョンまでに
  • 古いコードへのフォールバックは廃止
  • 機能群ごとでパッケージを分ける

って感じの、YetAnotherWebRTCAdapterならワンチャンあるなーと思ったけど、各自がアプリ側でハマったところだけ対策するのでも十分よな・・と個人的には思います。

なんかハマった時に、このコードを読み漁ってみて原因のヒントを探す・・くらいの使い方が良さそう。