🧊

WebRTCのDataChannelをもっと手軽に

使いたかったので、ライブラリを書きました。

`enhanced-datachannel`という名前でnpmからインストールできます。

リポジトリはこちら。

GitHub - leader22/enhanced-datachannel: Wanna `enhance(RTCDataChannel)` for general usage.

以下、ざっくり紹介と、DataChannelそのものについて書きます。

enhanced-datachannel

3つの関数をエクスポートしてます。

  • `based()`
  • `promised()`
  • `chunked()`

型はこの通り。

export declare function based(dc: RTCDataChannel): BasedDataChannel;
export declare function promised(dc: RTCDataChannel): PromisedDataChannel;
export declare function chunked(dc: RTCDataChannel): ChunkedDataChannel;

`RTCDataChannel`のインスタンスを受け取って、ラップして返します。

つまり、WebRTCのシグナリングには関与しないです。

モチベーション

  • WebRTCといったらビデオチャットでしょ感
  • ただデータもあれこれ送れるなら、他の用途でも使い込んでみたいなーと思った
  • しかしそのままのAPIだと使い勝手が微妙

というわけで、用途別にラップするものを作りました。

BasedDataChannel

`EventEmitter`でラップしただけです。

`send()`は、`RTCDataChannel#send()`にそのまま流れ、`on("message")`で、`RTCDataChannel#onmessage`が拾えます。

その他のプロパティもそのままアクセスできるようになってます。

基本的には後述する2クラスのベースとなるものであり、直接使うモチベーションはないかなーと思ってます。

PromisedDataChannel

`extends BasedDataChannel`です。

その名の通り、メッセージの送受信が`Promise`で扱えます。

// recv
promisedDC.on("message", (data, resolve, reject) => {
  try {
    console.log(data); // "Take this!"
    resolve("Thank you!");
  } catch (err) {
    reject(err);
  }
});

// send
const res = await promisedDC.send("Take this!");
console.log(res); // "Thank you!"

この例では文字列を送ってますが、JavaScriptのオブジェクトそのまま送れます。
内部的にJSON文字列にして送信して、受信時にオブジェクトに戻します。

これ、地味に便利だと思います。

ChunkedDataChannel

おなじく`extends BasedDataChannel`です。

これもその名の通り、1回の`send()`で送れないような大きなファイルをチャンクに分けて送信してくれるやつ。

// recv
chunkedDC.on("message", (blob, meta) => {
  // download it
  const $downloadLink = document.createElement("a");
  $downloadLink.href = URL.createObjectURL(blob);
  $downloadLink.download = meta.name;
  $downloadLink.textContent = meta.name;
  document.body.append($downloadLink);
});

// send
await chunkedDC.send(file, { name: "prof.png" });

ファイル名やらメタ情報は、オプショナルな第2引数で渡すようにしてます。

受信側では`Blob`形式になってるので、その後はよしなにします。

まとめ

  • `npm i enhanced-datachannel`してね
  • `promised()`と`chunked()`を用途にあわせて使ってね
  • それぞれ用途別の`sned()`と`on("message")`ができるようになるよ

以上でした!

DataChannelよもやま

あとは開発Tipsとしてのメモです。

前提として、WebRTCのブラウザのAPIはほぼほぼFixしてきた感もある + まあだいたいのケースでは問題なく動きます。

が、すごい細かいとこまで見ていくとイコールではないし、実装差異ももちろんあるし、まあこんなもんやろマインドで接するが吉です。

消された`reliable`

DataChannel(というかSCTP)では、データ送信の特性をある程度コントロールできるようになっていてます。

  • 信頼性
  • 配送順序

これをそれぞれいじれて、2人は排他で別々に設定できます。

信頼性については、`maxRetransmits`と`maxPacketLifeTime`というプロパティ、配送順序は`ordered`というプロパティです。

const dc = pc.createDataChannel("ch", {
  ordered: false,
  maxRetransmits: 1,
  maxPacketLifeTime: 1000,
});

デフォルトだと信頼性も配送順序も保証されるモード、このプロパティを1つでもいじったら、いじったものが保証されないモードに。

・・・的なことは、実は1年前に記事にしてたらしい。

WebRTCのDataChannelをunreliableモードで使う - console.lealog();

というのが建前で。

この古い記事内でも言ってるけど`reliable`プロパティの扱いに困ったという話です。

今回のケースだと`reliable`なものを引数に受けたかったので、そのバリデーションをしたかった。

しかしSafariでその値が取れない!

`maxRetransmits`と`maxPacketLifeTime`が、`createDataChannel()`した側と、`ondatachannel`された側で異なります。

  • した側はどちらも`null`
  • された側はどちらも`65536`

もちろん未指定で。

`65535`を無理やり指定してごまかす手もあったんですが、そうするとChromeFirefoxで`reliable: false`になって気持ち悪い・・。

まあ基本的にこんなオプションをいじるマニアックな人は、このライブラリを使うこともないやろということで、バリデーションは諦めた。

`reliable`が消えた経緯は、それぞれの指定値を見ればわかるやろ?ってことかもですが、

という感じで差があって、Specにはないけど`reliable`を見れるほうが楽なんやが・・という感じ。

`maxRetransmitTime`は古い

古いんですが、Chromeではまだ生きてます・・。

つまり、Chrome(少なくとも現在のM75)では、`dc.maxPacketLifeTime`が`undefined`です。
代わりに`dc.maxRetransmitTime`なら取れます。

しかし作成時の引数では、`maxPacketLifeTime`を指定することを要求されます。
間違って古い`maxRetransmitTime`を指定すると・・。

[Deprecation] maxRetransmitTime is deprecated and will be removed in M70, around October 2018. Please use maxPacketLifeTime instead. See https://www.chromestatus.com/features/5198350873788416 for more details.

だそうです。お察しです。

`binaryType`

  • Specによると初期値は`blob`
  • Firefoxの初期値は`blob`
  • Chrome / Safariだと`arraybuffer`

この時点でお察しなんですが、Chrome / Safariで`blob`を指定しようとすると・・・。

Chrome: M75
Uncaught DOMException: Failed to set the 'binaryType' property on 'RTCDataChannel': Blob support not implemented yet

Safari: 12.1.1
NotSupportedError: The operation is not supported.

チャンクのサイズ

Using WebRTC data channels - Web APIs | MDN

1度の`dc.send()`で送信できるデータのサイズについて。

今回の実装でも`16KB`単位で区切るようにしてますが、このへんはまあ込み入った事情がいろいろあるようで、簡単にはいかないよってことですね。

ちゃんとやるならBackPressureとかちゃんと考えてストリーミングすべきなのよね・・。