diff --git a/packages/calling/src/CallingClient/calling/call.test.ts b/packages/calling/src/CallingClient/calling/call.test.ts index 67742241c19..bf0b7375548 100644 --- a/packages/calling/src/CallingClient/calling/call.test.ts +++ b/packages/calling/src/CallingClient/calling/call.test.ts @@ -8,7 +8,12 @@ import * as Utils from '../../common/Utils'; import {CALL_EVENT_KEYS, CallEvent, RoapEvent, RoapMessage} from '../../Events/types'; import {DEFAULT_SESSION_TIMER, ICE_CANDIDATES_TIMEOUT} from '../constants'; import {CallDirection, CallType, ServiceIndicator, WebexRequestPayload} from '../../common/types'; -import {METRIC_EVENT, TRANSFER_ACTION, METRIC_TYPE} from '../../Metrics/types'; +import { + METRIC_EVENT, + TRANSFER_ACTION, + METRIC_TYPE, + MEDIA_CONNECTION_ACTION, +} from '../../Metrics/types'; import {Call, createCall} from './call'; import { MobiusCallState, @@ -452,6 +457,262 @@ describe('Call Tests', () => { ); }); + it('registers and unregisters media connection listeners with stable handlers', () => { + const mockStream = { + outputStream: { + getAudioTracks: jest.fn().mockReturnValue([mockTrack]), + }, + on: jest.fn(), + getEffectByKind: jest.fn().mockReturnValue(undefined), + }; + const localAudioStream = mockStream as unknown as InternalMediaCoreModule.LocalMicrophoneStream; + const call = createCall( + activeUrl, + webex, + CallDirection.OUTBOUND, + deviceId, + mockLineId, + deleteCallFromCollection, + defaultServiceIndicator, + dest + ); + + call.dial(localAudioStream); + + const mediaOnMock = call['mediaConnection'].on as jest.Mock; + const mediaOffSpy = jest.spyOn(call['mediaConnection'], 'off'); + + expect(mediaOnMock).toBeCalledWith( + InternalMediaCoreModule.MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND, + expect.any(Function) + ); + expect(mediaOnMock).toBeCalledWith( + InternalMediaCoreModule.MediaConnectionEventNames.ROAP_FAILURE, + expect.any(Function) + ); + expect(mediaOnMock).toBeCalledWith( + InternalMediaCoreModule.MediaConnectionEventNames.REMOTE_TRACK_ADDED, + expect.any(Function) + ); + expect(mediaOnMock).toBeCalledWith( + InternalMediaCoreModule.MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED, + expect.any(Function) + ); + expect(mediaOnMock).toBeCalledWith( + InternalMediaCoreModule.MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED, + expect.any(Function) + ); + expect(mediaOnMock).toBeCalledWith( + InternalMediaCoreModule.MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED, + expect.any(Function) + ); + expect(mediaOnMock).toBeCalledWith( + InternalMediaCoreModule.MediaConnectionEventNames.ICE_CANDIDATE_ERROR, + expect.any(Function) + ); + + call['unregisterMediaConnectionListeners'](); + + expect(mediaOffSpy).toBeCalledWith( + InternalMediaCoreModule.MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND, + expect.any(Function) + ); + expect(mediaOffSpy).toBeCalledWith( + InternalMediaCoreModule.MediaConnectionEventNames.ROAP_FAILURE, + expect.any(Function) + ); + expect(mediaOffSpy).toBeCalledWith( + InternalMediaCoreModule.MediaConnectionEventNames.REMOTE_TRACK_ADDED, + expect.any(Function) + ); + expect(mediaOffSpy).toBeCalledWith( + InternalMediaCoreModule.MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED, + expect.any(Function) + ); + expect(mediaOffSpy).toBeCalledWith( + InternalMediaCoreModule.MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED, + expect.any(Function) + ); + expect(mediaOffSpy).toBeCalledWith( + InternalMediaCoreModule.MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED, + expect.any(Function) + ); + expect(mediaOffSpy).toBeCalledWith( + InternalMediaCoreModule.MediaConnectionEventNames.ICE_CANDIDATE_ERROR, + expect.any(Function) + ); + }); + + it('handles ICE listener payloads and submits metrics with event names', () => { + const mockStream = { + outputStream: { + getAudioTracks: jest.fn().mockReturnValue([mockTrack]), + }, + on: jest.fn(), + getEffectByKind: jest.fn().mockReturnValue(undefined), + }; + const localAudioStream = mockStream as unknown as InternalMediaCoreModule.LocalMicrophoneStream; + const call = createCall( + activeUrl, + webex, + CallDirection.OUTBOUND, + deviceId, + mockLineId, + deleteCallFromCollection, + defaultServiceIndicator, + dest + ); + + call.dial(localAudioStream); + + const metricSpy = jest.spyOn(call['metricManager'], 'submitMediaMetric'); + const warnSpy = jest.spyOn(log, 'warn'); + + const getHandlerForEvent = (eventName: string) => + (call['mediaConnection'].on as jest.Mock).mock.calls.find( + ([name]) => name === eventName + )?.[1]; + + const iceGatheringHandler = getHandlerForEvent( + InternalMediaCoreModule.MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED + ); + const peerConnectionHandler = getHandlerForEvent( + InternalMediaCoreModule.MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED + ); + const iceConnectionHandler = getHandlerForEvent( + InternalMediaCoreModule.MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED + ); + const iceCandidateErrorHandler = getHandlerForEvent( + InternalMediaCoreModule.MediaConnectionEventNames.ICE_CANDIDATE_ERROR + ); + + iceGatheringHandler({iceGatheringState: 'gathering'}); + peerConnectionHandler({connectionState: 'connected'}); + iceConnectionHandler({iceConnectionState: 'completed'}); + iceCandidateErrorHandler({ + errorCode: 701, + errorText: 'STUN host lookup failed', + url: 'stun:example.org:3478', + }); + + expect(metricSpy).toHaveBeenNthCalledWith( + 1, + METRIC_EVENT.MEDIA, + MEDIA_CONNECTION_ACTION.ICE_GATHERING_STATE_CHANGED, + METRIC_TYPE.BEHAVIORAL, + call.getCallId(), + call.getCorrelationId(), + undefined, + undefined, + 'gathering' + ); + expect(metricSpy).toHaveBeenNthCalledWith( + 2, + METRIC_EVENT.MEDIA, + MEDIA_CONNECTION_ACTION.PEER_CONNECTION_STATE_CHANGED, + METRIC_TYPE.BEHAVIORAL, + call.getCallId(), + call.getCorrelationId(), + undefined, + undefined, + 'connected' + ); + expect(metricSpy).toHaveBeenNthCalledWith( + 3, + METRIC_EVENT.MEDIA, + MEDIA_CONNECTION_ACTION.ICE_CONNECTION_STATE_CHANGED, + METRIC_TYPE.BEHAVIORAL, + call.getCallId(), + call.getCorrelationId(), + undefined, + undefined, + 'completed' + ); + expect(metricSpy).toHaveBeenNthCalledWith( + 4, + METRIC_EVENT.MEDIA_ERROR, + MEDIA_CONNECTION_ACTION.ICE_CANDIDATE_ERROR, + METRIC_TYPE.BEHAVIORAL, + call.getCallId(), + call.getCorrelationId(), + undefined, + undefined, + undefined, + expect.any(CallError) + ); + const mediaErrorCall = metricSpy.mock.calls[3]; + + expect((mediaErrorCall[mediaErrorCall.length - 1] as CallError).getCallError().message).toBe( + 'ICE candidate error occurred: {"address":null,"errorCode":701,"errorText":"STUN host lookup failed","port":null,"url":"stun:example.org:3478"}' + ); + + expect(warnSpy).toHaveBeenCalledWith( + 'ICE candidate error occurred: {"address":null,"errorCode":701,"errorText":"STUN host lookup failed","port":null,"url":"stun:example.org:3478"}', + {file: 'call', method: 'mediaIceEventsListener'} + ); + }); + + it('handles ROAP failure listener and submits media error metric', () => { + const mockStream = { + outputStream: { + getAudioTracks: jest.fn().mockReturnValue([mockTrack]), + }, + on: jest.fn(), + getEffectByKind: jest.fn().mockReturnValue(undefined), + }; + const localAudioStream = mockStream as unknown as InternalMediaCoreModule.LocalMicrophoneStream; + const call = createCall( + activeUrl, + webex, + CallDirection.OUTBOUND, + deviceId, + mockLineId, + deleteCallFromCollection, + defaultServiceIndicator, + dest + ); + + call.dial(localAudioStream); + + const metricSpy = jest.spyOn(call['metricManager'], 'submitMediaMetric'); + const warnSpy = jest.spyOn(log, 'warn'); + const roapFailureHandler = (call['mediaConnection'].on as jest.Mock).mock.calls.find( + ([name]) => name === InternalMediaCoreModule.MediaConnectionEventNames.ROAP_FAILURE + )?.[1]; + + const roapFailure = new Error('Failed to process remote SDP'); + + roapFailureHandler(roapFailure); + + expect(metricSpy).toHaveBeenCalledWith( + METRIC_EVENT.MEDIA_ERROR, + MEDIA_CONNECTION_ACTION.ROAP_FAILURE, + METRIC_TYPE.BEHAVIORAL, + call.getCallId(), + call.getCorrelationId(), + undefined, + undefined, + undefined, + expect.any(CallError) + ); + + const roapFailureMetricCall = metricSpy.mock.calls.find( + ([name, metricAction]) => + name === METRIC_EVENT.MEDIA_ERROR && metricAction === MEDIA_CONNECTION_ACTION.ROAP_FAILURE + ); + + expect(roapFailureMetricCall).toBeDefined(); + + expect( + (roapFailureMetricCall?.[roapFailureMetricCall.length - 1] as CallError).getCallError() + .message + ).toBe('ROAP failure occurred: Failed to process remote SDP'); + expect(warnSpy).toHaveBeenCalledWith('ROAP failure occurred: Failed to process remote SDP', { + file: 'call', + method: 'mediaRoapEventsListener', + }); + }); + it('sends connect before ROAP answer when inbound offer is delayed', async () => { const mockStream = { outputStream: { diff --git a/packages/calling/src/CallingClient/calling/call.ts b/packages/calling/src/CallingClient/calling/call.ts index 3cc6c910ef8..18294ce898d 100644 --- a/packages/calling/src/CallingClient/calling/call.ts +++ b/packages/calling/src/CallingClient/calling/call.ts @@ -93,7 +93,13 @@ import { import log from '../../Logger'; import {ICallerId} from './CallerId/types'; import {createCallerId} from './CallerId'; -import {IMetricManager, METRIC_TYPE, METRIC_EVENT, TRANSFER_ACTION} from '../../Metrics/types'; +import { + IMetricManager, + METRIC_TYPE, + METRIC_EVENT, + TRANSFER_ACTION, + MEDIA_CONNECTION_ACTION, +} from '../../Metrics/types'; import {getMetricManager} from '../../Metrics'; import {METHOD_START_MESSAGE, SERVICES_ENDPOINT} from '../../common/constants'; @@ -101,6 +107,8 @@ import {METHOD_START_MESSAGE, SERVICES_ENDPOINT} from '../../common/constants'; * */ export class Call extends Eventing implements ICall { + private static readonly UNKNOWN_STATE = 'unknown'; + private sdkConnector: ISDKConnector; private webex: WebexSDK; @@ -174,6 +182,237 @@ export class Call extends Eventing implements ICall { private callKeepaliveRetryCount = 0; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private handleMediaRoapEvent = async (event: any) => { + log.info( + `ROAP message to send (rcv from MEDIA-SDK) : + \n type: ${event.roapMessage?.messageType}, seq: ${event.roapMessage.seq} , version: ${event.roapMessage.version}`, + {file: CALL_FILE, method: METHODS.MEDIA_ROAP_EVENTS_LISTENER} + ); + + log.info(`SDP message to send : \n ${event.roapMessage?.sdp}`, { + file: CALL_FILE, + method: METHODS.MEDIA_ROAP_EVENTS_LISTENER, + }); + + switch (event.roapMessage.messageType) { + case RoapScenario.OK: { + const mediaOk = { + received: false, + message: event.roapMessage, + }; + + this.sendMediaStateMachineEvt({type: 'E_ROAP_OK', data: mediaOk}); + break; + } + + case RoapScenario.OFFER: { + // TODO: Remove these after the Media-Core adds the fix + // Check if at least one IPv6 "c=" line is present + log.info(`before modifying sdp: ${event.roapMessage.sdp}`, { + file: CALL_FILE, + method: METHODS.MEDIA_ROAP_EVENTS_LISTENER, + }); + + event.roapMessage.sdp = modifySdpForIPv4(event.roapMessage.sdp); + + const sdpVideoPortZero = event.roapMessage.sdp.replace(/^m=(video) (?:\d+) /gim, 'm=$1 0 '); + + log.info(`after modification sdp: ${sdpVideoPortZero}`, { + file: CALL_FILE, + method: METHODS.MEDIA_ROAP_EVENTS_LISTENER, + }); + + event.roapMessage.sdp = sdpVideoPortZero; + this.localRoapMessage = event.roapMessage; + this.sendCallStateMachineEvt({type: 'E_SEND_CALL_SETUP', data: event.roapMessage}); + break; + } + + case RoapScenario.ANSWER: + event.roapMessage.sdp = modifySdpForIPv4(event.roapMessage.sdp); + this.localRoapMessage = event.roapMessage; + if (this.connectPending) { + this.sendCallStateMachineEvt({type: 'E_SEND_CALL_CONNECT'}); + } + this.sendMediaStateMachineEvt({type: 'E_SEND_ROAP_ANSWER', data: event.roapMessage}); + break; + + case RoapScenario.ERROR: + this.sendMediaStateMachineEvt({type: 'E_ROAP_ERROR', data: event.roapMessage}); + break; + + case RoapScenario.OFFER_RESPONSE: + event.roapMessage.sdp = modifySdpForIPv4(event.roapMessage.sdp); + this.localRoapMessage = event.roapMessage; + if (this.connectPending) { + this.sendCallStateMachineEvt({type: 'E_SEND_CALL_CONNECT'}); + } + this.sendMediaStateMachineEvt({type: 'E_SEND_ROAP_OFFER', data: event.roapMessage}); + break; + + default: + } + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private handleRemoteTrackAdded = (event: any) => { + if (event.type === MEDIA_CONNECTION_EVENT_KEYS.MEDIA_TYPE_AUDIO) { + this.emit(CALL_EVENT_KEYS.REMOTE_MEDIA, event.track); + } + }; + + private static getPeerConnectionStateFromEvent( + event: { + state?: string; + connectionState?: string; + iceConnectionState?: string; + iceGatheringState?: string; + }, + preferredKey: 'connectionState' | 'iceConnectionState' | 'iceGatheringState' + ): string { + return event[preferredKey] || event.state || Call.UNKNOWN_STATE; + } + + private handleIceGatheringStateChanged = (event: { + state?: string; + iceGatheringState?: string; + }) => { + const iceGatheringState = Call.getPeerConnectionStateFromEvent(event, 'iceGatheringState'); + + log.info(`ICE gathering state changed to: ${iceGatheringState}`, { + file: CALL_FILE, + method: METHODS.MEDIA_ICE_EVENTS_LISTENER, + }); + + this.metricManager.submitMediaMetric( + METRIC_EVENT.MEDIA, + MEDIA_CONNECTION_ACTION.ICE_GATHERING_STATE_CHANGED, + METRIC_TYPE.BEHAVIORAL, + this.callId, + this.correlationId, + undefined, + undefined, + iceGatheringState + ); + }; + + private handlePeerConnectionStateChanged = (event: { + state?: string; + connectionState?: string; + }) => { + const connectionState = Call.getPeerConnectionStateFromEvent(event, 'connectionState'); + + log.info(`Peer connection state changed to: ${connectionState}`, { + file: CALL_FILE, + method: METHODS.MEDIA_ICE_EVENTS_LISTENER, + }); + + this.metricManager.submitMediaMetric( + METRIC_EVENT.MEDIA, + MEDIA_CONNECTION_ACTION.PEER_CONNECTION_STATE_CHANGED, + METRIC_TYPE.BEHAVIORAL, + this.callId, + this.correlationId, + undefined, + undefined, + connectionState + ); + }; + + private handleIceConnectionStateChanged = (event: { + state?: string; + iceConnectionState?: string; + }) => { + const iceConnectionState = Call.getPeerConnectionStateFromEvent(event, 'iceConnectionState'); + + log.info(`ICE connection state changed to: ${iceConnectionState}`, { + file: CALL_FILE, + method: METHODS.MEDIA_ICE_EVENTS_LISTENER, + }); + + this.metricManager.submitMediaMetric( + METRIC_EVENT.MEDIA, + MEDIA_CONNECTION_ACTION.ICE_CONNECTION_STATE_CHANGED, + METRIC_TYPE.BEHAVIORAL, + this.callId, + this.correlationId, + undefined, + undefined, + iceConnectionState + ); + }; + + private handleIceCandidateError = (event: { + address?: string | null; + errorCode?: number; + errorText?: string; + port?: number | null; + url?: string; + }) => { + const iceErrorPayload = { + address: event.address ?? null, + errorCode: event.errorCode, + errorText: event.errorText, + port: event.port ?? null, + url: event.url, + }; + + log.warn(`ICE candidate error occurred: ${JSON.stringify(iceErrorPayload)}`, { + file: CALL_FILE, + method: METHODS.MEDIA_ICE_EVENTS_LISTENER, + }); + + const callError = createCallError( + `ICE candidate error occurred: ${JSON.stringify(iceErrorPayload)}`, + {file: CALL_FILE, method: METHODS.MEDIA_ICE_EVENTS_LISTENER}, + ERROR_TYPE.CALL_ERROR, + this.correlationId, + ERROR_LAYER.MEDIA + ); + + this.metricManager.submitMediaMetric( + METRIC_EVENT.MEDIA_ERROR, + MEDIA_CONNECTION_ACTION.ICE_CANDIDATE_ERROR, + METRIC_TYPE.BEHAVIORAL, + this.callId, + this.correlationId, + undefined, + undefined, + undefined, + callError + ); + }; + + private handleRoapFailure = (error: {message?: string; stack?: string; code?: string}) => { + const failureMessage = error.message || 'Unknown ROAP failure received from media SDK'; + + log.warn(`ROAP failure occurred: ${failureMessage}`, { + file: CALL_FILE, + method: METHODS.MEDIA_ROAP_EVENTS_LISTENER, + }); + + const callError = createCallError( + `ROAP failure occurred: ${failureMessage}`, + {file: CALL_FILE, method: METHODS.MEDIA_ROAP_EVENTS_LISTENER}, + ERROR_TYPE.CALL_ERROR, + this.correlationId, + ERROR_LAYER.MEDIA + ); + + this.metricManager.submitMediaMetric( + METRIC_EVENT.MEDIA_ERROR, + MEDIA_CONNECTION_ACTION.ROAP_FAILURE, + METRIC_TYPE.BEHAVIORAL, + this.callId, + this.correlationId, + undefined, + undefined, + undefined, + callError + ); + }; + /** * Getter to check if the call is muted or not. * @@ -1435,6 +1674,7 @@ export class Call extends Eventing implements ICall { /* istanbul ignore else */ if (this.mediaConnection) { + this.unregisterMediaConnectionListeners(); this.mediaConnection.close(); log.info('Closing media channel', { file: CALL_FILE, @@ -1497,6 +1737,7 @@ export class Call extends Eventing implements ICall { /* istanbul ignore else */ if (this.mediaConnection) { + this.unregisterMediaConnectionListeners(); this.mediaConnection.close(); log.info('Closing media channel', { file: CALL_FILE, @@ -1672,6 +1913,7 @@ export class Call extends Eventing implements ICall { } if (this.mediaConnection) { + this.unregisterMediaConnectionListeners(); this.mediaConnection.close(); log.info('Closing media channel', { file: CALL_FILE, @@ -2261,6 +2503,7 @@ export class Call extends Eventing implements ICall { this.initMediaConnection(localAudioTrack); this.mediaRoapEventsListener(); this.mediaTrackListener(); + this.mediaIceEventsListener(); this.registerListeners(localAudioStream); } @@ -2304,6 +2547,7 @@ export class Call extends Eventing implements ICall { this.initMediaConnection(localAudioTrack); this.mediaRoapEventsListener(); this.mediaTrackListener(); + this.mediaIceEventsListener(); this.registerListeners(localAudioStream); } @@ -2662,82 +2906,9 @@ export class Call extends Eventing implements ICall { private mediaRoapEventsListener() { this.mediaConnection.on( MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async (event: any) => { - log.info( - `ROAP message to send (rcv from MEDIA-SDK) : - \n type: ${event.roapMessage?.messageType}, seq: ${event.roapMessage.seq} , version: ${event.roapMessage.version}`, - {file: CALL_FILE, method: METHODS.MEDIA_ROAP_EVENTS_LISTENER} - ); - - log.info(`SDP message to send : \n ${event.roapMessage?.sdp}`, { - file: CALL_FILE, - method: METHODS.MEDIA_ROAP_EVENTS_LISTENER, - }); - - switch (event.roapMessage.messageType) { - case RoapScenario.OK: { - const mediaOk = { - received: false, - message: event.roapMessage, - }; - - this.sendMediaStateMachineEvt({type: 'E_ROAP_OK', data: mediaOk}); - break; - } - - case RoapScenario.OFFER: { - // TODO: Remove these after the Media-Core adds the fix - // Check if at least one IPv6 "c=" line is present - log.info(`before modifying sdp: ${event.roapMessage.sdp}`, { - file: CALL_FILE, - method: METHODS.MEDIA_ROAP_EVENTS_LISTENER, - }); - - event.roapMessage.sdp = modifySdpForIPv4(event.roapMessage.sdp); - - const sdpVideoPortZero = event.roapMessage.sdp.replace( - /^m=(video) (?:\d+) /gim, - 'm=$1 0 ' - ); - - log.info(`after modification sdp: ${sdpVideoPortZero}`, { - file: CALL_FILE, - method: METHODS.MEDIA_ROAP_EVENTS_LISTENER, - }); - - event.roapMessage.sdp = sdpVideoPortZero; - this.localRoapMessage = event.roapMessage; - this.sendCallStateMachineEvt({type: 'E_SEND_CALL_SETUP', data: event.roapMessage}); - break; - } - - case RoapScenario.ANSWER: - event.roapMessage.sdp = modifySdpForIPv4(event.roapMessage.sdp); - this.localRoapMessage = event.roapMessage; - if (this.connectPending) { - this.sendCallStateMachineEvt({type: 'E_SEND_CALL_CONNECT'}); - } - this.sendMediaStateMachineEvt({type: 'E_SEND_ROAP_ANSWER', data: event.roapMessage}); - break; - - case RoapScenario.ERROR: - this.sendMediaStateMachineEvt({type: 'E_ROAP_ERROR', data: event.roapMessage}); - break; - - case RoapScenario.OFFER_RESPONSE: - event.roapMessage.sdp = modifySdpForIPv4(event.roapMessage.sdp); - this.localRoapMessage = event.roapMessage; - if (this.connectPending) { - this.sendCallStateMachineEvt({type: 'E_SEND_CALL_CONNECT'}); - } - this.sendMediaStateMachineEvt({type: 'E_SEND_ROAP_OFFER', data: event.roapMessage}); - break; - - default: - } - } + this.handleMediaRoapEvent ); + this.mediaConnection.on(MediaConnectionEventNames.ROAP_FAILURE, this.handleRoapFailure); } /* istanbul ignore next */ @@ -2745,12 +2916,65 @@ export class Call extends Eventing implements ICall { * Setup a listener for remote track added event emitted by the media sdk. */ private mediaTrackListener() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.mediaConnection.on(MediaConnectionEventNames.REMOTE_TRACK_ADDED, (e: any) => { - if (e.type === MEDIA_CONNECTION_EVENT_KEYS.MEDIA_TYPE_AUDIO) { - this.emit(CALL_EVENT_KEYS.REMOTE_MEDIA, e.track); - } - }); + this.mediaConnection.on( + MediaConnectionEventNames.REMOTE_TRACK_ADDED, + this.handleRemoteTrackAdded + ); + } + + /* istanbul ignore next */ + /** + * Setup listeners for ICE-related media connection events. + */ + private mediaIceEventsListener() { + this.mediaConnection.on( + MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED, + this.handleIceGatheringStateChanged + ); + this.mediaConnection.on( + MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED, + this.handlePeerConnectionStateChanged + ); + this.mediaConnection.on( + MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED, + this.handleIceConnectionStateChanged + ); + this.mediaConnection.on( + MediaConnectionEventNames.ICE_CANDIDATE_ERROR, + this.handleIceCandidateError + ); + } + + private unregisterMediaConnectionListeners() { + if (!this.mediaConnection || typeof this.mediaConnection.off !== 'function') { + return; + } + + this.mediaConnection.off( + MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND, + this.handleMediaRoapEvent + ); + this.mediaConnection.off(MediaConnectionEventNames.ROAP_FAILURE, this.handleRoapFailure); + this.mediaConnection.off( + MediaConnectionEventNames.REMOTE_TRACK_ADDED, + this.handleRemoteTrackAdded + ); + this.mediaConnection.off( + MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED, + this.handleIceGatheringStateChanged + ); + this.mediaConnection.off( + MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED, + this.handlePeerConnectionStateChanged + ); + this.mediaConnection.off( + MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED, + this.handleIceConnectionStateChanged + ); + this.mediaConnection.off( + MediaConnectionEventNames.ICE_CANDIDATE_ERROR, + this.handleIceCandidateError + ); } private onEffectEnabled = () => { @@ -2865,6 +3089,7 @@ export class Call extends Eventing implements ICall { this.correlationId, this.localRoapMessage.sdp, this.remoteRoapMessage?.sdp, + undefined, error ); } diff --git a/packages/calling/src/CallingClient/constants.ts b/packages/calling/src/CallingClient/constants.ts index f9a639f41eb..dea6e33aa69 100644 --- a/packages/calling/src/CallingClient/constants.ts +++ b/packages/calling/src/CallingClient/constants.ts @@ -182,6 +182,7 @@ export const METHODS = { POST_MEDIA: 'postMedia', MEDIA_ROAP_EVENTS_LISTENER: 'mediaRoapEventsListener', MEDIA_TRACK_LISTENER: 'mediaTrackListener', + MEDIA_ICE_EVENTS_LISTENER: 'mediaIceEventsListener', ON_EFFECT_ENABLED: 'onEffectEnabled', ON_EFFECT_DISABLED: 'onEffectDisabled', UPDATE_TRACK: 'updateTrack', diff --git a/packages/calling/src/Metrics/index.test.ts b/packages/calling/src/Metrics/index.test.ts index 784c723f350..58972e10a46 100644 --- a/packages/calling/src/Metrics/index.test.ts +++ b/packages/calling/src/Metrics/index.test.ts @@ -391,6 +391,7 @@ describe('CALLING: Metric tests', () => { mockCorrelationId, mockSdp, mockSdp, + undefined, callError ); expect(submitClientMetricSpy).toBeCalledOnceWith(METRIC_EVENT.MEDIA_ERROR, expectedData); diff --git a/packages/calling/src/Metrics/index.ts b/packages/calling/src/Metrics/index.ts index daedb58a676..e324c1bee0d 100644 --- a/packages/calling/src/Metrics/index.ts +++ b/packages/calling/src/Metrics/index.ts @@ -403,6 +403,7 @@ class MetricManager implements IMetricManager { correlationId: CorrelationId, localSdp?: string, remoteSdp?: string, + state?: string, callError?: CallError ) { let data; @@ -423,6 +424,7 @@ class MetricManager implements IMetricManager { correlation_id: correlationId, local_media_details: localSdp, remote_media_details: remoteSdp, + state, }, type, }; diff --git a/packages/calling/src/Metrics/types.ts b/packages/calling/src/Metrics/types.ts index a6548508f34..890686504e9 100644 --- a/packages/calling/src/Metrics/types.ts +++ b/packages/calling/src/Metrics/types.ts @@ -61,6 +61,14 @@ export enum CONNECTION_ACTION { MERCURY_UP = 'mercury_up', } +export enum MEDIA_CONNECTION_ACTION { + ICE_GATHERING_STATE_CHANGED = 'ICE_GATHERING_STATE_CHANGED', + PEER_CONNECTION_STATE_CHANGED = 'PEER_CONNECTION_STATE_CHANGED', + ICE_CONNECTION_STATE_CHANGED = 'ICE_CONNECTION_STATE_CHANGED', + ICE_CANDIDATE_ERROR = 'ICE_CANDIDATE_ERROR', + ROAP_FAILURE = 'ROAP_FAILURE', +} + export interface IMetricManager { setDeviceInfo: (deviceInfo: IDeviceInfo) => void; @@ -99,6 +107,7 @@ export interface IMetricManager { correlationId: CorrelationId, localSdp?: string, remoteSdp?: string, + state?: string, callError?: CallError ) => void;