'use strict' const User = use('App/Models/User'); const UserChildUtils = use('App/Utils/UserChildUtils'); const CallUtils = use('App/Utils/CallUtils'); const Child = use('App/Models/Child'); const IceServer = use('App/Models/IceServer'); const UserChannel = use('App/Controllers/Ws/UserChannelController') const calls = {}; class SignalingController { constructor({socket, request, auth, call}) { this.callId = socket.topic.split(':')[1]; this.user = auth.user; this.socket = socket; this.request = request; this.register(call) .then(_ => { console.log(`User #${this.user.id} connected to call ${this.callId}`); }) .catch(e => { console.error(e); }); } async register(callModel) { if (!calls[this.callId]) calls[this.callId] = new CallSession(callModel, this.onCallEnded.bind(this)); else console.log(`Call #${this.callId} Already Found`); const callSession = calls[this.callId]; const success = await callSession.registerUser(this.user, this.socket) if (!success) throw new Error('Invalid User'); return true; } onCallEnded(callId) { console.log(`Call ${callId} Ended`); delete calls[callId]; } onClose() { calls[this.callId].removeUser(this.user); console.log(`User #${this.user.id} left call ${this.callId}`); } } class CallSession { // states: NEW -> STARTED -> IN_PROGRESS -> ENDED constructor(callModel, onCallEndedCallback) { this.onCallEndedCallback = onCallEndedCallback; this.callId = callModel.id; this.callModel = callModel; this.callBooks = null; this.hostId = 2; this.state = callModel.state; this.sessionState = {page: 'lobby', activity: {type: null, model: null}}; this.parent = { id: callModel.parent_id, socket: null, userModel: null, isParent: true }; this.guest = { id: callModel.guest_id, socket: null, userModel: null, isParent: false }; this.startTime = Date.now(); this.heartbeat = setInterval(this.onHeartbeat.bind(this), 1000); // Every second this.userMap = new Map(); // Reference to this.parent/guest by userId; } onHeartbeat() { const now = Date.now(); const elapsed = ((now - this.startTime) / 60000).toPrecision(1); const newStateTimeout = 5; // 5 min const startedStateTimeout = 10; // 10 min // console.log(`Heartbeat for call #${this.callId} State: ${ // this.state}, time-elapsed: ${(elapsed)}min`); if (this.state === 'ENDED') return this.endCall(); if (this.state === 'NEW' && elapsed >= newStateTimeout) return this.endCall(); if (this.state === 'STARTED' && elapsed >= startedStateTimeout) return this.endCall(); } removeUser(user) { let userToRemove = this.userMap.get(user.id); userToRemove.userModel = null; userToRemove.socket = null; this.userMap.delete(user.id); this.updateState(); if (this.state === 'ENDED') this.endCall(); } async registerUser(user, socket) { if (!this.child) this.child = await this.callModel.child().fetch(); if (!this.callBooks) this.callBooks = await CallUtils.getBooks( this.callModel.parent_id, this.callModel.guest_id); let isParent = this.parent.id === user.id; let peerId = isParent ? this.guest.id : this.parent.id; if (isParent) { this.parent.userModel = user; this.parent.socket = socket; this.userMap.set(user.id, this.parent); } else { this.guest.userModel = user; this.guest.socket = socket; this.userMap.set(user.id, this.guest); peerId = this.parent.id; } socket.on('wrtc:sdp:offer', this.onSdpOffer.bind(this)); socket.on('wrtc:sdp:answer', this.onSdpAnswer.bind(this)); socket.on('wrtc:ice', this.onIceCandidate.bind(this)); socket.on('call:host:changed', this.onHostChanged.bind(this)); socket.on('book:action:flip-page', this.onActionBookFlip.bind(this)); await this.updateState(); if (this.state === 'STARTED') { await this.sendStandby(socket, user.id, peerId); // Send event to other user about the call console.log(`trying to find peer's ${peerId} channel...`); const otherUserChannel = UserChannel.getUserChannel(peerId); if (otherUserChannel) { // console.log(otherUserChannel); console.log(`Sending notification to peer ${peerId}`); const payload = {callId: this.callId, child: this.child.toJSON()}; console.dir(payload); otherUserChannel.emit('call:incoming', payload); } } else if (this.state === 'IN_PROGRESS') { await this.sendStart(socket, user.id, peerId); } return true; } endCall() { this.state = 'ENDED'; if (this.callModel.state != 'ENDED') { this.callModel.state = this.state; this.callModel.save(); } if (this.parent.socket) this.parent.socket.close(); if (this.guest.socket) this.guest.socket.close(); clearInterval(this.heartbeat); this.onCallEndedCallback(this.callId); } async sendStandby(socket, userId, peerId) { console.log(`Call #${this.callId} sendStandby -> ${userId}`); const iceServers = (await IceServer.all()).rows.map(i => i.toJSON()); console.log(await this.callModel.parent().fetch()); socket.emit('call:standby', { iceServers, peerId, books: this.callBooks, child: this.child.toJSON(), users: await Promise.all( [this.callModel.parent().fetch(), this.callModel.guest().fetch()]), hostId: this.hostId }); } async sendStart(socket, userId, peerId) { console.log(`Call #${this.callId} sendStart -> ${userId}`); const iceServers = (await IceServer.all()).rows.map(i => i.toJSON()); socket.emit('call:start', { iceServers, peerId, books: this.callBooks, child: this.child.toJSON(), users: await Promise.all( [this.callModel.parent().fetch(), this.callModel.guest().fetch()]), hostId: this.hostId }); } async updateState() { console.log(`Call #${this.callId} state=${this.state}`); switch (this.state) { case 'NEW': if (this.areAllPartnersConnected()) this.state = 'IN_PROGRESS'; else this.state = 'STARTED'; break; case 'STARTED': if (this.areAllPartnersConnected()) this.state = 'IN_PROGRESS'; break; case 'IN_PROGRESS': if (!this.areAllPartnersConnected()) this.state = 'ENDED'; break; } this.callModel.state = this.state; await this.callModel.save(); console.log(`Call #${this.callId} state=${this.state}`); } areAllPartnersConnected() { return !!this.parent.socket && !!this.guest.socket; } async onIceCandidate(payload) { const {peerId, userId, ice} = payload; this.userMap.get(peerId).socket.emit('wrtc:ice', {ice}); console.log(`[Signal] [onIceCandidate] ${userId} -> ${peerId}`); return true; } async onSdpOffer(payload) { const {peerId, userId, sdp} = payload; this.userMap.get(peerId).socket.emit('wrtc:sdp:offer', {sdp}); console.log(`[Signal] [onSdpOffer] ${userId} -> ${peerId}`); return true; } async onSdpAnswer(payload) { const {peerId, userId, sdp} = payload; this.userMap.get(peerId).socket.emit('wrtc:sdp:answer', {sdp}); console.log(`[Signal] [onSdpAnswer] ${userId} -> ${peerId}`); return true; } onActionBookFlip(payload) { const {peerId, userId, direction} = payload; this.userMap.get(peerId).socket.emit('book:action:flip-page', {direction}); console.log(`[Signal] [book] [action] [flip] [${direction}] ${userId} -> ${ peerId}`); return true; } async onHostChanged(payload) { const {peerId, userId} = payload; this.hostId = this.hostId === userId ? peerId : userId; console.log('Host: ', this.hostId); this.userMap.get(userId).socket.emit( 'call:host:changed', {hostId: this.hostId}); try { this.userMap.get(peerId).socket.emit( 'call:host:changed', {hostId: this.hostId}); } catch (e) { } return true; } } module.exports = SignalingController