import WebSocketService from "../scripts/websocket.service"; import { EventEmitter } from "events"; export default class CallManager { private inCall: boolean; public peerId: number; private localStream: MediaStream; private remoteStream: MediaStream; private signalingChannel; private needToAddStream: boolean = true; private emitter = new EventEmitter(); private pc: RTCPeerConnection; public child; public guest = { avatar: '' }; public isHost = false; constructor(private ws: WebSocketService, private callId: number, private userId: number) { this.inCall = false; this.peerId = -1; this.pc = null; this.remoteStream = new MediaStream(); } async connectToCall(mediaConstraints: MediaStreamConstraints): Promise { if (this.inCall) throw new Error('Already connected to call'); console.log('connecting to call'); await this.getUserMedia(mediaConstraints); this.signalingChannel = this.ws.subscribe(`call:${this.callId}`); const signalingChannel = this.signalingChannel; const self = this; return new Promise((resolve, reject) => { signalingChannel.on('close', self.close.bind(self)); signalingChannel.on('call:start', self.onCallStart.bind(self)); signalingChannel.on('call:standby', self.onCallStandby.bind(self)); signalingChannel.on('wrtc:sdp:offer', self.onRemoteOffer.bind(self)); signalingChannel.on('wrtc:sdp:answer', self.onRemoteAnswer.bind(self)); signalingChannel.on('wrtc:ice', self.onRemoteIce.bind(self)); signalingChannel.on('book:action:flip-page', self.onActionBookFlip.bind(self)); signalingChannel.on('call:host:changed', self.onRemoteHostChanged.bind(self)); signalingChannel.on('error', (e) => { console.error(e); resolve(false) }); signalingChannel.on('ready', () => { console.log('in Ready'); self.inCall = true; resolve(true) }); }); } on(event: ECallEvents, callback: (...args) => void) { this.emitter.on(event, callback); } private emit(event: ECallEvents, data: any) { this.emitter.emit(event, data); } private send(event: string, payload: { [key: string]: any }) { this.signalingChannel.emit(event, { id: this.peerId, ...payload }) } async onCallStart(payload: { iceServers: RTCIceServer[], id: number, users: any[], child: any, hostId: number }) { console.log('onCallStart'); console.log(payload); this.peerId = payload.id; this.isHost = this.peerId === payload.hostId; this.pc = new RTCPeerConnection({ iceServers: payload.iceServers }); this.child = payload.child; payload.users.forEach(u => { if (u.id !== this.userId) this.guest = u; }); this.emit(ECallEvents.CALL_HOST_CHANGED, { hostId: payload.hostId }); console.log('Created PeerConnection'); console.log('adding tracks to pc'); this.localStream.getTracks().forEach(t => this.pc.addTrack(t, this.localStream)); this.setupPeerConnectionListeners(); const sdp = await this.pc.createOffer(); await this.pc.setLocalDescription(sdp); console.log('Local description Set', sdp.sdp); this.send('wrtc:sdp:offer', { sdp }); return true; } async onCallStandby(payload: { iceServers: RTCIceServer[], id: number, users: any[], child: any, hostId: number }) { console.log('onCallStandby'); console.log(payload); this.peerId = payload.id; this.isHost = this.peerId === payload.hostId; this.pc = new RTCPeerConnection({ iceServers: payload.iceServers }); this.child = payload.child; payload.users.forEach(u => { if (u.id !== this.userId) this.guest = u; }); this.emit(ECallEvents.CALL_HOST_CHANGED, { hostId: payload.hostId }); console.log('Created PeerConnection'); console.log('adding tracks to pc'); this.localStream.getTracks().forEach(t => this.pc.addTrack(t, this.localStream)); this.setupPeerConnectionListeners(); return true; } setupPeerConnectionListeners() { this.pc.addEventListener("icecandidate", this.onLocalIce.bind(this)); this.pc.addEventListener('connectionstatechange', event => { console.log(`PC Connection state: ${this.pc.connectionState}`); // if (this.pc.connectionState === 'connected') { // // Peers connected! // } }); this.pc.addEventListener('iceconnectionstatechange', event => { console.log('iceconnectionstatechange'); console.log(this.pc.iceConnectionState); }) this.pc.addEventListener('track', async (event) => { console.log('On remote track!'); this.remoteStream.addTrack(event.track); }); this.pc.addEventListener('icegatheringstatechange', event => { console.log('icegatheringstatechange', this.pc.iceGatheringState); }); } onLocalIce(event) { if (event.candidate) { console.log('Sending candidate'); this.send('wrtc:ice', { ice: event.candidate, }); } } async onRemoteOffer(payload) { const offer = new RTCSessionDescription(payload.sdp); await this.pc.setRemoteDescription(offer); console.log('Remote offer Set', offer.sdp); const sdp = await this.pc.createAnswer(); this.send('wrtc:sdp:answer', { sdp }); await this.pc.setLocalDescription(sdp); console.log('Local answer Set', sdp.sdp); return true; } async onRemoteAnswer(payload) { const answer = new RTCSessionDescription(payload.sdp); await this.pc.setRemoteDescription(answer); console.log('Remote answer Set', answer.sdp); return true; } async onRemoteIce(payload) { const ice = payload.ice; await this.pc.addIceCandidate(ice); return true; } async getUserMedia(constraints: MediaStreamConstraints = { video: true, audio: true }) { if (this.localStream) return this.localStream; this.localStream = await navigator.mediaDevices.getUserMedia(constraints); return this.localStream; } getRemoteStream() { return this.remoteStream; } onActionBookFlip(payload) { this.emit(ECallEvents.ACTION_BOOK_FLIP, payload); }; changeHost() { this.isHost = !this.isHost; const guestPeerId = this.peerId === 1 ? 2 : 1; const hostId = this.isHost ? this.peerId : guestPeerId; this.emit(ECallEvents.CALL_HOST_CHANGED, { hostId }); this.send('call:host:changed', { hostId }); } onRemoteHostChanged(payload) { this.isHost = this.peerId === payload.hostId; this.emit(ECallEvents.CALL_HOST_CHANGED, payload); } close() { console.log('Closing...'); if (!this.inCall) return; this.emit(ECallEvents.CLOSE, this.callId); if (this.signalingChannel) this.signalingChannel.close(); this.signalingChannel = null; if (this.pc) this.pc.close(); if (this.localStream) this.localStream.getTracks().forEach(t => t.stop()); this.localStream = null; this.remoteStream = null; this.inCall = false; } } export enum ECallEvents { CLOSE = 'CLOSE', REMOTE_STREAM = 'REMOTE_STREAM', ACTION_BOOK_FLIP = 'ACTION_BOOK_FLIP', CALL_HOST_CHANGED = "CALL_HOST_CHANGED", }