UNPKG

@libp2p/webrtc-peer

Version:
105 lines 4.38 kB
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