Skip to content

Commit 7d90a62

Browse files
committed
[STREAM-1180] Change media handling to only allow headsets to stop being used if there are no active conversations; also only disconnect idle connections
1 parent 6e10f68 commit 7d90a62

7 files changed

Lines changed: 102 additions & 46 deletions

File tree

src/client.ts

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { HeadsetProxyService } from './headsets/headset';
4444
import { Constants } from 'stanza';
4545
import { setupWebrtcForWindows11 } from './windows11-first-session-hack';
4646
import { ISdkHeadsetService } from './headsets/headset-types';
47+
import { SoftphoneSessionHandler } from '.';
4748

4849
const ENVIRONMENTS = [
4950
'mypurecloud.com',
@@ -111,7 +112,7 @@ export class GenesysCloudWebrtcSdk extends (EventEmitter as { new(): StrictEvent
111112
_customerData: ICustomerData;
112113
_hasConnected: boolean;
113114
_config: ISdkFullConfig;
114-
_mediaHandling = MediaHandling.media;
115+
_mediaHandling = MediaHandling.standardMedia;
115116

116117
get isInitialized (): boolean {
117118
return !!this._streamingConnection;
@@ -863,25 +864,41 @@ export class GenesysCloudWebrtcSdk extends (EventEmitter as { new(): StrictEvent
863864
this.media.setDefaultAudioStream(stream);
864865
}
865866

867+
/**
868+
* **Genesys internal use only** - non-Genesys apps may experience unexpected behavior.
869+
*
870+
* Change the media handling for softphone sessions, which should be
871+
* be chosen based on the alerting leader status of the consuming client.
872+
*
873+
* If media handling is reduced, idle persistent connections will be
874+
* disconnected.
875+
*
876+
* @param mediaHandling how softphone media should be handled
877+
*/
866878
setMediaHandling (mediaHandling: MediaHandling): void {
867-
this._mediaHandling = mediaHandling;
879+
const activeConversations = this.sessionManager.getAllActiveConversations();
880+
console.log('Hjon: mediaHandling:', mediaHandling);
881+
const useHeadsets = !(mediaHandling === MediaHandling.reducedMediaNoHeadsets);
882+
console.log('Hjon: useHeadsets:', useHeadsets);
868883

869-
// The intent behind this comes from requirements for alerting leader
870-
// where a client shouldn't handle media (mostly) if it isn't the alerting
871-
// leader. Ideally this would only handle outbound calls, but we can't
872-
// distinguish between outbound calls and inbound autoanswer ACD calls when
873-
// we receive the `propose`.
874-
if (mediaHandling === MediaHandling.autoAnswerOnly) {
875-
this.setUseHeadsets(false);
876-
877-
// Disconnect any persistent connections we might have
878-
this.sessionManager.getAllActiveSessions()
879-
.filter(session => session.sessionType === SessionTypes.softphone)
880-
.forEach(softphoneSession => {
881-
this.sessionManager.forceTerminateSession(softphoneSession.id);
882-
});
883-
} else {
884-
this.setUseHeadsets(true);
884+
if (activeConversations.length !== 0 && !useHeadsets) {
885+
throw createAndEmitSdkError.call(this, SdkErrorTypes.not_supported, 'Cannot downgrade media handling to stop using headsets during an active call');
886+
}
887+
888+
this._mediaHandling = mediaHandling;
889+
this.setUseHeadsets(useHeadsets);
890+
891+
const reduceMediaHandling = mediaHandling === MediaHandling.reducedMediaHeadsets || mediaHandling === MediaHandling.reducedMediaNoHeadsets;
892+
if (reduceMediaHandling) {
893+
const conversationSessionIds = activeConversations.map(conversation => conversation.sessionId);
894+
// Disconnect connections that aren't associated with an active conversation.
895+
// When a client is no longer the alerting leader, it needs to give up any
896+
// persistent connections. Active calls should be maintained.
897+
this.sessionManager.getAllSessions().forEach(session => {
898+
if (session.sessionType === SessionTypes.softphone && !conversationSessionIds.includes(session.id)) {
899+
this.sessionManager.forceTerminateSession(session.id);
900+
}
901+
});
885902
}
886903
}
887904

src/headsets/headset.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ export class HeadsetProxyService implements ISdkHeadsetService {
4949
// TODO: PCM-2060 - remove this
5050
this.useHeadsetOrchestration = !this.sdk._config.disableHeadsetControlsOrchestration;
5151

52-
if (this.sdk._mediaHandling === MediaHandling.autoAnswerOnly) {
53-
this.sdk.logger.warn('setUseHeadsets was called with `true` but headsets are not supported in this configuration - not handling media. Not activating headsets.');
52+
if (this.sdk._mediaHandling === MediaHandling.reducedMediaNoHeadsets) {
53+
this.sdk.logger.warn('setUseHeadsets was called with `true` but media handling is set to `reducedMediaNoHeadsets`; headsets are not supported in this configuration - not handling media. Not activating headsets.');
5454
useHeadsets = false;
5555
}
5656

src/sessions/softphone-session-handler.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,12 @@ export class SoftphoneSessionHandler extends BaseSessionHandler {
149149
async handlePropose (pendingSession: IPendingSession): Promise<void> {
150150
const isPrivAnswerAuto = pendingSession.privAnswerMode === 'Auto';
151151
const eagerConnectionEstablishmentMode = this.sdk._config.eagerPersistentConnectionEstablishment;
152-
const autoAnswerOnly = this.sdk._mediaHandling === MediaHandling.autoAnswerOnly;
153152
const logInfo = { sessionId: pendingSession?.id, conversationId: pendingSession.conversationId };
153+
const reducedMediaHandling = this.sdk._mediaHandling === MediaHandling.reducedMediaHeadsets || this.sdk._mediaHandling === MediaHandling.reducedMediaNoHeadsets;
154+
155+
if (reducedMediaHandling) {
156+
this.log('info', 'received a propose while the SDK is configured for reduced media handling', logInfo);
154157

155-
if (autoAnswerOnly) {
156158
if (pendingSession.autoAnswer) {
157159
// emit the pendingSession event
158160
await super.handlePropose(pendingSession);
@@ -164,7 +166,7 @@ export class SoftphoneSessionHandler extends BaseSessionHandler {
164166
await this.proceedWithSession(pendingSession);
165167
}
166168
} else {
167-
this.log('info', 'media handling is set to "auto-answer-only", but this propose is not marked as autoAnswer and will be ignored', logInfo);
169+
this.log('info', 'media handling is reduced, but this propose is not marked as autoAnswer and will be ignored', logInfo);
168170
return;
169171
}
170172
} else {

src/types/enums.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,26 @@ export enum JingleReasons {
4545
alternativeSession = 'alternative-session'
4646
}
4747

48+
/** These currently only affect softphone media */
4849
export enum MediaHandling {
4950
/** Handle all media; headset controls use traditional orchestration */
50-
media = 'media',
51+
standardMedia = 'standard-media',
5152
/** Handle all media; headset controls follow alerting leader */
5253
alertingLeaderMedia = 'alerting-leader-media',
53-
/** Only handle autoAnswer calls; headset controls are not used */
54-
// Ideally we would only allow outbound calls, but currently we can't distinguish between an outbound call and an inbound autoanswer ACD call
55-
autoAnswerOnly = 'auto-answer-only'
54+
/**
55+
* Handle some media (see below); headset controls use traditional orchestration
56+
*
57+
* - New eager persistent connections will be ignored.
58+
* - Auto-answer calls will be handled, which could result in a
59+
* persistent connection being established.
60+
*/
61+
reducedMediaHeadsets = 'reduced-media-headsets',
62+
/**
63+
* Handle some media (see below); headset controls are not used
64+
*
65+
* - New eager persistent connections will be ignored.
66+
* - Auto-answer calls will be handled, which could result in a
67+
* persistent connection being established.
68+
*/
69+
reducedMediaNoHeadsets = 'reduced-media-no-headsets',
5670
}

test/unit/client.test.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ import {
3030
IPersonDetails,
3131
ISessionIdAndConversationId,
3232
VideoSessionHandler,
33-
MediaHandling
33+
MediaHandling,
34+
IActiveConversationDescription
3435
} from '../../src';
3536
import * as utils from '../../src/utils';
3637
import { RetryPromise } from 'genesys-cloud-streaming-client/dist/es/utils';
@@ -1161,33 +1162,50 @@ describe('Client', () => {
11611162
});
11621163

11631164
describe('setMediaHandling()', () => {
1164-
it('should stop using headsets and disconnect any active sessions when set to "autoAnswerOnly"', () => {
1165+
it('should throw if media handling will not use headsets but there is an active conversation', () => {
11651166
sdk = constructSdk();
1166-
const useHeadsetsSpy = jest.fn();
1167-
sdk.setUseHeadsets = useHeadsetsSpy;
1167+
const conversations = [{ conversationId: 'test-conversation-id' }] as IActiveConversationDescription[];
1168+
sessionManagerMock.getAllActiveConversations.mockReturnValue(conversations);
1169+
1170+
expect(() => {
1171+
sdk.setMediaHandling(MediaHandling.reducedMediaNoHeadsets);
1172+
}).toThrow();
1173+
});
1174+
1175+
it('should disconnect any sessions not connected to an active conversation when set to a reduced handling of media', () => {
1176+
sdk = constructSdk();
1177+
sdk.setUseHeadsets = jest.fn();
1178+
11681179
const mockSession = new MockSession(SessionTypes.softphone);
1169-
const sessionId = mockSession.id;
1170-
const sessions = [mockSession] as unknown as IExtendedMediaSession[];
1171-
sessionManagerMock.getAllActiveSessions.mockReturnValue(sessions);
1180+
const conversations = [{ sessionId: mockSession.id }] as IActiveConversationDescription[];
1181+
sessionManagerMock.getAllActiveConversations.mockReturnValue(conversations);
1182+
const idleSession = new MockSession(SessionTypes.softphone);
1183+
const idleSessionId = idleSession.id;
1184+
const sessions = [mockSession, idleSession] as unknown as IExtendedMediaSession[];
1185+
sessionManagerMock.getAllSessions.mockReturnValue(sessions);
11721186
const forceTerminateSpy = jest.fn();
11731187
sessionManagerMock.forceTerminateSession = forceTerminateSpy;
11741188

1175-
sdk.setMediaHandling(MediaHandling.autoAnswerOnly);
1189+
sdk.setMediaHandling(MediaHandling.reducedMediaHeadsets);
11761190

1177-
expect(useHeadsetsSpy).toHaveBeenCalledWith(false);
1178-
expect(forceTerminateSpy).toHaveBeenCalledWith(sessionId);
1191+
expect(forceTerminateSpy).toHaveBeenCalledTimes(1);
1192+
expect(forceTerminateSpy).toHaveBeenCalledWith(idleSessionId);
11791193
});
11801194

11811195
it('should use headsets when handling any media', () => {
11821196
sdk = constructSdk();
1197+
sessionManagerMock.getAllActiveConversations.mockReturnValue([]);
11831198
const useHeadsetsSpy = jest.fn();
11841199
sdk.setUseHeadsets = useHeadsetsSpy;
11851200

1186-
sdk.setMediaHandling(MediaHandling.media);
1201+
sdk.setMediaHandling(MediaHandling.standardMedia);
11871202
expect(useHeadsetsSpy).toHaveBeenCalledWith(true);
11881203

11891204
sdk.setMediaHandling(MediaHandling.alertingLeaderMedia);
11901205
expect(useHeadsetsSpy).toHaveBeenCalledWith(true);
1206+
1207+
sdk.setMediaHandling(MediaHandling.reducedMediaHeadsets);
1208+
expect(useHeadsetsSpy).toHaveBeenCalledWith(true);
11911209
});
11921210
});
11931211

test/unit/headset/headset.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,8 +301,8 @@ describe('HeadsetProxyService', () => {
301301
expect(proxyService['currentHeadsetService']).toBeInstanceOf(SdkHeadsetServiceFake);
302302
});
303303

304-
it('should use fake service if useHeadsets and only handling autoAnswer calls', () => {
305-
proxyService['sdk']._mediaHandling = MediaHandling.autoAnswerOnly;
304+
it('should use fake service if useHeadsets and media is set to reducedMediaNoHeadsets', () => {
305+
proxyService['sdk']._mediaHandling = MediaHandling.reducedMediaNoHeadsets;
306306
const spy = jest.spyOn(proxyService, 'updateAudioInputDevice');
307307

308308
proxyService.setUseHeadsets(true);

test/unit/sessions/softphone-session-handler.test.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,31 +70,36 @@ describe('shouldHandleSessionByJid()', () => {
7070
});
7171

7272
describe('handlePropose()', () => {
73-
it('should ignore the propose if mediaHandling is "autoAnswerOnly" and autoAnswer is false', async () => {
73+
it('should ignore the propose if mediaHandling is a reducedMedia variant and autoAnswer is false', async () => {
7474
const superSpyHandlePropose = jest.spyOn(BaseSessionHandler.prototype, 'handlePropose');
7575
const superSpyProceed = jest.spyOn(BaseSessionHandler.prototype, 'proceedWithSession').mockImplementation();
7676
const spy = jest.fn();
7777
mockSdk.on('pendingSession', spy);
7878
const pendingSession = createPendingSession(SessionTypes.softphone);
7979
pendingSession.autoAnswer = false;
80-
mockSdk._mediaHandling = MediaHandling.autoAnswerOnly;
8180

81+
mockSdk._mediaHandling = MediaHandling.reducedMediaHeadsets;
8282
await handler.handlePropose(pendingSession);
83+
expect(spy).not.toHaveBeenCalled();
84+
expect(superSpyHandlePropose).not.toHaveBeenCalled();
85+
expect(superSpyProceed).not.toHaveBeenCalled();
8386

87+
mockSdk._mediaHandling = MediaHandling.reducedMediaNoHeadsets;
88+
await handler.handlePropose(pendingSession);
8489
expect(spy).not.toHaveBeenCalled();
8590
expect(superSpyHandlePropose).not.toHaveBeenCalled();
8691
expect(superSpyProceed).not.toHaveBeenCalled();
8792
});
8893

89-
it('should not autoAnswer if mediaHandling is "autoAnswerOnly", autoAnswer is true, but disableAutoAnswer is configured', async () => {
94+
it('should not autoAnswer if mediaHandling is reducedMedia, autoAnswer is true, but disableAutoAnswer is configured', async () => {
9095
const superSpyHandlePropose = jest.spyOn(BaseSessionHandler.prototype, 'handlePropose');
9196
const superSpyProceed = jest.spyOn(BaseSessionHandler.prototype, 'proceedWithSession').mockImplementation();
9297
const spy = jest.fn();
9398
mockSdk.on('pendingSession', spy);
9499
mockSdk._config.disableAutoAnswer = true;
95100
const pendingSession = createPendingSession(SessionTypes.softphone);
96101
pendingSession.autoAnswer = true;
97-
mockSdk._mediaHandling = MediaHandling.autoAnswerOnly;
102+
mockSdk._mediaHandling = MediaHandling.reducedMediaHeadsets;
98103

99104
await handler.handlePropose(pendingSession);
100105

@@ -103,15 +108,15 @@ describe('handlePropose()', () => {
103108
expect(superSpyProceed).not.toHaveBeenCalled();
104109
});
105110

106-
it('should emit pending session and proceed immediately if mediaHandling is "autoAnswerOnly", autoAnswer is true, and disableAutoAnswer is not configured', async () => {
111+
it('should emit pending session and proceed immediately if mediaHandling is reducedMedia, autoAnswer is true, and disableAutoAnswer is not configured', async () => {
107112
const superSpyHandlePropose = jest.spyOn(BaseSessionHandler.prototype, 'handlePropose');
108113
const superSpyProceed = jest.spyOn(BaseSessionHandler.prototype, 'proceedWithSession').mockImplementation();
109114
const spy = jest.fn();
110115
mockSdk.on('pendingSession', spy);
111116
mockSdk._config.disableAutoAnswer = false;
112117
const pendingSession = createPendingSession(SessionTypes.softphone);
113118
pendingSession.autoAnswer = true;
114-
mockSdk._mediaHandling = MediaHandling.autoAnswerOnly;
119+
mockSdk._mediaHandling = MediaHandling.reducedMediaNoHeadsets;
115120

116121
await handler.handlePropose(pendingSession);
117122

0 commit comments

Comments
 (0)