UNPKG

pigeonrtc

Version:

Pluggable cross-browser compatible WebRTC library for PeerPigeon

264 lines (231 loc) 7.54 kB
import { MDNSResolver } from './MDNSResolver.js'; /** * Managed peer connection with built-in signaling support */ export class PeerConnection extends EventTarget { constructor(rtcInstance, signalingClient, config = {}) { super(); this.rtc = rtcInstance; this.signaling = signalingClient; this.config = config; this.pc = null; this.dataChannels = new Map(); this.remoteId = null; this.isInitiator = false; this.mdnsResolver = new MDNSResolver({ serverUrl: config.mdnsServerUrl || 'http://localhost:5380' }); this._mdnsEnabled = config.enableMDNS !== false; // enabled by default } /** * Initialize peer connection * @private */ async _init() { // Initialize mDNS resolver if enabled if (this._mdnsEnabled) { await this.mdnsResolver.initialize(); } this.pc = this.rtc.createPeerConnection(this.config); // Handle ICE candidates this.pc.onicecandidate = async (event) => { if (event.candidate && this.remoteId) { let candidateToSend = event.candidate; // Try to resolve .local candidates if mDNS is enabled if (this._mdnsEnabled && this.mdnsResolver.isAvailable() && this.mdnsResolver.isLocalCandidate(event.candidate)) { const resolvedCandidate = await this.mdnsResolver.resolveCandidate(event.candidate); if (resolvedCandidate) { candidateToSend = resolvedCandidate; console.log('Resolved .local ICE candidate:', event.candidate.candidate, '->', resolvedCandidate.candidate); } } this.signaling.sendIceCandidate(this.remoteId, candidateToSend); } }; // Handle connection state changes this.pc.onconnectionstatechange = () => { this.dispatchEvent(new CustomEvent('connectionstatechange', { detail: this.pc.connectionState })); if (this.pc.connectionState === 'connected') { this.dispatchEvent(new CustomEvent('connected')); } else if (this.pc.connectionState === 'failed') { this.dispatchEvent(new CustomEvent('failed')); } }; // Handle ICE connection state changes this.pc.oniceconnectionstatechange = () => { this.dispatchEvent(new CustomEvent('iceconnectionstatechange', { detail: this.pc.iceConnectionState })); }; // Handle remote tracks this.pc.ontrack = (event) => { this.dispatchEvent(new CustomEvent('track', { detail: { track: event.track, streams: event.streams } })); }; // Handle incoming data channels this.pc.ondatachannel = (event) => { const channel = event.channel; this.dataChannels.set(channel.label, channel); this._setupDataChannel(channel); this.dispatchEvent(new CustomEvent('datachannel', { detail: channel })); }; } /** * Connect to a remote peer * @param {string|number} peerId - Remote peer ID * @param {MediaStream} [localStream] - Optional local media stream * @returns {Promise<void>} */ async connect(peerId, localStream = null) { this.remoteId = peerId; this.isInitiator = true; await this._init(); // Add local tracks if provided if (localStream) { localStream.getTracks().forEach(track => { this.pc.addTrack(track, localStream); }); } // Create and send offer const offer = await this.pc.createOffer(); await this.pc.setLocalDescription(offer); this.signaling.sendOffer(peerId, offer); } /** * Handle incoming offer from remote peer * @param {string|number} peerId - Remote peer ID * @param {RTCSessionDescriptionInit} offer - WebRTC offer * @param {MediaStream} [localStream] - Optional local media stream * @returns {Promise<void>} */ async handleOffer(peerId, offer, localStream = null) { this.remoteId = peerId; this.isInitiator = false; await this._init(); // Add local tracks if provided if (localStream) { localStream.getTracks().forEach(track => { this.pc.addTrack(track, localStream); }); } // Set remote description and create answer await this.pc.setRemoteDescription(offer); const answer = await this.pc.createAnswer(); await this.pc.setLocalDescription(answer); this.signaling.sendAnswer(peerId, answer); } /** * Handle incoming answer from remote peer * @param {RTCSessionDescriptionInit} answer - WebRTC answer * @returns {Promise<void>} */ async handleAnswer(answer) { await this.pc.setRemoteDescription(answer); } /** * Handle incoming ICE candidate * @param {RTCIceCandidateInit} candidate - ICE candidate * @returns {Promise<void>} */ async handleIceCandidate(candidate) { if (this.pc) { let candidateToAdd = candidate; // Try to resolve .local candidates if mDNS is enabled if (this._mdnsEnabled && this.mdnsResolver.isAvailable() && this.mdnsResolver.isLocalCandidate(candidate)) { const resolvedCandidate = await this.mdnsResolver.resolveCandidate(candidate); if (resolvedCandidate) { candidateToAdd = resolvedCandidate; console.log('Resolved incoming .local ICE candidate:', candidate.candidate, '->', resolvedCandidate.candidate); } } await this.pc.addIceCandidate(candidateToAdd); } } /** * Create a data channel * @param {string} label - Channel label * @param {RTCDataChannelInit} [options] - Data channel options * @returns {RTCDataChannel} */ createDataChannel(label, options = {}) { if (!this.pc) { throw new Error('Peer connection not initialized'); } const channel = this.pc.createDataChannel(label, options); this.dataChannels.set(label, channel); this._setupDataChannel(channel); return channel; } /** * Setup data channel event handlers * @private */ _setupDataChannel(channel) { channel.onopen = () => { this.dispatchEvent(new CustomEvent('channelopen', { detail: channel })); }; channel.onmessage = (event) => { this.dispatchEvent(new CustomEvent('message', { detail: { channel: channel.label, data: event.data } })); }; channel.onclose = () => { this.dataChannels.delete(channel.label); this.dispatchEvent(new CustomEvent('channelclose', { detail: channel })); }; } /** * Send data on a channel * @param {string} channelLabel - Channel label * @param {string|ArrayBuffer|Blob} data - Data to send */ send(channelLabel, data) { const channel = this.dataChannels.get(channelLabel); if (channel && channel.readyState === 'open') { channel.send(data); } else { throw new Error(`Channel ${channelLabel} not open`); } } /** * Get a data channel by label * @param {string} label - Channel label * @returns {RTCDataChannel|undefined} */ getDataChannel(label) { return this.dataChannels.get(label); } /** * Get the underlying RTCPeerConnection * @returns {RTCPeerConnection} */ getRTCPeerConnection() { return this.pc; } /** * Close the peer connection */ close() { if (this.pc) { this.pc.close(); this.pc = null; } this.dataChannels.clear(); this.remoteId = null; // Clean up mDNS resolver if (this.mdnsResolver) { this.mdnsResolver.dispose(); } } }