261 lines
9.1 KiB
JavaScript
261 lines
9.1 KiB
JavaScript
'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 = callModel.guest_id;
|
|
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('call:view:lobby', this.onCallViewLobby.bind(this));
|
|
socket.on('call:view:book', this.onCallViewBook.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) {
|
|
if (!this.areAllPartnersConnected()) return true;
|
|
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) {
|
|
if (!this.areAllPartnersConnected()) return true;
|
|
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) {
|
|
if (!this.areAllPartnersConnected()) return true;
|
|
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) {
|
|
if (!this.areAllPartnersConnected()) return true;
|
|
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;
|
|
}
|
|
onCallViewLobby(payload) {
|
|
if (!this.areAllPartnersConnected()) return true;
|
|
const {peerId, userId, direction} = payload;
|
|
this.userMap.get(peerId).socket.emit('call:view:lobby', {});
|
|
console.log(
|
|
`[Signal] [call] [view] [lobby] [${direction}] ${userId} -> ${peerId}`);
|
|
return true;
|
|
}
|
|
|
|
onCallViewBook(payload) {
|
|
if (!this.areAllPartnersConnected()) return true;
|
|
const {peerId, userId, direction, bookId} = payload;
|
|
this.userMap.get(peerId).socket.emit('call:view:book', {bookId});
|
|
console.log(
|
|
`[Signal] [call] [view] [book] [${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});
|
|
if (this.userMap.get(peerId) && this.userMap.get(peerId).socket)
|
|
this.userMap.get(peerId).socket.emit(
|
|
'call:host:changed', {hostId: this.hostId});
|
|
return true;
|
|
}
|
|
}
|
|
|
|
module.exports = SignalingController
|