diff --git a/changelog.md b/changelog.md index 843bd0dc..c901424a 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). # [Unreleased](https://github.com/MyPureCloud/genesys-cloud-webrtc-sdk/compare/v13.0.0...HEAD) +### Added +* [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. # [v13.0.0](https://github.com/MyPureCloud/genesys-cloud-webrtc-sdk/compare/v12.1.0...HEAD) ### Breaking Changes diff --git a/src/client.ts b/src/client.ts index f142c5f4..46339642 100644 --- a/src/client.ts +++ b/src/client.ts @@ -37,13 +37,14 @@ import { } from './client-private'; import { requestApi, createAndEmitSdkError, defaultConfigOption, requestApiWithRetry } from './utils'; import { setupLogging } from './logging'; -import { SdkErrorTypes, SessionTypes } from './types/enums'; +import { MediaHandling, SdkErrorTypes, SessionTypes } from './types/enums'; import { SessionManager } from './sessions/session-manager'; import { SdkMedia } from './media/media'; import { HeadsetProxyService } from './headsets/headset'; import { Constants } from 'stanza'; import { setupWebrtcForWindows11 } from './windows11-first-session-hack'; import { ISdkHeadsetService } from './headsets/headset-types'; +import { SoftphoneSessionHandler } from '.'; const ENVIRONMENTS = [ 'mypurecloud.com', @@ -111,6 +112,7 @@ export class GenesysCloudWebrtcSdk extends (EventEmitter as { new(): StrictEvent _customerData: ICustomerData; _hasConnected: boolean; _config: ISdkFullConfig; + _mediaHandling = MediaHandling.standardMedia; get isInitialized (): boolean { return !!this._streamingConnection; @@ -862,6 +864,51 @@ export class GenesysCloudWebrtcSdk extends (EventEmitter as { new(): StrictEvent this.media.setDefaultAudioStream(stream); } + /** + * **Genesys internal use only** - non-Genesys apps may experience unexpected behavior. + * + * Change the media handling for softphone sessions, which should be + * be chosen based on the alerting leader status of the consuming client. + * + * If media handling is reduced, idle persistent connections will be + * disconnected. + * + * @param mediaHandling how softphone media should be handled + */ + setMediaHandling (mediaHandling: MediaHandling): void { + const useHeadsets = !(mediaHandling === MediaHandling.reducedMedia); + + if (!this.sessionManager) { + this._mediaHandling = mediaHandling; + this.setUseHeadsets(useHeadsets); + return; + } + + const activeConversations = this.sessionManager.getAllActiveConversations(); + + if (activeConversations.length !== 0 && !useHeadsets) { + this._mediaHandling = MediaHandling.reducedMediaHeadsets; + this.setUseHeadsets(true); + throw createAndEmitSdkError.call(this, SdkErrorTypes.not_supported, 'Cannot downgrade media handling to stop using headsets during an active call'); + } + + this._mediaHandling = mediaHandling; + this.setUseHeadsets(useHeadsets); + + const reduceMediaHandling = mediaHandling === MediaHandling.reducedMediaHeadsets || mediaHandling === MediaHandling.reducedMedia; + if (reduceMediaHandling) { + const conversationSessionIds = activeConversations.map(conversation => conversation.sessionId); + // Disconnect connections that aren't associated with an active conversation. + // When a client is no longer the alerting leader, it needs to give up any + // persistent connections. Active calls should be maintained. + this.sessionManager.getAllSessions().forEach(session => { + if (session.sessionType === SessionTypes.softphone && !conversationSessionIds.includes(session.id)) { + this.sessionManager.forceTerminateSession(session.id); + } + }); + } + } + /** * Accept a pending session based on the passed in conversation ID. * diff --git a/src/headsets/headset.ts b/src/headsets/headset.ts index 4b1b9cf6..8550fbc4 100644 --- a/src/headsets/headset.ts +++ b/src/headsets/headset.ts @@ -9,6 +9,7 @@ import { SdkHeadsetService } from './sdk-headset-service'; import { HeadsetRequestType } from '../types/interfaces'; import { ExpandedConsumedHeadsetEvents, ISdkHeadsetService, OrchestrationState } from './headset-types'; import { HeadsetChangesQueue } from './headset-utils'; +import { MediaHandling } from '../types/enums'; const REQUEST_PRIORITY: {[key in HeadsetControlsRequestType]: number} = { 'mediaHelper': 30, @@ -48,10 +49,15 @@ export class HeadsetProxyService implements ISdkHeadsetService { // TODO: PCM-2060 - remove this this.useHeadsetOrchestration = !this.sdk._config.disableHeadsetControlsOrchestration; + if (this.sdk._mediaHandling === MediaHandling.reducedMedia) { + 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.'); + useHeadsets = false; + } + // currently only softphone is supported const headsetsIsSupported = this.sdk._config.allowedSessionTypes.includes(SessionTypes.softphone); if (useHeadsets && !headsetsIsSupported) { - this.sdk.logger.warn('setUseHeadsets was called with `true` but headsets are not supported in this configuration. Not activating headsets.'); + this.sdk.logger.warn('setUseHeadsets was called with `true` but headsets are not supported in this configuration - headset is not supported. Not activating headsets.'); useHeadsets = false; } @@ -141,14 +147,21 @@ export class HeadsetProxyService implements ISdkHeadsetService { this.sdk.logger.info('Starting headsetCallControls orchestration'); + let requestType: HeadsetControlsRequestType; + if (this.sdk._mediaHandling === MediaHandling.alertingLeaderMedia) { + requestType = 'prioritized'; + } else { + requestType = this.sdk._config.headsetRequestType || 'standard'; + } + const headsetControlsRequest: HeadsetControlsRequest = { jsonrpc: '2.0', method: 'headsetControlsRequest', params: { - requestType: this.sdk._config.headsetRequestType || 'standard' + requestType } }; - + this.sdk._streamingConnection.messenger.broadcastMessage({ mediaMessage: headsetControlsRequest }); @@ -163,7 +176,7 @@ export class HeadsetProxyService implements ISdkHeadsetService { if (state === this.orchestrationState && !forceUpdate) { return; } - + this.sdk.logger.debug('Headset Orchestration state change', { oldState: this.orchestrationState, newState: state }); if (state === 'alternativeClient') { @@ -188,7 +201,7 @@ export class HeadsetProxyService implements ISdkHeadsetService { if (msg.fromMyClient) { return; } - + switch(msg.mediaMessage.method) { case 'headsetControlsRequest': this.handleHeadsetControlsRequest(msg); @@ -217,6 +230,21 @@ export class HeadsetProxyService implements ISdkHeadsetService { const mediaMessage = msg.mediaMessage as HeadsetControlsRequest; this.sdk.logger.debug('Received headsetControlsRequest message', { requestType: mediaMessage.params.requestType }); + if (this.sdk._mediaHandling === MediaHandling.alertingLeaderMedia) { + // we still yield to media-helper + if (this.getRequestPriority(mediaMessage.params.requestType) === this.getRequestPriority('mediaHelper')) { + this.sdk.logger.info('Handling alerting leader media, but yielding headset controls to media-helper', { requestType: mediaMessage.params.requestType }); + this.setOrchestrationState('alternativeClient'); + } else if (this.getRequestPriority(mediaMessage.params.requestType) === this.getRequestPriority('prioritized')) { + this.sdk.logger.info('Currently handling alerting leader media, but yielding headset controls to new alerting leader', { requestType: mediaMessage.params.requestType }); + this.setOrchestrationState('alternativeClient'); + } else { + this.sendControlsRejectionMessage(msg, 'priority'); + } + + return; + } + // if incoming request is lower priority, reject if (this.getRequestPriority(mediaMessage.params.requestType) < this.getRequestPriority(this.sdk._config.headsetRequestType)) { this.sendControlsRejectionMessage(msg, this.sdk._config.headsetRequestType === 'mediaHelper' ? 'mediaHelper' : 'priority'); @@ -311,7 +339,7 @@ export class HeadsetProxyService implements ISdkHeadsetService { endAllCalls (): Promise { return this.currentHeadsetService.endAllCalls(); } - + answerIncomingCall (conversationId: string, autoAnswer: boolean): Promise { return this.currentHeadsetService.answerIncomingCall(conversationId, autoAnswer); } @@ -331,4 +359,4 @@ export class HeadsetProxyService implements ISdkHeadsetService { resetHeadsetStateForCall(conversationId: string): Promise { return this.currentHeadsetService.resetHeadsetStateForCall(conversationId); } -} \ No newline at end of file +} diff --git a/src/sessions/softphone-session-handler.ts b/src/sessions/softphone-session-handler.ts index 091dbbc6..55d361ac 100644 --- a/src/sessions/softphone-session-handler.ts +++ b/src/sessions/softphone-session-handler.ts @@ -19,7 +19,7 @@ import { PersistentConnectionEvent, HawkNotification } from '../types/interfaces'; -import { SessionTypes, SdkErrorTypes, JingleReasons, CommunicationStates } from '../types/enums'; +import { SessionTypes, SdkErrorTypes, JingleReasons, CommunicationStates, MediaHandling } from '../types/enums'; import { attachAudioMedia, logDeviceChange, createUniqueAudioMediaElement } from '../media/media-utils'; import { requestApi, isSoftphoneJid, createAndEmitSdkError, isPeerConnectionDisconnected } from '../utils'; import { HeadsetChangesQueue } from '../headsets/headset-utils'; @@ -150,31 +150,51 @@ export class SoftphoneSessionHandler extends BaseSessionHandler { const isPrivAnswerAuto = pendingSession.privAnswerMode === 'Auto'; const eagerConnectionEstablishmentMode = this.sdk._config.eagerPersistentConnectionEstablishment; const logInfo = { sessionId: pendingSession?.id, conversationId: pendingSession.conversationId }; + const reducedMediaHandling = this.sdk._mediaHandling === MediaHandling.reducedMediaHeadsets || this.sdk._mediaHandling === MediaHandling.reducedMedia; - if (isPrivAnswerAuto) { - this.log('info', 'received a propose with privAnswerMode=Auto', logInfo); + if (reducedMediaHandling) { + this.log('info', 'received a propose while the SDK is configured for reduced media handling', logInfo); - if (eagerConnectionEstablishmentMode === 'none') { - this.log('info', 'eagerPersistentConnectionEstablishment is "none" so propose with privAnswerMode=Auto will be ignored', logInfo); - return; - } else if (eagerConnectionEstablishmentMode === 'auto') { - // we don't need to emit a pendingSession event when we auto-answer eager persistent connections - return await this.proceedWithSession(pendingSession); - } else { + if (pendingSession.autoAnswer) { + // emit the pendingSession event await super.handlePropose(pendingSession); - } - } else { - // we want to emit the pendingSession event in all other cases - await super.handlePropose(pendingSession); - // calls will can be marked as auto-answer or priv-answer-mode: Auto, but never both - if (pendingSession.autoAnswer) { if (this.sdk._config.disableAutoAnswer) { // It is possible that the consuming client has its own logic for auto-answering calls (e.g. web-dir). this.log('info', 'received an autoAnswer tagged propose but the SDK was configured to not auto-answer, deferring to the consuming client.', logInfo); } else { await this.proceedWithSession(pendingSession); } + } else { + this.log('info', 'media handling is reduced, but this propose is not marked as autoAnswer and will be ignored', logInfo); + return; + } + } else { + if (isPrivAnswerAuto) { + this.log('info', 'received a propose with privAnswerMode=Auto', logInfo); + + if (eagerConnectionEstablishmentMode === 'none') { + this.log('info', 'eagerPersistentConnectionEstablishment is "none" so propose with privAnswerMode=Auto will be ignored', logInfo); + return; + } else if (eagerConnectionEstablishmentMode === 'auto') { + // we don't need to emit a pendingSession event when we auto-answer eager persistent connections + return await this.proceedWithSession(pendingSession); + } else { + await super.handlePropose(pendingSession); + } + } else { + // we want to emit the pendingSession event in all other cases + await super.handlePropose(pendingSession); + + // calls will can be marked as auto-answer or priv-answer-mode: Auto, but never both + if (pendingSession.autoAnswer) { + if (this.sdk._config.disableAutoAnswer) { + // It is possible that the consuming client has its own logic for auto-answering calls (e.g. web-dir). + this.log('info', 'received an autoAnswer tagged propose but the SDK was configured to not auto-answer, deferring to the consuming client.', logInfo); + } else { + await this.proceedWithSession(pendingSession); + } + } } } } diff --git a/src/types/enums.ts b/src/types/enums.ts index 772f1591..40e78f23 100644 --- a/src/types/enums.ts +++ b/src/types/enums.ts @@ -44,3 +44,27 @@ export enum JingleReasons { connectivityError = 'connectivity-error', alternativeSession = 'alternative-session' } + +/** These currently only affect softphone media */ +export enum MediaHandling { + /** Handle all media; headset controls use traditional orchestration */ + standardMedia = 'standard-media', + /** Handle all media; headset controls follow alerting leader */ + alertingLeaderMedia = 'alerting-leader-media', + /** + * Handle some media (see below); headset controls are not used + * + * - New eager persistent connections will be ignored. + * - Auto-answer calls will be handled, which could result in a + * persistent connection being established. + */ + reducedMedia = 'reduced-media', + /** + * SDK internal use only. Handle some media (see below); headset controls use traditional orchestration. + * + * - New eager persistent connections will be ignored. + * - Auto-answer calls will be handled, which could result in a + * persistent connection being established. + */ + reducedMediaHeadsets = 'reduced-media-headsets', +} diff --git a/test/unit/client.test.ts b/test/unit/client.test.ts index 616e3333..298e7f42 100644 --- a/test/unit/client.test.ts +++ b/test/unit/client.test.ts @@ -29,7 +29,9 @@ import { IStation, IPersonDetails, ISessionIdAndConversationId, - VideoSessionHandler + VideoSessionHandler, + MediaHandling, + IActiveConversationDescription } from '../../src'; import * as utils from '../../src/utils'; import { RetryPromise } from 'genesys-cloud-streaming-client/dist/es/utils'; @@ -1159,6 +1161,71 @@ describe('Client', () => { }); }); + describe('setMediaHandling()', () => { + it('should just set media handling and headsets if there is no sessionManager yet', () => { + const mockSdk = constructSdk(); + (sdk as any).sessionManager = null; + const useHeadsetsSpy = jest.fn(); + mockSdk.setUseHeadsets = useHeadsetsSpy; + + sdk.setMediaHandling(MediaHandling.reducedMediaHeadsets); + + expect(sdk._mediaHandling).toBe(MediaHandling.reducedMediaHeadsets); + expect(useHeadsetsSpy).toHaveBeenCalledWith(true); + }); + + it('should throw and reduce media handling if new media handling will not use headsets but there is an active conversation', () => { + sdk = constructSdk(); + const conversations = [{ conversationId: 'test-conversation-id' }] as IActiveConversationDescription[]; + sessionManagerMock.getAllActiveConversations.mockReturnValue(conversations); + + expect(() => { + sdk.setMediaHandling(MediaHandling.reducedMedia); + }).toThrow(); + expect(sdk._mediaHandling).toBe(MediaHandling.reducedMediaHeadsets); + }); + + it('should disconnect any sessions not connected to an active conversation when set to a reduced handling of media', () => { + sdk = constructSdk(); + sdk.setUseHeadsets = jest.fn(); + + const mockSession = new MockSession(SessionTypes.softphone); + const conversations = [{ sessionId: mockSession.id }] as IActiveConversationDescription[]; + sessionManagerMock.getAllActiveConversations.mockReturnValue(conversations); + const idleSession = new MockSession(SessionTypes.softphone); + const idleSessionId = idleSession.id; + const sessions = [mockSession, idleSession] as unknown as IExtendedMediaSession[]; + sessionManagerMock.getAllSessions.mockReturnValue(sessions); + const forceTerminateSpy = jest.fn(); + sessionManagerMock.forceTerminateSession = forceTerminateSpy; + + sdk.setMediaHandling(MediaHandling.reducedMediaHeadsets); + + expect(sdk._mediaHandling).toBe(MediaHandling.reducedMediaHeadsets); + expect(forceTerminateSpy).toHaveBeenCalledTimes(1); + expect(forceTerminateSpy).toHaveBeenCalledWith(idleSessionId); + }); + + it('should use headsets when handling any media', () => { + sdk = constructSdk(); + sessionManagerMock.getAllActiveConversations.mockReturnValue([]); + const useHeadsetsSpy = jest.fn(); + sdk.setUseHeadsets = useHeadsetsSpy; + + sdk.setMediaHandling(MediaHandling.standardMedia); + expect(sdk._mediaHandling).toBe(MediaHandling.standardMedia); + expect(useHeadsetsSpy).toHaveBeenCalledWith(true); + + sdk.setMediaHandling(MediaHandling.alertingLeaderMedia); + expect(sdk._mediaHandling).toBe(MediaHandling.alertingLeaderMedia); + expect(useHeadsetsSpy).toHaveBeenCalledWith(true); + + sdk.setMediaHandling(MediaHandling.reducedMediaHeadsets); + expect(sdk._mediaHandling).toBe(MediaHandling.reducedMediaHeadsets); + expect(useHeadsetsSpy).toHaveBeenCalledWith(true); + }); + }); + describe('destroy()', () => { it('should log, end all sessions, remove listeners, destory media, and disconnect ws', async () => { sdk = constructSdk(); diff --git a/test/unit/headset/headset.test.ts b/test/unit/headset/headset.test.ts index fd4ad248..44121b0d 100644 --- a/test/unit/headset/headset.test.ts +++ b/test/unit/headset/headset.test.ts @@ -1,5 +1,5 @@ import { Observable } from 'rxjs'; -import GenesysCloudWebrtSdk, { JingleReason, JingleReasonCondition } from "../../../src"; +import GenesysCloudWebrtSdk, { MediaHandling } from "../../../src"; import HeadsetService, { ConsumedHeadsetEvents } from 'softphone-vendor-headsets'; import { SimpleMockSdk, flushPromises } from '../../test-utils'; import { SdkHeadsetService } from '../../../src/headsets/sdk-headset-service'; @@ -301,6 +301,16 @@ describe('HeadsetProxyService', () => { expect(proxyService['currentHeadsetService']).toBeInstanceOf(SdkHeadsetServiceFake); }); + it('should use fake service if useHeadsets and media is set to reducedMedia', () => { + proxyService['sdk']._mediaHandling = MediaHandling.reducedMedia; + const spy = jest.spyOn(proxyService, 'updateAudioInputDevice'); + + proxyService.setUseHeadsets(true); + + expect(spy).not.toHaveBeenCalled(); + expect(proxyService['currentHeadsetService']).toBeInstanceOf(SdkHeadsetServiceFake); + }); + it('should unsubscribe if currentEventSubscription', () => { const spy = jest.fn(); proxyService['currentEventSubscription'] = { unsubscribe: spy } as any; @@ -359,6 +369,21 @@ describe('HeadsetProxyService', () => { }); }); + describe('startHeadsetOrchestration', () => { + it('should use "prioritized" requestType if handling alerting leader media', async () => { + const broadcastSpy = jest.fn(); + proxyService['sdk']._streamingConnection.messenger.broadcastMessage = broadcastSpy; + proxyService['sdk']._mediaHandling = MediaHandling.alertingLeaderMedia; + const device = { deviceId: 'device1Id', groupId: 'device1GroupId', label: 'device1Label', kind: 'audioinput' } as any; + + await proxyService['startHeadsetOrchestration'](device); + + const expectedRequestSubset = { mediaMessage: { params: { requestType: 'prioritized' } } }; + expect(broadcastSpy).toHaveBeenCalled(); + expect(broadcastSpy.mock.lastCall[0]).toMatchObject(expectedRequestSubset); + }); + }); + describe('handleMediaMessage', () => { let requestSpy: jest.Mock; let rejectionSpy: jest.Mock; @@ -590,6 +615,36 @@ describe('HeadsetProxyService', () => { expect(proxyService['orchestrationState']).toBe('alternativeClient'); }); + it('should not take controls if a media-helper request is received during orchestration even if handling alerting leader media', async () => { + proxyService['sdk']._mediaHandling = MediaHandling.alertingLeaderMedia; + + expect(proxyService['orchestrationWaitTimer']).toBeFalsy(); + const promise = proxyService['startHeadsetOrchestration'](device); + await flushPromises(); + + broadcastSpy.mockReset(); + + expect(proxyService['orchestrationWaitTimer']).toBeTruthy(); + + // request is received 1 second after starting + jest.advanceTimersByTime(1000); + const request: HeadsetControlsRequest = { + jsonrpc: '2.0', + method: 'headsetControlsRequest', + params: { + requestType: 'mediaHelper', + } + }; + triggerMediaMessageEvent({ to: 'to', from: 'from', mediaMessage: request, fromMyClient: false, fromMyUser: true }); + + expect(proxyService['orchestrationState']).toBe('alternativeClient'); + jest.advanceTimersByTime(1500); + + expect(updateAudioSpy).not.toHaveBeenCalled(); + expect(broadcastSpy).not.toHaveBeenCalled() + expect(proxyService['orchestrationState']).toBe('alternativeClient'); + }); + it('should send a rejection if a request is received during orchestration and is lower priority', async () => { expect(proxyService['orchestrationWaitTimer']).toBeFalsy(); @@ -713,6 +768,75 @@ describe('HeadsetProxyService', () => { expect(broadcastSpy).toHaveBeenCalledWith(expect.objectContaining({ mediaMessage: rejection })); }); + it('should send a rejection if a request is received while handling alerting leader media and is lower priority', async () => { + expect(proxyService['orchestrationWaitTimer']).toBeFalsy(); + + proxyService['sdk']._config.headsetRequestType = 'standard'; + proxyService['sdk']._mediaHandling = MediaHandling.alertingLeaderMedia; + + const promise = proxyService['startHeadsetOrchestration'](device); + await flushPromises(); + + broadcastSpy.mockReset(); + + expect(proxyService['orchestrationWaitTimer']).toBeTruthy(); + + // request is received 1 second after starting + jest.advanceTimersByTime(1000); + const request: HeadsetControlsRequest = { + jsonrpc: '2.0', + method: 'headsetControlsRequest', + params: { + requestType: 'standard', + } + }; + triggerMediaMessageEvent({ id:'req1', to: 'to', from: 'from', mediaMessage: request, fromMyClient: false, fromMyUser: true }); + + jest.advanceTimersByTime(1500); + + const rejection: HeadsetControlsRejection = { + jsonrpc: '2.0', + method: 'headsetControlsRejection', + params: { + reason: 'priority', + requestId: 'req1' + } + }; + expect(broadcastSpy).toHaveBeenCalledWith(expect.objectContaining({ mediaMessage: rejection })); + + expect(updateAudioSpy).toHaveBeenCalled(); + const expectedMediaMessage: HeadsetControlsChanged = { + jsonrpc: '2.0', + method: 'headsetControlsChanged', + params: { + hasControls: true + } + }; + expect(broadcastSpy).toHaveBeenCalledWith(expect.objectContaining({ mediaMessage: expectedMediaMessage })); + expect(proxyService['orchestrationState']).toBe('hasControls'); + }); + + it('should not take controls if a prioritized request is received during orchestration even if handling alerting leader media', async () => { + proxyService['sdk']._mediaHandling = MediaHandling.alertingLeaderMedia; + proxyService['orchestrationState'] = 'hasControls'; + + const request: HeadsetControlsRequest = { + jsonrpc: '2.0', + method: 'headsetControlsRequest', + params: { + requestType: 'prioritized', + } + }; + triggerMediaMessageEvent({ to: 'to', from: 'from', mediaMessage: request, fromMyClient: false, fromMyUser: true }); + + expect(proxyService['orchestrationState']).toBe('alternativeClient'); + jest.advanceTimersByTime(1500); + + expect(updateAudioSpy).not.toHaveBeenCalled(); + expect(broadcastSpy).not.toHaveBeenCalled() + expect(proxyService['orchestrationState']).toBe('alternativeClient'); + }); + it('should do nothing if persistent connection but no active session', async () => { expect(proxyService['orchestrationWaitTimer']).toBeFalsy(); diff --git a/test/unit/sessions/softphone-session-handler.test.ts b/test/unit/sessions/softphone-session-handler.test.ts index cadf05b7..1f453ea4 100644 --- a/test/unit/sessions/softphone-session-handler.test.ts +++ b/test/unit/sessions/softphone-session-handler.test.ts @@ -28,7 +28,8 @@ import { SdkErrorTypes, IPendingSession, JingleReasons, - ISessionMuteRequest + ISessionMuteRequest, + MediaHandling } from '../../../src'; import { SessionManager } from '../../../src/sessions/session-manager'; import BaseSessionHandler from '../../../src/sessions/base-session-handler'; @@ -69,6 +70,61 @@ describe('shouldHandleSessionByJid()', () => { }); describe('handlePropose()', () => { + it('should ignore the propose if mediaHandling is a reducedMedia variant and autoAnswer is false', async () => { + const superSpyHandlePropose = jest.spyOn(BaseSessionHandler.prototype, 'handlePropose'); + const superSpyProceed = jest.spyOn(BaseSessionHandler.prototype, 'proceedWithSession').mockImplementation(); + const spy = jest.fn(); + mockSdk.on('pendingSession', spy); + const pendingSession = createPendingSession(SessionTypes.softphone); + pendingSession.autoAnswer = false; + + mockSdk._mediaHandling = MediaHandling.reducedMediaHeadsets; + await handler.handlePropose(pendingSession); + expect(spy).not.toHaveBeenCalled(); + expect(superSpyHandlePropose).not.toHaveBeenCalled(); + expect(superSpyProceed).not.toHaveBeenCalled(); + + mockSdk._mediaHandling = MediaHandling.reducedMedia; + await handler.handlePropose(pendingSession); + expect(spy).not.toHaveBeenCalled(); + expect(superSpyHandlePropose).not.toHaveBeenCalled(); + expect(superSpyProceed).not.toHaveBeenCalled(); + }); + + it('should not autoAnswer if mediaHandling is reducedMedia, autoAnswer is true, but disableAutoAnswer is configured', async () => { + const superSpyHandlePropose = jest.spyOn(BaseSessionHandler.prototype, 'handlePropose'); + const superSpyProceed = jest.spyOn(BaseSessionHandler.prototype, 'proceedWithSession').mockImplementation(); + const spy = jest.fn(); + mockSdk.on('pendingSession', spy); + mockSdk._config.disableAutoAnswer = true; + const pendingSession = createPendingSession(SessionTypes.softphone); + pendingSession.autoAnswer = true; + mockSdk._mediaHandling = MediaHandling.reducedMediaHeadsets; + + await handler.handlePropose(pendingSession); + + expect(spy).toHaveBeenCalled(); + expect(superSpyHandlePropose).toHaveBeenCalled(); + expect(superSpyProceed).not.toHaveBeenCalled(); + }); + + it('should emit pending session and proceed immediately if mediaHandling is reducedMedia, autoAnswer is true, and disableAutoAnswer is not configured', async () => { + const superSpyHandlePropose = jest.spyOn(BaseSessionHandler.prototype, 'handlePropose'); + const superSpyProceed = jest.spyOn(BaseSessionHandler.prototype, 'proceedWithSession').mockImplementation(); + const spy = jest.fn(); + mockSdk.on('pendingSession', spy); + mockSdk._config.disableAutoAnswer = false; + const pendingSession = createPendingSession(SessionTypes.softphone); + pendingSession.autoAnswer = true; + mockSdk._mediaHandling = MediaHandling.reducedMedia; + + await handler.handlePropose(pendingSession); + + expect(spy).toHaveBeenCalled(); + expect(superSpyHandlePropose).toHaveBeenCalled(); + expect(superSpyProceed).toHaveBeenCalled(); + }); + it('should emit pending session and proceed immediately if autoAnswer', async () => { const superSpyHandlePropose = jest.spyOn(BaseSessionHandler.prototype, 'handlePropose'); const superSpyProceed = jest.spyOn(BaseSessionHandler.prototype, 'proceedWithSession').mockImplementation();