Files
webRtc/WebApp/client/src/peer.js

188 lines
6.3 KiB
JavaScript
Raw Normal View History

2026-02-27 18:35:40 +08:00
import * as Logger from "./logger.js";
export default class Peer extends EventTarget {
constructor(connectionId, polite, config, resendIntervalMsec = 5000) {
super();
const _this = this;
this.connectionId = connectionId;
this.polite = polite;
this.config = config;
this.pc = new RTCPeerConnection(this.config);
this.makingOffer = false;
this.waitingAnswer = false;
this.ignoreOffer = false;
this.srdAnswerPending = false;
this.log = str => void Logger.log(`[${_this.polite ? 'POLITE' : 'IMPOLITE'}] ${str}`);
this.warn = str => void Logger.warn(`[${_this.polite ? 'POLITE' : 'IMPOLITE'}] ${str}`);
this.assert_equals = window.assert_equals ? window.assert_equals : (a, b, msg) => { if (a === b) { return; } throw new Error(`${msg} expected ${b} but got ${a}`); };
this.interval = resendIntervalMsec;
this.sleep = msec => new Promise(resolve => setTimeout(resolve, msec));
this.pc.ontrack = e => {
_this.log(`ontrack:${e}`);
_this.dispatchEvent(new CustomEvent('trackevent', { detail: e }));
};
this.pc.ondatachannel = e => {
_this.log(`ondatachannel:${e}`);
_this.dispatchEvent(new CustomEvent('adddatachannel', { detail: e }));
};
this.pc.onicecandidate = ({ candidate }) => {
_this.log(`send candidate:${candidate}`);
if (candidate == null) {
return;
}
_this.dispatchEvent(new CustomEvent('sendcandidate', { detail: { connectionId: _this.connectionId, candidate: candidate.candidate, sdpMLineIndex: candidate.sdpMLineIndex, sdpMid: candidate.sdpMid } }));
};
this.pc.onnegotiationneeded = this._onNegotiation.bind(this);
this.pc.onsignalingstatechange = () => {
_this.log(`signalingState changed:${_this.pc.signalingState}`);
};
this.pc.oniceconnectionstatechange = () => {
_this.log(`iceConnectionState changed:${_this.pc.iceConnectionState}`);
if (_this.pc.iceConnectionState === 'disconnected') {
this.dispatchEvent(new Event('disconnect'));
}
};
this.pc.onicegatheringstatechange = () => {
_this.log(`iceGatheringState changed:${_this.pc.iceGatheringState}'`);
};
this.loopResendOffer();
}
async _onNegotiation() {
try {
this.log(`SLD due to negotiationneeded`);
this.assert_equals(this.pc.signalingState, 'stable', 'negotiationneeded always fires in stable state');
this.assert_equals(this.makingOffer, false, 'negotiationneeded not already in progress');
this.makingOffer = true;
await this.pc.setLocalDescription();
this.assert_equals(this.pc.signalingState, 'have-local-offer', 'negotiationneeded not racing with onmessage');
this.assert_equals(this.pc.localDescription.type, 'offer', 'negotiationneeded SLD worked');
this.waitingAnswer = true;
this.dispatchEvent(new CustomEvent('sendoffer', { detail: { connectionId: this.connectionId, sdp: this.pc.localDescription.sdp } }));
} catch (e) {
this.log(e);
} finally {
this.makingOffer = false;
}
}
async loopResendOffer() {
while (this.connectionId) {
if (this.pc && this.waitingAnswer) {
this.dispatchEvent(new CustomEvent('sendoffer', { detail: { connectionId: this.connectionId, sdp: this.pc.localDescription.sdp } }));
}
await this.sleep(this.interval);
}
}
close() {
this.connectionId = null;
if (this.pc) {
this.pc.close();
this.pc = null;
}
}
getTransceivers(connectionId) {
if (this.connectionId != connectionId) {
return null;
}
return this.pc.getTransceivers();
}
addTrack(connectionId, track) {
if (this.connectionId != connectionId) {
return null;
}
return this.pc.addTrack(track);
}
addTransceiver(connectionId, trackOrKind, init) {
if (this.connectionId != connectionId) {
return null;
}
return this.pc.addTransceiver(trackOrKind, init);
}
createDataChannel(connectionId, label) {
if (this.connectionId != connectionId) {
return null;
}
return this.pc.createDataChannel(label);
}
async getStats(connectionId) {
if (this.connectionId != connectionId) {
return null;
}
return await this.pc.getStats();
}
async onGotDescription(connectionId, description) {
if (this.connectionId != connectionId) {
return;
}
const _this = this;
const isStable =
this.pc.signalingState == 'stable' ||
(this.pc.signalingState == 'have-local-offer' && this.srdAnswerPending);
this.ignoreOffer =
description.type == 'offer' && !this.polite && (this.makingOffer || !isStable);
if (this.ignoreOffer) {
_this.log(`glare - ignoring offer`);
return;
}
this.waitingAnswer = false;
this.srdAnswerPending = description.type == 'answer';
_this.log(`SRD(${description.type})`);
await this.pc.setRemoteDescription(description);
this.srdAnswerPending = false;
if (description.type == 'offer') {
_this.dispatchEvent(new CustomEvent('ongotoffer', { detail: { connectionId: _this.connectionId } }));
_this.assert_equals(this.pc.signalingState, 'have-remote-offer', 'Remote offer');
_this.assert_equals(this.pc.remoteDescription.type, 'offer', 'SRD worked');
_this.log('SLD to get back to stable');
await this.pc.setLocalDescription();
_this.assert_equals(this.pc.signalingState, 'stable', 'onmessage not racing with negotiationneeded');
_this.assert_equals(this.pc.localDescription.type, 'answer', 'onmessage SLD worked');
_this.dispatchEvent(new CustomEvent('sendanswer', { detail: { connectionId: _this.connectionId, sdp: _this.pc.localDescription.sdp } }));
} else {
_this.dispatchEvent(new CustomEvent('ongotanswer', { detail: { connectionId: _this.connectionId } }));
_this.assert_equals(this.pc.remoteDescription.type, 'answer', 'Answer was set');
_this.assert_equals(this.pc.signalingState, 'stable', 'answered');
this.pc.dispatchEvent(new Event('negotiated'));
}
}
async onGotCandidate(connectionId, candidate) {
if (this.connectionId != connectionId) {
return;
}
try {
await this.pc.addIceCandidate(candidate);
} catch (e) {
if (this.pc && !this.ignoreOffer)
this.warn(`${this.pc} this candidate can't accept current signaling state ${this.pc.signalingState}.`);
}
}
}