Skip to content

Commit

Permalink
Spark 586376 add calling rtms to u2c (#4053)
Browse files Browse the repository at this point in the history
  • Loading branch information
rsarika authored Feb 12, 2025
1 parent 6d0eb73 commit 28a4548
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 65 deletions.
2 changes: 1 addition & 1 deletion packages/@webex/plugin-cc/src/cc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
connectionConfig: this.getConnectionConfig(),
});

this.webCallingService = new WebCallingService(this.$webex, this.$config.callingClientConfig);
this.webCallingService = new WebCallingService(this.$webex);
this.taskManager = TaskManager.getTaskManager(
this.services.contact,
this.webCallingService,
Expand Down
12 changes: 0 additions & 12 deletions packages/@webex/plugin-cc/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import {LOGGER} from '@webex/calling';

export default {
cc: {
allowMultiLogin: false,
Expand All @@ -11,15 +9,5 @@ export default {
clientName: 'WEBEX_JS_SDK',
clientType: 'WebexCCSDK',
},
callingClientConfig: {
logger: {
level: LOGGER.INFO,
},
serviceData: {
indicator: 'contactcenter',
// TODO: This should be dynamic based on the environment
domain: 'rtw.prod-us1.rtmsprod.net',
},
},
},
};
43 changes: 38 additions & 5 deletions packages/@webex/plugin-cc/src/services/WebCallingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,27 @@ import {
ICallingClient,
ILine,
LINE_EVENTS,
CallingClientConfig,
ServiceIndicator,
LocalMicrophoneStream,
CALL_EVENT_KEYS,
LOGGER,
} from '@webex/calling';
import {LoginOption, WebexSDK} from '../types';
import {TIMEOUT_DURATION, WEB_CALLING_SERVICE_FILE} from '../constants';
import LoggerProxy from '../logger-proxy';
import {DEFAULT_RTMS_DOMAIN, POST_AUTH, WCC_CALLING_RTMS_DOMAIN} from './constants';

export default class WebCallingService extends EventEmitter {
private callingClient: ICallingClient;
private callingClientConfig: CallingClientConfig;
private line: ILine;
private call: ICall | undefined;
private webex: WebexSDK;
public loginOption: LoginOption;
private callTaskMap: Map<string, string>;

constructor(webex: WebexSDK, callingClientConfig: CallingClientConfig) {
constructor(webex: WebexSDK) {
super();
this.webex = webex;
this.callingClientConfig = callingClientConfig;
this.callTaskMap = new Map();
}

Expand Down Expand Up @@ -62,8 +62,41 @@ export default class WebCallingService extends EventEmitter {
}
}

private async getRTMSDomain() {
await this.webex.internal.services.waitForCatalog(POST_AUTH);

const rtmsURL = this.webex.internal.services.get(WCC_CALLING_RTMS_DOMAIN);

try {
const url = new URL(rtmsURL);

return url.hostname;
} catch (error) {
LoggerProxy.error(
`Invalid URL from u2c catalogue: ${rtmsURL} so falling back to default domain`,
{
module: WEB_CALLING_SERVICE_FILE,
}
);

return DEFAULT_RTMS_DOMAIN;
}
}

public async registerWebCallingLine(): Promise<void> {
this.callingClient = await createClient(this.webex as any, this.callingClientConfig);
const rtmsDomain = await this.getRTMSDomain(); // get the RTMS domain from the u2c catalogue

const callingClientConfig = {
logger: {
level: LOGGER.INFO,
},
serviceData: {
indicator: ServiceIndicator.CONTACT_CENTER,
domain: rtmsDomain,
},
};

this.callingClient = await createClient(this.webex as any, callingClientConfig);
this.line = Object.values(this.callingClient.getLines())[0];

this.line.on(LINE_EVENTS.UNREGISTERED, () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/@webex/plugin-cc/src/services/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export const POST_AUTH = 'postauth';
export const WCC_API_GATEWAY = 'wcc-api-gateway';
export const WCC_CALLING_RTMS_DOMAIN = 'wcc-calling-rtms-domain';
export const DEFAULT_RTMS_DOMAIN = 'rtw.prod-us1.rtmsprod.net';
export const WEBSOCKET_EVENT_TIMEOUT = 20000;

export const AGENT = 'agent';
Expand Down
70 changes: 36 additions & 34 deletions packages/@webex/plugin-cc/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,41 @@ export enum LOGGING_LEVEL {
info = 'INFO',
trace = 'TRACE',
}

interface IWebexInternal {
mercury: {
on: Listener;
off: ListenerOff;
connected: boolean;
connecting: boolean;
};
device: {
url: string;
userId: string;
orgId: string;
version: string;
callingBehavior: string;
};
presence: unknown;
services: {
get: (service: string) => string;
waitForCatalog: (service: string) => Promise<void>;
_hostCatalog: Record<string, ServiceHost[]>;
_serviceUrls: {
mobius: string;
identity: string;
janus: string;
wdm: string;
broadworksIdpProxy: string;
hydra: string;
mercuryApi: string;
'ucmgmt-gateway': string;
contactsService: string;
};
};
metrics: {
submitClientMetrics: (name: string, data: unknown) => void;
};
}
export interface WebexSDK {
version: string;
canAuthorize: boolean;
Expand All @@ -95,39 +129,7 @@ export interface WebexSDK {
request: <T>(payload: WebexRequestPayload) => Promise<T>;
once: (event: string, callBack: () => void) => void;
// internal plugins
internal: {
mercury: {
on: Listener;
off: ListenerOff;
connected: boolean;
connecting: boolean;
};
device: {
url: string;
userId: string;
orgId: string;
version: string;
callingBehavior: string;
};
presence: unknown;
services: {
_hostCatalog: Record<string, ServiceHost[]>;
_serviceUrls: {
mobius: string;
identity: string;
janus: string;
wdm: string;
broadworksIdpProxy: string;
hydra: string;
mercuryApi: string;
'ucmgmt-gateway': string;
contactsService: string;
};
};
metrics: {
submitClientMetrics: (name: string, data: unknown) => void;
};
};
internal: IWebexInternal;
// public plugins
logger: Logger;
}
Expand Down
124 changes: 111 additions & 13 deletions packages/@webex/plugin-cc/test/unit/spec/services/WebCallingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ import {
ICallingClient,
ILine,
LINE_EVENTS,
CallingClientConfig,
CALL_EVENT_KEYS,
LocalMicrophoneStream,
} from '@webex/calling';
import {LoginOption, WebexSDK} from '../../../../src/types';
import config from '../../../../src/config';
import { WebexSDK} from '../../../../src/types';
import LoggerProxy from '../../../../src/logger-proxy';
import {WEB_CALLING_SERVICE_FILE} from '../../../../src/constants';
jest.mock('@webex/calling');
Expand All @@ -37,7 +35,13 @@ describe('WebCallingService', () => {
logger: {
log: jest.fn(),
error: jest.fn(),
info: jest.fn()
info: jest.fn(),
},
internal: {
services: {
waitForCatalog: jest.fn().mockResolvedValue(undefined),
get: jest.fn()
},
},
} as unknown as WebexSDK;

Expand All @@ -55,7 +59,6 @@ describe('WebCallingService', () => {

webRTCCalling = new WebCallingService(
webex,
config.cc.callingClientConfig as CallingClientConfig
);

mockCall = {
Expand All @@ -77,7 +80,9 @@ describe('WebCallingService', () => {
});

describe('registerWebCallingLine', () => {

it('should register the web calling line successfully', async () => {
webex.internal.services.get.mockReturnValue(undefined); // this is to test fallback to default rtms domain
line = callingClient.getLines().line1 as ILine;
const deviceInfo = {
mobiusDeviceId: 'device123',
Expand All @@ -98,15 +103,100 @@ describe('WebCallingService', () => {

await expect(webRTCCalling.registerWebCallingLine()).resolves.toBeUndefined();

expect(createClient).toHaveBeenCalledWith(webex, config.cc.callingClientConfig);
expect(createClient).toHaveBeenCalledWith(webex, {
logger: {
level: 'info',
},
serviceData: {
indicator: 'contactcenter',
domain: 'rtw.prod-us1.rtmsprod.net',
},
});
expect(lineOnSpy).toHaveBeenCalledWith(LINE_EVENTS.REGISTERED, expect.any(Function));
expect(line.register).toHaveBeenCalled();
expect(LoggerProxy.log).toHaveBeenCalledWith(
`WxCC-SDK: Desktop registered successfully, mobiusDeviceId: ${deviceInfo.mobiusDeviceId}`,
{method: 'registerWebCallingLine', module: WEB_CALLING_SERVICE_FILE}
);
}, 20000); // Increased timeout to 20 seconds

it('should register WebCallingLine with custom rtms url', async () => {
webex.internal.services.get.mockReturnValue('sip://rtw.prod-us2.rtmsprod.net');

line = callingClient.getLines().line1 as ILine;
const deviceInfo = {
mobiusDeviceId: 'device123',
status: 'registered',
setError: jest.fn(),
getError: jest.fn(),
type: 'line',
id: 'line1',
};

const registeredHandler = jest.fn();
const lineOnSpy = jest.spyOn(line, 'on').mockImplementation((event, handler) => {
if (event === LINE_EVENTS.REGISTERED) {
registeredHandler.mockImplementation(handler);
handler(deviceInfo);
}
});
await expect(webRTCCalling.registerWebCallingLine()).resolves.toBeUndefined();
expect(createClient).toHaveBeenCalledWith(webex, {
logger: {
level: 'info',
},
serviceData: {
indicator: 'contactcenter',
domain: 'rtw.prod-us2.rtmsprod.net',
},
});
expect(lineOnSpy).toHaveBeenCalledWith(LINE_EVENTS.REGISTERED, expect.any(Function));
expect(line.register).toHaveBeenCalled();
expect(LoggerProxy.log).toHaveBeenCalledWith(
`WxCC-SDK: Desktop registered successfully, mobiusDeviceId: ${deviceInfo.mobiusDeviceId}`,
{"method": "registerWebCallingLine", "module": WEB_CALLING_SERVICE_FILE}
{method: 'registerWebCallingLine', module: WEB_CALLING_SERVICE_FILE}
);
}, 20000); // Increased timeout to 20 seconds

it('should handle error when invalid rtms url is provided', async () => {
webex.internal.services.get.mockReturnValue('invalid-url');

line = callingClient.getLines().line1 as ILine;
const deviceInfo = {
mobiusDeviceId: 'device123',
status: 'registered',
setError: jest.fn(),
getError: jest.fn(),
type: 'line',
id: 'line1',
};

const registeredHandler = jest.fn();
const lineOnSpy = jest.spyOn(line, 'on').mockImplementation((event, handler) => {
if (event === LINE_EVENTS.REGISTERED) {
registeredHandler.mockImplementation(handler);
handler(deviceInfo);
}
});
await expect(webRTCCalling.registerWebCallingLine()).resolves.toBeUndefined();
expect(createClient).toHaveBeenCalledWith(webex, {
logger: {
level: 'info',
},
serviceData: {
indicator: 'contactcenter',
domain: 'rtw.prod-us1.rtmsprod.net',
},
});
expect(lineOnSpy).toHaveBeenCalledWith(LINE_EVENTS.REGISTERED, expect.any(Function));
expect(line.register).toHaveBeenCalled();
expect(LoggerProxy.error).toHaveBeenCalledWith(
`Invalid URL from u2c catalogue: invalid-url so falling back to default domain`,
{module: WEB_CALLING_SERVICE_FILE}
);

});

it('should reject if registration times out', async () => {
line = callingClient.getLines().line1 as ILine;

Expand Down Expand Up @@ -171,7 +261,7 @@ describe('WebCallingService', () => {
const mockStream = {
outputStream: {
getAudioTracks: jest.fn().mockReturnValue(['']),
}
},
};

const localAudioStream = mockStream as unknown as LocalMicrophoneStream;
Expand All @@ -185,10 +275,14 @@ describe('WebCallingService', () => {

it('should log error and throw when call.answer fails', () => {
const error = new Error('Failed to answer');
mockCall.answer.mockImplementation(() => { throw error; });
mockCall.answer.mockImplementation(() => {
throw error;
});

expect(() => webRTCCalling.answerCall(localAudioStream, 'task-id')).toThrow(error);
expect(webex.logger.error).toHaveBeenCalledWith(`Failed to answer call for task-id. Error: ${error}`);
expect(webex.logger.error).toHaveBeenCalledWith(
`Failed to answer call for task-id. Error: ${error}`
);
});

it('should log when there is no call to answer', () => {
Expand All @@ -203,7 +297,7 @@ describe('WebCallingService', () => {
const mockStream = {
outputStream: {
getAudioTracks: jest.fn().mockReturnValue(['']),
}
},
};

const localAudioStream = mockStream as unknown as LocalMicrophoneStream;
Expand Down Expand Up @@ -233,10 +327,14 @@ describe('WebCallingService', () => {

it('should log error and throw when call.end fails', () => {
const error = new Error('Failed to end call');
mockCall.end.mockImplementation(() => { throw error; });
mockCall.end.mockImplementation(() => {
throw error;
});

expect(() => webRTCCalling.declineCall('task-id')).toThrow(error);
expect(webex.logger.error).toHaveBeenCalledWith(`Failed to end call: task-id. Error: ${error}`);
expect(webex.logger.error).toHaveBeenCalledWith(
`Failed to end call: task-id. Error: ${error}`
);
});

it('should log when there is no call to end', () => {
Expand Down
Loading

0 comments on commit 28a4548

Please sign in to comment.