diff --git a/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.util.ts b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.util.ts index 074571df275..475c267ff0f 100644 --- a/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.util.ts +++ b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.util.ts @@ -179,6 +179,33 @@ export const isSdpOfferCreationError = (rawError: any) => { return false; }; +/** + * Checks if the given error is a browser media error by its name. + * Returns true if the error name matches any known browser media error name in the mapping. + * + * @param {Object} rawError - The error object to check. + * @returns {boolean} True if the error is a browser media error, false otherwise. + */ +export const isBrowserMediaError = (rawError) => { + // eslint-disable-next-line no-use-before-define + if (isBrowserMediaErrorName(rawError.name)) { + return true; + } + + return false; +}; + +/** + * Returns the client error code mapped to the given browser media error name. + * If the error name is not found in the mapping, returns undefined. + * + * @param {Object} rawError - The error object containing the error name. + * @returns {string|undefined} The mapped client error code, or undefined if not found. + */ +export const getBrowserMediaErrorCode = (rawError) => { + return BROWSER_MEDIA_ERROR_NAME_TO_CLIENT_ERROR_CODES_MAP[rawError.name]; +}; + /** * MDN Media Devices getUserMedia() method returns a name if it errs * Documentation can be found here: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia diff --git a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts index 92dca99d89a..aa42360859c 100644 --- a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts +++ b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts @@ -10,6 +10,7 @@ import { ICE_AND_REACHABILITY_FAILED_CLIENT_CODE, ICE_FAILED_WITH_TURN_TLS_CLIENT_CODE, MISSING_ROAP_ANSWER_CLIENT_CODE, + BROWSER_MEDIA_ERROR_NAME_TO_CLIENT_ERROR_CODES_MAP, } from '../../../../src/call-diagnostic/config'; import Logger from '@webex/plugin-logger'; @@ -685,4 +686,34 @@ describe('internal-plugin-metrics', () => { }); }); }); + + describe('isBrowserMediaError', () => { + it('should return true if error name is in BROWSER_MEDIA_ERROR_NAME_TO_CLIENT_ERROR_CODES_MAP', () => { + // Use a known browser media error name from the config map + const errorName = Object.keys(BROWSER_MEDIA_ERROR_NAME_TO_CLIENT_ERROR_CODES_MAP)[0]; + const error = {name: errorName}; + assert.isTrue(CallDiagnosticUtils.isBrowserMediaError(error)); + }); + + it('should return false if error name is not in BROWSER_MEDIA_ERROR_NAME_TO_CLIENT_ERROR_CODES_MAP', () => { + const error = {name: 'SomeOtherError'}; + assert.isFalse(CallDiagnosticUtils.isBrowserMediaError(error)); + }); + }); + + describe('getBrowserMediaErrorCode', () => { + it('should return correct error code for known error name', () => { + const errorName = Object.keys(BROWSER_MEDIA_ERROR_NAME_TO_CLIENT_ERROR_CODES_MAP)[0]; + const error = {name: errorName}; + assert.strictEqual( + CallDiagnosticUtils.getBrowserMediaErrorCode(error), + BROWSER_MEDIA_ERROR_NAME_TO_CLIENT_ERROR_CODES_MAP[errorName] + ); + }); + + it('should return undefined for unknown error name', () => { + const error = {name: 'UnknownError'}; + assert.isUndefined(CallDiagnosticUtils.getBrowserMediaErrorCode(error)); + }); + }); }); diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index e54300fb473..c965a2dbf90 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -1,5 +1,5 @@ import uuid from 'uuid'; -import {cloneDeep, isEqual, isEmpty} from 'lodash'; +import {cloneDeep, isEqual, isEmpty, merge} from 'lodash'; import jwtDecode from 'jwt-decode'; // @ts-ignore - Fix this import {StatelessWebexPlugin} from '@webex/webex-core'; @@ -50,6 +50,11 @@ import { type MeetingTranscriptPayload, } from '@webex/internal-plugin-voicea'; +import { + getBrowserMediaErrorCode, + isBrowserMediaError, + isBrowserMediaErrorName, +} from '@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.util'; import {processNewCaptions} from './voicea-meeting'; import { @@ -5440,6 +5445,20 @@ export default class Meeting extends StatelessWebexPlugin { shouldRetry = false; } + if (CallDiagnosticUtils.isBrowserMediaError(error)) { + shouldRetry = false; + // eslint-disable-next-line no-ex-assign + error = merge({ + error: { + body: { + errorCode: CallDiagnosticUtils.getBrowserMediaErrorCode(error), + message: error?.message, + name: error?.name, + }, + }, + }); + } + // we only want to call leave if join was successful and this was a retry or we won't be doing any more retries if (joined && (isRetry || !shouldRetry)) { try { diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js index f15e5aff9fc..c39cf7a46a6 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js @@ -714,7 +714,7 @@ describe('plugin-meetings', () => { }); }); - describe('#joinWithMedia', () => { + describe.only('#joinWithMedia', () => { it('should have #joinWithMedia', () => { assert.exists(meeting.joinWithMedia); }); @@ -1003,6 +1003,35 @@ describe('plugin-meetings', () => { ); }); + it('should call leave() if addMediaInternal() fails ', async () => { + const addMediaError = new Error('fake addMedia error'); + addMediaError.name = 'TypeError'; + + const rejectError = { + error: { + body: { + errorCode: 2729, + message: 'fake addMedia error', + name: 'TypeError' + } + } + }; + meeting.addMediaInternal.rejects(addMediaError); + sinon.stub(meeting, 'leave').resolves(); + + await assert.isRejected( + meeting.joinWithMedia({ + joinOptions, + mediaOptions, + }), + rejectError + ); + + assert.calledOnce(meeting.join); + assert.calledOnce(meeting.addMediaInternal); + assert.calledOnce(Metrics.sendBehavioralMetric); + }); + it('should not call leave() if addMediaInternal() fails the first time and succeeds the second time and should only call join() once', async () => { const addMediaError = new Error('fake addMedia error'); const leaveStub = sinon.stub(meeting, 'leave');