MWSE/sdk/webrtc/DataChannel.js

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);
}
}