@libp2p/webrtc-peer
Version:
Simple one-to-one WebRTC data channels
105 lines • 4.38 kB
JavaScript
import { logger } from '@libp2p/logger';
import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events';
import errCode from 'err-code';
import randombytes from 'iso-random-stream/src/random.js';
import { toString as uint8ArrayToString } from 'uint8arrays/to-string';
import { pushable } from 'it-pushable';
import defer from 'p-defer';
import { WebRTCDataChannel } from './channel.js';
import delay from 'delay';
// const ICECOMPLETE_TIMEOUT = 5 * 1000
const DEFAULT_PEER_CONNECTION_CONFIG = {
iceServers: [{
urls: [
'stun:stun.l.google.com:19302',
'stun:global.stun.twilio.com:3478'
]
}]
};
function getBrowserRTC() {
if (typeof globalThis === 'undefined') {
throw errCode(new Error('No WebRTC support detected'), 'ERR_WEBRTC_SUPPORT');
}
const wrtc = {
// @ts-expect-error browser-specific properties
RTCPeerConnection: globalThis.RTCPeerConnection ?? globalThis.mozRTCPeerConnection ?? globalThis.webkitRTCPeerConnection,
// @ts-expect-error browser-specific properties
RTCSessionDescription: globalThis.RTCSessionDescription ?? globalThis.mozRTCSessionDescription ?? globalThis.webkitRTCSessionDescription,
// @ts-expect-error browser-specific properties
RTCIceCandidate: globalThis.RTCIceCandidate ?? globalThis.mozRTCIceCandidate ?? globalThis.webkitRTCIceCandidate
};
if (wrtc.RTCPeerConnection == null) {
throw errCode(new Error('No WebRTC support detected'), 'ERR_WEBRTC_SUPPORT');
}
return wrtc;
}
export class WebRTCPeer extends EventEmitter {
constructor(opts) {
super();
this.id = opts.id ?? uint8ArrayToString(randombytes(4), 'hex').slice(0, 7);
this.log = logger(`libp2p:webrtc-peer:${opts.logPrefix}:${this.id}`);
this.wrtc = opts.wrtc ?? getBrowserRTC();
this.peerConnection = new this.wrtc.RTCPeerConnection(Object.assign({}, DEFAULT_PEER_CONNECTION_CONFIG, opts.peerConnectionConfig));
this.closed = false;
this.connected = defer();
// duplex properties
this.source = pushable();
this.sink = async (source) => {
await this.connected.promise;
if (this.channel == null) {
throw errCode(new Error('Connected but no channel?!'), 'ERR_DATA_CHANNEL');
}
for await (const buf of source) {
await this.channel.send(buf);
}
await this.close();
};
}
handleDataChannelEvent(event) {
const dataChannel = event.channel;
if (dataChannel == null) {
// In some situations `pc.createDataChannel()` returns `undefined` (in wrtc),
// which is invalid behavior. Handle it gracefully.
// See: https://github.com/feross/simple-peer/issues/163
this.close(errCode(new Error('Data channel event is missing `channel` property'), 'ERR_DATA_CHANNEL'))
.catch(err => {
this.log('Error closing after event channel was found to be null', err);
});
return;
}
this.channel = new WebRTCDataChannel(dataChannel, {
log: this.log,
onMessage: (event) => {
this.source.push(new Uint8Array(event.data));
},
onOpen: () => {
this.connected.resolve();
this.dispatchEvent(new CustomEvent('ready'));
},
onClose: () => {
this.close().catch(err => {
this.log('error closing connection after channel close', err);
});
},
onError: (err) => {
this.close(err).catch(err => {
this.log('error closing connection after channel error', err);
});
}
});
}
async close(err) {
this.closed = true;
if (err == null && this.channel != null) {
// wait for the channel to flush all data before closing the channel
while (this.channel.bufferedAmount > 0) {
await delay(100);
}
}
this.channel?.close();
this.peerConnection.close();
this.source.end(err);
this.dispatchEvent(new CustomEvent('close'));
}
}
//# sourceMappingURL=peer.js.map