Skip to content

Commit 957ee2f

Browse files
committed
Description : This code manages the registering of SIP endpoint on a button click on the side bar. To achieve this, following things needs to be considered. 1. Voip User Object will be used by multiple UI components. e.g Register and Incoming call component need to use this component. 2. Once initialised, it will remain in the context till the Agent UI is active. 3. Voip user object uses callback mechanism to notify about different events such as incoming call, call establishment and call termination. Design alternatives and decisions: 1. Placement of Voip User Object : It was earlier thought that it might make sense to add this object in its own Context and be managed by its own context provider. Voip object makes REST API calls. The requirement is also that, it must get initialised before sidebar/sections/Omnichannel.tsx comes to life. Hence (Considering my current knowledge of code), it must have got created before Omnichannel provider. Which means that OmnichannelProvider must be a child for Voip, which is not true as Omnichannel provider is not dependent on the creation of VoIP. So for timebeing, the Voip user object has been placed in |OmnichannelContext| and initialised in |OmnichannelProvider|. 2. Callbacks vs waiting on Promise: Most of the code in the repository is written without use of any callbacks. So there was a thought on if we could write this code without using callback. But considering the nature of voip calls and the way of using sip.js, it was more natural to write it using emitters and callbacks. There are multiple events happen when the call is received or dialed out. e.g Call getting established (This is must to handle becuase the call may fail because of some codec mismatch), call getting terminated. etc. Waiting for each of such promise and managing the state on UI client would have been a tricky job. Current Design : 1. A simple wrapper |SimpleVoipUser| is written on top of more feature rich class |VoIPUser|. This class should be able to provide what we need in our omnichannel voip. 2. This |SimpleVoipUser| class is a part of |OmnichannelContext| and gets initialised in |OmnichannelProvider|. |OmnichannelContext| also contains |extensionConfig|, which is the necessary information needed for registering the extension. 3. In |OmnichannelProvider|, function initVoipLib is used for fetching necessary values using the REST API. |extensionConfig| and |SimpleVoipUser| objects get initialised there and they are ready to be used when |OmnichannelProvider| is loaded. 4. Media elements for rendering local and remote streams have been pulled out from |VoIPUserConfiguration|. They are converted in to a type |IMediaStreamRenderer|. The reason is that the configuration is passed when the component gets initialised. But because the calling component is going to be different, media elements will not be available during the creation of |SimpleVoipUser| (Which in turns needs these media elements for creating |VoIPUser|). So instead of passing it as a part of |VoIPUserConfiguration|, it can be passed as an argument to the constructor of |VoIPUser| if that information is available during the creation time, or can be passed in acceptCall function. Newly passed |IMediaStreamRenderer| replaces the old value if the old one is passed in the constructor. 5. |VoIPLayout| uses this new way of creating the Voip user objects and demonstrates how it will be used.
1 parent 6880fb3 commit 957ee2f

File tree

9 files changed

+683
-464
lines changed

9 files changed

+683
-464
lines changed

client/components/voip/ICallEventDelegate.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,20 @@
44
* This interface is implemented by a class which is
55
* interested in handling call events.
66
*/
7+
8+
export interface ICallerInfo {
9+
callerId: string;
10+
callerName: string;
11+
host: string;
12+
}
13+
714
export interface ICallEventDelegate {
815
/**
916
* Called when a call is received
1017
* @remarks
1118
* Callback for handling incoming call
1219
*/
13-
onIncomingCall?(callingPartyName: string): void;
20+
onIncomingCall?(calledId: ICallerInfo): void;
1421
/**
1522
* Called when call is established
1623
* @remarks
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Delegate interface for SIP extension information.
3+
* @remarks
4+
* This interface is implemented by a class which is
5+
* interested SIP registration events.
6+
*/
7+
export interface IExtensionConfig {
8+
/**
9+
* extension.
10+
*/
11+
extension: string;
12+
/**
13+
* password.
14+
*/
15+
password: string;
16+
/**
17+
* SIP Registrar address.
18+
* @defaultValue `""`
19+
*/
20+
sipRegistrar: string;
21+
/**
22+
* SIP WebSocket Path
23+
* @defaultValue `""`
24+
*/
25+
websocketUri: string;
26+
}
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import { Emitter } from '@rocket.chat/emitter';
2+
3+
import { ClientLogger } from '../../../lib/ClientLogger';
4+
import { CallState } from './Callstate';
5+
import { ICallerInfo, ICallEventDelegate } from './ICallEventDelegate';
6+
import { IConnectionDelegate } from './IConnectionDelegate';
7+
import { IRegisterHandlerDelegate } from './IRegisterHandlerDelegate';
8+
import { VoIPUser } from './VoIPUser';
9+
import { IMediaStreamRenderer, VoIPUserConfiguration } from './VoIPUserConfiguration';
10+
11+
interface IState {
12+
isReady: boolean;
13+
enableVideo: boolean;
14+
}
15+
16+
export enum CallType {
17+
AUDIO,
18+
AUDIO_VIDEO,
19+
}
20+
21+
export enum VoipEvents {
22+
'initialised',
23+
'connected',
24+
'connectionerror',
25+
'registered',
26+
'registrationerror',
27+
'unregistered',
28+
'unregistrationerror',
29+
'incomingcall',
30+
'callestablished',
31+
'callterminated',
32+
'negotiationfailed',
33+
}
34+
export class SimpleVoipUser
35+
implements IRegisterHandlerDelegate, IConnectionDelegate, ICallEventDelegate
36+
{
37+
state: IState;
38+
39+
userHandler: VoIPUser | undefined;
40+
41+
userName: string;
42+
43+
password: string;
44+
45+
registrar: string;
46+
47+
webSocketPath: string;
48+
49+
callType: CallType | undefined;
50+
51+
mediaStreamRendered?: IMediaStreamRenderer;
52+
53+
config: VoIPUserConfiguration = {};
54+
55+
voipEventEmitter: Emitter;
56+
57+
logger: ClientLogger;
58+
59+
constructor(
60+
userName: string,
61+
password: string,
62+
registrar: string,
63+
webSocketPath: string,
64+
callType?: CallType,
65+
mediaStreamRendered?: IMediaStreamRenderer,
66+
) {
67+
this.state = {
68+
isReady: false,
69+
enableVideo: true,
70+
};
71+
this.userName = userName;
72+
this.password = password;
73+
this.registrar = registrar;
74+
this.webSocketPath = webSocketPath;
75+
this.callType = callType;
76+
this.mediaStreamRendered = mediaStreamRendered;
77+
this.voipEventEmitter = new Emitter();
78+
this.logger = new ClientLogger('SimpleVoipUser');
79+
}
80+
81+
/* RegisterHandlerDeligate implementation begin */
82+
onRegistered(): void {
83+
this.logger.info('onRegistered');
84+
if (this.voipEventEmitter.has(VoipEvents[VoipEvents.registered])) {
85+
this.voipEventEmitter.emit(VoipEvents[VoipEvents.registered]);
86+
}
87+
}
88+
89+
onRegistrationError(reason: any): void {
90+
this.logger.error(`onRegistrationError${reason}`);
91+
if (this.voipEventEmitter.has(VoipEvents[VoipEvents.registrationerror])) {
92+
this.voipEventEmitter.emit(VoipEvents[VoipEvents.registrationerror], reason);
93+
}
94+
}
95+
96+
onUnregistered(): void {
97+
this.logger.debug('onUnregistered');
98+
if (this.voipEventEmitter.has(VoipEvents[VoipEvents.unregistered])) {
99+
this.voipEventEmitter.emit(VoipEvents[VoipEvents.unregistered]);
100+
}
101+
}
102+
103+
onUnregistrationError(error: any): void {
104+
this.logger.error('onUnregistrationError');
105+
if (this.voipEventEmitter.has(VoipEvents[VoipEvents.unregistrationerror])) {
106+
this.voipEventEmitter.emit(VoipEvents[VoipEvents.unregistrationerror], error);
107+
}
108+
}
109+
/* RegisterHandlerDeligate implementation end */
110+
111+
/* ConnectionDelegate implementation begin */
112+
onConnected(): void {
113+
this.logger.debug('onConnected');
114+
if (this.voipEventEmitter.has(VoipEvents[VoipEvents.connected])) {
115+
this.voipEventEmitter.emit(VoipEvents[VoipEvents.connected]);
116+
}
117+
this.state.isReady = true;
118+
}
119+
120+
onConnectionError(error: any): void {
121+
this.logger.error(`onConnectionError${error}`);
122+
if (this.voipEventEmitter.has(VoipEvents[VoipEvents.connectionerror])) {
123+
this.voipEventEmitter.emit(VoipEvents[VoipEvents.connectionerror], error);
124+
}
125+
this.state.isReady = false;
126+
}
127+
128+
/* ConnectionDelegate implementation end */
129+
/* CallEventDelegate implementation begin */
130+
onIncomingCall(callerId: ICallerInfo): void {
131+
this.logger.debug('onIncomingCall');
132+
if (this.voipEventEmitter.has(VoipEvents[VoipEvents.incomingcall])) {
133+
this.voipEventEmitter.emit(VoipEvents[VoipEvents.incomingcall], callerId);
134+
}
135+
}
136+
137+
onCallEstablished(): void {
138+
this.logger.debug('onCallEstablished');
139+
if (this.voipEventEmitter.has(VoipEvents[VoipEvents.callestablished])) {
140+
this.voipEventEmitter.emit(VoipEvents[VoipEvents.callestablished]);
141+
}
142+
}
143+
144+
onCallTermination(): void {
145+
this.logger.debug('onCallTermination');
146+
if (this.voipEventEmitter.has(VoipEvents[VoipEvents.callterminated])) {
147+
this.voipEventEmitter.emit(VoipEvents[VoipEvents.callterminated]);
148+
}
149+
}
150+
151+
/* CallEventDelegate implementation end */
152+
isReady(): boolean {
153+
return this.state.isReady;
154+
}
155+
156+
async initUserAgent(): Promise<void> {
157+
this.config.authUserName = this.userName;
158+
this.config.authPassword = this.password;
159+
this.config.sipRegistrarHostnameOrIP = this.registrar;
160+
this.config.webSocketURI = this.webSocketPath;
161+
162+
this.config.enableVideo = this.callType === CallType.AUDIO_VIDEO;
163+
this.config.connectionDelegate = this;
164+
/**
165+
* Note : Following hardcoding needs to be removed. Where to get this data from, needs to
166+
* be decided. Administration -> RateLimiter -> WebRTC has a setting for stun/turn servers.
167+
* Nevertheless, whether it is configurebla by agent or not is to be found out.
168+
* Agent will control these settings.
169+
*/
170+
this.config.iceServers = [{ urls: 'stun:stun.l.google.com:19302' }];
171+
this.userHandler = new VoIPUser(this.config, this, this, this, this.mediaStreamRendered);
172+
await this.userHandler.init();
173+
}
174+
175+
async resetUserAgent(): Promise<void> {
176+
this.config = {};
177+
this.userHandler = undefined;
178+
}
179+
180+
async registerEndpoint(): Promise<void> {
181+
if (!this.userHandler) {
182+
try {
183+
await this.initUserAgent();
184+
} catch (error) {
185+
this.logger.error('registerEndpoint() Error in getting extension Info', error);
186+
throw error;
187+
}
188+
}
189+
// await this._apitest_debug();
190+
this.userHandler?.register();
191+
}
192+
193+
unregisterEndpoint(): void {
194+
this.userHandler?.unregister();
195+
this.resetUserAgent();
196+
}
197+
198+
async acceptCall(mediaRenderer?: IMediaStreamRenderer): Promise<any> {
199+
if (mediaRenderer) {
200+
this.mediaStreamRendered = mediaRenderer;
201+
}
202+
return this.userHandler?.acceptCall(this.mediaStreamRendered);
203+
}
204+
205+
async rejectCall(): Promise<any> {
206+
return this.userHandler?.rejectCall();
207+
}
208+
209+
async endCall(): Promise<any> {
210+
return this.userHandler?.endCall();
211+
}
212+
213+
setListener(event: VoipEvents, listener: (evData?: any) => void): void {
214+
this.voipEventEmitter.on(VoipEvents[event], listener);
215+
}
216+
217+
removeListener(event: VoipEvents, listener: (evData?: any) => void): void {
218+
if (this.voipEventEmitter.has(VoipEvents[event])) {
219+
this.voipEventEmitter.off(VoipEvents[event], listener);
220+
} else {
221+
this.logger.error('removeListener() Event listener not found', VoipEvents[event]);
222+
}
223+
}
224+
225+
getState(): CallState | undefined {
226+
return this.userHandler?.callState;
227+
}
228+
}

client/components/voip/VoIPUser.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ import { SessionDescriptionHandler } from 'sip.js/lib/platform/web';
2222

2323
import { ClientLogger } from '../../../lib/ClientLogger';
2424
import { CallState } from './Callstate';
25-
import { ICallEventDelegate } from './ICallEventDelegate';
25+
import { ICallEventDelegate, ICallerInfo } from './ICallEventDelegate';
2626
import { IConnectionDelegate } from './IConnectionDelegate';
2727
import { IRegisterHandlerDelegate } from './IRegisterHandlerDelegate';
2828
import { Operation } from './Operations';
29-
import { VoIPUserConfiguration } from './VoIPUserConfiguration';
29+
import { IMediaStreamRenderer, VoIPUserConfiguration } from './VoIPUserConfiguration';
3030
import Stream from './media/Stream';
3131
// User state is based on whether the User has sent an invite(UAC) or it
3232
// has received an invite (UAS)
@@ -35,6 +35,7 @@ enum UserState {
3535
UAC,
3636
UAS,
3737
}
38+
3839
export class VoIPUser implements UserAgentDelegate, OutgoingRequestDelegate {
3940
private connectionDelegate: IConnectionDelegate;
4041

@@ -54,6 +55,8 @@ export class VoIPUser implements UserAgentDelegate, OutgoingRequestDelegate {
5455

5556
registerer: Registerer | undefined;
5657

58+
mediaStreamRendered?: IMediaStreamRenderer;
59+
5760
logger: ClientLogger;
5861

5962
private _callState: CallState = CallState.IDLE;
@@ -93,12 +96,14 @@ export class VoIPUser implements UserAgentDelegate, OutgoingRequestDelegate {
9396
cDelegate: IConnectionDelegate,
9497
rDelegate: IRegisterHandlerDelegate,
9598
cEventDelegate: ICallEventDelegate,
99+
mediaRenderer?: IMediaStreamRenderer,
96100
) {
97101
this.config = config;
98102
this.connectionDelegate = cDelegate;
99103
this.registrationDelegate = rDelegate;
100104
this.callEventDelegate = cEventDelegate;
101105
this._userState = UserState.IDLE;
106+
this.mediaStreamRendered = mediaRenderer;
102107
this.logger = new ClientLogger('VoIPUser');
103108
}
104109

@@ -156,7 +161,12 @@ export class VoIPUser implements UserAgentDelegate, OutgoingRequestDelegate {
156161
this._userState = UserState.UAS;
157162
this.session = invitation;
158163
this.setupSessionEventHandlers(invitation);
159-
this.callEventDelegate.onIncomingCall?.(invitation.id);
164+
const callerId: ICallerInfo = {
165+
callerId: invitation.remoteIdentity.uri.user ? invitation.remoteIdentity.uri.user : '',
166+
callerName: invitation.remoteIdentity.displayName,
167+
host: invitation.remoteIdentity.uri.host,
168+
};
169+
this.callEventDelegate.onIncomingCall?.(callerId);
160170
} else {
161171
this.logger.warn('handleIncomingCall() Rejecting. Incorrect state', this.callState);
162172
await invitation.reject();
@@ -245,7 +255,7 @@ export class VoIPUser implements UserAgentDelegate, OutgoingRequestDelegate {
245255
}
246256

247257
this.remoteStream = new Stream(remoteStream);
248-
const mediaElement = this.config.mediaElements?.remoteStreamMediaElement;
258+
const mediaElement = this.mediaStreamRendered?.remoteMediaElement;
249259

250260
if (mediaElement) {
251261
this.remoteStream.init(mediaElement);
@@ -265,7 +275,7 @@ export class VoIPUser implements UserAgentDelegate, OutgoingRequestDelegate {
265275
* Once initialized, it starts the userAgent.
266276
*/
267277

268-
async init(): Promise<any> {
278+
async init(): Promise<void> {
269279
this.logger.debug('init()');
270280
const sipUri = `sip:${this.config.authUserName}@${this.config.sipRegistrarHostnameOrIP}`;
271281
this.logger.verbose('init() endpoint identity = ', sipUri);
@@ -327,7 +337,10 @@ export class VoIPUser implements UserAgentDelegate, OutgoingRequestDelegate {
327337
* @remarks
328338
*/
329339

330-
async acceptCall(): Promise<void> {
340+
async acceptCall(mediaRenderer?: IMediaStreamRenderer): Promise<void> {
341+
if (mediaRenderer) {
342+
this.mediaStreamRendered = mediaRenderer;
343+
}
331344
this.logger.info('acceptCall()');
332345
// Call state must be in offer_received.
333346
if (

client/components/voip/VoIPUserConfiguration.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,9 @@ export interface VoIPUserConfiguration {
3737
* @defaultValue undefined
3838
*/
3939
iceServers?: Array<object>;
40+
}
4041

41-
/**
42-
* mediaElements to render local and remote stream
43-
* @defaultValue undefined
44-
*/
45-
mediaElements?: {
46-
remoteStreamMediaElement?: HTMLMediaElement;
47-
localStreamMediaElement?: HTMLMediaElement;
48-
};
42+
export interface IMediaStreamRenderer {
43+
localMediaElement?: HTMLMediaElement;
44+
remoteMediaElement?: HTMLMediaElement;
4945
}

0 commit comments

Comments
 (0)