123 lines
4.7 KiB
JavaScript
123 lines
4.7 KiB
JavaScript
// RTCPeerConnection wrapper with comprehensive state monitoring.
|
|
// Fixes the original SDK's missed-disconnect bug by subscribing to BOTH
|
|
// connectionstatechange (the modern event) and iceconnectionstatechange
|
|
// (the legacy fallback) and never letting either go unobserved.
|
|
//
|
|
// Emits: 'connected', 'disconnected', 'failed',
|
|
// 'state-change'(state), 'ice-state-change'(state),
|
|
// 'gathering-change'(state), 'signaling-change'(state),
|
|
// 'ice-candidate'(candidate|null), 'track'(track, streams),
|
|
// 'datachannel'(channel), 'negotiation-needed'
|
|
import MWSEEventTarget from '../EventTarget.js';
|
|
|
|
export default class PeerConnection extends MWSEEventTarget {
|
|
constructor(config) {
|
|
super();
|
|
this._pc = new RTCPeerConnection(config);
|
|
this._wire();
|
|
}
|
|
|
|
// ---- RTCPeerConnection passthrough ----------------------------------
|
|
|
|
get pc() { return this._pc; }
|
|
get connectionState() { return this._pc.connectionState; }
|
|
get iceConnectionState() { return this._pc.iceConnectionState; }
|
|
get signalingState() { return this._pc.signalingState; }
|
|
get iceGatheringState() { return this._pc.iceGatheringState; }
|
|
get localDescription() { return this._pc.localDescription; }
|
|
get remoteDescription() { return this._pc.remoteDescription; }
|
|
|
|
addTrack(track, ...streams) { return this._pc.addTrack(track, ...streams); }
|
|
removeTrack(sender) { this._pc.removeTrack(sender); }
|
|
async createOffer(opts) { return this._pc.createOffer(opts); }
|
|
async createAnswer() { return this._pc.createAnswer(); }
|
|
async setLocalDescription(d) { await this._pc.setLocalDescription(d); }
|
|
async setRemoteDescription(d){ await this._pc.setRemoteDescription(d); }
|
|
|
|
async addIceCandidate(c) {
|
|
if (!c) return;
|
|
await this._pc.addIceCandidate(c);
|
|
}
|
|
|
|
createDataChannel(label, opts) {
|
|
return this._pc.createDataChannel(label, opts);
|
|
}
|
|
|
|
async restartIce() {
|
|
if (typeof this._pc.restartIce === 'function') {
|
|
this._pc.restartIce();
|
|
} else {
|
|
const offer = await this._pc.createOffer({ iceRestart: true });
|
|
await this._pc.setLocalDescription(offer);
|
|
}
|
|
}
|
|
|
|
close() {
|
|
const pc = this._pc;
|
|
pc.onconnectionstatechange = null;
|
|
pc.oniceconnectionstatechange = null;
|
|
pc.onicegatheringstatechange = null;
|
|
pc.onsignalingstatechange = null;
|
|
pc.onicecandidate = null;
|
|
pc.ontrack = null;
|
|
pc.ondatachannel = null;
|
|
pc.onnegotiationneeded = null;
|
|
pc.close();
|
|
}
|
|
|
|
// ---- Internal event wiring ------------------------------------------
|
|
|
|
_wire() {
|
|
const pc = this._pc;
|
|
|
|
// connectionstatechange is the primary signal on modern browsers.
|
|
// It reliably reports 'failed' and 'closed' — states that
|
|
// iceconnectionstatechange can miss under certain NAT conditions.
|
|
pc.onconnectionstatechange = () => {
|
|
const s = pc.connectionState;
|
|
this.emit('state-change', s);
|
|
if (s === 'connected') this.emit('connected');
|
|
if (s === 'disconnected' || s === 'closed') this.emit('disconnected');
|
|
if (s === 'failed') this.emit('failed');
|
|
};
|
|
|
|
// Keep iceconnectionstatechange as a fallback for environments where
|
|
// connectionstatechange never fires (some mobile browsers, old Chrome).
|
|
pc.oniceconnectionstatechange = () => {
|
|
const s = pc.iceConnectionState;
|
|
this.emit('ice-state-change', s);
|
|
|
|
// Only act if connectionState hasn't given us a verdict yet.
|
|
if (pc.connectionState === 'new' || pc.connectionState === 'connecting') {
|
|
if (s === 'connected' || s === 'completed') this.emit('connected');
|
|
if (s === 'disconnected') this.emit('disconnected');
|
|
if (s === 'failed') this.emit('failed');
|
|
}
|
|
};
|
|
|
|
pc.onicegatheringstatechange = () => {
|
|
this.emit('gathering-change', pc.iceGatheringState);
|
|
};
|
|
|
|
pc.onsignalingstatechange = () => {
|
|
this.emit('signaling-change', pc.signalingState);
|
|
};
|
|
|
|
pc.onicecandidate = ({ candidate }) => {
|
|
this.emit('ice-candidate', candidate);
|
|
};
|
|
|
|
pc.ontrack = ({ track, streams }) => {
|
|
this.emit('track', track, streams);
|
|
};
|
|
|
|
pc.ondatachannel = ({ channel }) => {
|
|
this.emit('datachannel', channel);
|
|
};
|
|
|
|
pc.onnegotiationneeded = () => {
|
|
this.emit('negotiation-needed');
|
|
};
|
|
}
|
|
}
|