Skip to content

Commit 7bb9c54

Browse files
authored
[STREAM-1180] Adjust media handling based on alerting leader (#994)
1 parent 34b9967 commit 7bb9c54

8 files changed

Lines changed: 395 additions & 27 deletions

File tree

changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
44
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
55

66
# [Unreleased](https://github.com/MyPureCloud/genesys-cloud-webrtc-sdk/compare/v13.0.0...HEAD)
7+
### Added
8+
* [STREAM-1180](https://inindca.atlassian.net/browse/STREAM-1180) - Allow for different media handling strategies. This supports alerting leader functionality, where one instance of the SDK needs to handle media, but other instances should not automatically handle media.
79

810
# [v13.0.0](https://github.com/MyPureCloud/genesys-cloud-webrtc-sdk/compare/v12.1.0...HEAD)
911
### Breaking Changes

src/client.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,14 @@ import {
3737
} from './client-private';
3838
import { requestApi, createAndEmitSdkError, defaultConfigOption, requestApiWithRetry } from './utils';
3939
import { setupLogging } from './logging';
40-
import { SdkErrorTypes, SessionTypes } from './types/enums';
40+
import { MediaHandling, SdkErrorTypes, SessionTypes } from './types/enums';
4141
import { SessionManager } from './sessions/session-manager';
4242
import { SdkMedia } from './media/media';
4343
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,6 +112,7 @@ export class GenesysCloudWebrtcSdk extends (EventEmitter as { new(): StrictEvent
111112
_customerData: ICustomerData;
112113
_hasConnected: boolean;
113114
_config: ISdkFullConfig;
115+
_mediaHandling = MediaHandling.standardMedia;
114116

115117
get isInitialized (): boolean {
116118
return !!this._streamingConnection;
@@ -862,6 +864,51 @@ export class GenesysCloudWebrtcSdk extends (EventEmitter as { new(): StrictEvent
862864
this.media.setDefaultAudioStream(stream);
863865
}
864866

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+
*/
878+
setMediaHandling (mediaHandling: MediaHandling): void {
879+
const useHeadsets = !(mediaHandling === MediaHandling.reducedMedia);
880+
881+
if (!this.sessionManager) {
882+
this._mediaHandling = mediaHandling;
883+
this.setUseHeadsets(useHeadsets);
884+
return;
885+
}
886+
887+
const activeConversations = this.sessionManager.getAllActiveConversations();
888+
889+
if (activeConversations.length !== 0 && !useHeadsets) {
890+
this._mediaHandling = MediaHandling.reducedMediaHeadsets;
891+
this.setUseHeadsets(true);
892+
throw createAndEmitSdkError.call(this, SdkErrorTypes.not_supported, 'Cannot downgrade media handling to stop using headsets during an active call');
893+
}
894+
895+
this._mediaHandling = mediaHandling;
896+
this.setUseHeadsets(useHeadsets);
897+
898+
const reduceMediaHandling = mediaHandling === MediaHandling.reducedMediaHeadsets || mediaHandling === MediaHandling.reducedMedia;
899+
if (reduceMediaHandling) {
900+
const conversationSessionIds = activeConversations.map(conversation => conversation.sessionId);
901+
// Disconnect connections that aren't associated with an active conversation.
902+
// When a client is no longer the alerting leader, it needs to give up any
903+
// persistent connections. Active calls should be maintained.
904+
this.sessionManager.getAllSessions().forEach(session => {
905+
if (session.sessionType === SessionTypes.softphone && !conversationSessionIds.includes(session.id)) {
906+
this.sessionManager.forceTerminateSession(session.id);
907+
}
908+
});
909+
}
910+
}
911+
865912
/**
866913
* Accept a pending session based on the passed in conversation ID.
867914
*

src/headsets/headset.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { SdkHeadsetService } from './sdk-headset-service';
99
import { HeadsetRequestType } from '../types/interfaces';
1010
import { ExpandedConsumedHeadsetEvents, ISdkHeadsetService, OrchestrationState } from './headset-types';
1111
import { HeadsetChangesQueue } from './headset-utils';
12+
import { MediaHandling } from '../types/enums';
1213

1314
const REQUEST_PRIORITY: {[key in HeadsetControlsRequestType]: number} = {
1415
'mediaHelper': 30,
@@ -48,10 +49,15 @@ export class HeadsetProxyService implements ISdkHeadsetService {
4849
// TODO: PCM-2060 - remove this
4950
this.useHeadsetOrchestration = !this.sdk._config.disableHeadsetControlsOrchestration;
5051

52+
if (this.sdk._mediaHandling === MediaHandling.reducedMedia) {
53+
this.sdk.logger.warn('setUseHeadsets was called with `true` but media handling is set to `reducedMedia`; headsets are not supported in this configuration - not handling media. Not activating headsets.');
54+
useHeadsets = false;
55+
}
56+
5157
// currently only softphone is supported
5258
const headsetsIsSupported = this.sdk._config.allowedSessionTypes.includes(SessionTypes.softphone);
5359
if (useHeadsets && !headsetsIsSupported) {
54-
this.sdk.logger.warn('setUseHeadsets was called with `true` but headsets are not supported in this configuration. Not activating headsets.');
60+
this.sdk.logger.warn('setUseHeadsets was called with `true` but headsets are not supported in this configuration - headset is not supported. Not activating headsets.');
5561
useHeadsets = false;
5662
}
5763

@@ -141,14 +147,21 @@ export class HeadsetProxyService implements ISdkHeadsetService {
141147

142148
this.sdk.logger.info('Starting headsetCallControls orchestration');
143149

150+
let requestType: HeadsetControlsRequestType;
151+
if (this.sdk._mediaHandling === MediaHandling.alertingLeaderMedia) {
152+
requestType = 'prioritized';
153+
} else {
154+
requestType = this.sdk._config.headsetRequestType || 'standard';
155+
}
156+
144157
const headsetControlsRequest: HeadsetControlsRequest = {
145158
jsonrpc: '2.0',
146159
method: 'headsetControlsRequest',
147160
params: {
148-
requestType: this.sdk._config.headsetRequestType || 'standard'
161+
requestType
149162
}
150163
};
151-
164+
152165
this.sdk._streamingConnection.messenger.broadcastMessage({
153166
mediaMessage: headsetControlsRequest
154167
});
@@ -163,7 +176,7 @@ export class HeadsetProxyService implements ISdkHeadsetService {
163176
if (state === this.orchestrationState && !forceUpdate) {
164177
return;
165178
}
166-
179+
167180
this.sdk.logger.debug('Headset Orchestration state change', { oldState: this.orchestrationState, newState: state });
168181

169182
if (state === 'alternativeClient') {
@@ -188,7 +201,7 @@ export class HeadsetProxyService implements ISdkHeadsetService {
188201
if (msg.fromMyClient) {
189202
return;
190203
}
191-
204+
192205
switch(msg.mediaMessage.method) {
193206
case 'headsetControlsRequest':
194207
this.handleHeadsetControlsRequest(msg);
@@ -217,6 +230,21 @@ export class HeadsetProxyService implements ISdkHeadsetService {
217230
const mediaMessage = msg.mediaMessage as HeadsetControlsRequest;
218231
this.sdk.logger.debug('Received headsetControlsRequest message', { requestType: mediaMessage.params.requestType });
219232

233+
if (this.sdk._mediaHandling === MediaHandling.alertingLeaderMedia) {
234+
// we still yield to media-helper
235+
if (this.getRequestPriority(mediaMessage.params.requestType) === this.getRequestPriority('mediaHelper')) {
236+
this.sdk.logger.info('Handling alerting leader media, but yielding headset controls to media-helper', { requestType: mediaMessage.params.requestType });
237+
this.setOrchestrationState('alternativeClient');
238+
} else if (this.getRequestPriority(mediaMessage.params.requestType) === this.getRequestPriority('prioritized')) {
239+
this.sdk.logger.info('Currently handling alerting leader media, but yielding headset controls to new alerting leader', { requestType: mediaMessage.params.requestType });
240+
this.setOrchestrationState('alternativeClient');
241+
} else {
242+
this.sendControlsRejectionMessage(msg, 'priority');
243+
}
244+
245+
return;
246+
}
247+
220248
// if incoming request is lower priority, reject
221249
if (this.getRequestPriority(mediaMessage.params.requestType) < this.getRequestPriority(this.sdk._config.headsetRequestType)) {
222250
this.sendControlsRejectionMessage(msg, this.sdk._config.headsetRequestType === 'mediaHelper' ? 'mediaHelper' : 'priority');
@@ -311,7 +339,7 @@ export class HeadsetProxyService implements ISdkHeadsetService {
311339
endAllCalls (): Promise<void> {
312340
return this.currentHeadsetService.endAllCalls();
313341
}
314-
342+
315343
answerIncomingCall (conversationId: string, autoAnswer: boolean): Promise<void> {
316344
return this.currentHeadsetService.answerIncomingCall(conversationId, autoAnswer);
317345
}
@@ -331,4 +359,4 @@ export class HeadsetProxyService implements ISdkHeadsetService {
331359
resetHeadsetStateForCall(conversationId: string): Promise<void> {
332360
return this.currentHeadsetService.resetHeadsetStateForCall(conversationId);
333361
}
334-
}
362+
}

src/sessions/softphone-session-handler.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
PersistentConnectionEvent,
2020
HawkNotification
2121
} from '../types/interfaces';
22-
import { SessionTypes, SdkErrorTypes, JingleReasons, CommunicationStates } from '../types/enums';
22+
import { SessionTypes, SdkErrorTypes, JingleReasons, CommunicationStates, MediaHandling } from '../types/enums';
2323
import { attachAudioMedia, logDeviceChange, createUniqueAudioMediaElement } from '../media/media-utils';
2424
import { requestApi, isSoftphoneJid, createAndEmitSdkError, isPeerConnectionDisconnected } from '../utils';
2525
import { HeadsetChangesQueue } from '../headsets/headset-utils';
@@ -150,31 +150,51 @@ export class SoftphoneSessionHandler extends BaseSessionHandler {
150150
const isPrivAnswerAuto = pendingSession.privAnswerMode === 'Auto';
151151
const eagerConnectionEstablishmentMode = this.sdk._config.eagerPersistentConnectionEstablishment;
152152
const logInfo = { sessionId: pendingSession?.id, conversationId: pendingSession.conversationId };
153+
const reducedMediaHandling = this.sdk._mediaHandling === MediaHandling.reducedMediaHeadsets || this.sdk._mediaHandling === MediaHandling.reducedMedia;
153154

154-
if (isPrivAnswerAuto) {
155-
this.log('info', 'received a propose with privAnswerMode=Auto', logInfo);
155+
if (reducedMediaHandling) {
156+
this.log('info', 'received a propose while the SDK is configured for reduced media handling', logInfo);
156157

157-
if (eagerConnectionEstablishmentMode === 'none') {
158-
this.log('info', 'eagerPersistentConnectionEstablishment is "none" so propose with privAnswerMode=Auto will be ignored', logInfo);
159-
return;
160-
} else if (eagerConnectionEstablishmentMode === 'auto') {
161-
// we don't need to emit a pendingSession event when we auto-answer eager persistent connections
162-
return await this.proceedWithSession(pendingSession);
163-
} else {
158+
if (pendingSession.autoAnswer) {
159+
// emit the pendingSession event
164160
await super.handlePropose(pendingSession);
165-
}
166-
} else {
167-
// we want to emit the pendingSession event in all other cases
168-
await super.handlePropose(pendingSession);
169161

170-
// calls will can be marked as auto-answer or priv-answer-mode: Auto, but never both
171-
if (pendingSession.autoAnswer) {
172162
if (this.sdk._config.disableAutoAnswer) {
173163
// It is possible that the consuming client has its own logic for auto-answering calls (e.g. web-dir).
174164
this.log('info', 'received an autoAnswer tagged propose but the SDK was configured to not auto-answer, deferring to the consuming client.', logInfo);
175165
} else {
176166
await this.proceedWithSession(pendingSession);
177167
}
168+
} else {
169+
this.log('info', 'media handling is reduced, but this propose is not marked as autoAnswer and will be ignored', logInfo);
170+
return;
171+
}
172+
} else {
173+
if (isPrivAnswerAuto) {
174+
this.log('info', 'received a propose with privAnswerMode=Auto', logInfo);
175+
176+
if (eagerConnectionEstablishmentMode === 'none') {
177+
this.log('info', 'eagerPersistentConnectionEstablishment is "none" so propose with privAnswerMode=Auto will be ignored', logInfo);
178+
return;
179+
} else if (eagerConnectionEstablishmentMode === 'auto') {
180+
// we don't need to emit a pendingSession event when we auto-answer eager persistent connections
181+
return await this.proceedWithSession(pendingSession);
182+
} else {
183+
await super.handlePropose(pendingSession);
184+
}
185+
} else {
186+
// we want to emit the pendingSession event in all other cases
187+
await super.handlePropose(pendingSession);
188+
189+
// calls will can be marked as auto-answer or priv-answer-mode: Auto, but never both
190+
if (pendingSession.autoAnswer) {
191+
if (this.sdk._config.disableAutoAnswer) {
192+
// It is possible that the consuming client has its own logic for auto-answering calls (e.g. web-dir).
193+
this.log('info', 'received an autoAnswer tagged propose but the SDK was configured to not auto-answer, deferring to the consuming client.', logInfo);
194+
} else {
195+
await this.proceedWithSession(pendingSession);
196+
}
197+
}
178198
}
179199
}
180200
}

src/types/enums.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,27 @@ export enum JingleReasons {
4444
connectivityError = 'connectivity-error',
4545
alternativeSession = 'alternative-session'
4646
}
47+
48+
/** These currently only affect softphone media */
49+
export enum MediaHandling {
50+
/** Handle all media; headset controls use traditional orchestration */
51+
standardMedia = 'standard-media',
52+
/** Handle all media; headset controls follow alerting leader */
53+
alertingLeaderMedia = 'alerting-leader-media',
54+
/**
55+
* Handle some media (see below); headset controls are not used
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+
reducedMedia = 'reduced-media',
62+
/**
63+
* SDK internal use only. Handle some media (see below); headset controls use traditional orchestration.
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+
reducedMediaHeadsets = 'reduced-media-headsets',
70+
}

test/unit/client.test.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ import {
2929
IStation,
3030
IPersonDetails,
3131
ISessionIdAndConversationId,
32-
VideoSessionHandler
32+
VideoSessionHandler,
33+
MediaHandling,
34+
IActiveConversationDescription
3335
} from '../../src';
3436
import * as utils from '../../src/utils';
3537
import { RetryPromise } from 'genesys-cloud-streaming-client/dist/es/utils';
@@ -1159,6 +1161,71 @@ describe('Client', () => {
11591161
});
11601162
});
11611163

1164+
describe('setMediaHandling()', () => {
1165+
it('should just set media handling and headsets if there is no sessionManager yet', () => {
1166+
const mockSdk = constructSdk();
1167+
(sdk as any).sessionManager = null;
1168+
const useHeadsetsSpy = jest.fn();
1169+
mockSdk.setUseHeadsets = useHeadsetsSpy;
1170+
1171+
sdk.setMediaHandling(MediaHandling.reducedMediaHeadsets);
1172+
1173+
expect(sdk._mediaHandling).toBe(MediaHandling.reducedMediaHeadsets);
1174+
expect(useHeadsetsSpy).toHaveBeenCalledWith(true);
1175+
});
1176+
1177+
it('should throw and reduce media handling if new media handling will not use headsets but there is an active conversation', () => {
1178+
sdk = constructSdk();
1179+
const conversations = [{ conversationId: 'test-conversation-id' }] as IActiveConversationDescription[];
1180+
sessionManagerMock.getAllActiveConversations.mockReturnValue(conversations);
1181+
1182+
expect(() => {
1183+
sdk.setMediaHandling(MediaHandling.reducedMedia);
1184+
}).toThrow();
1185+
expect(sdk._mediaHandling).toBe(MediaHandling.reducedMediaHeadsets);
1186+
});
1187+
1188+
it('should disconnect any sessions not connected to an active conversation when set to a reduced handling of media', () => {
1189+
sdk = constructSdk();
1190+
sdk.setUseHeadsets = jest.fn();
1191+
1192+
const mockSession = new MockSession(SessionTypes.softphone);
1193+
const conversations = [{ sessionId: mockSession.id }] as IActiveConversationDescription[];
1194+
sessionManagerMock.getAllActiveConversations.mockReturnValue(conversations);
1195+
const idleSession = new MockSession(SessionTypes.softphone);
1196+
const idleSessionId = idleSession.id;
1197+
const sessions = [mockSession, idleSession] as unknown as IExtendedMediaSession[];
1198+
sessionManagerMock.getAllSessions.mockReturnValue(sessions);
1199+
const forceTerminateSpy = jest.fn();
1200+
sessionManagerMock.forceTerminateSession = forceTerminateSpy;
1201+
1202+
sdk.setMediaHandling(MediaHandling.reducedMediaHeadsets);
1203+
1204+
expect(sdk._mediaHandling).toBe(MediaHandling.reducedMediaHeadsets);
1205+
expect(forceTerminateSpy).toHaveBeenCalledTimes(1);
1206+
expect(forceTerminateSpy).toHaveBeenCalledWith(idleSessionId);
1207+
});
1208+
1209+
it('should use headsets when handling any media', () => {
1210+
sdk = constructSdk();
1211+
sessionManagerMock.getAllActiveConversations.mockReturnValue([]);
1212+
const useHeadsetsSpy = jest.fn();
1213+
sdk.setUseHeadsets = useHeadsetsSpy;
1214+
1215+
sdk.setMediaHandling(MediaHandling.standardMedia);
1216+
expect(sdk._mediaHandling).toBe(MediaHandling.standardMedia);
1217+
expect(useHeadsetsSpy).toHaveBeenCalledWith(true);
1218+
1219+
sdk.setMediaHandling(MediaHandling.alertingLeaderMedia);
1220+
expect(sdk._mediaHandling).toBe(MediaHandling.alertingLeaderMedia);
1221+
expect(useHeadsetsSpy).toHaveBeenCalledWith(true);
1222+
1223+
sdk.setMediaHandling(MediaHandling.reducedMediaHeadsets);
1224+
expect(sdk._mediaHandling).toBe(MediaHandling.reducedMediaHeadsets);
1225+
expect(useHeadsetsSpy).toHaveBeenCalledWith(true);
1226+
});
1227+
});
1228+
11621229
describe('destroy()', () => {
11631230
it('should log, end all sessions, remove listeners, destory media, and disconnect ws', async () => {
11641231
sdk = constructSdk();

0 commit comments

Comments
 (0)