import AdvancedWebSocket from './advanced-websocket'; import EventEmitter from './event-emitter' let peerConCount: number = 1 export class P2PConnectionHandler extends EventEmitter { peer: number shouldConnect: boolean ws: AdvancedWebSocket messageEmitter: EventEmitter audioSender: RTCRtpSender | undefined | null videoSender: RTCRtpSender | undefined | null setState: ((newState: any) => void) | undefined = undefined static rtcDescription = { iceServers: [ { "urls": ['turn:ideadesignmedia.com:3331'], username: 'IDM', credential: 'TURNME' }, { "urls": ['turn:ideadesignmedia.com:3332'], username: 'IDM', credential: 'TURNME' }, { "urls": [ "stun:stun1.l.google.com:19302" ] } ] } peerConnection: RTCPeerConnection private _connectionState: string = 'ready' connectionNumber: number peerConnectionNumber: number = 0 get connectionState() { return this._connectionState } set connectionState(state: string) { this._connectionState = state this.emit('connection-state-changed', state) } constructor(ws: AdvancedWebSocket, messageEmitter: EventEmitter, peer: number, shouldConnect: boolean, setState?: (newState: any) => void) { super() if (setState) this.setState = setState this.peer = peer this.shouldConnect = shouldConnect this.connectionNumber = peerConCount++ this.ws = ws this.messageEmitter = messageEmitter this.messageEmitter.on('key', this.handleKey) this.messageEmitter.on('offer', this.handleOffer) this.messageEmitter.on('answer', this.handleAnswer) this.messageEmitter.on('send-offer', this.handleShouldOffer) this.messageEmitter.on('rtc-connection-ready', this.handlePeerReady) this.messageEmitter.on('rtc-connection-error', this.handleError) this.messageEmitter.on('rtc-connection-initiated', this.handleInitiated) this.messageEmitter.on('rtc-connection-key-sent', this.handleKeySent) this.messageEmitter.on('rtc-connection-answer-sent', this.handleAnswerSent) this.messageEmitter.on('rtc-connection-offer-sent', this.handleOfferSent) this.messageEmitter.on('rtc-connection-offer-requested', this.handleOfferRequested) this.peerConnection = this.createConnection() if (shouldConnect) this.connect() } log = (...message: any[]) => { if (typeof this.setState === 'function') this.setState((state: any) => ({ ...state, logData: (state.logData || '') + `${this.connectionNumber} ${this.peer}: ${message.map(message => JSON.stringify(message, null, 2)).join(' ')}\n` })) else console.log(...message) } error = (...message: any[]) => { if (typeof this.setState === 'function') this.setState((state: any) => ({ ...state, logData: (state.logData || '') + `ERROR: ${this.connectionNumber} ${this.peer}: ${message.map(message => JSON.stringify(message, null, 2)).join(' ')}\n` })) else console.error(...message) } createConnection = () => { const connection = new RTCPeerConnection(P2PConnectionHandler.rtcDescription) const canvas = document.createElement('canvas') const stream = canvas.captureStream(24) const audioTrack = stream.getAudioTracks()[0] const videoTrack = stream.getVideoTracks()[0] if (audioTrack) { this.audioSender = connection.addTrack(audioTrack, stream) if (this.audioSender.track) this.audioSender.track.enabled = false } if (videoTrack) { this.videoSender = connection.addTrack(videoTrack, stream) if (this.videoSender.track) this.videoSender.track.enabled = false } connection.onconnectionstatechange = () => { // this.log('CONNECTION STATE', connection.connectionState) switch (connection.connectionState) { case "connected": { this.connectionState = 'connected' break; } case "disconnected": { connection.close() break } case "failed": { connection.close() break; } case "closed": { connection.close() break; } default: break } } connection.onsignalingstatechange = () => { // this.log('SIGNALING STATE', connection.signalingState) } connection.oniceconnectionstatechange = () => { // this.log('ICE CONNECTION STATE', connection.iceConnectionState) } connection.onicecandidate = e => { if (e.candidate) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.sendData({ type: 'key', data: { key: e.candidate, connectionNumber: this.connectionNumber } }) } } } return connection } disconnect = () => { this.peerConnection.close() this.messageEmitter.off('key', this.handleKey) this.messageEmitter.off('offer', this.handleOffer) this.messageEmitter.off('answer', this.handleAnswer) this.messageEmitter.off('send-offer', this.handleShouldOffer) this.messageEmitter.off('rtc-connection-ready', this.handlePeerReady) this.messageEmitter.off('rtc-connection-error', this.handleError) this.messageEmitter.off('rtc-connection-initiated', this.handleInitiated) this.messageEmitter.off('rtc-connection-key-sent', this.handleKeySent) this.messageEmitter.off('rtc-connection-answer-sent', this.handleAnswerSent) this.messageEmitter.off('rtc-connection-offer-sent', this.handleOfferSent) this.messageEmitter.off('rtc-connection-offer-requested', this.handleOfferRequested) } setStream = (stream: MediaStream) => { if (this.peerConnection.signalingState !== 'closed') { stream.getTracks().forEach(track => { const sender = this.peerConnection.getSenders().find(sender => (sender.track && sender.track.kind === track.kind)) if (sender) { if (sender.track !== track) { sender.replaceTrack(track).then(() => { if (sender.track) { sender.track.enabled = true } }) } } else { const s = this.peerConnection.addTrack(track, stream) if (track.kind === 'audio') { this.audioSender = s if (s.track) s.track.enabled = true } else if (track.kind === 'video') { this.videoSender = s if (s.track) s.track.enabled = true } } }) } } connect = () => { if (this.ws.readyState === WebSocket.OPEN) { this.ws.sendData({ type: 'ready', data: { connectionNumber: this.connectionNumber, selectedPeer: this.peer } }) } } reconnect = () => { this.peerConnection.close() this.connectionNumber = peerConCount++ this.peerConnection = this.createConnection() } sendOffer = async () => { const offer = await this.peerConnection.createOffer() await this.peerConnection.setLocalDescription(offer) if (this.ws.readyState === WebSocket.OPEN) this.ws.sendData({ type: 'offer', data: { offer, connectionNumber: this.connectionNumber } }) } createAnswer = async (offer: RTCSessionDescriptionInit) => { await this.peerConnection.setRemoteDescription(offer) const answer = await this.peerConnection.createAnswer() await this.peerConnection.setLocalDescription(answer) if (this.ws.readyState === WebSocket.OPEN) this.ws.sendData({ type: 'answer', data: { answer, connectionNumber: this.connectionNumber } }) } setAnswer = async (answer: RTCSessionDescriptionInit) => { await this.peerConnection.setRemoteDescription(answer).then(() => { }) } setKey = async (key: RTCIceCandidate) => { await this.peerConnection.addIceCandidate(key) } handleKey = ({ key, connectionNumber: responseFor }: { key: RTCIceCandidate, connectionNumber: number }) => { if (this.connectionNumber === responseFor) this.setKey(key).catch(e => { this.error('Error setting key', responseFor, this.peer, this.peerConnection.signalingState, e) }) } handleOffer = ({ offer, connectionNumber: responseFor }: { offer: RTCSessionDescriptionInit, connectionNumber: number }) => { if (this.connectionNumber === responseFor) { this.createAnswer(offer).catch(e => { this.error('Error creating answer', responseFor, this.peer, this.peerConnection.signalingState, e) }) } } handleAnswer = ({ answer, connectionNumber: responseFor }: { answer: RTCSessionDescriptionInit, connectionNumber: number }) => { if (this.connectionNumber === responseFor) { this.setAnswer(answer).catch(e => { this.error('Error setting answer', responseFor, this.peer, this.peerConnection.signalingState, e) }) } } handleShouldOffer = ({ connectionNumber: responseFor }: { connectionNumber: number, peerConnectionNumber: number, selectedPeer: number }) => { if (this.connectionNumber === responseFor) { this.sendOffer().catch(e => { this.error('Error sending offer', this.connectionNumber, this.peer, this.peerConnection.signalingState, e) }) } } handlePeerReady = ({ peerConnectionNumber, selectedPeer }: { peerConnectionNumber: number, selectedPeer: number }) => { if (selectedPeer === this.peer) { this.peerConnectionNumber = peerConnectionNumber if (this.ws?.readyState === WebSocket.OPEN) { this.ws.sendData({ type: 'join-peer', data: { connectionNumber: this.connectionNumber, peerConnectionNumber: this.peerConnectionNumber, selectedPeer: this.peer } }) this.connectionState = 'connecting' } } } handleError = ({ connectionNumber: requestFor, message }: { connectionNumber: number, message?: string }) => { if (this.connectionNumber === requestFor) this.error(`RTC Error(${this.connectionNumber} ${this.peer}): ${message}`) } handleInitiated = ({ connectionNumber: responseFor }: { connectionNumber: number }) => { if (this.connectionNumber === responseFor) { this.connectionState = 'connecting' } } handleKeySent = ({ connectionNumber: responseFor }: { connectionNumber: number }) => { if (this.connectionNumber === responseFor) { } } handleAnswerSent = ({ connectionNumber: responseFor }: { connectionNumber: number }) => { if (this.connectionNumber === responseFor) { } } handleOfferSent = ({ connectionNumber: responseFor }: { connectionNumber: number }) => { if (this.connectionNumber === responseFor) { } } handleOfferRequested = ({ connectionNumber: responseFor }: { connectionNumber: number }) => { if (this.connectionNumber === responseFor) { } } } export default P2PConnectionHandler