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 emitter = new EventEmitter(); private pc: RTCPeerConnection; public child; public peer = { avatar: '' }; public isHost = false; public books = []; public currentActivity = null; constructor(private ws: WebSocketService, readonly callId: number, private userId: number) { this.inCall = false; this.peerId = null; 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('call:view:lobby', self.onRemoteViewLobby.bind(self)); signalingChannel.on('call:view:book', self.onRemoteViewBook.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 }) { console.log(`Sending event: ${event}`); this.signalingChannel.emit(event, { userId: this.userId, peerId: this.peerId, ...payload }) } async onCallStart(payload: { iceServers: RTCIceServer[], peerId: number, users: any[], child: any, hostId: number, books: any[] }) { console.log('onCallStart'); console.log(payload); this.peerId = payload.peerId; this.books = payload.books; 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.peerId) this.peer = 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[], peerId: number, users: any[], child: any, hostId: number, books: any[] }) { console.log('onCallStandby'); console.log(payload); this.peerId = payload.peerId; this.books = payload.books; 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.peerId) this.peer = 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; } public selectBook(index: number, remote: boolean) { this.currentActivity = this.books[index]; console.log('-------> Selected Book ', index, 'bookId:', this.currentActivity.id); if (!remote) this.send('call:view:book', { bookId: this.currentActivity.id }); } public backToLobby() { console.log('-------> backToLobby'); this.send('call:view:lobby', {}); } 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.send('call:host:changed', {}); } onRemoteHostChanged(payload) { this.isHost = this.peerId === payload.hostId; this.emit(ECallEvents.CALL_HOST_CHANGED, payload); } onRemoteViewLobby(payload) { this.emit(ECallEvents.CALL_VIEW_LOBBY, null); } onRemoteViewBook(payload) { this.emit(ECallEvents.CALL_VIEW_BOOK, 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", CALL_VIEW_LOBBY = 'CALL_VIEW_LOBBY', CALL_VIEW_BOOK = 'CALL_VIEW_BOOK', }