diff --git a/packages/@webex/plugin-meetings/src/meetings/util.ts b/packages/@webex/plugin-meetings/src/meetings/util.ts index a3f1d98cfa2..b4f1f97b2d8 100644 --- a/packages/@webex/plugin-meetings/src/meetings/util.ts +++ b/packages/@webex/plugin-meetings/src/meetings/util.ts @@ -1,5 +1,6 @@ /* globals window */ +import {CapabilityState, WebCapabilities} from '@webex/web-capabilities'; import { _CREATED_, _INCOMING_, @@ -152,32 +153,6 @@ MeetingsUtil.parseDefaultSiteFromMeetingPreferences = (userPreferences) => { return result; }; -/** - * Will check to see if the H.264 media codec is supported. - * @async - * @private - * @returns {Promise} - */ -MeetingsUtil.hasH264Codec = async () => { - let hasCodec = false; - - try { - const pc = new window.RTCPeerConnection(); - const offer = await pc.createOffer({offerToReceiveVideo: true}); - - if (offer.sdp.match(/^a=rtpmap:\d+\s+H264\/\d+/m)) { - hasCodec = true; - } - pc.close(); - } catch (error) { - LoggerProxy.logger.warn( - 'Meetings:util#hasH264Codec --> Error creating peerConnection for H.264 test.' - ); - } - - return hasCodec; -}; - /** * Notifies the user whether or not the H.264 * codec is present. Will continuously check @@ -193,22 +168,21 @@ MeetingsUtil.checkH264Support = async function checkH264Support(options: { firstChecked: number; disableNotifications: boolean; }) { - const {hasH264Codec} = MeetingsUtil; const {firstChecked, disableNotifications} = options || {}; const delay = 5e3; // ms const maxDuration = 3e5; // ms const shouldTrigger = firstChecked === undefined; const shouldStopChecking = firstChecked && Date.now() - firstChecked >= maxDuration; - // Disable notifications and start H.264 download only if (disableNotifications) { - hasH264Codec(); - return; } + const isH264Available = + WebCapabilities.isCapableOfReceivingVideoCodec('video/H264') === CapabilityState.CAPABLE; + // Codec loaded trigger event notification - if (await hasH264Codec()) { + if (isH264Available) { Trigger.trigger( this, { diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meetings/utils.js b/packages/@webex/plugin-meetings/test/unit/spec/meetings/utils.js index dadda69b7f8..f1c7ecd8ed0 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meetings/utils.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meetings/utils.js @@ -1,7 +1,11 @@ import {assert} from '@webex/test-helper-chai'; import sinon from 'sinon'; +import {CapabilityState, WebCapabilities} from '@webex/web-capabilities'; +import Trigger from '@webex/plugin-meetings/src/common/events/trigger-proxy'; +import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy'; import MeetingsUtil from '@webex/plugin-meetings/src/meetings/util'; +import {EVENT_TRIGGERS} from '@webex/plugin-meetings/src/constants'; import Metrics from '@webex/plugin-meetings/src/metrics'; import BEHAVIORAL_METRICS from '@webex/plugin-meetings/src/metrics/constants'; @@ -350,4 +354,155 @@ describe('plugin-meetings', () => { assert.equal(correlationId, false); }); }); + + describe('#checkH264Support', () => { + let isCapableOfReceivingVideoCodecStub; + let triggerStub; + let loggerErrorStub; + let loggerLogStub; + + beforeEach(() => { + isCapableOfReceivingVideoCodecStub = sinon.stub( + WebCapabilities, + 'isCapableOfReceivingVideoCodec' + ); + triggerStub = sinon.stub(Trigger, 'trigger'); + loggerErrorStub = sinon.stub(LoggerProxy.logger, 'error'); + loggerLogStub = sinon.stub(LoggerProxy.logger, 'log'); + }); + + it('returns immediately without codec check when disableNotifications is true', async () => { + await MeetingsUtil.checkH264Support.call({}, {disableNotifications: true}); + + assert.notCalled(isCapableOfReceivingVideoCodecStub); + assert.notCalled(triggerStub); + }); + + it('triggers MEDIA_CODEC_LOADED when H.264 is capable', async () => { + isCapableOfReceivingVideoCodecStub.returns(CapabilityState.CAPABLE); + + await MeetingsUtil.checkH264Support.call({}); + + assert.calledWith(isCapableOfReceivingVideoCodecStub, 'video/H264'); + assert.calledWith( + triggerStub, + {}, + {file: 'meetings/util', function: 'checkH264Support'}, + EVENT_TRIGGERS.MEDIA_CODEC_LOADED + ); + }); + + it('triggers MEDIA_CODEC_MISSING and schedules retry when H.264 is not capable', async () => { + const clock = sinon.useFakeTimers(); + isCapableOfReceivingVideoCodecStub.returns(CapabilityState.NOT_CAPABLE); + + await MeetingsUtil.checkH264Support.call({}); + + assert.calledWith( + triggerStub, + {}, + {file: 'meetings/util', function: 'checkH264Support'}, + EVENT_TRIGGERS.MEDIA_CODEC_MISSING + ); + triggerStub.resetHistory(); + isCapableOfReceivingVideoCodecStub.resetHistory(); + + await clock.tick(5000); + + assert.called(isCapableOfReceivingVideoCodecStub); + clock.restore(); + }); + + it('logs error and stops retrying when past maxDuration', async () => { + const clock = sinon.useFakeTimers(); + isCapableOfReceivingVideoCodecStub.returns(CapabilityState.NOT_CAPABLE); + + const firstCheckTime = 1; + await clock.tick(firstCheckTime); + await MeetingsUtil.checkH264Support.call({}, {firstChecked: firstCheckTime}); + await clock.tick(300000); + + assert.calledWith( + loggerErrorStub, + 'Meetings:util#checkH264Support --> Timed out waiting for H264 codec to load.' + ); + clock.restore(); + }); + + it('does not re-trigger MEDIA_CODEC_MISSING on retries when codec remains not capable', async () => { + const clock = sinon.useFakeTimers(); + isCapableOfReceivingVideoCodecStub.returns(CapabilityState.NOT_CAPABLE); + + await MeetingsUtil.checkH264Support.call({}); + const missingTriggerCount = triggerStub.getCalls().filter( + (call) => call.args[2] === EVENT_TRIGGERS.MEDIA_CODEC_MISSING + ).length; + assert.equal(missingTriggerCount, 1, 'MEDIA_CODEC_MISSING should be triggered only once'); + + triggerStub.resetHistory(); + await clock.tick(5000); + + const missingTriggerCountAfterRetry = triggerStub.getCalls().filter( + (call) => call.args[2] === EVENT_TRIGGERS.MEDIA_CODEC_MISSING + ).length; + assert.equal( + missingTriggerCountAfterRetry, + 0, + 'MEDIA_CODEC_MISSING should not be re-triggered on retry' + ); + clock.restore(); + }); + + it('triggers MEDIA_CODEC_LOADED when codec becomes available on retry', async () => { + const clock = sinon.useFakeTimers(); + isCapableOfReceivingVideoCodecStub + .onFirstCall() + .returns(CapabilityState.NOT_CAPABLE) + .onSecondCall() + .returns(CapabilityState.CAPABLE); + + await MeetingsUtil.checkH264Support.call({}); + assert.calledWith( + triggerStub, + {}, + {file: 'meetings/util', function: 'checkH264Support'}, + EVENT_TRIGGERS.MEDIA_CODEC_MISSING + ); + + triggerStub.resetHistory(); + await clock.tick(5000); + + assert.calledWith( + triggerStub, + {}, + {file: 'meetings/util', function: 'checkH264Support'}, + EVENT_TRIGGERS.MEDIA_CODEC_LOADED + ); + clock.restore(); + }); + + it('logs loaded message when H.264 codec is capable', async () => { + isCapableOfReceivingVideoCodecStub.returns(CapabilityState.CAPABLE); + + await MeetingsUtil.checkH264Support.call({}); + + assert.calledWith( + loggerLogStub, + 'Meetings:util#checkH264Support --> H264 codec loaded successfully.' + ); + }); + + it('logs missing message when H.264 codec is not capable on first check', async () => { + const clock = sinon.useFakeTimers(); + isCapableOfReceivingVideoCodecStub.returns(CapabilityState.NOT_CAPABLE); + + await MeetingsUtil.checkH264Support.call({}); + + assert.calledWith( + loggerLogStub, + 'Meetings:util#checkH264Support --> H264 codec is missing.' + ); + clock.restore(); + }); + }); });