Able to change host during a call

This commit is contained in:
Sagi Dayan 2020-04-14 11:06:09 -04:00
parent 977d7a0a01
commit 38ca6b6133
14 changed files with 4210 additions and 34 deletions

View file

@ -47,6 +47,7 @@ class CallSession {
this.onCallEndedCallback = onCallEndedCallback; this.onCallEndedCallback = onCallEndedCallback;
this.callId = callModel.id; this.callId = callModel.id;
this.callModel = callModel; this.callModel = callModel;
this.hostId = 2;
this.state = callModel.state; this.state = callModel.state;
this.user_1 = {id: callModel.user_1, socket: null, userModel: null}; this.user_1 = {id: callModel.user_1, socket: null, userModel: null};
this.user_2 = {id: callModel.user_2, socket: null, userModel: null}; this.user_2 = {id: callModel.user_2, socket: null, userModel: null};
@ -93,6 +94,7 @@ class CallSession {
socket.on('wrtc:sdp:offer', this.onSdpOffer.bind(this)); socket.on('wrtc:sdp:offer', this.onSdpOffer.bind(this));
socket.on('wrtc:sdp:answer', this.onSdpAnswer.bind(this)); socket.on('wrtc:sdp:answer', this.onSdpAnswer.bind(this));
socket.on('wrtc:ice', this.onIceCandidate.bind(this)); socket.on('wrtc:ice', this.onIceCandidate.bind(this));
socket.on('call:host:changed', this.onHostChanged.bind(this));
socket socket
.on('book:action:flip-page', this.onActionBookFlip.bind(this)) .on('book:action:flip-page', this.onActionBookFlip.bind(this))
await this.updateState(); await this.updateState();
@ -125,15 +127,28 @@ class CallSession {
clearInterval(this.heartbeat); clearInterval(this.heartbeat);
this.onCallEndedCallback(this.callId); this.onCallEndedCallback(this.callId);
} }
async sendStandby(socket, userIndex) { async sendStandby(socket, userIndex) {
console.log(`Call #${this.callId} sendStandby -> ${userIndex}`); console.log(`Call #${this.callId} sendStandby -> ${userIndex}`);
const iceServers = (await IceServer.all()).rows.map(i => i.toJSON()); const iceServers = (await IceServer.all()).rows.map(i => i.toJSON());
socket.emit('call:standby', {iceServers, id: userIndex}); socket.emit('call:standby', {
iceServers,
id: userIndex,
child: this.child.toJSON(),
users: await this.callModel.getUsers(),
hostId: this.hostId
});
} }
async sendStart(socket, userIndex) { async sendStart(socket, userIndex) {
console.log(`Call #${this.callId} sendStart -> ${userIndex}`); console.log(`Call #${this.callId} sendStart -> ${userIndex}`);
const iceServers = (await IceServer.all()).rows.map(i => i.toJSON()); const iceServers = (await IceServer.all()).rows.map(i => i.toJSON());
socket.emit('call:start', {iceServers, id: userIndex}); socket.emit('call:start', {
iceServers,
id: userIndex,
child: this.child.toJSON(),
users: await this.callModel.getUsers(),
hostId: this.hostId
});
} }
async updateState() { async updateState() {
console.log(`Call #${this.callId} state=${this.state}`); console.log(`Call #${this.callId} state=${this.state}`);
@ -189,6 +204,15 @@ class CallSession {
`[Signal] [book] [action] [flip] [${direction}] ${from} -> ${to}`); `[Signal] [book] [action] [flip] [${direction}] ${from} -> ${to}`);
return true; return true;
} }
async onHostChanged(payload) {
const {from = payload.id, hostId} = payload;
const to = from === 1 ? 2 : 1;
this.hostId = hostId;
this[`user_${to}`].socket.emit('call:host:changed', {hostId});
console.log(
`[Signal] [host] [changed] [hostId=${hostId}] ${from} -> ${to}`);
return true;
}
} }
module.exports = SignalingController module.exports = SignalingController

View file

@ -2,8 +2,17 @@
/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
const Model = use('Model') const Model = use('Model')
const User = use('App/Models/User');
class Call extends Model { class Call extends Model {
async getUsers() {
const users = [
(await User.find(this.user_1)).toJSON(),
(await User.find(this.user_2)).toJSON()
];
return users;
}
} }
module.exports = Call module.exports = Call

View file

@ -39,10 +39,10 @@
"adonis-vue-websocket": "^2.0.2", "adonis-vue-websocket": "^2.0.2",
"animate.css": "^3.7.2", "animate.css": "^3.7.2",
"bulma": "^0.8.0", "bulma": "^0.8.0",
"flipbook-vue": "^0.8.1",
"fork-awesome": "^1.1.7", "fork-awesome": "^1.1.7",
"moment": "^2.24.0", "moment": "^2.24.0",
"regenerator-runtime": "^0.13.5", "regenerator-runtime": "^0.13.5",
"rematrix": "^0.5.0",
"sqlite3": "^4.1.1", "sqlite3": "^4.1.1",
"typescript": "^3.7.5", "typescript": "^3.7.5",
"uuid": "^7.0.3", "uuid": "^7.0.3",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -9,7 +9,10 @@ export default class CallManager {
private needToAddStream: boolean = true; private needToAddStream: boolean = true;
private emitter = new EventEmitter(); private emitter = new EventEmitter();
private pc: RTCPeerConnection; private pc: RTCPeerConnection;
constructor(private ws: WebSocketService, private callId: number) { public child;
public guest = { avatar: '' };
public isHost = false;
constructor(private ws: WebSocketService, private callId: number, private userId: number) {
this.inCall = false; this.inCall = false;
this.peerId = -1; this.peerId = -1;
this.pc = null; this.pc = null;
@ -31,7 +34,8 @@ export default class CallManager {
signalingChannel.on('wrtc:sdp:offer', self.onRemoteOffer.bind(self)); signalingChannel.on('wrtc:sdp:offer', self.onRemoteOffer.bind(self));
signalingChannel.on('wrtc:sdp:answer', self.onRemoteAnswer.bind(self)); signalingChannel.on('wrtc:sdp:answer', self.onRemoteAnswer.bind(self));
signalingChannel.on('wrtc:ice', self.onRemoteIce.bind(self)); signalingChannel.on('wrtc:ice', self.onRemoteIce.bind(self));
signalingChannel.on('book:action:flip-page', self.onActionBookFlip.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) => { signalingChannel.on('error', (e) => {
console.error(e); console.error(e);
resolve(false) resolve(false)
@ -55,11 +59,17 @@ export default class CallManager {
...payload ...payload
}) })
} }
async onCallStart(payload: { iceServers: RTCIceServer[], id: number }) { async onCallStart(payload: { iceServers: RTCIceServer[], id: number, users: any[], child: any, hostId: number }) {
console.log('onCallStart'); console.log('onCallStart');
console.log(payload); console.log(payload);
this.peerId = payload.id; this.peerId = payload.id;
this.isHost = this.peerId === payload.hostId;
this.pc = new RTCPeerConnection({ iceServers: payload.iceServers }); 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('Created PeerConnection');
console.log('adding tracks to pc'); console.log('adding tracks to pc');
this.localStream.getTracks().forEach(t => this.pc.addTrack(t, this.localStream)); this.localStream.getTracks().forEach(t => this.pc.addTrack(t, this.localStream));
@ -73,11 +83,17 @@ export default class CallManager {
return true; return true;
} }
async onCallStandby(payload: { iceServers: RTCIceServer[], id: number }) { async onCallStandby(payload: { iceServers: RTCIceServer[], id: number, users: any[], child: any, hostId: number }) {
console.log('onCallStandby'); console.log('onCallStandby');
console.log(payload); console.log(payload);
this.peerId = payload.id; this.peerId = payload.id;
this.isHost = this.peerId === payload.hostId;
this.pc = new RTCPeerConnection({ iceServers: payload.iceServers }); 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('Created PeerConnection');
console.log('adding tracks to pc'); console.log('adding tracks to pc');
this.localStream.getTracks().forEach(t => this.pc.addTrack(t, this.localStream)); this.localStream.getTracks().forEach(t => this.pc.addTrack(t, this.localStream));
@ -156,6 +172,19 @@ export default class CallManager {
this.emit(ECallEvents.ACTION_BOOK_FLIP, 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() { close() {
console.log('Closing...'); console.log('Closing...');
if (!this.inCall) return; if (!this.inCall) return;
@ -174,5 +203,6 @@ export default class CallManager {
export enum ECallEvents { export enum ECallEvents {
CLOSE = 'CLOSE', CLOSE = 'CLOSE',
REMOTE_STREAM = 'REMOTE_STREAM', REMOTE_STREAM = 'REMOTE_STREAM',
ACTION_BOOK_FLIP = 'ACTION_BOOK_FLIP' ACTION_BOOK_FLIP = 'ACTION_BOOK_FLIP',
CALL_HOST_CHANGED = "CALL_HOST_CHANGED",
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -4,6 +4,18 @@
<Loading /> <Loading />
</div> </div>
<div v-else class> <div v-else class>
<div class="floating-host is-flex" style="position:fixed;top:60px;left:100px">
<h1 class="subtitle m-r-md">Host:</h1>
<div class="me">
<figure class="image is-24x24">
<img
:src="isHost? user.avatar : callManager.guest.avatar"
class="is-rounded is-avatar"
@click="changeHost()"
/>
</figure>
</div>
</div>
<div class="video-strip m-b-md is-outlined"> <div class="video-strip m-b-md is-outlined">
<video <video
:src-object.prop.camel="localStream" :src-object.prop.camel="localStream"
@ -14,16 +26,32 @@
/> />
<video :src-object.prop.camel="remoteStream" autoplay style="max-width:20%" /> <video :src-object.prop.camel="remoteStream" autoplay style="max-width:20%" />
</div> </div>
<div class> <div class="is-flex">
<div class="go-left m-r-sm" style="display: flex; align-items: center;">
<button class="button is-outlined" :disabled="!isHost" @click="onLeftClicked()">
<i class="fa fa-fw fa-arrow-left"></i>
</button>
</div>
<flipbook <flipbook
class="flipbook" class="flipbook"
:pages="createPages(book)" :pages="createPages(book)"
forwardDirection="left" forwardDirection="left"
:zooms="null" :zooms="null"
:enabled="isHost"
@flip-left-end="onFlip('left')" @flip-left-end="onFlip('left')"
@flip-right-end="onFlip('right')" @flip-right-end="onFlip('right')"
ref="flipbook" ref="flipbook"
></flipbook> v-slot="flipbook"
>
<div class="page-progress has-text-centered m-b-none">
<p>Page {{ flipbook.page }} of {{ flipbook.numPages }}</p>
</div>
</flipbook>
<div class="go-right m-l-sm" style="display: flex; align-items: center;">
<button class="button is-outlined" :disabled="!isHost" @click="onRightClicked()">
<i class="fa fa-fw fa-arrow-right"></i>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -33,7 +61,7 @@
import Loading from "../../shared/components/Loading/Loading.vue"; import Loading from "../../shared/components/Loading/Loading.vue";
import WebsocketService from "../scripts/websocket.service"; import WebsocketService from "../scripts/websocket.service";
import CallManager, { ECallEvents } from "../classes/call.manager"; import CallManager, { ECallEvents } from "../classes/call.manager";
import Flipbook from "flipbook-vue"; import Flipbook from "../components/flipbook/flipbook.cjs.js";
import Services from "../../services/index"; import Services from "../../services/index";
import { mapActions, mapGetters } from "vuex"; import { mapActions, mapGetters } from "vuex";
export default { export default {
@ -42,19 +70,28 @@ export default {
Flipbook Flipbook
}, },
name: "Call", name: "Call",
mounted() {
const self = this;
setTimeout(() => {
self.isMounted = true;
}, 1000);
},
async created() { async created() {
this.loading = true; this.loading = true;
try { try {
const callId = Number(this.$route.params.id); const callId = Number(this.$route.params.id);
const ws = await WebsocketService.getInstance(); const ws = await WebsocketService.getInstance();
this.callManager = new CallManager(ws, callId); this.callManager = new CallManager(ws, callId, this.user.id);
this.callManager.on(ECallEvents.CLOSE, this.callEnded); this.callManager.on(ECallEvents.CLOSE, this.callEnded);
this.callManager.on(ECallEvents.ACTION_BOOK_FLIP, this.onRemoteFlip); this.callManager.on(ECallEvents.ACTION_BOOK_FLIP, this.onRemoteFlip);
const success = await this.callManager.connectToCall({ const success = await this.callManager.connectToCall({
video: true, video: true,
audio: true audio: true
}); });
this.callManager.on(
ECallEvents.CALL_HOST_CHANGED,
this.onRemoteHostChanged
);
if (!success) { if (!success) {
this.notify({ message: "Can find this call...", level: "danger" }); this.notify({ message: "Can find this call...", level: "danger" });
this.$router.push({ path: `/` }); this.$router.push({ path: `/` });
@ -80,9 +117,15 @@ export default {
return true; return true;
}, },
onFlip(direction: "left" | "right") { onFlip(direction: "left" | "right") {
if (this.callManager.peerId == 2) if (this.isHost)
this.callManager.send(`book:action:flip-page`, { direction }); this.callManager.send(`book:action:flip-page`, { direction });
}, },
onLeftClicked() {
this.$refs.flipbook.flipLeft();
},
onRightClicked() {
this.$refs.flipbook.flipRight();
},
onRemoteFlip(payload) { onRemoteFlip(payload) {
switch (payload.direction) { switch (payload.direction) {
case "left": case "left":
@ -104,10 +147,19 @@ export default {
this.notify({ message: `Call #${callId} Ended` }); this.notify({ message: `Call #${callId} Ended` });
this.$router.push({ path: `/` }); this.$router.push({ path: `/` });
}, },
onRemoteHostChanged(payload) {
console.log("-----------");
console.log(payload);
this.guest = this.callManager.guest;
this.isHost = this.callManager.isHost;
},
changeHost() {
this.callManager.changeHost();
},
...mapActions(["notify"]) ...mapActions(["notify"])
}, },
computed: { computed: {
...mapGetters([]) ...mapGetters(["user"])
}, },
data() { data() {
return { return {
@ -118,7 +170,11 @@ export default {
loading: true, loading: true,
localStream: null, localStream: null,
remoteStream: null, remoteStream: null,
callManager: null callManager: null,
isHost: false,
// currentPage: 0,
// totalPages: 34,
isMounted: false
}; };
}, },
beforeCreate: () => {} beforeCreate: () => {}

View file

@ -3142,13 +3142,6 @@ flagged-respawn@^1.0.0:
resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41" resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41"
integrity sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q== integrity sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==
flipbook-vue@^0.8.1:
version "0.8.1"
resolved "https://registry.yarnpkg.com/flipbook-vue/-/flipbook-vue-0.8.1.tgz#1b3d29a5a8d0d6921ff0be1a91e4165b0d9d6d6e"
integrity sha512-HPWrGe6fwIGSG2IV+kN2Bz16pp8DtsgvNrObxaXtr+XNwxgaLWpbKzM8QV5alBcMFJtyo+7d2Ds1zLrCp8h1LQ==
dependencies:
rematrix "^0.4.1"
flush-write-stream@^1.0.0: flush-write-stream@^1.0.0:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
@ -6000,10 +5993,10 @@ regjsparser@^0.1.4:
dependencies: dependencies:
jsesc "~0.5.0" jsesc "~0.5.0"
rematrix@^0.4.1: rematrix@^0.5.0:
version "0.4.1" version "0.5.0"
resolved "https://registry.yarnpkg.com/rematrix/-/rematrix-0.4.1.tgz#496d40b48c79dd3820bc40352b8d7bec44b32887" resolved "https://registry.yarnpkg.com/rematrix/-/rematrix-0.5.0.tgz#9e8cd1abc82a0c1f4114dba333ad9a9c560673fe"
integrity sha512-nH4OjL09zw4yhdBClK4B93kRWbHu+std7ZV6ljS2Zsgy2YrPcayL67axKLfMxoFool8mPyVY614m8SFE7sJQLw== integrity sha512-vlyfr4SoiHeH3gj/AkwgChPmA6Nb4UqFomB9deUcFQugWUC7DijAAAF+MX9K3yuvxm9aURoED5wofRDl9ISi2w==
remove-trailing-separator@^1.0.1: remove-trailing-separator@^1.0.1:
version "1.1.0" version "1.1.0"