188 lines
6.3 KiB
JavaScript
188 lines
6.3 KiB
JavaScript
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 === 'failed') {
|
|
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}.`);
|
|
}
|
|
}
|
|
}
|