v1.3.0-beta/voice call implemented

This commit is contained in:
2020-05-31 18:14:19 -04:00
parent aa15c4eb30
commit 5428c1e299
12 changed files with 256 additions and 97 deletions
+14 -4
View File
@@ -33,14 +33,16 @@ export const getPlayerSource = () => {
}; };
export const LocalData = () => { export const LocalData = () => {
return JSON.parse(window.localStorage.getItem('WatchParty')) || {}; return window.localStorage.getItem('WatchParty')
? JSON.parse(window.localStorage.getItem('WatchParty'))
: {};
}; };
export const updateLocalData = (data) => { export const updateLocalData = (data) => {
window.localStorage.setItem('WatchParty', JSON.stringify(Object.assign({}, LocalData(), data))); window.localStorage.setItem('WatchParty', JSON.stringify(Object.assign({}, LocalData(), data)));
}; };
export const emptyLocalData = () => window.localStorage.setItem('WatchParty', undefined); export const emptyLocalData = () => window.localStorage.reject('WatchParty');
export const getUserUid = async () => export const getUserUid = async () =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
@@ -100,14 +102,14 @@ export const SendMessageApi = async (data) => {
export const LeaveRoomApi = async (data) => { export const LeaveRoomApi = async (data) => {
return fetcher({ return fetcher({
endpoint: `leave/${data.roomId}/users/${data.uid}`, endpoint: `leave/${data.partyName}/users/${data.userUid}`,
method: 'DELETE', method: 'DELETE',
}); });
}; };
export const DeleteRoomApi = async (data) => { export const DeleteRoomApi = async (data) => {
return fetcher({ return fetcher({
endpoint: `delete/${data.roomId}`, endpoint: `delete/${data.partyName}`,
method: 'DELETE', method: 'DELETE',
}); });
}; };
@@ -133,3 +135,11 @@ export const GetRefreshedVoxeetTokenApi = async (data) => {
method: 'GET', method: 'GET',
}); });
}; };
export const UpdateVoxeetApi = async (data) => {
return fetcher({
endpoint: 'updateVoxeet',
method: 'POST',
body: JSON.stringify(data),
});
};
+122 -38
View File
@@ -1,65 +1,148 @@
import { VoxeetSDK } from '@voxeet/voxeet-web-sdk'; import VoxeetSDK from '@voxeet/voxeet-web-sdk';
import { GetVoxeetTokenApi, GetRefreshedVoxeetTokenApi } from './Api'; import {
GetVoxeetTokenApi,
GetRefreshedVoxeetTokenApi,
LocalData,
UpdateVoxeetApi,
updateLocalData,
} from './Api';
export default class CallManager { export default class CallManager {
constructor(listener) { constructor(listener, notify) {
this.listener = listener; this.listener = listener;
this.__refreshToken = null; this.notify = notify;
this.__accessToken = null; this.__refreshToken = LocalData().vaT;
this.__accessToken = LocalData().vrT;
this.__sessionInitialized = false; this.__sessionInitialized = false;
this.__admin = false;
} }
toggleCall(onCall, conferenceRoomId) { toggleCall(onCall, conference, admin) {
this.__admin = admin;
if (onCall === true) this.leaveCall(); if (onCall === true) this.leaveCall();
else this.boot(conferenceRoomId); else this.boot(conference);
} }
async boot(conferenceRoomId) { toggleMute(muted) {
if ( VoxeetSDK.conference.mute(VoxeetSDK.session.participant, !muted);
this.__accessToken !== undefined || this.listener({ muted: !muted });
this.__refreshToken !== undefined || }
this.__accessToken !== null || async boot(conference) {
this.__refreshToken !== null await this.initialize();
)
await this.initialize();
const opened = await this.openSession(); const opened = await this.openSession();
if (opened && conferenceRoomId) this.joinCall(conferenceRoomId); if (opened && conference) {
else if (opened && !conferenceRoomId) this.startCall(conferenceRoomId); await VoxeetSDK.conference
else console.log('Unable to open Voxeet Session'); .fetch(conference._id)
.then(this.joinCall)
.catch((err) => {
console.error('Failed to fetch conference', err);
this.startCall();
});
} else if (opened && !conference) this.startCall();
else console.error('Unable to open Voxeet Session');
} }
async startCall(conferenceRoomId) { async startCall() {
VoxeetSDK.conference.create({ alias: window.localStorage.getItem('watchPartyRoomId') }); if (this.__admin === true) {
VoxeetSDK.conference
.create({ alias: LocalData().partyName, pin: LocalData().partyPin })
.then(async (conference) => {
const updatedVoxeet = await UpdateVoxeetApi({
conference,
partyName: LocalData().partyName,
partyPin: LocalData().partyPin,
userName: LocalData().userName,
})
.then((res) => res.json())
.then((res) => res.status <= 299)
.catch((err) => console.error('Update Voxeet API Error', err));
if (updatedVoxeet === true) {
this.joinCall(conference);
} else {
console.error('Failed to update party Voxeet');
}
});
} else {
this.notify('Ask your party admin to start the call.');
this.leaveCall();
}
}
async joinCall(conference) {
const joined = await VoxeetSDK.conference
.join(conference, { constraints: { audio: true, video: false } })
.then(() => true);
if (!joined) {
this.startCall();
}
}
async leaveCall() {
VoxeetSDK.conference.leave().then(() => {
VoxeetSDK.session.close();
this.listener({ onCall: false });
});
} }
async joinCall(conferenceRoomId) {}
async leaveCall() {}
async openSession() { async openSession() {
if (!this.__sessionInitialized) { if (!this.__sessionInitialized) {
try { try {
await VoxeetSDK.session.open({ await VoxeetSDK.session.open({
externalId: window.localStorage.getItem('watchPartyUserUid'), externalId: LocalData().userUid,
name: window.localStorage.getItem('watchPartyUserName'), name: LocalData().userName,
}); });
return true; return true;
} catch (error) { } catch (error) {
console.error('Error Opening Voxeet Session', error); console.error('Error Opening Voxeet Session', error);
} }
console.error('Session not initialized');
return false; return false;
} }
return true; return true;
} }
async initialize() { async initialize() {
GetVoxeetTokenApi() VoxeetSDK.conference.on('joined', () => {
.then((res) => res.json()) this.listener({ onCall: true });
.then((tokenResult) => { });
if (tokenResult.status <= 299) {
const { access_token, refresh_token } = tokenResult; VoxeetSDK.conference.on('left', () => {
this.updateTokens(access_token, refresh_token); this.listener({ onCall: false });
VoxeetSDK.initializeToken(access_token, this.refreshToken); });
} else {
console.error('Get Voxeet Token API Result Error', tokenResult); VoxeetSDK.conference.on('streamAdded', (participant, stream) => {
} if (participant !== VoxeetSDK.session.participant) {
}) const voiceNode = document.createElement('video');
.catch((err) => { voiceNode.srcObject = stream;
console.error('Get Voxeet Token API error', err); voiceNode.id = participant.id;
}); voiceNode.width = 0;
voiceNode.height = 0;
voiceNode.autoplay = true;
voiceNode.classList.add('voxeetAudioOut');
document.body.appendChild(voiceNode);
}
});
VoxeetSDK.conference.on('streamRemoved', (participant, stream) => {
document.getElementById(participant.id).remove();
});
if (
this.__accessToken !== undefined ||
this.__refreshToken !== undefined ||
this.__accessToken !== null ||
this.__refreshToken !== null
) {
await GetVoxeetTokenApi()
.then((res) => res.json())
.then((tokenResult) => {
if (tokenResult.status <= 299) {
const { access_token, refresh_token } = tokenResult;
this.updateTokens(access_token, refresh_token);
VoxeetSDK.initializeToken(access_token, refresh_token);
} else {
console.error('Get Voxeet Token API Result Error', tokenResult);
}
})
.catch((err) => {
console.error('Get Voxeet Token API error', err);
});
} else {
VoxeetSDK.initializeToken(this.__accessToken, this.__refreshToken);
}
} }
async refreshToken() { async refreshToken() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -79,5 +162,6 @@ export default class CallManager {
updateTokens(aT, rT) { updateTokens(aT, rT) {
this.__accessToken = aT; this.__accessToken = aT;
this.__refreshToken = rT; this.__refreshToken = rT;
updateLocalData({ vaT: aT, vrT: rT });
} }
} }
+51 -36
View File
@@ -12,6 +12,7 @@ import {
ExpansionPanelDetails, ExpansionPanelDetails,
Card, Card,
CardContent, CardContent,
Tooltip,
} from '@material-ui/core'; } from '@material-ui/core';
import Thread from './Thread'; import Thread from './Thread';
import Send from '@material-ui/icons/Send'; import Send from '@material-ui/icons/Send';
@@ -34,18 +35,13 @@ import {
PLAYER_SOURCE, PLAYER_SOURCE,
LocalData, LocalData,
PLAYER_STATUS, PLAYER_STATUS,
emptyLocalData,
} from './Api'; } from './Api';
import { goTo } from 'route-lite'; import { goTo } from 'route-lite';
import Create from './Create'; import Create from './Create';
import VideoListener from './VideoListener'; import VideoListener from './VideoListener';
import CallManager from './CallManager'; import CallManager from './CallManager';
import { import { MoreVert, PauseCircleFilled, LinkOutlined, PlayCircleFilled } from '@material-ui/icons';
MoreVert,
FileCopyTwoTone,
PauseCircleFilled,
LinkOutlined,
PlayCircleFilled,
} from '@material-ui/icons';
const giphyFetch = new GiphyFetch('ojJbIMUN6XRMn4mHiFFav3rMCpXUg8sq'); const giphyFetch = new GiphyFetch('ojJbIMUN6XRMn4mHiFFav3rMCpXUg8sq');
const alertMessage = { const alertMessage = {
@@ -78,7 +74,7 @@ export default class Chat extends Component {
this.getCarousel = this.getCarousel.bind(this); this.getCarousel = this.getCarousel.bind(this);
this.handlePhoneCall = this.handlePhoneCall.bind(this); this.handlePhoneCall = this.handlePhoneCall.bind(this);
this.callEventListener = this.callEventListener.bind(this); this.callEventListener = this.callEventListener.bind(this);
this.mCallManager = new CallManager(this.callEventListener); this.mCallManager = new CallManager(this.callEventListener, this.props.notify);
this.sendMessageInvoker = this.sendMessageInvoker.bind(this); this.sendMessageInvoker = this.sendMessageInvoker.bind(this);
this.videoEventListener = this.videoEventListener.bind(this); this.videoEventListener = this.videoEventListener.bind(this);
this.mVideoListener = new VideoListener(this.videoEventListener); this.mVideoListener = new VideoListener(this.videoEventListener);
@@ -129,7 +125,9 @@ export default class Chat extends Component {
videoEventListener(video) { videoEventListener(video) {
this.syncPlayer(video); this.syncPlayer(video);
} }
callEventListener(event) {} callEventListener(event) {
this.setState(event);
}
syncPlayer(video) { syncPlayer(video) {
const { status, showName, time, source, element, showChanged } = video; const { status, showName, time, source, element, showChanged } = video;
if (element) { if (element) {
@@ -158,12 +156,12 @@ export default class Chat extends Component {
this.props.notify('Ending party...'); this.props.notify('Ending party...');
this.props.updateLoading(true); this.props.updateLoading(true);
const data = { const data = {
roomId: LocalData().partyName, partyName: LocalData().partyName,
}; };
DeleteRoomApi(data) DeleteRoomApi(data)
.then((res) => { .then((res) => {
this.props.notify('Ended party'); this.props.notify('Ended party');
// window.localStorage.removeItem('watchPartyRoomId'); emptyLocalData();
goTo(Create, this.props); goTo(Create, this.props);
this.props.updateLoading(false); this.props.updateLoading(false);
}) })
@@ -177,13 +175,13 @@ export default class Chat extends Component {
this.props.notify('Leaving party...'); this.props.notify('Leaving party...');
this.props.updateLoading(true); this.props.updateLoading(true);
const data = { const data = {
roomId: LocalData().partyName, partyName: LocalData().partyName,
uid: LocalData().userUid, userUid: LocalData().userUid,
}; };
LeaveRoomApi(data) LeaveRoomApi(data)
.then((res) => { .then((res) => {
this.props.notify('Left party'); this.props.notify('Left party');
// window.localStorage.removeItem('watchPartyRoomId'); emptyLocalData();
goTo(Create, this.props); goTo(Create, this.props);
this.props.updateLoading(false); this.props.updateLoading(false);
}) })
@@ -206,9 +204,9 @@ export default class Chat extends Component {
} }
sendMessage(message) { sendMessage(message) {
const data = { const data = {
roomId: window.localStorage.getItem('watchPartyRoomId'), roomId: LocalData().partyName,
name: window.localStorage.getItem('watchPartyUserName'), name: LocalData().userName,
uid: window.localStorage.getItem('watchPartyUserUid'), uid: LocalData().userUid,
text: message, text: message,
time: new Date(), time: new Date(),
type: '', type: '',
@@ -236,12 +234,12 @@ export default class Chat extends Component {
? giphyFetch.trending({ offset, limit: 1 }) ? giphyFetch.trending({ offset, limit: 1 })
: giphyFetch.search(this.state.newMessage, { offset, limit: 1 }); : giphyFetch.search(this.state.newMessage, { offset, limit: 1 });
} }
async handlePhoneCall() { handlePhoneCall() {
this.setState({ onCall: !this.state.onCall }); const { onCall, conference, admin } = this.state;
this.mCallManager.toggleCall(onCall, conference, admin);
} }
handleMute() { handleMute() {
this.mCallManager.toggleMute(); this.mCallManager.toggleMute(this.state.muted);
this.setState({ muted: !this.state.muted });
} }
getPrettyTime(t) { getPrettyTime(t) {
return t > 3600 return t > 3600
@@ -258,10 +256,13 @@ export default class Chat extends Component {
<ExpansionPanel elevation={0} className="expander"> <ExpansionPanel elevation={0} className="expander">
<ExpansionPanelSummary <ExpansionPanelSummary
expandIcon={ expandIcon={
<MoreVert <Tooltip
alt="Share, leave, and other controls" placement="bottom-end"
title="Share, leave, and other controls"
aria-label="Share, leave, and other controls" aria-label="Share, leave, and other controls"
/> >
<MoreVert />
</Tooltip>
} }
> >
<div className="party-chat-header-text"> <div className="party-chat-header-text">
@@ -290,11 +291,13 @@ export default class Chat extends Component {
fullWidth fullWidth
InputProps={{ InputProps={{
endAdornment: ( endAdornment: (
<InputAdornment position="end"> <Tooltip title="Share party" aria-label="Share party">
<IconButton onClick={this.shareParty}> <InputAdornment position="end">
<Share /> <IconButton onClick={this.shareParty}>
</IconButton> <Share />
</InputAdornment> </IconButton>
</InputAdornment>
</Tooltip>
), ),
}} }}
/> />
@@ -359,17 +362,29 @@ export default class Chat extends Component {
<div className="text-phone-panel"> <div className="text-phone-panel">
{gif || newMessage ? null : ( {gif || newMessage ? null : (
<IconButton onClick={this.handlePhoneCall} color={!onCall ? 'secondary' : 'primary'}> <IconButton onClick={this.handlePhoneCall} color={!onCall ? 'secondary' : 'primary'}>
{onCall ? <PhoneDisable /> : <PhoneEnable />} {onCall ? (
<Tooltip placement="top" title="Leave Call">
<PhoneDisable />
</Tooltip>
) : (
<Tooltip placement="top" title="Join Call">
<PhoneEnable />
</Tooltip>
)}
</IconButton> </IconButton>
)} )}
{!gif && !newMessage && onCall && !muted ? ( {!gif && !newMessage && onCall && !muted ? (
<IconButton onClick={this.handleMute} color="primary"> <Tooltip title="Mute Mic" placement="top">
<MicOff /> <IconButton onClick={this.handleMute} color="primary">
</IconButton> <MicOff />
</IconButton>
</Tooltip>
) : !gif && !newMessage && onCall && muted ? ( ) : !gif && !newMessage && onCall && muted ? (
<IconButton onClick={this.handleMute} color="secondary"> <Tooltip title="Unmute Mic" placement="top">
<MicOn /> <IconButton onClick={this.handleMute} color="secondary">
</IconButton> <MicOn />
</IconButton>
</Tooltip>
) : null} ) : null}
<TextField <TextField
color="secondary" color="secondary"
+1 -1
View File
@@ -68,7 +68,7 @@ img.party-message {
} }
.thread-item .name { .thread-item .name {
color: rgb(149, 149, 149); color: rgb(149, 149, 149);
font-size: 10px !important; font-size: 12px !important;
} }
.time { .time {
top: 0; top: 0;
+5 -13
View File
@@ -1,29 +1,20 @@
import React from 'react'; import React from 'react';
import { Typography } from '@material-ui/core'; import { Typography } from '@material-ui/core';
import './Thread.css'; import './Thread.css';
import { LocalData } from './Api';
const Thread = (props) => const Thread = (props) =>
props.messages.map((m, midx) => { props.messages.map((m, midx) => {
const userUid = window.localStorage.getItem('watchPartyUserUid'); const { userUid } = LocalData();
if (m.uid === '<system>') { if (m.uid === '<system>') {
if (m.name === '<alert>') { // SYSTEM MESSAGES
return (
<Typography
variant="body2"
className="thread-item system-message alert"
key={m.id}
disabled
>
{m.text}
</Typography>
);
}
return ( return (
<Typography variant="body2" className="thread-item system-message" key={m.id} disabled> <Typography variant="body2" className="thread-item system-message" key={m.id} disabled>
{m.text} {m.text}
</Typography> </Typography>
); );
} else if (m.uid === userUid) { } else if (m.uid === userUid) {
// OWN MESSAGE
return ( return (
<div <div
className={`thread-item ${ className={`thread-item ${
@@ -42,6 +33,7 @@ const Thread = (props) =>
</div> </div>
); );
} else { } else {
// OTHER USER MESSAGE
return ( return (
<div <div
className={`thread-item ${ className={`thread-item ${
+7
View File
@@ -371,3 +371,10 @@ textarea::-webkit-scrollbar {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.MuiTooltip-popper {
z-index: 99999 !important;
}
.MuiTooltip-popper * {
font-size: 1rem !important;
}
+2 -1
View File
@@ -11,8 +11,9 @@ app.get('/refresher/:token', FunctionModules.tokenRefresher);
app.post('/create', FunctionModules.createRoom); app.post('/create', FunctionModules.createRoom);
app.post('/join', FunctionModules.joinRoom); app.post('/join', FunctionModules.joinRoom);
app.post('/message', FunctionModules.sendMessage); app.post('/message', FunctionModules.sendMessage);
app.post('/updatePlayer', FunctionModules.updatePlayer);
app.post('/updateVoxeet', FunctionModules.updateVoxeet);
app.delete('/leave/:rid/users/:uid', FunctionModules.leaveRoom); app.delete('/leave/:rid/users/:uid', FunctionModules.leaveRoom);
app.delete('/delete/:rid', FunctionModules.deleteRoom); app.delete('/delete/:rid', FunctionModules.deleteRoom);
app.post('/updatePlayer', FunctionModules.updatePlayer);
module.exports = app; module.exports = app;
+2
View File
@@ -10,6 +10,7 @@ const timeDiff = require('./timeDiff');
const updatePlayer = require('./updatePlayer'); const updatePlayer = require('./updatePlayer');
const tokenValidator = require('./tokenValidator'); const tokenValidator = require('./tokenValidator');
const tokenRefresher = require('./tokenRefresher'); const tokenRefresher = require('./tokenRefresher');
const updateVoxeet = require('./updateVoxeet');
module.exports = { module.exports = {
authenticator, authenticator,
@@ -24,4 +25,5 @@ module.exports = {
timeDiff, timeDiff,
tokenValidator, tokenValidator,
tokenRefresher, tokenRefresher,
updateVoxeet,
}; };
+2 -1
View File
@@ -1,7 +1,8 @@
const VoxeetSecret = require('../VoxeetSecret.json'); const VoxeetSecret = require('../VoxeetSecret.json');
const fetch = require('node-fetch');
module.exports = async (req, res) => { module.exports = async (req, res) => {
const { key, secret } = VoxeetSecret; const { key, secret } = VoxeetSecret;
const credentials = btoa(encodeURI(key) + ':' + encodeURI(secret)); const credentials = Buffer.from(encodeURI(key) + ':' + encodeURI(secret)).toString('base64');
const headers = { const headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Cache-Control': 'no-cache', 'Cache-Control': 'no-cache',
+47
View File
@@ -0,0 +1,47 @@
const admin = require('firebase-admin');
module.exports = async (req, res) => {
const { conference, partyName, partyPin, userName } = JSON.parse(req.body);
// GET ROOM PIN
const roomPin = await admin
.firestore()
.collection('rooms')
.doc(partyName)
.get()
.then((roomSnap) => (roomSnap.exists ? roomSnap.data().pin : null));
// CHECK IF ROOM EXISTS
if (roomPin) {
// CHECK FOR CORRECT PIN
if (roomPin === partyPin) {
// UPDATE CONFERENCE DETAILS
await admin
.firestore()
.collection('rooms')
.doc(partyName)
.update({ conference })
.catch((err) =>
res.status(500).send({ message: 'Unable to update the call details', error: err })
);
// ADD SYSTEM MESSAGE
await admin
.firestore()
.collection('rooms')
.doc(partyName)
.collection('messages')
.add({
uid: '<system>',
name: '',
text: `${userName} started the call.`,
time: new Date(),
})
.catch((err) => console.error('Failed to add update conference message', err));
res.status(200).send({ message: 'Started the conference' });
} else {
// WRONG ROOM PIN
return res.status(401).send({ message: 'Wrong party pin, try rejoining' });
}
} else {
// ROOM DOESN'T EXIST
return res.status(404).send({ message: 'Party Not Found' });
}
};
+1 -2
View File
@@ -2085,8 +2085,7 @@
"node-fetch": { "node-fetch": {
"version": "2.6.0", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
"optional": true
}, },
"node-forge": { "node-forge": {
"version": "0.7.4", "version": "0.7.4",
+2 -1
View File
@@ -17,7 +17,8 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.17.1", "express": "^4.17.1",
"firebase-admin": "^8.6.0", "firebase-admin": "^8.6.0",
"firebase-functions": "^3.3.0" "firebase-functions": "^3.3.0",
"node-fetch": "^2.6.0"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^5.12.0", "eslint": "^5.12.0",