MWSE/sdk/webrtc/PeerConnection.js

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