92 lines
2.9 KiB
JavaScript
92 lines
2.9 KiB
JavaScript
// Primary RTCDataChannel manager.
|
|
//
|
|
// - Messages queued while the channel is not yet open.
|
|
// - If the channel closes while the RTCPeerConnection is still 'connected',
|
|
// it is recreated automatically (brief-disconnect recovery).
|
|
// - Both sides can safely call initiate() — the one that creates the channel
|
|
// triggers onnegotiationneeded; the other side receives ondatachannel.
|
|
// Use { negotiated: true, id: 0 } if you want to skip the SDP dance.
|
|
import MWSEEventTarget from '../EventTarget.js';
|
|
|
|
export default class DataChannel extends MWSEEventTarget {
|
|
constructor(pc, label = 'mwse') {
|
|
super();
|
|
this._pc = pc;
|
|
this._label = label;
|
|
this._ch = null;
|
|
this._queue = [];
|
|
this._open = false;
|
|
|
|
// Accept channels offered by the remote side.
|
|
pc.on('datachannel', ch => {
|
|
if (ch.label === this._label) this._attach(ch);
|
|
});
|
|
}
|
|
|
|
// Create the channel on this side (makes us the "offering" side for the
|
|
// channel SDP line; triggers onnegotiationneeded).
|
|
initiate() {
|
|
if (this._ch?.readyState === 'open' || this._ch?.readyState === 'connecting') return;
|
|
const ch = this._pc.createDataChannel(this._label, { ordered: true });
|
|
this._attach(ch);
|
|
return ch;
|
|
}
|
|
|
|
// Send any JSON-serialisable value or an ArrayBuffer.
|
|
send(data) {
|
|
const wire = (typeof data === 'string' || data instanceof ArrayBuffer)
|
|
? data
|
|
: JSON.stringify(data);
|
|
|
|
if (this._open) {
|
|
this._ch.send(wire);
|
|
} else {
|
|
this._queue.push(wire);
|
|
}
|
|
}
|
|
|
|
get open() { return this._open; }
|
|
get state() { return this._ch?.readyState ?? 'closed'; }
|
|
|
|
close() {
|
|
this._ch?.close();
|
|
this._ch = null;
|
|
this._open = false;
|
|
}
|
|
|
|
// ---- Internal -------------------------------------------------------
|
|
|
|
_attach(ch) {
|
|
this._ch = ch;
|
|
ch.binaryType = 'arraybuffer';
|
|
|
|
ch.onopen = () => {
|
|
this._open = true;
|
|
this.emit('open');
|
|
for (const msg of this._queue) ch.send(msg);
|
|
this._queue = [];
|
|
};
|
|
|
|
ch.onmessage = ({ data }) => {
|
|
let parsed = data;
|
|
if (typeof data === 'string') {
|
|
try { parsed = JSON.parse(data); } catch (_) {}
|
|
}
|
|
this.emit('message', parsed);
|
|
};
|
|
|
|
ch.onclose = () => {
|
|
this._open = false;
|
|
this.emit('close');
|
|
// Auto-recreate the channel if the connection is still alive.
|
|
// (Channels can close without the full connection closing, e.g. on
|
|
// some mobile browsers when the page is backgrounded briefly.)
|
|
if (this._pc.connectionState === 'connected') {
|
|
setTimeout(() => this.initiate(), 800);
|
|
}
|
|
};
|
|
|
|
ch.onerror = err => this.emit('error', err);
|
|
}
|
|
}
|