Skip to content

Commit

Permalink
[NEW] P2P WebRTC Connection Establishment (#22847)
Browse files Browse the repository at this point in the history
* [NEW] WebRTC P2P Connection with Basic Call UI

* [FIX] Set Stream on a stable connection

* [FIX] userId typecheck error

* [REFACTOR]
 - Restore type of userId to string by removing `| undefined`
 - Add translation for visitor does not exist toastr
 - Set visitorId from room object fetched instead of fetching from livechat widget as query param
 - Type Checking

* [FIX] Running startCall 2 times for agent

* [FIX] Call declined Page
  • Loading branch information
dhruvjain99 authored Aug 9, 2021
1 parent 202bf73 commit 8fe45ae
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 33 deletions.
13 changes: 12 additions & 1 deletion app/models/server/raw/Subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,18 @@ export class SubscriptionsRaw extends BaseRaw<T> {
return this.find(query, options);
}

countByRoomIdAndUserId(rid: string, uid: string): Promise<number> {
findByLivechatRoomIdAndNotUserId(roomId: string, userId: string, options: FindOneOptions<T> = {}): Cursor<T> {
const query = {
rid: roomId,
'servedBy._id': {
$ne: userId,
},
};

return this.find(query, options);
}

countByRoomIdAndUserId(rid: string, uid: string | undefined): Promise<number> {
const query = {
rid,
'u._id': uid,
Expand Down
10 changes: 5 additions & 5 deletions app/notifications/client/lib/Notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ class Notifications {
return this.streamRoom.on(`${ room }/${ eventName }`, callback);
}

async onUser(eventName, callback) {
await this.streamUser.on(`${ Meteor.userId() }/${ eventName }`, callback);
return () => this.unUser(eventName, callback);
async onUser(eventName, callback, visitorId = null) {
await this.streamUser.on(`${ Meteor.userId() || visitorId }/${ eventName }`, callback);
return () => this.unUser(eventName, callback, visitorId);
}

unAll(callback) {
Expand All @@ -92,8 +92,8 @@ class Notifications {
return this.streamRoom.removeListener(`${ room }/${ eventName }`, callback);
}

unUser(eventName, callback) {
return this.streamUser.removeListener(`${ Meteor.userId() }/${ eventName }`, callback);
unUser(eventName, callback, visitorId = null) {
return this.streamUser.removeListener(`${ Meteor.userId() || visitorId }/${ eventName }`, callback);
}
}

Expand Down
50 changes: 32 additions & 18 deletions app/webrtc/client/WebRTCClass.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class WebRTCClass {
@param room {String}
*/

constructor(selfId, room) {
constructor(selfId, room, autoAccept = false) {
this.config = {
iceServers: [],
};
Expand Down Expand Up @@ -153,7 +153,7 @@ class WebRTCClass {
this.active = false;
this.remoteMonitoring = false;
this.monitor = false;
this.autoAccept = false;
this.autoAccept = autoAccept;
this.navigator = undefined;
const userAgent = navigator.userAgent.toLocaleLowerCase();

Expand Down Expand Up @@ -503,6 +503,7 @@ class WebRTCClass {
this.audioEnabled.set(this.media.audio === true);
const { peerConnections } = this;
Object.entries(peerConnections).forEach(([, peerConnection]) => peerConnection.addStream(stream));
document.querySelector('video#localVideo').srcObject = stream;
callback(null, this.localStream);
};
const onError = (error) => {
Expand Down Expand Up @@ -663,7 +664,6 @@ class WebRTCClass {

onRemoteCall(data) {
if (this.autoAccept === true) {
goToRoomById(data.room);
Meteor.defer(() => {
this.joinCall({
to: data.from,
Expand Down Expand Up @@ -873,6 +873,7 @@ class WebRTCClass {
if (peerConnection.iceConnectionState !== 'closed' && peerConnection.iceConnectionState !== 'failed' && peerConnection.iceConnectionState !== 'disconnected' && peerConnection.iceConnectionState !== 'completed') {
peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
}
document.querySelector('video#remoteVideo').srcObject = this.remoteItems.get()[0]?.url;
}


Expand Down Expand Up @@ -916,28 +917,41 @@ const WebRTC = new class {
this.instancesByRoomId = {};
}

getInstanceByRoomId(rid) {
const subscription = ChatSubscription.findOne({ rid });
if (!subscription) {
return;
}
getInstanceByRoomId(rid, visitorId = null) {
let enabled = false;
switch (subscription.t) {
case 'd':
enabled = settings.get('WebRTC_Enable_Direct');
break;
case 'p':
enabled = settings.get('WebRTC_Enable_Private');
break;
case 'c':
enabled = settings.get('WebRTC_Enable_Channel');
if (!visitorId) {
const subscription = ChatSubscription.findOne({ rid });
if (!subscription) {
return;
}
switch (subscription.t) {
case 'd':
enabled = settings.get('WebRTC_Enable_Direct');
break;
case 'p':
enabled = settings.get('WebRTC_Enable_Private');
break;
case 'c':
enabled = settings.get('WebRTC_Enable_Channel');
break;
case 'l':
enabled = settings.get('Omnichannel_call_provider') === 'WebRTC';
}
} else {
enabled = settings.get('Omnichannel_call_provider') === 'WebRTC';
}
enabled &&= settings.get('WebRTC_Enabled');
if (enabled === false) {
return;
}
if (this.instancesByRoomId[rid] == null) {
this.instancesByRoomId[rid] = new WebRTCClass(Meteor.userId(), rid);
let uid = Meteor.userId();
let autoAccept = false;
if (visitorId) {
uid = visitorId;
autoAccept = true;
}
this.instancesByRoomId[rid] = new WebRTCClass(uid, rid, autoAccept);
}
return this.instancesByRoomId[rid];
}
Expand Down
2 changes: 2 additions & 0 deletions app/webrtc/client/actionLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { actionLinks } from '../../action-links/client';
import { APIClient } from '../../utils/client';
import { Rooms } from '../../models/client';
import { IMessage } from '../../../definition/IMessage';
import { Notifications } from '../../notifications/client';

actionLinks.register('joinLivechatWebRTCCall', (message: IMessage) => {
const { callStatus, _id } = Rooms.findOne({ _id: message.rid });
Expand All @@ -22,4 +23,5 @@ actionLinks.register('endLivechatWebRTCCall', async (message: IMessage) => {
return;
}
await APIClient.v1.put(`livechat/webrtc.call/${ message._id }`, {}, { rid: _id, status: 'ended' });
Notifications.notifyRoom(_id, 'webrtc', 'callStatus', { callStatus: 'ended' });
});
23 changes: 22 additions & 1 deletion client/startup/routes.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Meteor } from 'meteor/meteor';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';
import { lazy } from 'react';
import toastr from 'toastr';

import { KonchatNotification } from '../../app/ui/client';
import { handleError } from '../../app/utils/client';
import { handleError, APIClient } from '../../app/utils/client';
import { IUser } from '../../definition/IUser';
import { appLayout } from '../lib/appLayout';
import { createTemplateForComponent } from '../lib/portals/createTemplateForComponent';

const SetupWizardRoute = lazy(() => import('../views/setupWizard/SetupWizardRoute'));
const MailerUnsubscriptionPage = lazy(() => import('../views/mailer/MailerUnsubscriptionPage'));
const NotFoundPage = lazy(() => import('../views/notFound/NotFoundPage'));
const MeetPage = lazy(() => import('../views/meet/MeetPage'));

FlowRouter.wait();

Expand Down Expand Up @@ -50,6 +52,25 @@ FlowRouter.route('/login', {
},
});

FlowRouter.route('/meet/:rid', {
name: 'meet',

async action(_params, queryParams) {
if (queryParams?.token !== undefined) {
// visitor login
const visitor = await APIClient.v1.get(`/livechat/visitor/${queryParams?.token}`);
if (visitor?.visitor) {
return appLayout.render({ component: MeetPage });
}
return toastr.error(TAPi18n.__('Visitor_does_not_exist'));
}
if (!Meteor.userId()) {
FlowRouter.go('home');
}
appLayout.render({ component: MeetPage });
},
});

FlowRouter.route('/home', {
name: 'home',

Expand Down
133 changes: 133 additions & 0 deletions client/views/meet/CallPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { Box, Flex } from '@rocket.chat/fuselage';
import React, { useEffect, useState } from 'react';

import { Notifications } from '../../../app/notifications/client';
import { WebRTC } from '../../../app/webrtc/client';
import { WEB_RTC_EVENTS } from '../../../app/webrtc/index';
import { useTranslation } from '../../contexts/TranslationContext';

function CallPage({ roomId, visitorToken, visitorId, status, setStatus }) {
const [isAgentActive, setIsAgentActive] = useState(false);
const t = useTranslation();
useEffect(() => {
if (visitorToken) {
const webrtcInstance = WebRTC.getInstanceByRoomId(roomId, visitorId);
Notifications.onUser(
WEB_RTC_EVENTS.WEB_RTC,
(type, data) => {
if (data.room == null) {
return;
}
webrtcInstance.onUserStream(type, data);
},
visitorId,
);
Notifications.onRoom(roomId, 'webrtc', (type, data) => {
if (type === 'callStatus' && data.callStatus === 'ended') {
webrtcInstance.stop();
setStatus(data.callStatus);
}
});
Notifications.notifyRoom(roomId, 'webrtc', 'callStatus', { callStatus: 'inProgress' });
} else if (!isAgentActive) {
const webrtcInstance = WebRTC.getInstanceByRoomId(roomId);
if (status === 'inProgress') {
webrtcInstance.startCall({
audio: true,
video: {
width: { ideal: 1920 },
height: { ideal: 1080 },
},
});
}
Notifications.onRoom(roomId, 'webrtc', (type, data) => {
if (type === 'callStatus') {
switch (data.callStatus) {
case 'ended':
webrtcInstance.stop();
break;
case 'inProgress':
webrtcInstance.startCall({
audio: true,
video: {
width: { ideal: 1920 },
height: { ideal: 1080 },
},
});
}
setStatus(data.callStatus);
}
});
setIsAgentActive(true);
}
}, [isAgentActive, status, setStatus, visitorId, roomId, visitorToken]);

switch (status) {
case 'ringing':
// Todo Deepak
return (
<h1 style={{ color: 'white', textAlign: 'center', marginTop: 250 }}>
Waiting for the visitor to join ...
</h1>
);
case 'declined':
return (
<Box
minHeight='90%'
display='flex'
justifyContent='center'
alignItems='center'
color='white'
fontSize='s1'
>
{t('Call_declined')}
</Box>
);
case 'inProgress':
return (
<Flex.Container direction='column' justifyContent='center'>
<Box
width='full'
minHeight='sh'
textAlign='center'
backgroundColor='neutral-900'
overflow='hidden'
position='relative'
>
<Box
position='absolute'
zIndex='1'
style={{
top: '5%',
right: '2%',
}}
w='x200'
>
<video
id='localVideo'
autoPlay
playsInline
muted
style={{
width: '100%',
transform: 'scaleX(-1)',
}}
></video>
</Box>
<video
id='remoteVideo'
autoPlay
playsInline
muted
style={{
width: '100%',
transform: 'scaleX(-1)',
}}
></video>
</Box>
</Flex.Container>
);
}
}

export default CallPage;
Loading

0 comments on commit 8fe45ae

Please sign in to comment.