Skip to content

Commit 8fe45ae

Browse files
authored
[NEW] P2P WebRTC Connection Establishment (#22847)
* [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
1 parent 202bf73 commit 8fe45ae

File tree

10 files changed

+301
-33
lines changed

10 files changed

+301
-33
lines changed

app/models/server/raw/Subscriptions.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,18 @@ export class SubscriptionsRaw extends BaseRaw<T> {
3636
return this.find(query, options);
3737
}
3838

39-
countByRoomIdAndUserId(rid: string, uid: string): Promise<number> {
39+
findByLivechatRoomIdAndNotUserId(roomId: string, userId: string, options: FindOneOptions<T> = {}): Cursor<T> {
40+
const query = {
41+
rid: roomId,
42+
'servedBy._id': {
43+
$ne: userId,
44+
},
45+
};
46+
47+
return this.find(query, options);
48+
}
49+
50+
countByRoomIdAndUserId(rid: string, uid: string | undefined): Promise<number> {
4051
const query = {
4152
rid,
4253
'u._id': uid,

app/notifications/client/lib/Notifications.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ class Notifications {
7575
return this.streamRoom.on(`${ room }/${ eventName }`, callback);
7676
}
7777

78-
async onUser(eventName, callback) {
79-
await this.streamUser.on(`${ Meteor.userId() }/${ eventName }`, callback);
80-
return () => this.unUser(eventName, callback);
78+
async onUser(eventName, callback, visitorId = null) {
79+
await this.streamUser.on(`${ Meteor.userId() || visitorId }/${ eventName }`, callback);
80+
return () => this.unUser(eventName, callback, visitorId);
8181
}
8282

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

95-
unUser(eventName, callback) {
96-
return this.streamUser.removeListener(`${ Meteor.userId() }/${ eventName }`, callback);
95+
unUser(eventName, callback, visitorId = null) {
96+
return this.streamUser.removeListener(`${ Meteor.userId() || visitorId }/${ eventName }`, callback);
9797
}
9898
}
9999

app/webrtc/client/WebRTCClass.js

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ class WebRTCClass {
115115
@param room {String}
116116
*/
117117

118-
constructor(selfId, room) {
118+
constructor(selfId, room, autoAccept = false) {
119119
this.config = {
120120
iceServers: [],
121121
};
@@ -153,7 +153,7 @@ class WebRTCClass {
153153
this.active = false;
154154
this.remoteMonitoring = false;
155155
this.monitor = false;
156-
this.autoAccept = false;
156+
this.autoAccept = autoAccept;
157157
this.navigator = undefined;
158158
const userAgent = navigator.userAgent.toLocaleLowerCase();
159159

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

664665
onRemoteCall(data) {
665666
if (this.autoAccept === true) {
666-
goToRoomById(data.room);
667667
Meteor.defer(() => {
668668
this.joinCall({
669669
to: data.from,
@@ -873,6 +873,7 @@ class WebRTCClass {
873873
if (peerConnection.iceConnectionState !== 'closed' && peerConnection.iceConnectionState !== 'failed' && peerConnection.iceConnectionState !== 'disconnected' && peerConnection.iceConnectionState !== 'completed') {
874874
peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
875875
}
876+
document.querySelector('video#remoteVideo').srcObject = this.remoteItems.get()[0]?.url;
876877
}
877878

878879

@@ -916,28 +917,41 @@ const WebRTC = new class {
916917
this.instancesByRoomId = {};
917918
}
918919

919-
getInstanceByRoomId(rid) {
920-
const subscription = ChatSubscription.findOne({ rid });
921-
if (!subscription) {
922-
return;
923-
}
920+
getInstanceByRoomId(rid, visitorId = null) {
924921
let enabled = false;
925-
switch (subscription.t) {
926-
case 'd':
927-
enabled = settings.get('WebRTC_Enable_Direct');
928-
break;
929-
case 'p':
930-
enabled = settings.get('WebRTC_Enable_Private');
931-
break;
932-
case 'c':
933-
enabled = settings.get('WebRTC_Enable_Channel');
922+
if (!visitorId) {
923+
const subscription = ChatSubscription.findOne({ rid });
924+
if (!subscription) {
925+
return;
926+
}
927+
switch (subscription.t) {
928+
case 'd':
929+
enabled = settings.get('WebRTC_Enable_Direct');
930+
break;
931+
case 'p':
932+
enabled = settings.get('WebRTC_Enable_Private');
933+
break;
934+
case 'c':
935+
enabled = settings.get('WebRTC_Enable_Channel');
936+
break;
937+
case 'l':
938+
enabled = settings.get('Omnichannel_call_provider') === 'WebRTC';
939+
}
940+
} else {
941+
enabled = settings.get('Omnichannel_call_provider') === 'WebRTC';
934942
}
935943
enabled &&= settings.get('WebRTC_Enabled');
936944
if (enabled === false) {
937945
return;
938946
}
939947
if (this.instancesByRoomId[rid] == null) {
940-
this.instancesByRoomId[rid] = new WebRTCClass(Meteor.userId(), rid);
948+
let uid = Meteor.userId();
949+
let autoAccept = false;
950+
if (visitorId) {
951+
uid = visitorId;
952+
autoAccept = true;
953+
}
954+
this.instancesByRoomId[rid] = new WebRTCClass(uid, rid, autoAccept);
941955
}
942956
return this.instancesByRoomId[rid];
943957
}

app/webrtc/client/actionLink.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { actionLinks } from '../../action-links/client';
55
import { APIClient } from '../../utils/client';
66
import { Rooms } from '../../models/client';
77
import { IMessage } from '../../../definition/IMessage';
8+
import { Notifications } from '../../notifications/client';
89

910
actionLinks.register('joinLivechatWebRTCCall', (message: IMessage) => {
1011
const { callStatus, _id } = Rooms.findOne({ _id: message.rid });
@@ -22,4 +23,5 @@ actionLinks.register('endLivechatWebRTCCall', async (message: IMessage) => {
2223
return;
2324
}
2425
await APIClient.v1.put(`livechat/webrtc.call/${ message._id }`, {}, { rid: _id, status: 'ended' });
26+
Notifications.notifyRoom(_id, 'webrtc', 'callStatus', { callStatus: 'ended' });
2527
});

client/startup/routes.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
import { FlowRouter } from 'meteor/kadira:flow-router';
22
import { Meteor } from 'meteor/meteor';
3+
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
34
import { Session } from 'meteor/session';
45
import { Tracker } from 'meteor/tracker';
56
import { lazy } from 'react';
67
import toastr from 'toastr';
78

89
import { KonchatNotification } from '../../app/ui/client';
9-
import { handleError } from '../../app/utils/client';
10+
import { handleError, APIClient } from '../../app/utils/client';
1011
import { IUser } from '../../definition/IUser';
1112
import { appLayout } from '../lib/appLayout';
1213
import { createTemplateForComponent } from '../lib/portals/createTemplateForComponent';
1314

1415
const SetupWizardRoute = lazy(() => import('../views/setupWizard/SetupWizardRoute'));
1516
const MailerUnsubscriptionPage = lazy(() => import('../views/mailer/MailerUnsubscriptionPage'));
1617
const NotFoundPage = lazy(() => import('../views/notFound/NotFoundPage'));
18+
const MeetPage = lazy(() => import('../views/meet/MeetPage'));
1719

1820
FlowRouter.wait();
1921

@@ -50,6 +52,25 @@ FlowRouter.route('/login', {
5052
},
5153
});
5254

55+
FlowRouter.route('/meet/:rid', {
56+
name: 'meet',
57+
58+
async action(_params, queryParams) {
59+
if (queryParams?.token !== undefined) {
60+
// visitor login
61+
const visitor = await APIClient.v1.get(`/livechat/visitor/${queryParams?.token}`);
62+
if (visitor?.visitor) {
63+
return appLayout.render({ component: MeetPage });
64+
}
65+
return toastr.error(TAPi18n.__('Visitor_does_not_exist'));
66+
}
67+
if (!Meteor.userId()) {
68+
FlowRouter.go('home');
69+
}
70+
appLayout.render({ component: MeetPage });
71+
},
72+
});
73+
5374
FlowRouter.route('/home', {
5475
name: 'home',
5576

client/views/meet/CallPage.js

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { Box, Flex } from '@rocket.chat/fuselage';
2+
import React, { useEffect, useState } from 'react';
3+
4+
import { Notifications } from '../../../app/notifications/client';
5+
import { WebRTC } from '../../../app/webrtc/client';
6+
import { WEB_RTC_EVENTS } from '../../../app/webrtc/index';
7+
import { useTranslation } from '../../contexts/TranslationContext';
8+
9+
function CallPage({ roomId, visitorToken, visitorId, status, setStatus }) {
10+
const [isAgentActive, setIsAgentActive] = useState(false);
11+
const t = useTranslation();
12+
useEffect(() => {
13+
if (visitorToken) {
14+
const webrtcInstance = WebRTC.getInstanceByRoomId(roomId, visitorId);
15+
Notifications.onUser(
16+
WEB_RTC_EVENTS.WEB_RTC,
17+
(type, data) => {
18+
if (data.room == null) {
19+
return;
20+
}
21+
webrtcInstance.onUserStream(type, data);
22+
},
23+
visitorId,
24+
);
25+
Notifications.onRoom(roomId, 'webrtc', (type, data) => {
26+
if (type === 'callStatus' && data.callStatus === 'ended') {
27+
webrtcInstance.stop();
28+
setStatus(data.callStatus);
29+
}
30+
});
31+
Notifications.notifyRoom(roomId, 'webrtc', 'callStatus', { callStatus: 'inProgress' });
32+
} else if (!isAgentActive) {
33+
const webrtcInstance = WebRTC.getInstanceByRoomId(roomId);
34+
if (status === 'inProgress') {
35+
webrtcInstance.startCall({
36+
audio: true,
37+
video: {
38+
width: { ideal: 1920 },
39+
height: { ideal: 1080 },
40+
},
41+
});
42+
}
43+
Notifications.onRoom(roomId, 'webrtc', (type, data) => {
44+
if (type === 'callStatus') {
45+
switch (data.callStatus) {
46+
case 'ended':
47+
webrtcInstance.stop();
48+
break;
49+
case 'inProgress':
50+
webrtcInstance.startCall({
51+
audio: true,
52+
video: {
53+
width: { ideal: 1920 },
54+
height: { ideal: 1080 },
55+
},
56+
});
57+
}
58+
setStatus(data.callStatus);
59+
}
60+
});
61+
setIsAgentActive(true);
62+
}
63+
}, [isAgentActive, status, setStatus, visitorId, roomId, visitorToken]);
64+
65+
switch (status) {
66+
case 'ringing':
67+
// Todo Deepak
68+
return (
69+
<h1 style={{ color: 'white', textAlign: 'center', marginTop: 250 }}>
70+
Waiting for the visitor to join ...
71+
</h1>
72+
);
73+
case 'declined':
74+
return (
75+
<Box
76+
minHeight='90%'
77+
display='flex'
78+
justifyContent='center'
79+
alignItems='center'
80+
color='white'
81+
fontSize='s1'
82+
>
83+
{t('Call_declined')}
84+
</Box>
85+
);
86+
case 'inProgress':
87+
return (
88+
<Flex.Container direction='column' justifyContent='center'>
89+
<Box
90+
width='full'
91+
minHeight='sh'
92+
textAlign='center'
93+
backgroundColor='neutral-900'
94+
overflow='hidden'
95+
position='relative'
96+
>
97+
<Box
98+
position='absolute'
99+
zIndex='1'
100+
style={{
101+
top: '5%',
102+
right: '2%',
103+
}}
104+
w='x200'
105+
>
106+
<video
107+
id='localVideo'
108+
autoPlay
109+
playsInline
110+
muted
111+
style={{
112+
width: '100%',
113+
transform: 'scaleX(-1)',
114+
}}
115+
></video>
116+
</Box>
117+
<video
118+
id='remoteVideo'
119+
autoPlay
120+
playsInline
121+
muted
122+
style={{
123+
width: '100%',
124+
transform: 'scaleX(-1)',
125+
}}
126+
></video>
127+
</Box>
128+
</Flex.Container>
129+
);
130+
}
131+
}
132+
133+
export default CallPage;

0 commit comments

Comments
 (0)