Skip to content

Commit 5681697

Browse files
author
Aidan Zimmermann
committed
STREAM-777: try again
1 parent 8e49394 commit 5681697

11 files changed

Lines changed: 488 additions & 20 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@ test/test-pages/*/index.html
1414

1515
*.tgz
1616
.ignore
17+
.DS_Store
1718

1819
## vscode specific files
1920
/.vscode
2021

2122
## webstorm specific files
2223
/.idea
2324

25+
## jetbrains specific files
26+
genesys-cloud-webrtc-sdk.iml
27+
2428
# tap specific files
2529
/.nyc_output
2630
/coverage

src/client.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
IPendingSessionActionParams,
2626
IExtendedMediaSession,
2727
ScreenRecordingMediaSession,
28+
ScreenRecordingMetadata,
2829
VideoMediaSession,
2930
IVideoResolution,
3031
JWTDetails,
@@ -402,6 +403,25 @@ export class GenesysCloudWebrtcSdk extends (EventEmitter as { new(): StrictEvent
402403
return this.sessionManager.startSession({ meetingId, sessionType: SessionTypes.collaborateVideo });
403404
}
404405

406+
/**
407+
* Start a screen conference session that identifies the primary screen
408+
* from the provided metadata and joins a conference using that screen as video.
409+
*
410+
* `initialize()` must be called first.
411+
*
412+
* @param conferenceJid JID of the conference to join
413+
* @param screenRecordingMetadatas Array of screen metadata to identify primary screen
414+
*
415+
* @returns a promise with an object with the newly created 'conversationId'
416+
*/
417+
async startLiveMonitoringSession (conferenceJid: string, screenRecordingMetadatas: ScreenRecordingMetadata[]): Promise<{ conversationId: string }> {
418+
return this.sessionManager.startSession({
419+
conferenceJid,
420+
screenRecordingMetadatas,
421+
sessionType: SessionTypes.collaborateVideo
422+
});
423+
}
424+
405425
/**
406426
* Start a softphone session with the given peer or peers.
407427
* `initialize()` must be called first.

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export * from './sessions/softphone-session-handler';
88
export * from './sessions/video-session-handler';
99
export * from './sessions/screen-recording-session-handler';
1010
export * from './sessions/screen-share-session-handler';
11+
export * from './sessions/live-monitoring-session-handler';
1112
export { ExpandedDeviceConnectionStatus } from './headsets/headset-types';
1213

1314
import * as utils from './utils';
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import {
2+
IPendingSession,
3+
IExtendedMediaSession,
4+
IAcceptSessionRequest,
5+
ScreenRecordingMetadata,
6+
IUpdateOutgoingMedia, IStartScreenConferenceSessionParams,
7+
} from '../types/interfaces';
8+
import BaseSessionHandler from './base-session-handler';
9+
import { SessionTypes, SdkErrorTypes } from '../types/enums';
10+
import {createAndEmitSdkError, isMonitorJid, requestApi} from '../utils';
11+
12+
export class LiveMonitoringSessionHandler extends BaseSessionHandler {
13+
sessionType = SessionTypes.collaborateVideo;
14+
private primaryScreenMediaStream?: MediaStream;
15+
16+
shouldHandleSessionByJid(jid: string): boolean {
17+
return isMonitorJid(jid);
18+
}
19+
20+
handleConversationUpdate(): void {
21+
/* no-op */
22+
return;
23+
}
24+
25+
async handlePropose (pendingSession: IPendingSession): Promise<void> {
26+
return this.proceedWithSession(pendingSession);
27+
}
28+
29+
async startSession(params: IStartScreenConferenceSessionParams): Promise<{ conversationId: string }> {
30+
const primaryScreen = this.identifyPrimaryScreen(params.screenRecordingMetadatas);
31+
32+
if (!primaryScreen) {
33+
throw createAndEmitSdkError.call(this.sdk, SdkErrorTypes.invalid_options, 'No primary screen found in metadata');
34+
}
35+
36+
const screenStream = await this.getScreenMediaForPrimary(primaryScreen);
37+
38+
return this.joinConferenceWithScreen(params.conferenceJid, screenStream);
39+
}
40+
41+
private identifyPrimaryScreen(metadatas: ScreenRecordingMetadata[]): ScreenRecordingMetadata | null {
42+
return metadatas.find(metadata => metadata.primary) || null;
43+
}
44+
45+
private async getScreenMediaForPrimary(primaryScreen: ScreenRecordingMetadata): Promise<MediaStream> {
46+
try {
47+
const constraints = {
48+
video: {
49+
deviceId: primaryScreen.screenId
50+
}
51+
} as DisplayMediaStreamOptions;
52+
53+
return await window.navigator.mediaDevices.getDisplayMedia(constraints);
54+
} catch (error) {
55+
throw createAndEmitSdkError.call(this.sdk, SdkErrorTypes.session, 'Failed to get screen media', { error });
56+
}
57+
}
58+
59+
private async joinConferenceWithScreen(conferenceJid: string, screenStream: MediaStream): Promise<{ conversationId: string }> {
60+
const data = JSON.stringify({
61+
roomId: conferenceJid,
62+
participant: { address: this.sdk._personDetails.chat.jabberId }
63+
});
64+
65+
try {
66+
const response = await requestApi.call(this.sdk, '/conversations/videos', {
67+
method: 'post',
68+
data
69+
});
70+
71+
// Store screen stream for later use in acceptSession
72+
this.primaryScreenMediaStream = screenStream;
73+
74+
return { conversationId: response.data.conversationId };
75+
} catch (error) {
76+
screenStream.getTracks().forEach(track => track.stop());
77+
throw createAndEmitSdkError.call(this.sdk, SdkErrorTypes.session, 'Failed to join conference', { error });
78+
}
79+
}
80+
81+
async acceptSession(session: IExtendedMediaSession, params: IAcceptSessionRequest): Promise<any> {
82+
if (!params.screenRecordingMetadatas?.length) {
83+
throw createAndEmitSdkError.call(this.sdk, SdkErrorTypes.not_supported, 'acceptSession must be called with a `screenRecordingMetadatas` property for live monitoring sessions');
84+
}
85+
86+
// Set outbound stream to primary video media stream
87+
session._outboundStream = this.primaryScreenMediaStream;
88+
89+
// Add primary screen tracks to session
90+
for (const track of this.primaryScreenMediaStream.getTracks()) {
91+
session.pc.addTrack(track);
92+
}
93+
94+
this.primaryScreenMediaStream = undefined;
95+
96+
return super.acceptSession(session, params);
97+
}
98+
99+
updateOutgoingMedia(session: IExtendedMediaSession, options: IUpdateOutgoingMedia): never {
100+
this.log('warn', 'Cannot update outgoing media for live monitoring sessions', { sessionId: session.id, sessionType: session.sessionType });
101+
throw createAndEmitSdkError.call(this.sdk, SdkErrorTypes.not_supported, 'Cannot update outgoing media for live monitoring sessions');
102+
}
103+
}
104+
105+
export default LiveMonitoringSessionHandler;

src/sessions/session-manager.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,21 @@ import {
2121
IConversationHeldRequest,
2222
IPendingSessionActionParams,
2323
VideoMediaSession,
24-
IActiveConversationDescription
24+
IActiveConversationDescription, IStartScreenConferenceSessionParams
2525
} from '../types/interfaces';
2626
import { ConversationUpdate } from '../conversations/conversation-update';
2727
import { SessionTypesAsStrings } from 'genesys-cloud-streaming-client';
2828
import { Constants } from 'stanza';
2929
import ScreenRecordingSessionHandler from './screen-recording-session-handler';
30+
import LiveMonitoringSessionHandler from './live-monitoring-session-handler';
3031
import { WebrtcExtensionAPI } from 'genesys-cloud-streaming-client/dist/es/webrtc';
3132

3233
const sessionHandlersToConfigure: any[] = [
3334
SoftphoneSessionHandler,
3435
VideoSessionHandler,
3536
ScreenShareSessionHandler,
36-
ScreenRecordingSessionHandler
37+
ScreenRecordingSessionHandler,
38+
LiveMonitoringSessionHandler
3739
];
3840

3941
export class SessionManager {
@@ -175,7 +177,7 @@ export class SessionManager {
175177
return handler;
176178
}
177179

178-
async startSession (startSessionParams: IStartSessionParams | IStartVideoSessionParams | IStartVideoMeetingSessionParams | IStartSoftphoneSessionParams): Promise<any> {
180+
async startSession (startSessionParams: IStartSessionParams | IStartVideoSessionParams | IStartVideoMeetingSessionParams | IStartSoftphoneSessionParams | IStartScreenConferenceSessionParams): Promise<any> {
179181
if (!this.sdk.connected) {
180182
throw createAndEmitSdkError.call(this.sdk, SdkErrorTypes.session, 'A session cannot be started as streaming client is not yet connected', { sessionType: startSessionParams.sessionType });
181183
}
@@ -400,7 +402,7 @@ export class SessionManager {
400402
}
401403

402404
session._alreadyAccepted = true;
403-
405+
404406
const sessionHandler = this.getSessionHandler({ jingleSession: session });
405407
return sessionHandler.acceptSession(session, params);
406408
}

src/types/interfaces.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,6 +1215,13 @@ export interface IStartSoftphoneSessionParams extends IStartSessionParams {
12151215
uuiData?: string;
12161216
}
12171217

1218+
export interface IStartScreenConferenceSessionParams extends IStartSessionParams {
1219+
/** Conference room JID to join */
1220+
conferenceJid: string;
1221+
/** Screen recording metadata to identify primary screen */
1222+
screenRecordingMetadatas: ScreenRecordingMetadata[];
1223+
}
1224+
12181225
export interface ISdkSoftphoneDestination {
12191226
/** address or phone number */
12201227
address: string;

src/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ export const isAgentVideoJid = function (jid: string) {
124124
return isVideoJid(jid) && jid.startsWith('agent-');
125125
};
126126

127+
export const isMonitorJid = function (jid: string) {
128+
return isVideoJid(jid) && jid.startsWith('livemonitor-');
129+
};
130+
127131
export const isVideoJid = function (jid: string): boolean {
128132
return jid && !!jid.match(/@conference/) && !isAcdJid(jid) && !isScreenRecordingJid(jid);
129133
};

test/test-utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ class MockPC extends EventTarget {
157157
_addReceiver (track: MockTrack) {
158158
this._receivers.push(new MockReceiver(track));
159159
}
160+
161+
addTrack (track: MockTrack) {
162+
this._addSender(track);
163+
}
160164
}
161165

162166
export class MockSession extends EventEmitter {

test/unit/client.test.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
IStation,
3030
IPersonDetails,
3131
ISessionIdAndConversationId,
32-
VideoSessionHandler
32+
VideoSessionHandler, ScreenRecordingMetadata, IStartScreenConferenceSessionParams
3333
} from '../../src';
3434
import * as utils from '../../src/utils';
3535
import { RetryPromise } from 'genesys-cloud-streaming-client/dist/es/utils';
@@ -331,24 +331,22 @@ describe('Client', () => {
331331
});
332332
});
333333

334-
describe('startVideoMeeting()', () => {
335-
it('should call session manager to start video meeting', async () => {
334+
describe('startLiveMonitoringSession()', () => {
335+
it('should call session manager to start a live monitoring session', async () => {
336336
sdk = constructSdk();
337+
const conferenceJid = 'livemonitor@example.com';
338+
const screenRecordingMetadatas = [{ screenId: 'screen1', primary: true } as ScreenRecordingMetadata];
337339

338-
sessionManagerMock.startSession.mockResolvedValue({});
339-
await sdk.startVideoMeeting('123abc');
340-
expect(sessionManagerMock.startSession).toBeCalledWith({ meetingId: '123abc', sessionType: SessionTypes.collaborateVideo });
341-
});
340+
sessionManagerMock.startSession = jest.fn().mockResolvedValue({ conversationId: 'conv123' });
342341

343-
it('should throw if guest user', async () => {
344-
sdk = constructSdk({ organizationId: 'some-org' }); // no access_token is a guest user
345-
try {
346-
await sdk.startVideoMeeting('123');
347-
fail('should have failed');
348-
} catch (e) {
349-
expect(e).toEqual(new Error('video conferencing meetings not supported for guests'));
350-
expect(sessionManagerMock.startSession).not.toHaveBeenCalled();
351-
}
342+
const result = await sdk.startLiveMonitoringSession(conferenceJid, screenRecordingMetadatas);
343+
344+
expect(sessionManagerMock.startSession).toBeCalledWith({
345+
conferenceJid,
346+
screenRecordingMetadatas,
347+
sessionType: SessionTypes.collaborateVideo
348+
} as IStartScreenConferenceSessionParams);
349+
expect(result).toEqual({ conversationId: 'conv123' });
352350
});
353351
});
354352

0 commit comments

Comments
 (0)