diff --git a/packages/@webex/media-helpers/package.json b/packages/@webex/media-helpers/package.json index 4d7f8b4219d..85b6c69b22d 100644 --- a/packages/@webex/media-helpers/package.json +++ b/packages/@webex/media-helpers/package.json @@ -22,7 +22,7 @@ "deploy:npm": "yarn npm publish" }, "dependencies": { - "@webex/internal-media-core": "2.8.0", + "@webex/internal-media-core": "2.9.0", "@webex/ts-events": "^1.1.0", "@webex/web-media-effects": "2.18.0" }, diff --git a/packages/@webex/media-helpers/test/unit/spec/webrtc-core.js b/packages/@webex/media-helpers/test/unit/spec/webrtc-core.js index abbcf0a5002..b95683039e7 100644 --- a/packages/@webex/media-helpers/test/unit/spec/webrtc-core.js +++ b/packages/@webex/media-helpers/test/unit/spec/webrtc-core.js @@ -13,7 +13,7 @@ import { createDisplayStream, createDisplayStreamWithAudio, } from '@webex/media-helpers'; -import * as wcmestreams from '@webex/internal-media-core'; +import * as InternalMediaCoreModule from '@webex/internal-media-core'; describe('media-helpers', () => { describe('webrtc-core', () => { @@ -120,7 +120,7 @@ describe('media-helpers', () => { it('checks creating tracks', async () => { const constraints = {deviceId: 'abc'}; - const spy = sinon.stub(wcmestreams, spyFn).returns('something'); + const spy = sinon.stub(InternalMediaCoreModule, spyFn).returns('something'); const result = await createFn(constraints); assert.equal(result, 'something'); @@ -132,7 +132,7 @@ describe('media-helpers', () => { describe('createDisplayStream', () => { it('checks createDisplayStream', async () => { - const spy = sinon.stub(wcmestreams, 'createDisplayStream').returns('something'); + const spy = sinon.stub(InternalMediaCoreModule, 'createDisplayStream').returns('something'); const result = await createDisplayStream(); assert.equal(result, 'something'); assert.calledOnceWithExactly(spy, LocalDisplayStream); @@ -141,7 +141,7 @@ describe('media-helpers', () => { describe('createDisplayStreamWithAudio', () => { it('checks createDisplayStreamWithAudio', async () => { - const spy = sinon.stub(wcmestreams, 'createDisplayStreamWithAudio').returns('something'); + const spy = sinon.stub(InternalMediaCoreModule, 'createDisplayStreamWithAudio').returns('something'); const result = await createDisplayStreamWithAudio(); assert.equal(result, 'something'); assert.calledOnceWithExactly(spy, LocalDisplayStream, LocalSystemAudioStream); diff --git a/packages/@webex/plugin-meetings/package.json b/packages/@webex/plugin-meetings/package.json index 6d79c7fe7ce..223152e14ab 100644 --- a/packages/@webex/plugin-meetings/package.json +++ b/packages/@webex/plugin-meetings/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@webex/common": "workspace:*", - "@webex/internal-media-core": "2.8.0", + "@webex/internal-media-core": "2.9.0", "@webex/internal-plugin-conversation": "workspace:*", "@webex/internal-plugin-device": "workspace:*", "@webex/internal-plugin-llm": "workspace:*", diff --git a/packages/@webex/plugin-meetings/src/constants.ts b/packages/@webex/plugin-meetings/src/constants.ts index 71b1d9b661c..26fbfcd6c7b 100644 --- a/packages/@webex/plugin-meetings/src/constants.ts +++ b/packages/@webex/plugin-meetings/src/constants.ts @@ -250,23 +250,6 @@ export const ASSIGN_ROLES_ERROR_CODES = { ReclaimHostIsHostAlreadyErrorCode: 2409150, }; -export const DEFAULT_GET_STATS_FILTER = { - types: [ - 'track', - 'transport', - 'candidate-pair', - 'outbound-rtp', - 'outboundrtp', - 'inbound-rtp', - 'inboundrtp', - 'remote-inbound-rtp', - 'remote-outbound-rtp', - 'remote-candidate', - 'local-candidate', - 'media-source', - ], -}; - export const RECORDING_STATE = { RECORDING: 'recording', IDLE: 'idle', diff --git a/packages/@webex/plugin-meetings/src/media/MediaConnectionAwaiter.ts b/packages/@webex/plugin-meetings/src/media/MediaConnectionAwaiter.ts index 52a40599f85..1c19efcdc7f 100644 --- a/packages/@webex/plugin-meetings/src/media/MediaConnectionAwaiter.ts +++ b/packages/@webex/plugin-meetings/src/media/MediaConnectionAwaiter.ts @@ -1,5 +1,5 @@ import {Defer} from '@webex/common'; -import {ConnectionState, Event} from '@webex/internal-media-core'; +import {ConnectionState, MediaConnectionEventNames} from '@webex/internal-media-core'; import LoggerProxy from '../common/logs/logger-proxy'; import {ICE_AND_DTLS_CONNECTION_TIMEOUT} from '../constants'; @@ -69,15 +69,15 @@ export default class MediaConnectionAwaiter { */ private clearCallbacks(): void { this.webrtcMediaConnection.off( - Event.ICE_GATHERING_STATE_CHANGED, + MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED, this.iceGatheringStateCallback ); this.webrtcMediaConnection.off( - Event.PEER_CONNECTION_STATE_CHANGED, + MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED, this.peerConnectionStateCallback ); this.webrtcMediaConnection.off( - Event.ICE_CONNECTION_STATE_CHANGED, + MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED, this.iceConnectionStateCallback ); } @@ -228,17 +228,17 @@ export default class MediaConnectionAwaiter { } this.webrtcMediaConnection.on( - Event.PEER_CONNECTION_STATE_CHANGED, + MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED, this.peerConnectionStateCallback ); this.webrtcMediaConnection.on( - Event.ICE_CONNECTION_STATE_CHANGED, + MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED, this.iceConnectionStateCallback ); this.webrtcMediaConnection.on( - Event.ICE_GATHERING_STATE_CHANGED, + MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED, this.iceGatheringStateCallback ); diff --git a/packages/@webex/plugin-meetings/src/mediaQualityMetrics/config.ts b/packages/@webex/plugin-meetings/src/mediaQualityMetrics/config.ts deleted file mode 100644 index 1958f080cc9..00000000000 --- a/packages/@webex/plugin-meetings/src/mediaQualityMetrics/config.ts +++ /dev/null @@ -1,266 +0,0 @@ -export const emptyMqaInterval = { - audioReceive: [], - audioTransmit: [], - intervalMetadata: { - peerReflexiveIP: '0.0.0.0', - peripherals: [], - cpuInfo: { - numberOfCores: 1, // default value from spec if CpuInfo.getNumLogicalCores cannot be determined - description: 'NA', - architecture: 'unknown', - }, - processAverageCPU: 0, - processMaximumCPU: 0, - systemAverageCPU: 0, - systemMaximumCPU: 0, - screenWidth: 0, - screenHeight: 0, - screenResolution: 0, - appWindowWidth: 0, - appWindowHeight: 0, - appWindowSize: 0, - }, - networkType: '', - intervalNumber: 0, - videoReceive: [], - videoTransmit: [], -}; - -export const emptyAudioReceive = { - common: { - common: { - direction: 'inactive', - isMain: true, - mariFecEnabled: false, - mariRtxEnabled: false, - mariQosEnabled: false, - mariLiteEnabled: false, - multistreamEnabled: false, - }, - dtlsBitrate: 0, - dtlsPackets: 0, - fecBitrate: 0, - fecPackets: 0, - maxBitrate: 0, - mediaHopByHopLost: 0, - rtcpBitrate: 0, - rtcpPackets: 0, - rtpBitrate: 0, - rtpHopByHopLost: 0, - rtpPackets: 0, - rtpRecovered: 0, - rtxBitrate: 0, - rtxPackets: 0, - srtcpUnprotectErrors: 0, - srtpUnprotectErrors: 0, - stunBitrate: 0, - stunPackets: 0, - transportType: 'UDP', - }, - streams: [], -}; - -export const emptyAudioReceiveStream = { - common: { - codec: 'opus', - concealedFrames: 0, - csi: [], - maxConcealRunLength: 0, - optimalBitrate: 0, - optimalFrameRate: 0, - receivedBitrate: 0, - receivedFrameRate: 0, - renderedFrameRate: 0, - requestedBitrate: 0, - requestedFrameRate: 0, - rtpEndToEndLost: 0, - maxRtpJitter: 0, - meanRtpJitter: 0, - rtpPackets: 0, - ssci: 0, - }, -}; - -export const emptyAudioTransmit = { - common: { - availableBitrate: 0, - common: { - direction: 'inactive', - isMain: true, - mariFecEnabled: false, - mariRtxEnabled: false, - mariQosEnabled: false, - mariLiteEnabled: false, - multistreamEnabled: false, - }, - dtlsBitrate: 0, - dtlsPackets: 0, - fecBitrate: 0, - fecPackets: 0, - maxBitrate: 0, - queueDelay: 0, - remoteJitter: 0, - remoteLossRate: 0, - roundTripTime: 0, - rtcpBitrate: 0, - rtcpPackets: 0, - rtpBitrate: 0, - rtpPackets: 0, - rtxBitrate: 0, - rtxPackets: 0, - stunBitrate: 0, - stunPackets: 0, - transportType: 'UDP', - }, - streams: [], -}; - -export const emptyAudioTransmitStream = { - common: { - codec: 'opus', - csi: [], - requestedBitrate: 0, - requestedFrames: 0, - rtpPackets: 0, - ssci: 0, - transmittedBitrate: 0, - transmittedFrameRate: 0, - }, -}; - -export const emptyVideoReceive = { - common: { - common: { - direction: 'inactive', - isMain: true, // Not avaliable - mariFecEnabled: false, - mariRtxEnabled: false, - mariQosEnabled: false, - mariLiteEnabled: false, - multistreamEnabled: false, - }, - dtlsBitrate: 0, // Not avaliable - dtlsPackets: 0, // Not avaliable - fecBitrate: 0, // Not avaliable - fecPackets: 0, // Not avaliable - maxBitrate: 0, - mediaHopByHopLost: 0, - rtcpBitrate: 0, // Not avaliable - rtcpPackets: 0, // Not avaliable - rtpBitrate: 0, - rtpHopByHopLost: 0, - rtpPackets: 0, - rtpRecovered: 0, // Not avaliable - rtxBitrate: 0, // Not avaliable - rtxPackets: 0, // Not avaliable - srtcpUnprotectErrors: 0, // Not avaliable - srtpUnprotectErrors: 0, // Not avaliable - stunBitrate: 0, // Not avaliable - stunPackets: 0, // Not avaliable - transportType: 'UDP', - }, - streams: [], -}; - -export const emptyVideoReceiveStream = { - common: { - codec: 'H264', - concealedFrames: 0, // Not avaliable - csi: [], - maxConcealRunLength: 0, // Not avaliable - optimalBitrate: 0, - optimalFrameRate: 0, - receivedBitrate: 0, - receivedFrameRate: 0, - renderedFrameRate: 0, // Not avaliable - requestedBitrate: 0, - requestedFrameRate: 0, - rtpEndToEndLost: 0, - rtpJitter: 0, - rtpPackets: 0, - ssci: 0, // Not avaliable - }, - h264CodecProfile: 'BP', - isActiveSpeaker: false, - optimalFrameSize: 0, // Not avaliable - receivedFrameSize: 0, - receivedHeight: 0, - receivedKeyFrames: 0, - receivedKeyFramesForRequest: 0, - receivedKeyFramesSourceChange: 0, - receivedKeyFramesUnknown: 0, - receivedWidth: 0, - requestedFrameSize: 0, - requestedKeyFrames: 0, -}; - -export const emptyVideoTransmit = { - common: { - availableBitrate: 0, // Not avaliable currently hardcoded - common: { - direction: 'inactive', - isMain: true, - mariFecEnabled: false, - mariRtxEnabled: false, - mariQosEnabled: false, - mariLiteEnabled: false, - multistreamEnabled: false, - }, - dtlsBitrate: 0, // Not avaliable - dtlsPackets: 0, // Not avaliable - fecBitrate: 0, // Not avaliable - fecPackets: 0, // TODO: check inbound-rtp// Not avaliable - maxBitrate: 0, // Currently hardcoded - queueDelay: 0, - remoteJitter: 0, // remoteInboundRtp.jitter - remoteLossRate: 0, // comparedResults.lossRate - roundTripTime: 0, // compareResults.roundTripTime - rtcpBitrate: 0, // Dont have access to it - rtcpPackets: 0, // Dont have access to rtcp - rtpBitrate: 0, // Dont have access - rtpPackets: 0, // outboundRtp.packetsSent - rtxBitrate: 0, // Dont have access to it - rtxPackets: 0, // Dont have access to it - stunBitrate: 0, // Dont have access to it - stunPackets: 0, // Dont have access to it - transportType: 'UDP', // TODO: need to calculate - }, - streams: [], -}; - -export const emptyVideoTransmitStream = { - common: { - codec: 'H264', - csi: [], - duplicateSsci: 0, // Not Avaliable - requestedBitrate: 0, // TODO: from remote SDP - requestedFrames: 0, // TODO: from remote SDP - rtpPackets: 0, // same as rtp packets - ssci: 0, - transmittedBitrate: 0, // TODO: get in the candidate pair - transmittedFrameRate: 0, // TODO: from track info - }, - h264CodecProfile: 'BP', // TODO: from localSDP - isAvatar: false, // Not Avaliable - isHardwareEncoded: false, // Not Avaliable - localConfigurationChanges: 2, // Not Avaliable - maxFrameQp: 0, // Not Avaliable - maxNoiseLevel: 0, // Not Avaliable - minRegionQp: 0, // Not Avaliable - remoteConfigurationChanges: 0, // Not Avaliable - requestedFrameSize: 0, // TODO: from remote SDP - requestedKeyFrames: 0, // outbound Fir request - transmittedFrameSize: 0, // Not Avaliable - transmittedHeight: 0, - transmittedKeyFrames: 0, // Key frames encoded - transmittedKeyFramesClient: 0, // Not Avaliable - transmittedKeyFramesConfigurationChange: 0, // Not Avaliable - transmittedKeyFramesFeedback: 0, // Not Avaliable - transmittedKeyFramesLocalDrop: 0, // Not Avaliable - transmittedKeyFramesOtherLayer: 0, // Not Avaliable - transmittedKeyFramesPeriodic: 0, // Not Avaliable - transmittedKeyFramesSceneChange: 0, // Not Avaliable - transmittedKeyFramesStartup: 0, // Not Avaliable - transmittedKeyFramesUnknown: 0, // Not Avaliable - transmittedWidth: 0, -}; diff --git a/packages/@webex/plugin-meetings/src/meeting/connectionStateHandler.ts b/packages/@webex/plugin-meetings/src/meeting/connectionStateHandler.ts index 2b1b9d028b3..751b5c7c92e 100644 --- a/packages/@webex/plugin-meetings/src/meeting/connectionStateHandler.ts +++ b/packages/@webex/plugin-meetings/src/meeting/connectionStateHandler.ts @@ -1,4 +1,4 @@ -import {Event, ConnectionState} from '@webex/internal-media-core'; +import {MediaConnectionEventNames, ConnectionState} from '@webex/internal-media-core'; import EventsScope from '../common/events/events-scope'; import {Enum} from '../constants'; @@ -32,12 +32,12 @@ export class ConnectionStateHandler extends EventsScope { this.webrtcMediaConnection = webrtcMediaConnection; this.webrtcMediaConnection.on( - Event.PEER_CONNECTION_STATE_CHANGED, + MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED, this.handleConnectionStateChange.bind(this) ); this.webrtcMediaConnection.on( - Event.ICE_CONNECTION_STATE_CHANGED, + MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED, this.handleConnectionStateChange.bind(this) ); } diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index 78ab7948ace..e255e3f633b 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -17,11 +17,13 @@ import { ConnectionState, Errors, ErrorType, - Event, + MediaConnectionEventNames, MediaContent, MediaType, RemoteTrackType, RoapMessage, + StatsAnalyzer, + StatsAnalyzerEventNames, } from '@webex/internal-media-core'; import { @@ -41,6 +43,7 @@ import { TURN_ON_CAPTION_STATUS, type MeetingTranscriptPayload, } from '@webex/internal-plugin-voicea'; + import {processNewCaptions} from './voicea-meeting'; import { @@ -51,7 +54,6 @@ import { AddMediaFailed, } from '../common/errors/webex-errors'; -import {StatsAnalyzer, EVENTS as StatsAnalyzerEvents} from '../statsAnalyzer'; import NetworkQualityMonitor from '../networkQualityMonitor'; import LoggerProxy from '../common/logs/logger-proxy'; import EventsUtil from '../common/events/util'; @@ -116,9 +118,7 @@ import { MEETING_PERMISSION_TOKEN_REFRESH_THRESHOLD_IN_SEC, MEETING_PERMISSION_TOKEN_REFRESH_REASON, ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT, - RECONNECTION, NAMED_MEDIA_GROUP_TYPE_AUDIO, - LANGUAGE_ENGLISH, } from '../constants'; import BEHAVIORAL_METRICS from '../metrics/constants'; import ParameterError from '../common/errors/parameter'; @@ -5726,234 +5726,252 @@ export default class Meeting extends StatelessWebexPlugin { * @returns {undefined} */ setupSdpListeners = () => { - this.mediaProperties.webrtcMediaConnection.on(Event.REMOTE_SDP_ANSWER_PROCESSED, () => { - // @ts-ignore - const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies; + this.mediaProperties.webrtcMediaConnection.on( + MediaConnectionEventNames.REMOTE_SDP_ANSWER_PROCESSED, + () => { + // @ts-ignore + const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies; - // @ts-ignore - this.webex.internal.newMetrics.submitClientEvent({ - name: 'client.media-engine.remote-sdp-received', - options: {meetingId: this.id}, - }); - Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_OFFER_TO_ANSWER_LATENCY, { - correlation_id: this.correlationId, - latency: cdl.getLocalSDPGenRemoteSDPRecv(), - meetingId: this.id, - }); + // @ts-ignore + this.webex.internal.newMetrics.submitClientEvent({ + name: 'client.media-engine.remote-sdp-received', + options: {meetingId: this.id}, + }); + Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_OFFER_TO_ANSWER_LATENCY, { + correlation_id: this.correlationId, + latency: cdl.getLocalSDPGenRemoteSDPRecv(), + meetingId: this.id, + }); - if (this.deferSDPAnswer) { - this.deferSDPAnswer.resolve(); - clearTimeout(this.sdpResponseTimer); - this.sdpResponseTimer = undefined; + if (this.deferSDPAnswer) { + this.deferSDPAnswer.resolve(); + clearTimeout(this.sdpResponseTimer); + this.sdpResponseTimer = undefined; + } } - }); + ); - this.mediaProperties.webrtcMediaConnection.on(Event.LOCAL_SDP_OFFER_GENERATED, () => { - // @ts-ignore - this.webex.internal.newMetrics.submitClientEvent({ - name: 'client.media-engine.local-sdp-generated', - options: {meetingId: this.id}, - }); + this.mediaProperties.webrtcMediaConnection.on( + MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED, + () => { + // @ts-ignore + this.webex.internal.newMetrics.submitClientEvent({ + name: 'client.media-engine.local-sdp-generated', + options: {meetingId: this.id}, + }); - // Instantiate Defer so that the SDP offer/answer exchange timeout can start, see waitForRemoteSDPAnswer() - this.deferSDPAnswer = new Defer(); - }); + // Instantiate Defer so that the SDP offer/answer exchange timeout can start, see waitForRemoteSDPAnswer() + this.deferSDPAnswer = new Defer(); + } + ); - this.mediaProperties.webrtcMediaConnection.on(Event.LOCAL_SDP_ANSWER_GENERATED, () => { - // we are sending "remote-sdp-received" only after we've generated the answer - this indicates that we've fully processed that incoming offer - // @ts-ignore - this.webex.internal.newMetrics.submitClientEvent({ - name: 'client.media-engine.remote-sdp-received', - options: {meetingId: this.id}, - }); - }); + this.mediaProperties.webrtcMediaConnection.on( + MediaConnectionEventNames.LOCAL_SDP_ANSWER_GENERATED, + () => { + // we are sending "remote-sdp-received" only after we've generated the answer - this indicates that we've fully processed that incoming offer + // @ts-ignore + this.webex.internal.newMetrics.submitClientEvent({ + name: 'client.media-engine.remote-sdp-received', + options: {meetingId: this.id}, + }); + } + ); }; setupMediaConnectionListeners = () => { this.setupSdpListeners(); - this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_STARTED, () => { + this.mediaProperties.webrtcMediaConnection.on(MediaConnectionEventNames.ROAP_STARTED, () => { this.isRoapInProgress = true; }); - this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_DONE, () => { + this.mediaProperties.webrtcMediaConnection.on(MediaConnectionEventNames.ROAP_DONE, () => { this.mediaNegotiatedEvent(); this.isRoapInProgress = false; this.processNextQueuedMediaUpdate(); }); - this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_FAILURE, this.handleRoapFailure); - - this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_MESSAGE_TO_SEND, (event) => { - const LOG_HEADER = `Meeting:index#setupMediaConnectionListeners.ROAP_MESSAGE_TO_SEND --> correlationId=${this.correlationId}`; - - switch (event.roapMessage.messageType) { - case 'OK': - logRequest( - this.roap.sendRoapOK({ - seq: event.roapMessage.seq, - mediaId: this.mediaId, - correlationId: this.correlationId, - }), - { - logText: `${LOG_HEADER} Roap OK`, - } - ); - break; + this.mediaProperties.webrtcMediaConnection.on( + MediaConnectionEventNames.ROAP_FAILURE, + this.handleRoapFailure + ); - case 'OFFER': - logRequest( - this.roap - .sendRoapMediaRequest({ - sdp: event.roapMessage.sdp, + this.mediaProperties.webrtcMediaConnection.on( + MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND, + (event) => { + const LOG_HEADER = `Meeting:index#setupMediaConnectionListeners.ROAP_MESSAGE_TO_SEND --> correlationId=${this.correlationId}`; + + switch (event.roapMessage.messageType) { + case 'OK': + logRequest( + this.roap.sendRoapOK({ seq: event.roapMessage.seq, - tieBreaker: event.roapMessage.tieBreaker, - meeting: this, // or can pass meeting ID - }) - .then(({roapAnswer}) => { - if (roapAnswer) { - LoggerProxy.logger.log(`${LOG_HEADER} received Roap ANSWER in http response`); - - this.roapMessageReceived(roapAnswer); - } + mediaId: this.mediaId, + correlationId: this.correlationId, }), - { - logText: `${LOG_HEADER} Roap Offer`, - } - ).catch((error) => { - // @ts-ignore - this.webex.internal.newMetrics.submitClientEvent({ - name: 'client.media-engine.remote-sdp-received', - payload: { - canProceed: false, - errors: [ - // @ts-ignore - this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode( - { - clientErrorCode: CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE, - } - ), - ], - }, - options: {meetingId: this.id, rawError: error}, - }); + { + logText: `${LOG_HEADER} Roap OK`, + } + ); + break; - this.deferSDPAnswer.reject(new Error('failed to send ROAP SDP offer')); - clearTimeout(this.sdpResponseTimer); - this.sdpResponseTimer = undefined; - }); - break; + case 'OFFER': + logRequest( + this.roap + .sendRoapMediaRequest({ + sdp: event.roapMessage.sdp, + seq: event.roapMessage.seq, + tieBreaker: event.roapMessage.tieBreaker, + meeting: this, // or can pass meeting ID + }) + .then(({roapAnswer}) => { + if (roapAnswer) { + LoggerProxy.logger.log(`${LOG_HEADER} received Roap ANSWER in http response`); + + this.roapMessageReceived(roapAnswer); + } + }), + { + logText: `${LOG_HEADER} Roap Offer`, + } + ).catch((error) => { + // @ts-ignore + this.webex.internal.newMetrics.submitClientEvent({ + name: 'client.media-engine.remote-sdp-received', + payload: { + canProceed: false, + errors: [ + // @ts-ignore + this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode( + { + clientErrorCode: CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE, + } + ), + ], + }, + options: {meetingId: this.id, rawError: error}, + }); - case 'ANSWER': - logRequest( - this.roap.sendRoapAnswer({ - sdp: event.roapMessage.sdp, - seq: event.roapMessage.seq, - mediaId: this.mediaId, - correlationId: this.correlationId, - }), - { - logText: `${LOG_HEADER} Roap Answer`, - } - ).catch((error) => { - const metricName = BEHAVIORAL_METRICS.ROAP_ANSWER_FAILURE; - const data = { - correlation_id: this.correlationId, - locus_id: this.locusUrl.split('/').pop(), - reason: error.message, - stack: error.stack, - }; - const metadata = { - type: error.name, - }; + this.deferSDPAnswer.reject(new Error('failed to send ROAP SDP offer')); + clearTimeout(this.sdpResponseTimer); + this.sdpResponseTimer = undefined; + }); + break; - Metrics.sendBehavioralMetric(metricName, data, metadata); - }); - break; + case 'ANSWER': + logRequest( + this.roap.sendRoapAnswer({ + sdp: event.roapMessage.sdp, + seq: event.roapMessage.seq, + mediaId: this.mediaId, + correlationId: this.correlationId, + }), + { + logText: `${LOG_HEADER} Roap Answer`, + } + ).catch((error) => { + const metricName = BEHAVIORAL_METRICS.ROAP_ANSWER_FAILURE; + const data = { + correlation_id: this.correlationId, + locus_id: this.locusUrl.split('/').pop(), + reason: error.message, + stack: error.stack, + }; + const metadata = { + type: error.name, + }; - case 'ERROR': - if ( - event.roapMessage.errorType === ErrorType.CONFLICT || - event.roapMessage.errorType === ErrorType.DOUBLECONFLICT - ) { - Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_GLARE_CONDITION, { - correlation_id: this.correlationId, - locus_id: this.locusUrl.split('/').pop(), - sequence: event.roapMessage.seq, + Metrics.sendBehavioralMetric(metricName, data, metadata); }); - } - logRequest( - this.roap.sendRoapError({ - seq: event.roapMessage.seq, - errorType: event.roapMessage.errorType, - mediaId: this.mediaId, - correlationId: this.correlationId, - }), - { - logText: `${LOG_HEADER} Roap Error (${event.roapMessage.errorType})`, + break; + + case 'ERROR': + if ( + event.roapMessage.errorType === ErrorType.CONFLICT || + event.roapMessage.errorType === ErrorType.DOUBLECONFLICT + ) { + Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_GLARE_CONDITION, { + correlation_id: this.correlationId, + locus_id: this.locusUrl.split('/').pop(), + sequence: event.roapMessage.seq, + }); } - ); - break; + logRequest( + this.roap.sendRoapError({ + seq: event.roapMessage.seq, + errorType: event.roapMessage.errorType, + mediaId: this.mediaId, + correlationId: this.correlationId, + }), + { + logText: `${LOG_HEADER} Roap Error (${event.roapMessage.errorType})`, + } + ); + break; - default: - LoggerProxy.logger.error( - `${LOG_HEADER} Unsupported message type: ${event.roapMessage.messageType}` - ); - break; + default: + LoggerProxy.logger.error( + `${LOG_HEADER} Unsupported message type: ${event.roapMessage.messageType}` + ); + break; + } } - }); + ); // eslint-disable-next-line no-param-reassign - this.mediaProperties.webrtcMediaConnection.on(Event.REMOTE_TRACK_ADDED, (event) => { - LoggerProxy.logger.log( - `Meeting:index#setupMediaConnectionListeners --> REMOTE_TRACK_ADDED event received for webrtcMediaConnection: ${JSON.stringify( - event - )}` - ); - - if (event.track) { - const mediaTrack = event.track; - const remoteStream = new RemoteStream(MediaUtil.createMediaStream([mediaTrack])); + this.mediaProperties.webrtcMediaConnection.on( + MediaConnectionEventNames.REMOTE_TRACK_ADDED, + (event) => { + LoggerProxy.logger.log( + `Meeting:index#setupMediaConnectionListeners --> REMOTE_TRACK_ADDED event received for webrtcMediaConnection: ${JSON.stringify( + event + )}` + ); - // eslint-disable-next-line @typescript-eslint/no-shadow - let eventType; + if (event.track) { + const mediaTrack = event.track; + const remoteStream = new RemoteStream(MediaUtil.createMediaStream([mediaTrack])); + + // eslint-disable-next-line @typescript-eslint/no-shadow + let eventType; + + switch (event.type) { + case RemoteTrackType.AUDIO: + eventType = EVENT_TYPES.REMOTE_AUDIO; + this.mediaProperties.setRemoteAudioStream(remoteStream); + break; + case RemoteTrackType.VIDEO: + eventType = EVENT_TYPES.REMOTE_VIDEO; + this.mediaProperties.setRemoteVideoStream(remoteStream); + break; + case RemoteTrackType.SCREENSHARE_VIDEO: + eventType = EVENT_TYPES.REMOTE_SHARE; + this.mediaProperties.setRemoteShareStream(remoteStream); + break; + default: { + LoggerProxy.logger.log( + 'Meeting:index#setupMediaConnectionListeners --> unexpected track' + ); + } + } - switch (event.type) { - case RemoteTrackType.AUDIO: - eventType = EVENT_TYPES.REMOTE_AUDIO; - this.mediaProperties.setRemoteAudioStream(remoteStream); - break; - case RemoteTrackType.VIDEO: - eventType = EVENT_TYPES.REMOTE_VIDEO; - this.mediaProperties.setRemoteVideoStream(remoteStream); - break; - case RemoteTrackType.SCREENSHARE_VIDEO: - eventType = EVENT_TYPES.REMOTE_SHARE; - this.mediaProperties.setRemoteShareStream(remoteStream); - break; - default: { - LoggerProxy.logger.log( - 'Meeting:index#setupMediaConnectionListeners --> unexpected track' + if (eventType && mediaTrack) { + Trigger.trigger( + this, + { + file: 'meeting/index', + function: 'setupRemoteTrackListener:MediaConnectionEventNames.REMOTE_TRACK_ADDED', + }, + EVENT_TRIGGERS.MEDIA_READY, + { + type: eventType, + stream: remoteStream.outputStream, + } ); } } - - if (eventType && mediaTrack) { - Trigger.trigger( - this, - { - file: 'meeting/index', - function: 'setupRemoteTrackListener:Event.REMOTE_TRACK_ADDED', - }, - EVENT_TRIGGERS.MEDIA_READY, - { - type: eventType, - stream: remoteStream.outputStream, - } - ); - } } - }); + ); this.connectionStateHandler = new ConnectionStateHandler( this.mediaProperties.webrtcMediaConnection @@ -5988,7 +6006,6 @@ export default class Meeting extends StatelessWebexPlugin { // @ts-ignore const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies; - switch (event.state) { case ConnectionState.Connecting: if (!this.hasMediaConnectionConnectedAtLeastOnce) { @@ -6045,25 +6062,28 @@ export default class Meeting extends StatelessWebexPlugin { } }); - this.mediaProperties.webrtcMediaConnection.on(Event.ACTIVE_SPEAKERS_CHANGED, (csis) => { - Trigger.trigger( - this, - { - file: 'meeting/index', - function: 'setupMediaConnectionListeners', - }, - EVENT_TRIGGERS.ACTIVE_SPEAKER_CHANGED, - { - memberIds: csis - // @ts-ignore - .map((csi) => this.members.findMemberByCsi(csi)?.id) - .filter((item) => item !== undefined), - } - ); - }); + this.mediaProperties.webrtcMediaConnection.on( + MediaConnectionEventNames.ACTIVE_SPEAKERS_CHANGED, + (csis) => { + Trigger.trigger( + this, + { + file: 'meeting/index', + function: 'setupMediaConnectionListeners', + }, + EVENT_TRIGGERS.ACTIVE_SPEAKER_CHANGED, + { + memberIds: csis + // @ts-ignore + .map((csi) => this.members.findMemberByCsi(csi)?.id) + .filter((item) => item !== undefined), + } + ); + } + ); this.mediaProperties.webrtcMediaConnection.on( - Event.VIDEO_SOURCES_COUNT_CHANGED, + MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED, (numTotalSources, numLiveSources, mediaContent) => { Trigger.trigger( this, @@ -6086,7 +6106,7 @@ export default class Meeting extends StatelessWebexPlugin { ); this.mediaProperties.webrtcMediaConnection.on( - Event.AUDIO_SOURCES_COUNT_CHANGED, + MediaConnectionEventNames.AUDIO_SOURCES_COUNT_CHANGED, (numTotalSources, numLiveSources, mediaContent) => { Trigger.trigger( this, @@ -6105,37 +6125,43 @@ export default class Meeting extends StatelessWebexPlugin { ); this.iceCandidateErrors.clear(); - this.mediaProperties.webrtcMediaConnection.on(Event.ICE_CANDIDATE_ERROR, (event) => { - const {errorCode} = event.error; - let {errorText} = event.error; + this.mediaProperties.webrtcMediaConnection.on( + MediaConnectionEventNames.ICE_CANDIDATE_ERROR, + (event) => { + const {errorCode} = event.error; + let {errorText} = event.error; - if ( - errorCode === 600 && - errorText === 'Address not associated with the desired network interface.' - ) { - return; - } + if ( + errorCode === 600 && + errorText === 'Address not associated with the desired network interface.' + ) { + return; + } - if (errorText.endsWith('.')) { - errorText = errorText.slice(0, -1); - } + if (errorText.endsWith('.')) { + errorText = errorText.slice(0, -1); + } - errorText = errorText.toLowerCase(); - errorText = errorText.replace(/ /g, '_'); + errorText = errorText.toLowerCase(); + errorText = errorText.replace(/ /g, '_'); - const error = `${errorCode}_${errorText}`; + const error = `${errorCode}_${errorText}`; - const count = this.iceCandidateErrors.get(error) || 0; + const count = this.iceCandidateErrors.get(error) || 0; - this.iceCandidateErrors.set(error, count + 1); - }); + this.iceCandidateErrors.set(error, count + 1); + } + ); this.iceCandidatesCount = 0; - this.mediaProperties.webrtcMediaConnection.on(Event.ICE_CANDIDATE, (event) => { - if (event.candidate) { - this.iceCandidatesCount += 1; + this.mediaProperties.webrtcMediaConnection.on( + MediaConnectionEventNames.ICE_CANDIDATE, + (event) => { + if (event.candidate) { + this.iceCandidatesCount += 1; + } } - }); + ); }; /** @@ -6145,7 +6171,7 @@ export default class Meeting extends StatelessWebexPlugin { * @memberof Meetings */ setupStatsAnalyzerEventHandlers = () => { - this.statsAnalyzer.on(StatsAnalyzerEvents.MEDIA_QUALITY, (options) => { + this.statsAnalyzer.on(StatsAnalyzerEventNames.MEDIA_QUALITY, (options) => { // TODO: might have to send the same event to the developer // Add ip address info if geo hint is present // @ts-ignore fix type @@ -6159,14 +6185,15 @@ export default class Meeting extends StatelessWebexPlugin { name: 'client.mediaquality.event', options: { meetingId: this.id, - networkType: options.networkType, + networkType: options.data.networkType, }, payload: { intervals: [options.data], }, }); }); - this.statsAnalyzer.on(StatsAnalyzerEvents.LOCAL_MEDIA_STARTED, (data) => { + + this.statsAnalyzer.on(StatsAnalyzerEventNames.LOCAL_MEDIA_STARTED, (data) => { Trigger.trigger( this, { @@ -6180,28 +6207,28 @@ export default class Meeting extends StatelessWebexPlugin { this.webex.internal.newMetrics.submitClientEvent({ name: 'client.media.tx.start', payload: { - mediaType: data.type, - shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined, + mediaType: data.mediaType, + shareInstanceId: data.mediaType === 'share' ? this.localShareInstanceId : undefined, }, options: { meetingId: this.id, }, }); }); - this.statsAnalyzer.on(StatsAnalyzerEvents.LOCAL_MEDIA_STOPPED, (data) => { + this.statsAnalyzer.on(StatsAnalyzerEventNames.LOCAL_MEDIA_STOPPED, (data) => { // @ts-ignore this.webex.internal.newMetrics.submitClientEvent({ name: 'client.media.tx.stop', payload: { - mediaType: data.type, - shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined, + mediaType: data.mediaType, + shareInstanceId: data.mediaType === 'share' ? this.localShareInstanceId : undefined, }, options: { meetingId: this.id, }, }); }); - this.statsAnalyzer.on(StatsAnalyzerEvents.REMOTE_MEDIA_STARTED, (data) => { + this.statsAnalyzer.on(StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED, (data) => { Trigger.trigger( this, { @@ -6215,15 +6242,15 @@ export default class Meeting extends StatelessWebexPlugin { this.webex.internal.newMetrics.submitClientEvent({ name: 'client.media.rx.start', payload: { - mediaType: data.type, - shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined, + mediaType: data.mediaType, + shareInstanceId: data.mediaType === 'share' ? this.remoteShareInstanceId : undefined, }, options: { meetingId: this.id, }, }); - if (data.type === 'share') { + if (data.mediaType === 'share') { // @ts-ignore this.webex.internal.newMetrics.submitClientEvent({ name: 'client.media.render.start', @@ -6237,20 +6264,20 @@ export default class Meeting extends StatelessWebexPlugin { }); } }); - this.statsAnalyzer.on(StatsAnalyzerEvents.REMOTE_MEDIA_STOPPED, (data) => { + this.statsAnalyzer.on(StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED, (data) => { // @ts-ignore this.webex.internal.newMetrics.submitClientEvent({ name: 'client.media.rx.stop', payload: { - mediaType: data.type, - shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined, + mediaType: data.mediaType, + shareInstanceId: data.mediaType === 'share' ? this.remoteShareInstanceId : undefined, }, options: { meetingId: this.id, }, }); - if (data.type === 'share') { + if (data.mediaType === 'share') { // @ts-ignore this.webex.internal.newMetrics.submitClientEvent({ name: 'client.media.render.stop', @@ -6468,7 +6495,6 @@ export default class Meeting extends StatelessWebexPlugin { this.statsAnalyzer = new StatsAnalyzer({ // @ts-ignore - config coming from registerPlugin config: this.config.stats, - receiveSlotCallback: (ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc), networkQualityMonitor: this.networkQualityMonitor, isMultistream: this.isMultistream, }); diff --git a/packages/@webex/plugin-meetings/src/statsAnalyzer/global.ts b/packages/@webex/plugin-meetings/src/statsAnalyzer/global.ts deleted file mode 100644 index ee6fa153930..00000000000 --- a/packages/@webex/plugin-meetings/src/statsAnalyzer/global.ts +++ /dev/null @@ -1,37 +0,0 @@ -const STATS_DEFAULT = { - encryption: 'sha-256', - bandwidth: { - systemBandwidth: 0, - sentPerSecond: 0, - encodedPerSecond: 0, - helper: { - audioBytesSent: 0, - videoBytestSent: 0, - }, - speed: 0, - }, - results: {}, - connectionType: { - systemNetworkType: 'unknown', - systemIpAddress: '0.0.0.0', - local: { - candidateType: [], - transport: [], - ipAddress: [], - networkType: [], - }, - remote: { - candidateType: [], - transport: [], - ipAddress: [], - networkType: [], - }, - }, - resolutions: {}, - internal: { - remote: {}, - candidates: {}, - }, -}; - -export default STATS_DEFAULT; diff --git a/packages/@webex/plugin-meetings/src/statsAnalyzer/index.ts b/packages/@webex/plugin-meetings/src/statsAnalyzer/index.ts deleted file mode 100644 index a3464b325ea..00000000000 --- a/packages/@webex/plugin-meetings/src/statsAnalyzer/index.ts +++ /dev/null @@ -1,1379 +0,0 @@ -import {cloneDeep, isEmpty} from 'lodash'; -import {CpuInfo} from '@webex/web-capabilities'; -import {ConnectionState} from '@webex/internal-media-core'; -import EventsScope from '../common/events/events-scope'; -import { - DEFAULT_GET_STATS_FILTER, - STATS, - MQA_INTERVAL, - NETWORK_TYPE, - MEDIA_DEVICES, - _UNKNOWN_, -} from '../constants'; -import { - emptyAudioReceive, - emptyAudioTransmit, - emptyMqaInterval, - emptyVideoReceive, - emptyVideoTransmit, - emptyAudioReceiveStream, - emptyAudioTransmitStream, - emptyVideoReceiveStream, - emptyVideoTransmitStream, -} from '../mediaQualityMetrics/config'; -import LoggerProxy from '../common/logs/logger-proxy'; - -import defaultStats from './global'; -import { - getAudioSenderMqa, - getAudioReceiverMqa, - getVideoSenderMqa, - getVideoReceiverMqa, - getAudioSenderStreamMqa, - getAudioReceiverStreamMqa, - getVideoSenderStreamMqa, - getVideoReceiverStreamMqa, - isStreamRequested, -} from './mqaUtil'; -import {ReceiveSlot} from '../multistream/receiveSlot'; - -export const EVENTS = { - MEDIA_QUALITY: 'MEDIA_QUALITY', - LOCAL_MEDIA_STARTED: 'LOCAL_MEDIA_STARTED', - LOCAL_MEDIA_STOPPED: 'LOCAL_MEDIA_STOPPED', - REMOTE_MEDIA_STARTED: 'REMOTE_MEDIA_STARTED', - REMOTE_MEDIA_STOPPED: 'REMOTE_MEDIA_STOPPED', -}; - -const emptySender = { - trackLabel: '', - maxPacketLossRatio: 0, - availableBandwidth: 0, - bytesSent: 0, - meanRemoteJitter: [], - meanRoundTripTime: [], -}; - -const emptyReceiver = { - availableBandwidth: 0, - bytesReceived: 0, - meanRtpJitter: [], - meanRoundTripTime: [], -}; - -type ReceiveSlotCallback = (csi: number) => ReceiveSlot | undefined; -type MediaStatus = { - actual?: any; - expected?: any; -}; -/** - * Stats Analyzer class that will emit events based on detected quality - * - * @export - * @class StatsAnalyzer - * @extends {EventsScope} - */ -export class StatsAnalyzer extends EventsScope { - config: any; - correlationId: any; - lastEmittedStartStopEvent: any; - lastMqaDataSent: any; - lastStatsResults: any; - meetingMediaStatus: any; - mqaInterval: NodeJS.Timeout; - mqaSentCount: any; - networkQualityMonitor: any; - mediaConnection: any; - statsInterval: NodeJS.Timeout; - statsResults: any; - statsStarted: any; - successfulCandidatePair: any; - localIpAddress: string; // Returns the local IP address for diagnostics. this is the local IP of the interface used for the current media connection a host can have many local Ip Addresses - shareVideoEncoderImplementation?: string; - receiveSlotCallback: ReceiveSlotCallback; - isMultistream: boolean; - - /** - * Creates a new instance of StatsAnalyzer - * @constructor - * @public - * @param {Object} config - SDK Configuration Object - * @param {Function} receiveSlotCallback - Callback used to access receive slots. - * @param {Object} networkQualityMonitor - Class for assessing network characteristics (jitter, packetLoss, latency) - * @param {Object} statsResults - Default properties for stats - * @param {boolean | undefined} isMultistream - Param indicating if the media connection is multistream or not - */ - constructor({ - config, - receiveSlotCallback = () => undefined, - networkQualityMonitor = {}, - statsResults = defaultStats, - isMultistream = false, - }: { - config: any; - receiveSlotCallback: ReceiveSlotCallback; - networkQualityMonitor: any; - statsResults?: any; - isMultistream?: boolean; - }) { - super(); - this.statsStarted = false; - this.statsResults = statsResults; - this.lastStatsResults = null; - this.config = config; - this.networkQualityMonitor = networkQualityMonitor; - this.correlationId = config.correlationId; - this.mqaSentCount = -1; - this.lastMqaDataSent = {}; - this.lastEmittedStartStopEvent = {}; - this.receiveSlotCallback = receiveSlotCallback; - this.successfulCandidatePair = {}; - this.localIpAddress = ''; - this.isMultistream = isMultistream; - } - - /** - * Resets cumulative stats arrays. - * - * @public - * @memberof StatsAnalyzer - * @returns {void} - */ - resetStatsResults() { - Object.keys(this.statsResults).forEach((mediaType) => { - if (mediaType.includes('recv')) { - this.statsResults[mediaType].recv.meanRtpJitter = []; - } - - if (mediaType.includes('send')) { - this.statsResults[mediaType].send.meanRemoteJitter = []; - this.statsResults[mediaType].send.meanRoundTripTime = []; - } - }); - } - - /** - * sets mediaStatus status for analyzing metrics - * - * @public - * @param {Object} status for the audio and video - * @memberof StatsAnalyzer - * @returns {void} - */ - public updateMediaStatus(status: MediaStatus) { - this.meetingMediaStatus = { - actual: { - ...this.meetingMediaStatus?.actual, - ...status?.actual, - }, - expected: { - ...this.meetingMediaStatus?.expected, - ...status?.expected, - }, - }; - } - - /** - * captures MQA data from media connection - * - * @public - * @memberof StatsAnalyzer - * @returns {void} - */ - sendMqaData() { - const newMqa = cloneDeep(emptyMqaInterval); - - // Fill in empty stats items for lastMqaDataSent - Object.keys(this.statsResults).forEach((mediaType) => { - if (!this.lastMqaDataSent[mediaType]) { - this.lastMqaDataSent[mediaType] = {}; - } - - if (!this.lastMqaDataSent[mediaType].send && mediaType.includes('-send')) { - this.lastMqaDataSent[mediaType].send = {}; - } - - if (!this.lastMqaDataSent[mediaType].recv && mediaType.includes('-recv')) { - this.lastMqaDataSent[mediaType].recv = {}; - } - }); - - // Create stats the first level, totals for senders and receivers - const audioSender = cloneDeep(emptyAudioTransmit); - const audioShareSender = cloneDeep(emptyAudioTransmit); - const audioReceiver = cloneDeep(emptyAudioReceive); - const audioShareReceiver = cloneDeep(emptyAudioReceive); - const videoSender = cloneDeep(emptyVideoTransmit); - const videoShareSender = cloneDeep(emptyVideoTransmit); - const videoReceiver = cloneDeep(emptyVideoReceive); - const videoShareReceiver = cloneDeep(emptyVideoReceive); - - getAudioSenderMqa({ - audioSender, - statsResults: this.statsResults, - lastMqaDataSent: this.lastMqaDataSent, - baseMediaType: 'audio-send', - isMultistream: this.isMultistream, - }); - newMqa.audioTransmit.push(audioSender); - - getAudioSenderMqa({ - audioSender: audioShareSender, - statsResults: this.statsResults, - lastMqaDataSent: this.lastMqaDataSent, - baseMediaType: 'audio-share-send', - isMultistream: this.isMultistream, - }); - newMqa.audioTransmit.push(audioShareSender); - - getAudioReceiverMqa({ - audioReceiver, - statsResults: this.statsResults, - lastMqaDataSent: this.lastMqaDataSent, - baseMediaType: 'audio-recv', - isMultistream: this.isMultistream, - }); - newMqa.audioReceive.push(audioReceiver); - - getAudioReceiverMqa({ - audioReceiver: audioShareReceiver, - statsResults: this.statsResults, - lastMqaDataSent: this.lastMqaDataSent, - baseMediaType: 'audio-share-recv', - isMultistream: this.isMultistream, - }); - newMqa.audioReceive.push(audioShareReceiver); - - getVideoSenderMqa({ - videoSender, - statsResults: this.statsResults, - lastMqaDataSent: this.lastMqaDataSent, - baseMediaType: 'video-send', - isMultistream: this.isMultistream, - }); - newMqa.videoTransmit.push(videoSender); - - getVideoSenderMqa({ - videoSender: videoShareSender, - statsResults: this.statsResults, - lastMqaDataSent: this.lastMqaDataSent, - baseMediaType: 'video-share-send', - isMultistream: this.isMultistream, - }); - newMqa.videoTransmit.push(videoShareSender); - - getVideoReceiverMqa({ - videoReceiver, - statsResults: this.statsResults, - lastMqaDataSent: this.lastMqaDataSent, - baseMediaType: 'video-recv', - isMultistream: this.isMultistream, - }); - newMqa.videoReceive.push(videoReceiver); - - getVideoReceiverMqa({ - videoReceiver: videoShareReceiver, - statsResults: this.statsResults, - lastMqaDataSent: this.lastMqaDataSent, - baseMediaType: 'video-share-recv', - isMultistream: this.isMultistream, - }); - newMqa.videoReceive.push(videoShareReceiver); - - // Add stats for individual streams - Object.keys(this.statsResults).forEach((mediaType) => { - if (mediaType.startsWith('audio-send')) { - const audioSenderStream = cloneDeep(emptyAudioTransmitStream); - - getAudioSenderStreamMqa({ - audioSenderStream, - statsResults: this.statsResults, - lastMqaDataSent: this.lastMqaDataSent, - mediaType, - }); - if (isStreamRequested(this.statsResults, mediaType, STATS.SEND_DIRECTION)) { - newMqa.audioTransmit[0].streams.push(audioSenderStream); - } - - this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send); - } else if (mediaType.startsWith('audio-share-send')) { - const audioSenderStream = cloneDeep(emptyAudioTransmitStream); - - getAudioSenderStreamMqa({ - audioSenderStream, - statsResults: this.statsResults, - lastMqaDataSent: this.lastMqaDataSent, - mediaType, - }); - if (isStreamRequested(this.statsResults, mediaType, STATS.SEND_DIRECTION)) { - newMqa.audioTransmit[1].streams.push(audioSenderStream); - } - - this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send); - } else if (mediaType.startsWith('audio-recv')) { - const audioReceiverStream = cloneDeep(emptyAudioReceiveStream); - - getAudioReceiverStreamMqa({ - audioReceiverStream, - statsResults: this.statsResults, - lastMqaDataSent: this.lastMqaDataSent, - mediaType, - }); - if (isStreamRequested(this.statsResults, mediaType, STATS.RECEIVE_DIRECTION)) { - newMqa.audioReceive[0].streams.push(audioReceiverStream); - } - - this.lastMqaDataSent[mediaType].recv = cloneDeep(this.statsResults[mediaType].recv); - } else if (mediaType.startsWith('audio-share-recv')) { - const audioReceiverStream = cloneDeep(emptyAudioReceiveStream); - - getAudioReceiverStreamMqa({ - audioReceiverStream, - statsResults: this.statsResults, - lastMqaDataSent: this.lastMqaDataSent, - mediaType, - }); - if (isStreamRequested(this.statsResults, mediaType, STATS.RECEIVE_DIRECTION)) { - newMqa.audioReceive[1].streams.push(audioReceiverStream); - } - this.lastMqaDataSent[mediaType].recv = cloneDeep(this.statsResults[mediaType].recv); - } else if (mediaType.startsWith('video-send-layer')) { - // We only want the stream-specific stats we get with video-send-layer-0, video-send-layer-1, etc. - const videoSenderStream = cloneDeep(emptyVideoTransmitStream); - - getVideoSenderStreamMqa({ - videoSenderStream, - statsResults: this.statsResults, - lastMqaDataSent: this.lastMqaDataSent, - mediaType, - }); - if (isStreamRequested(this.statsResults, mediaType, STATS.SEND_DIRECTION)) { - newMqa.videoTransmit[0].streams.push(videoSenderStream); - } - this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send); - } else if (mediaType.startsWith('video-share-send')) { - const videoSenderStream = cloneDeep(emptyVideoTransmitStream); - - getVideoSenderStreamMqa({ - videoSenderStream, - statsResults: this.statsResults, - lastMqaDataSent: this.lastMqaDataSent, - mediaType, - }); - if (isStreamRequested(this.statsResults, mediaType, STATS.SEND_DIRECTION)) { - newMqa.videoTransmit[1].streams.push(videoSenderStream); - } - - this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send); - } else if (mediaType.startsWith('video-recv')) { - const videoReceiverStream = cloneDeep(emptyVideoReceiveStream); - - getVideoReceiverStreamMqa({ - videoReceiverStream, - statsResults: this.statsResults, - lastMqaDataSent: this.lastMqaDataSent, - mediaType, - }); - if (isStreamRequested(this.statsResults, mediaType, STATS.RECEIVE_DIRECTION)) { - newMqa.videoReceive[0].streams.push(videoReceiverStream); - } - - this.lastMqaDataSent[mediaType].recv = cloneDeep(this.statsResults[mediaType].recv); - } else if (mediaType.startsWith('video-share-recv')) { - const videoReceiverStream = cloneDeep(emptyVideoReceiveStream); - - getVideoReceiverStreamMqa({ - videoReceiverStream, - statsResults: this.statsResults, - lastMqaDataSent: this.lastMqaDataSent, - mediaType, - }); - if (isStreamRequested(this.statsResults, mediaType, STATS.RECEIVE_DIRECTION)) { - newMqa.videoReceive[1].streams.push(videoReceiverStream); - } - this.lastMqaDataSent[mediaType].recv = cloneDeep(this.statsResults[mediaType].recv); - } - }); - - newMqa.intervalMetadata.peerReflexiveIP = this.statsResults.connectionType.local.ipAddress; - - newMqa.intervalMetadata.cpuInfo.numberOfCores = CpuInfo.getNumLogicalCores() || 1; - - // Adding peripheral information - newMqa.intervalMetadata.peripherals.push({information: _UNKNOWN_, name: MEDIA_DEVICES.SPEAKER}); - if (this.statsResults['audio-send']) { - newMqa.intervalMetadata.peripherals.push({ - information: this.statsResults['audio-send'].trackLabel || _UNKNOWN_, - name: MEDIA_DEVICES.MICROPHONE, - }); - } - - const existingVideoSender = Object.keys(this.statsResults).find((item) => - item.includes('video-send') - ); - - if (existingVideoSender) { - newMqa.intervalMetadata.peripherals.push({ - information: this.statsResults[existingVideoSender].trackLabel || _UNKNOWN_, - name: MEDIA_DEVICES.CAMERA, - }); - } - - newMqa.networkType = this.statsResults.connectionType.local.networkType; - - newMqa.intervalMetadata.screenWidth = window.screen.width; - newMqa.intervalMetadata.screenHeight = window.screen.height; - newMqa.intervalMetadata.screenResolution = Math.round( - (window.screen.width * window.screen.height) / 256 - ); - newMqa.intervalMetadata.appWindowWidth = window.innerWidth; - newMqa.intervalMetadata.appWindowHeight = window.innerHeight; - newMqa.intervalMetadata.appWindowSize = Math.round( - (window.innerWidth * window.innerHeight) / 256 - ); - - this.mqaSentCount += 1; - - newMqa.intervalNumber = this.mqaSentCount; - - this.resetStatsResults(); - - this.emit( - { - file: 'statsAnalyzer', - function: 'sendMqaData', - }, - EVENTS.MEDIA_QUALITY, - { - data: newMqa, - // @ts-ignore - networkType: newMqa.networkType, - } - ); - } - - /** - * updated the media connection when changed - * - * @private - * @memberof StatsAnalyzer - * @param {RoapMediaConnection} mediaConnection - * @returns {void} - */ - updateMediaConnection(mediaConnection: any) { - this.mediaConnection = mediaConnection; - } - - /** - * Returns the local IP address for diagnostics. - * this is the local IP of the interface used for the current media connection - * a host can have many local Ip Addresses - * @returns {string | undefined} The local IP address. - */ - getLocalIpAddress(): string { - return this.localIpAddress; - } - - /** - * Starts the stats analyzer on interval - * - * @public - * @memberof StatsAnalyzer - * @param {RoapMediaConnection} mediaConnection - * @returns {Promise} - */ - public startAnalyzer(mediaConnection: any) { - if (!this.statsStarted) { - this.statsStarted = true; - this.mediaConnection = mediaConnection; - - return this.getStatsAndParse().then(() => { - this.statsInterval = setInterval(() => { - this.getStatsAndParse(); - }, this.config.analyzerInterval); - // Trigger initial fetch - this.sendMqaData(); - this.mqaInterval = setInterval(() => { - this.sendMqaData(); - }, MQA_INTERVAL); - }); - } - - return Promise.resolve(); - } - - /** - * Cleans up the analyzer when done - * - * @public - * @memberof StatsAnalyzer - * @returns {void} - */ - public stopAnalyzer() { - const sendOneLastMqa = this.mqaInterval && this.statsInterval; - - if (this.statsInterval) { - clearInterval(this.statsInterval); - this.statsInterval = undefined; - } - - if (this.mqaInterval) { - clearInterval(this.mqaInterval); - this.mqaInterval = undefined; - } - - if (sendOneLastMqa) { - return this.getStatsAndParse().then(() => { - this.sendMqaData(); - this.mediaConnection = null; - }); - } - - return Promise.resolve(); - } - - /** - * Parse a single result of get stats - * - * @private - * @param {*} getStatsResult - * @param {String} type - * @param {boolean} isSender - * @returns {void} - * @memberof StatsAnalyzer - */ - private parseGetStatsResult(getStatsResult: any, type: string, isSender: boolean) { - if (!getStatsResult) { - return; - } - - // Generate empty stats results - if (!this.statsResults[type]) { - this.statsResults[type] = {}; - } - - if (isSender && !this.statsResults[type].send) { - this.statsResults[type].send = cloneDeep(emptySender); - } else if (!isSender && !this.statsResults[type].recv) { - this.statsResults[type].recv = cloneDeep(emptyReceiver); - } - - switch (getStatsResult.type) { - case 'outbound-rtp': - this.processOutboundRTPResult(getStatsResult, type); - break; - case 'inbound-rtp': - this.processInboundRTPResult(getStatsResult, type); - break; - case 'remote-inbound-rtp': - case 'remote-outbound-rtp': - this.compareSentAndReceived(getStatsResult, type); - break; - case 'remotecandidate': - case 'remote-candidate': - this.parseCandidate(getStatsResult, type, isSender, true); - break; - case 'local-candidate': - this.parseCandidate(getStatsResult, type, isSender, false); - break; - case 'media-source': - this.parseAudioSource(getStatsResult, type); - break; - default: - break; - } - } - - /** - * Filters the get stats results for types - * @private - * @param {Array} statsItem - * @param {String} type - * @param {boolean} isSender - * @returns {void} - */ - filterAndParseGetStatsResults(statsItem: any, type: string, isSender: boolean) { - const {types} = DEFAULT_GET_STATS_FILTER; - - // get the successful candidate pair before parsing stats. - statsItem.report.forEach((report) => { - if (report.type === 'candidate-pair' && report.state === 'succeeded') { - this.successfulCandidatePair = report; - } - }); - - let videoSenderIndex = 0; - statsItem.report.forEach((result) => { - if (types.includes(result.type)) { - // if the video sender has multiple streams in the report, it is a new stream object. - if (type === 'video-send' && result.type === 'outbound-rtp') { - const newType = `video-send-layer-${videoSenderIndex}`; - this.parseGetStatsResult(result, newType, isSender); - videoSenderIndex += 1; - - this.statsResults[newType].direction = statsItem.currentDirection; - this.statsResults[newType].trackLabel = statsItem.localTrackLabel; - this.statsResults[newType].csi = statsItem.csi; - } else if (type === 'video-share-send' && result.type === 'outbound-rtp') { - this.shareVideoEncoderImplementation = result.encoderImplementation; - this.parseGetStatsResult(result, type, isSender); - } else { - this.parseGetStatsResult(result, type, isSender); - } - } - }); - - if (this.statsResults[type]) { - this.statsResults[type].direction = statsItem.currentDirection; - this.statsResults[type].trackLabel = statsItem.localTrackLabel; - this.statsResults[type].csi = statsItem.csi; - this.extractAndSetLocalIpAddressInfoForDiagnostics( - this.successfulCandidatePair?.localCandidateId, - this.statsResults?.candidates - ); - // reset the successful candidate pair. - this.successfulCandidatePair = {}; - } - } - - /** - * parse the audio - * @param {String} result - * @param {boolean} type - * @returns {void} - */ - parseAudioSource(result: any, type: any) { - if (!result) { - return; - } - - if (type.includes('audio-send')) { - this.statsResults[type].send.audioLevel = result.audioLevel; - this.statsResults[type].send.totalAudioEnergy = result.totalAudioEnergy; - } - } - - /** - * emits started/stopped events for local/remote media by checking - * if given values are increasing or not. The previousValue, currentValue - * params can be any numerical value like number of receive packets or - * decoded frames, etc. - * - * @private - * @param {string} mediaType - * @param {number} previousValue - value to compare - * @param {number} currentValue - value to compare (must be same type of value as previousValue) - * @param {boolean} isLocal - true if stats are for local media being sent out, false for remote media being received - * @memberof StatsAnalyzer - * @returns {void} - */ - emitStartStopEvents = ( - mediaType: string, - previousValue: number, - currentValue: number, - isLocal: boolean - ) => { - if (mediaType !== 'audio' && mediaType !== 'video' && mediaType !== 'share') { - throw new Error(`Unsupported mediaType: ${mediaType}`); - } - - // eslint-disable-next-line no-param-reassign - if (previousValue === undefined) previousValue = 0; - // eslint-disable-next-line no-param-reassign - if (currentValue === undefined) currentValue = 0; - - if (!this.lastEmittedStartStopEvent[mediaType]) { - this.lastEmittedStartStopEvent[mediaType] = {}; - } - - const lastEmittedEvent = isLocal - ? this.lastEmittedStartStopEvent[mediaType].local - : this.lastEmittedStartStopEvent[mediaType].remote; - - let newEvent; - - if (currentValue - previousValue > 0) { - newEvent = isLocal ? EVENTS.LOCAL_MEDIA_STARTED : EVENTS.REMOTE_MEDIA_STARTED; - } else if (currentValue === previousValue && currentValue > 0) { - newEvent = isLocal ? EVENTS.LOCAL_MEDIA_STOPPED : EVENTS.REMOTE_MEDIA_STOPPED; - } - - if (newEvent && lastEmittedEvent !== newEvent) { - if (isLocal) { - this.lastEmittedStartStopEvent[mediaType].local = newEvent; - } else { - this.lastEmittedStartStopEvent[mediaType].remote = newEvent; - } - this.emit( - { - file: 'statsAnalyzer/index', - function: 'compareLastStatsResult', - }, - newEvent, - { - type: mediaType, - } - ); - } - }; - - /** - * compares current and previous stats to check if packets are not sent - * - * @private - * @memberof StatsAnalyzer - * @returns {void} - */ - private compareLastStatsResult() { - if (this.lastStatsResults !== null && this.meetingMediaStatus) { - const getCurrentStatsTotals = (keyPrefix: string, value: string): number => - Object.keys(this.statsResults) - .filter((key) => key.startsWith(keyPrefix)) - .reduce( - (prev, cur) => - prev + - (this.statsResults[cur]?.[keyPrefix.includes('send') ? 'send' : 'recv'][value] || 0), - 0 - ); - - const getPreviousStatsTotals = (keyPrefix: string, value: string): number => - Object.keys(this.statsResults) - .filter((key) => key.startsWith(keyPrefix)) - .reduce( - (prev, cur) => - prev + - (this.lastStatsResults[cur]?.[keyPrefix.includes('send') ? 'send' : 'recv'][value] || - 0), - 0 - ); - - // Audio Transmit - if (this.lastStatsResults['audio-send']) { - // compare audio stats sent - // NOTE: relies on there being only one sender. - const currentStats = this.statsResults['audio-send'].send; - const previousStats = this.lastStatsResults['audio-send'].send; - - if ( - (this.meetingMediaStatus.expected.sendAudio && - currentStats.totalPacketsSent === previousStats.totalPacketsSent) || - currentStats.totalPacketsSent === 0 - ) { - LoggerProxy.logger.info( - `StatsAnalyzer:index#compareLastStatsResult --> No audio RTP packets sent`, - currentStats.totalPacketsSent - ); - } else { - if ( - (this.meetingMediaStatus.expected.sendAudio && - currentStats.totalAudioEnergy === previousStats.totalAudioEnergy) || - currentStats.totalAudioEnergy === 0 - ) { - LoggerProxy.logger.info( - `StatsAnalyzer:index#compareLastStatsResult --> No audio Energy present`, - currentStats.totalAudioEnergy - ); - } - - if (this.meetingMediaStatus.expected.sendAudio && currentStats.audioLevel === 0) { - LoggerProxy.logger.info( - `StatsAnalyzer:index#compareLastStatsResult --> audio level is 0 for the user` - ); - } - } - - this.emitStartStopEvents( - 'audio', - previousStats.totalPacketsSent, - currentStats.totalPacketsSent, - true - ); - } - - // Audio Receive - const currentAudioPacketsReceived = getCurrentStatsTotals( - 'audio-recv', - 'totalPacketsReceived' - ); - const previousAudioPacketsReceived = getPreviousStatsTotals( - 'audio-recv', - 'totalPacketsReceived' - ); - - this.emitStartStopEvents( - 'audio', - previousAudioPacketsReceived, - currentAudioPacketsReceived, - false - ); - - const currentTotalPacketsSent = getCurrentStatsTotals('video-send', 'totalPacketsSent'); - const previousTotalPacketsSent = getPreviousStatsTotals('video-send', 'totalPacketsSent'); - - const currentFramesEncoded = getCurrentStatsTotals('video-send', 'framesEncoded'); - const previousFramesEncoded = getPreviousStatsTotals('video-send', 'framesEncoded'); - - const currentFramesSent = getCurrentStatsTotals('video-send', 'framesSent'); - const previousFramesSent = getPreviousStatsTotals('video-send', 'framesSent'); - - const doesVideoSendExist = Object.keys(this.lastStatsResults).some((item) => - item.includes('video-send') - ); - - // Video Transmit - if (doesVideoSendExist) { - // compare video stats sent - - if ( - this.meetingMediaStatus.expected.sendVideo && - (currentTotalPacketsSent === previousTotalPacketsSent || currentTotalPacketsSent === 0) - ) { - LoggerProxy.logger.info( - `StatsAnalyzer:index#compareLastStatsResult --> No video RTP packets sent`, - currentTotalPacketsSent - ); - } else { - if ( - this.meetingMediaStatus.expected.sendVideo && - (currentFramesEncoded === previousFramesEncoded || currentFramesEncoded === 0) - ) { - LoggerProxy.logger.info( - `StatsAnalyzer:index#compareLastStatsResult --> No video Frames Encoded`, - currentFramesEncoded - ); - } - - if ( - this.meetingMediaStatus.expected.sendVideo && - (currentFramesSent === previousFramesSent || currentFramesSent === 0) - ) { - LoggerProxy.logger.info( - `StatsAnalyzer:index#compareLastStatsResult --> No video Frames sent`, - currentFramesSent - ); - } - } - - this.emitStartStopEvents('video', previousFramesSent, currentFramesSent, true); - } - - // Video Receive - const currentVideoFramesDecoded = getCurrentStatsTotals('video-recv', 'framesDecoded'); - const previousVideoFramesDecoded = getPreviousStatsTotals('video-recv', 'framesDecoded'); - - this.emitStartStopEvents( - 'video', - previousVideoFramesDecoded, - currentVideoFramesDecoded, - false - ); - - // Share Transmit - if (this.lastStatsResults['video-share-send']) { - // compare share stats sent - - const currentStats = this.statsResults['video-share-send'].send; - const previousStats = this.lastStatsResults['video-share-send'].send; - - if ( - this.meetingMediaStatus.expected.sendShare && - (currentStats.totalPacketsSent === previousStats.totalPacketsSent || - currentStats.totalPacketsSent === 0) - ) { - LoggerProxy.logger.info( - `StatsAnalyzer:index#compareLastStatsResult --> No share RTP packets sent`, - currentStats.totalPacketsSent - ); - } else { - if ( - this.meetingMediaStatus.expected.sendShare && - (currentStats.framesEncoded === previousStats.framesEncoded || - currentStats.framesEncoded === 0) - ) { - LoggerProxy.logger.info( - `StatsAnalyzer:index#compareLastStatsResult --> No share frames getting encoded`, - currentStats.framesEncoded - ); - } - - if ( - this.meetingMediaStatus.expected.sendShare && - (this.statsResults['video-share-send'].send.framesSent === - this.lastStatsResults['video-share-send'].send.framesSent || - this.statsResults['video-share-send'].send.framesSent === 0) - ) { - LoggerProxy.logger.info( - `StatsAnalyzer:index#compareLastStatsResult --> No share frames sent`, - this.statsResults['video-share-send'].send.framesSent - ); - } - } - - this.emitStartStopEvents('share', previousStats.framesSent, currentStats.framesSent, true); - } - - // Share receive - const currentShareFramesDecoded = getCurrentStatsTotals('video-share-recv', 'framesDecoded'); - const previousShareFramesDecoded = getPreviousStatsTotals( - 'video-share-recv', - 'framesDecoded' - ); - - this.emitStartStopEvents( - 'share', - previousShareFramesDecoded, - currentShareFramesDecoded, - false - ); - } - } - - /** - * Does a `getStats` on all the transceivers and parses the results - * - * @private - * @memberof StatsAnalyzer - * @returns {Promise} - */ - private getStatsAndParse() { - if (!this.mediaConnection) { - return Promise.resolve(); - } - - if ( - this.mediaConnection && - this.mediaConnection.getConnectionState() === ConnectionState.Failed - ) { - LoggerProxy.logger.trace( - 'StatsAnalyzer:index#getStatsAndParse --> media connection is in failed state' - ); - - return Promise.resolve(); - } - - LoggerProxy.logger.trace('StatsAnalyzer:index#getStatsAndParse --> Collecting Stats'); - - return this.mediaConnection.getTransceiverStats().then((transceiverStats) => { - transceiverStats.video.receivers.forEach((receiver, i) => - this.filterAndParseGetStatsResults(receiver, `video-recv-${i}`, false) - ); - transceiverStats.audio.receivers.forEach((receiver, i) => - this.filterAndParseGetStatsResults(receiver, `audio-recv-${i}`, false) - ); - transceiverStats.screenShareVideo.receivers.forEach((receiver, i) => - this.filterAndParseGetStatsResults(receiver, `video-share-recv-${i}`, false) - ); - transceiverStats.screenShareAudio.receivers.forEach((receiver, i) => - this.filterAndParseGetStatsResults(receiver, `audio-share-recv-${i}`, false) - ); - - transceiverStats.video.senders.forEach((sender, i) => { - if (i > 0) { - throw new Error('Stats Analyzer does not support multiple senders.'); - } - this.filterAndParseGetStatsResults(sender, 'video-send', true); - }); - transceiverStats.audio.senders.forEach((sender, i) => { - if (i > 0) { - throw new Error('Stats Analyzer does not support multiple senders.'); - } - this.filterAndParseGetStatsResults(sender, 'audio-send', true); - }); - transceiverStats.screenShareVideo.senders.forEach((sender, i) => { - if (i > 0) { - throw new Error('Stats Analyzer does not support multiple senders.'); - } - this.filterAndParseGetStatsResults(sender, 'video-share-send', true); - }); - transceiverStats.screenShareAudio.senders.forEach((sender, i) => { - if (i > 0) { - throw new Error('Stats Analyzer does not support multiple senders.'); - } - this.filterAndParseGetStatsResults(sender, 'audio-share-send', true); - }); - - this.compareLastStatsResult(); - - // Save the last results to compare with the current - // DO Deep copy, for some reason it takes the reference all the time rather then old value set - this.lastStatsResults = JSON.parse(JSON.stringify(this.statsResults)); - - LoggerProxy.logger.trace( - 'StatsAnalyzer:index#getStatsAndParse --> Finished Collecting Stats' - ); - }); - } - - /** - * Processes OutboundRTP stats result and stores - * @private - * @param {*} result - * @param {*} mediaType - * @returns {void} - */ - private processOutboundRTPResult(result: any, mediaType: any) { - const sendrecvType = STATS.SEND_DIRECTION; - - if (result.bytesSent) { - const kilobytes = 0; - - if (result.frameWidth && result.frameHeight) { - this.statsResults[mediaType][sendrecvType].width = result.frameWidth; - this.statsResults[mediaType][sendrecvType].height = result.frameHeight; - this.statsResults[mediaType][sendrecvType].framesSent = result.framesSent; - this.statsResults[mediaType][sendrecvType].hugeFramesSent = result.hugeFramesSent; - } - - this.statsResults[mediaType][sendrecvType].availableBandwidth = kilobytes.toFixed(1); - - this.statsResults[mediaType][sendrecvType].framesEncoded = result.framesEncoded; - this.statsResults[mediaType][sendrecvType].keyFramesEncoded = result.keyFramesEncoded; - this.statsResults[mediaType][sendrecvType].packetsSent = result.packetsSent; - - // Data saved to send MQA metrics - - this.statsResults[mediaType][sendrecvType].totalKeyFramesEncoded = result.keyFramesEncoded; - this.statsResults[mediaType][sendrecvType].totalNackCount = result.nackCount; - this.statsResults[mediaType][sendrecvType].totalPliCount = result.pliCount; - this.statsResults[mediaType][sendrecvType].totalPacketsSent = result.packetsSent; - this.statsResults[mediaType][sendrecvType].totalFirCount = result.firCount; - this.statsResults[mediaType][sendrecvType].framesSent = result.framesSent; - this.statsResults[mediaType][sendrecvType].framesEncoded = result.framesEncoded; - this.statsResults[mediaType][sendrecvType].encoderImplementation = - result.encoderImplementation; - this.statsResults[mediaType][sendrecvType].qualityLimitationReason = - result.qualityLimitationReason; - this.statsResults[mediaType][sendrecvType].qualityLimitationResolutionChanges = - result.qualityLimitationResolutionChanges; - this.statsResults[mediaType][sendrecvType].totalRtxPacketsSent = - result.retransmittedPacketsSent; - this.statsResults[mediaType][sendrecvType].totalRtxBytesSent = result.retransmittedBytesSent; - this.statsResults[mediaType][sendrecvType].totalBytesSent = result.bytesSent; - this.statsResults[mediaType][sendrecvType].headerBytesSent = result.headerBytesSent; - this.statsResults[mediaType][sendrecvType].retransmittedBytesSent = - result.retransmittedBytesSent; - this.statsResults[mediaType][sendrecvType].isRequested = result.isRequested; - this.statsResults[mediaType][sendrecvType].lastRequestedUpdateTimestamp = - result.lastRequestedUpdateTimestamp; - this.statsResults[mediaType][sendrecvType].requestedBitrate = result.requestedBitrate; - this.statsResults[mediaType][sendrecvType].requestedFrameSize = result.requestedFrameSize; - } - } - - /** - * Processes InboundRTP stats result and stores - * @private - * @param {*} result - * @param {*} mediaType - * @returns {void} - */ - private processInboundRTPResult(result: any, mediaType: any) { - const sendrecvType = STATS.RECEIVE_DIRECTION; - - if (result.bytesReceived) { - let kilobytes = 0; - const receiveSlot = this.receiveSlotCallback(result.ssrc); - const sourceState = receiveSlot?.sourceState; - const idAndCsi = receiveSlot - ? `id: "${receiveSlot.id || ''}"${receiveSlot.csi ? ` and csi: ${receiveSlot.csi}` : ''}` - : ''; - - const bytes = - result.bytesReceived - this.statsResults[mediaType][sendrecvType].totalBytesReceived; - - kilobytes = bytes / 1024; - this.statsResults[mediaType][sendrecvType].availableBandwidth = kilobytes.toFixed(1); - - let currentPacketsLost = - result.packetsLost - this.statsResults[mediaType][sendrecvType].totalPacketsLost; - if (currentPacketsLost < 0) { - currentPacketsLost = 0; - } - const packetsReceivedDiff = - result.packetsReceived - this.statsResults[mediaType][sendrecvType].totalPacketsReceived; - this.statsResults[mediaType][sendrecvType].totalPacketsReceived = result.packetsReceived; - - if (packetsReceivedDiff === 0) { - if (receiveSlot && sourceState === 'live') { - LoggerProxy.logger.info( - `StatsAnalyzer:index#processInboundRTPResult --> No packets received for mediaType: ${mediaType}, receive slot ${idAndCsi}. Total packets received on slot: `, - result.packetsReceived - ); - } - } - - if (mediaType.startsWith('video') || mediaType.startsWith('share')) { - const videoFramesReceivedDiff = - result.framesReceived - this.statsResults[mediaType][sendrecvType].framesReceived; - - if (videoFramesReceivedDiff === 0) { - if (receiveSlot && sourceState === 'live') { - LoggerProxy.logger.info( - `StatsAnalyzer:index#processInboundRTPResult --> No frames received for mediaType: ${mediaType}, receive slot ${idAndCsi}. Total frames received on slot: `, - result.framesReceived - ); - } - } - - const videoFramesDecodedDiff = - result.framesDecoded - this.statsResults[mediaType][sendrecvType].framesDecoded; - - if (videoFramesDecodedDiff === 0) { - if (receiveSlot && sourceState === 'live') { - LoggerProxy.logger.info( - `StatsAnalyzer:index#processInboundRTPResult --> No frames decoded for mediaType: ${mediaType}, receive slot ${idAndCsi}. Total frames decoded on slot: `, - result.framesDecoded - ); - } - } - - const videoFramesDroppedDiff = - result.framesDropped - this.statsResults[mediaType][sendrecvType].framesDropped; - - if (videoFramesDroppedDiff > 10) { - if (receiveSlot && sourceState === 'live') { - LoggerProxy.logger.info( - `StatsAnalyzer:index#processInboundRTPResult --> Frames dropped for mediaType: ${mediaType}, receive slot ${idAndCsi}. Total frames dropped on slot: `, - result.framesDropped - ); - } - } - } - - if (mediaType.startsWith('video-recv')) { - this.statsResults[mediaType][sendrecvType].isActiveSpeaker = result.isActiveSpeaker; - this.statsResults[mediaType][sendrecvType].lastActiveSpeakerTimestamp = - result.lastActiveSpeakerUpdateTimestamp; - } - - // Check the over all packet Lost ratio - this.statsResults[mediaType][sendrecvType].currentPacketLossRatio = - currentPacketsLost > 0 - ? currentPacketsLost / (packetsReceivedDiff + currentPacketsLost) - : 0; - if (this.statsResults[mediaType][sendrecvType].currentPacketLossRatio > 3) { - LoggerProxy.logger.info( - `StatsAnalyzer:index#processInboundRTPResult --> Packets getting lost from the receiver with slot ${idAndCsi}`, - this.statsResults[mediaType][sendrecvType].currentPacketLossRatio - ); - } - - if (result.frameWidth && result.frameHeight) { - this.statsResults[mediaType][sendrecvType].width = result.frameWidth; - this.statsResults[mediaType][sendrecvType].height = result.frameHeight; - this.statsResults[mediaType][sendrecvType].framesReceived = result.framesReceived; - } - - // TODO: check the packet loss value is negative values here - - if (result.packetsLost) { - this.statsResults[mediaType][sendrecvType].totalPacketsLost = - result.packetsLost > 0 ? result.packetsLost : -result.packetsLost; - } else { - this.statsResults[mediaType][sendrecvType].totalPacketsLost = 0; - } - - this.statsResults[mediaType][sendrecvType].lastPacketReceivedTimestamp = - result.lastPacketReceivedTimestamp; - this.statsResults[mediaType][sendrecvType].requestedBitrate = result.requestedBitrate; - this.statsResults[mediaType][sendrecvType].requestedFrameSize = result.requestedFrameSize; - - // From Thin - this.statsResults[mediaType][sendrecvType].totalNackCount = result.nackCount; - this.statsResults[mediaType][sendrecvType].totalPliCount = result.pliCount; - this.statsResults[mediaType][sendrecvType].framesDecoded = result.framesDecoded; - this.statsResults[mediaType][sendrecvType].keyFramesDecoded = result.keyFramesDecoded; - this.statsResults[mediaType][sendrecvType].framesDropped = result.framesDropped; - - this.statsResults[mediaType][sendrecvType].decoderImplementation = - result.decoderImplementation; - this.statsResults[mediaType][sendrecvType].totalPacketsReceived = result.packetsReceived; - - this.statsResults[mediaType][sendrecvType].fecPacketsDiscarded = result.fecPacketsDiscarded; - this.statsResults[mediaType][sendrecvType].fecPacketsReceived = result.fecPacketsReceived; - this.statsResults[mediaType][sendrecvType].totalBytesReceived = result.bytesReceived; - this.statsResults[mediaType][sendrecvType].headerBytesReceived = result.headerBytesReceived; - this.statsResults[mediaType][sendrecvType].totalRtxPacketsReceived = - result.retransmittedPacketsReceived; - this.statsResults[mediaType][sendrecvType].totalRtxBytesReceived = - result.retransmittedBytesReceived; - - this.statsResults[mediaType][sendrecvType].meanRtpJitter.push(result.jitter); - - // Audio stats - - this.statsResults[mediaType][sendrecvType].audioLevel = result.audioLevel; - this.statsResults[mediaType][sendrecvType].totalAudioEnergy = result.totalAudioEnergy; - this.statsResults[mediaType][sendrecvType].totalSamplesReceived = - result.totalSamplesReceived || 0; - this.statsResults[mediaType][sendrecvType].totalSamplesDecoded = - result.totalSamplesDecoded || 0; - this.statsResults[mediaType][sendrecvType].concealedSamples = result.concealedSamples || 0; - this.statsResults[mediaType][sendrecvType].isRequested = result.isRequested; - this.statsResults[mediaType][sendrecvType].lastRequestedUpdateTimestamp = - result.lastRequestedUpdateTimestamp; - } - } - - /** - * extracts the local Ip address from the statsResult object by looking at stats results candidates - * and matches that ID with the successful candidate pair. It looks at the type of local candidate it is - * and then extracts the IP address from the relatedAddress or address property based on conditions known in webrtc - * note, there are known incompatibilities and it is possible for this to set undefined, or for the IP address to be the public IP address - * for example, firefox does not set the relayProtocol, and if the user is behind a NAT it might be the public IP - * @private - * @param {string} successfulCandidatePairId - The ID of the successful candidate pair. - * @param {Object} candidates - the stats result candidates - * @returns {void} - */ - extractAndSetLocalIpAddressInfoForDiagnostics = ( - successfulCandidatePairId: string, - candidates: {[key: string]: Record} - ) => { - let newIpAddress = ''; - if (successfulCandidatePairId && !isEmpty(candidates)) { - const localCandidate = candidates[successfulCandidatePairId]; - if (localCandidate) { - if (localCandidate.candidateType === 'host') { - // if it's a host candidate, use the address property - it will be the local IP - newIpAddress = `${localCandidate.address}`; - } else if (localCandidate.candidateType === 'prflx') { - // if it's a peer reflexive candidate and we're not using a relay (there is no relayProtocol set) - // then look at the relatedAddress - it will be the local - // - // Firefox doesn't populate the relayProtocol property - if (!localCandidate.relayProtocol) { - newIpAddress = `${localCandidate.relatedAddress}`; - } else { - // if it's a peer reflexive candidate and we are using a relay - - // in that case the relatedAddress will be the IP of the TURN server (Linus), - // so we can only look at the address, but it might be local IP or public IP, - // depending on if the user is behind a NAT or not - newIpAddress = `${localCandidate.address}`; - } - } - } - } - this.localIpAddress = newIpAddress; - }; - - /** - * Processes remote and local candidate result and stores - * @private - * @param {*} result - * @param {*} type - * @param {boolean} isSender - * @param {boolean} isRemote - * - * @returns {void} - */ - parseCandidate = (result: any, type: any, isSender: boolean, isRemote: boolean) => { - if (!result || !result.id) { - return; - } - - // We only care about the successful local candidate - if (this.successfulCandidatePair?.localCandidateId !== result.id) { - return; - } - - let transport; - if (result.relayProtocol) { - transport = result.relayProtocol.toUpperCase(); - } else if (result.protocol) { - transport = result.protocol.toUpperCase(); - } - - const sendRecvType = isSender ? STATS.SEND_DIRECTION : STATS.RECEIVE_DIRECTION; - const ipType = isRemote ? STATS.REMOTE : STATS.LOCAL; - - if (!this.statsResults.candidates) { - this.statsResults.candidates = {}; - } - - this.statsResults.candidates[result.id] = { - candidateType: result.candidateType, - ipAddress: result.ip, // TODO: add ports - relatedAddress: result.relatedAddress, - relatedPort: result.relatedPort, - relayProtocol: result.relayProtocol, - protocol: result.protocol, - address: result.address, - portNumber: result.port, - networkType: result.networkType, - priority: result.priority, - transport, - timestamp: result.time, - id: result.id, - type: result.type, - }; - - this.statsResults.connectionType[ipType].candidateType = result.candidateType; - this.statsResults.connectionType[ipType].ipAddress = result.ipAddress; - - this.statsResults.connectionType[ipType].networkType = - result.networkType === NETWORK_TYPE.VPN ? NETWORK_TYPE.UNKNOWN : result.networkType; - this.statsResults.connectionType[ipType].transport = transport; - - this.statsResults[type][sendRecvType].totalRoundTripTime = result.totalRoundTripTime; - }; - - /** - * - * @private - * @param {*} result - * @param {*} type - * @returns {void} - * @memberof StatsAnalyzer - */ - compareSentAndReceived(result, type) { - // Don't compare on transceivers without a sender. - if (!type || !this.statsResults[type].send) { - return; - } - - const mediaType = type; - - const currentPacketLoss = - result.packetsLost - this.statsResults[mediaType].send.totalPacketsLostOnReceiver; - - this.statsResults[mediaType].send.packetsLostOnReceiver = currentPacketLoss; - this.statsResults[mediaType].send.totalPacketsLostOnReceiver = result.packetsLost; - - this.statsResults[mediaType].send.meanRemoteJitter.push(result.jitter); - this.statsResults[mediaType].send.meanRoundTripTime.push(result.roundTripTime); - - this.statsResults[mediaType].send.timestamp = result.timestamp; - this.statsResults[mediaType].send.ssrc = result.ssrc; - this.statsResults[mediaType].send.reportsReceived = result.reportsReceived; - - // Total packloss ratio on this video section of the call - this.statsResults[mediaType].send.overAllPacketLossRatio = - this.statsResults[mediaType].send.totalPacketsLostOnReceiver > 0 - ? this.statsResults[mediaType].send.totalPacketsLostOnReceiver / - this.statsResults[mediaType].send.totalPacketsSent - : 0; - this.statsResults[mediaType].send.currentPacketLossRatio = - this.statsResults[mediaType].send.packetsLostOnReceiver > 0 - ? (this.statsResults[mediaType].send.packetsLostOnReceiver * 100) / - (this.statsResults[mediaType].send.packetsSent + - this.statsResults[mediaType].send.packetsLostOnReceiver) - : 0; - - if ( - this.statsResults[mediaType].send.maxPacketLossRatio < - this.statsResults[mediaType].send.currentPacketLossRatio - ) { - this.statsResults[mediaType].send.maxPacketLossRatio = - this.statsResults[mediaType].send.currentPacketLossRatio; - } - - if (result.type === 'remote-inbound-rtp') { - this.networkQualityMonitor.determineUplinkNetworkQuality({ - mediaType, - remoteRtpResults: result, - statsAnalyzerCurrentStats: this.statsResults, - }); - } - } -} diff --git a/packages/@webex/plugin-meetings/src/statsAnalyzer/mqaUtil.ts b/packages/@webex/plugin-meetings/src/statsAnalyzer/mqaUtil.ts deleted file mode 100644 index 1b2c7fde74a..00000000000 --- a/packages/@webex/plugin-meetings/src/statsAnalyzer/mqaUtil.ts +++ /dev/null @@ -1,511 +0,0 @@ -/* eslint-disable no-param-reassign, prefer-destructuring */ - -import {mean, max} from 'lodash'; - -import {MQA_INTERVAL, STATS} from '../constants'; - -/** - * Get the totals of a certain value from a certain media type. - * - * @param {object} stats - The large stats object. - * @param {string} sendrecvType - "send" or "recv". - * @param {string} baseMediaType - audio or video _and_ share or non-share. - * @param {string} value - The value we want to get the totals of. - * @returns {number} - */ -const getTotalValueFromBaseType = ( - stats: object, - sendrecvType: string, - baseMediaType: string, - value: string -): number => - Object.keys(stats) - .filter((mt) => mt.includes(baseMediaType)) - .reduce((acc, mt) => acc + (stats[mt]?.[sendrecvType]?.[value] || 0), 0); - -export const getAudioReceiverMqa = ({ - audioReceiver, - statsResults, - lastMqaDataSent, - baseMediaType, - isMultistream, -}) => { - const sendrecvType = STATS.RECEIVE_DIRECTION; - - const getLastTotalValue = (value: string) => - getTotalValueFromBaseType(lastMqaDataSent, sendrecvType, baseMediaType, value); - const getTotalValue = (value: string) => - getTotalValueFromBaseType(statsResults, sendrecvType, baseMediaType, value); - - const lastPacketsReceived = getLastTotalValue('totalPacketsReceived'); - const lastPacketsLost = getLastTotalValue('totalPacketsLost'); - const lastBytesReceived = getLastTotalValue('totalBytesReceived'); - const lastFecPacketsReceived = getLastTotalValue('fecPacketsReceived'); - const lastFecPacketsDiscarded = getLastTotalValue('fecPacketsDiscarded'); - - const totalPacketsReceived = getTotalValue('totalPacketsReceived'); - const packetsLost = getTotalValue('totalPacketsLost'); - const totalBytesReceived = getTotalValue('totalBytesReceived'); - const totalFecPacketsReceived = getTotalValue('fecPacketsReceived'); - const totalFecPacketsDiscarded = getTotalValue('fecPacketsDiscarded'); - - audioReceiver.common.common.direction = - statsResults[Object.keys(statsResults).find((mediaType) => mediaType.includes(baseMediaType))] - ?.direction || 'inactive'; - audioReceiver.common.common.isMain = !baseMediaType.includes('-share'); - audioReceiver.common.common.multistreamEnabled = isMultistream; - audioReceiver.common.transportType = statsResults.connectionType.local.transport; - - // add rtpPacket info inside common as also for call analyzer - audioReceiver.common.rtpPackets = totalPacketsReceived - lastPacketsReceived; - - // Hop by hop are numbers and not percentage so we compare on what we sent the last min - // collect the packets received for the last min - const totalPacketsLost = packetsLost - lastPacketsLost; - audioReceiver.common.mediaHopByHopLost = totalPacketsLost; - audioReceiver.common.rtpHopByHopLost = totalPacketsLost; - - const fecRecovered = - totalFecPacketsReceived - - lastFecPacketsReceived - - (totalFecPacketsDiscarded - lastFecPacketsDiscarded); - audioReceiver.common.fecPackets = totalFecPacketsReceived - lastFecPacketsReceived; - - audioReceiver.common.rtpRecovered = fecRecovered; - - audioReceiver.common.rtpBitrate = ((totalBytesReceived - lastBytesReceived) * 8) / 60 || 0; -}; - -export const getAudioReceiverStreamMqa = ({ - audioReceiverStream, - statsResults, - lastMqaDataSent, - mediaType, -}) => { - const sendrecvType = STATS.RECEIVE_DIRECTION; - - const lastPacketsDecoded = lastMqaDataSent[mediaType]?.[sendrecvType].totalSamplesDecoded || 0; - const lastSamplesReceived = lastMqaDataSent[mediaType]?.[sendrecvType].totalSamplesReceived || 0; - const lastConcealedSamples = lastMqaDataSent[mediaType]?.[sendrecvType].concealedSamples || 0; - const lastBytesReceived = lastMqaDataSent[mediaType]?.[sendrecvType].totalBytesReceived || 0; - const lastFecPacketsReceived = lastMqaDataSent[mediaType]?.[sendrecvType].fecPacketsReceived || 0; - const lastFecPacketsDiscarded = - lastMqaDataSent[mediaType]?.[sendrecvType].fecPacketsDiscarded || 0; - const lastPacketsReceived = lastMqaDataSent[mediaType]?.[sendrecvType].totalPacketsReceived || 0; - const lastPacketsLost = lastMqaDataSent[mediaType]?.[sendrecvType].totalPacketsLost || 0; - - const {csi} = statsResults[mediaType]; - if (csi && !audioReceiverStream.common.csi.includes(csi)) { - audioReceiverStream.common.csi.push(csi); - } - - audioReceiverStream.common.rtpPackets = - statsResults[mediaType][sendrecvType].totalPacketsReceived - lastPacketsReceived || 0; - - audioReceiverStream.common.maxRtpJitter = - // @ts-ignore - max(statsResults[mediaType][sendrecvType].meanRtpJitter) * 1000 || 0; - audioReceiverStream.common.meanRtpJitter = - mean(statsResults[mediaType][sendrecvType].meanRtpJitter) * 1000 || 0; - audioReceiverStream.common.rtpJitter = audioReceiverStream.common.maxRtpJitter; - - // Fec packets do come in as part of the FEC only for audio - const fecRecovered = - statsResults[mediaType][sendrecvType].fecPacketsReceived - - lastFecPacketsReceived - - (statsResults[mediaType][sendrecvType].fecPacketsDiscarded - lastFecPacketsDiscarded); - - audioReceiverStream.common.rtpEndToEndLost = - statsResults[mediaType][sendrecvType].totalPacketsLost - lastPacketsLost - fecRecovered || 0; - - audioReceiverStream.common.framesDropped = - statsResults[mediaType][sendrecvType].totalSamplesDecoded - lastPacketsDecoded || 0; - audioReceiverStream.common.renderedFrameRate = - (audioReceiverStream.common.framesDropped * 100) / 60 || 0; - - audioReceiverStream.common.framesReceived = - statsResults[mediaType][sendrecvType].totalSamplesReceived - lastSamplesReceived || 0; - audioReceiverStream.common.concealedFrames = - statsResults[mediaType][sendrecvType].concealedSamples - lastConcealedSamples || 0; - audioReceiverStream.common.receivedBitrate = - ((statsResults[mediaType][sendrecvType].totalBytesReceived - lastBytesReceived) * 8) / 60 || 0; -}; - -export const getAudioSenderMqa = ({ - audioSender, - statsResults, - lastMqaDataSent, - baseMediaType, - isMultistream, -}) => { - const sendrecvType = STATS.SEND_DIRECTION; - - const getLastTotalValue = (value: string) => - getTotalValueFromBaseType(lastMqaDataSent, sendrecvType, baseMediaType, value); - const getTotalValue = (value: string) => - getTotalValueFromBaseType(statsResults, sendrecvType, baseMediaType, value); - - const lastPacketsSent = getLastTotalValue('totalPacketsSent'); - const lastPacketsLostTotal = getLastTotalValue('totalPacketsLostOnReceiver'); - - const totalPacketsLostOnReceiver = getTotalValue('totalPacketsLostOnReceiver'); - const totalPacketsSent = getTotalValue('totalPacketsSent'); - - const meanRemoteJitter = Object.keys(statsResults) - .filter((mt) => mt.includes(baseMediaType)) - .reduce((acc, mt) => acc.concat(statsResults[mt][sendrecvType].meanRemoteJitter), []); - const meanRoundTripTime = Object.keys(statsResults) - .filter((mt) => mt.includes(baseMediaType)) - .reduce((acc, mt) => acc.concat(statsResults[mt][sendrecvType].meanRoundTripTime), []); - - audioSender.common.common.direction = - statsResults[Object.keys(statsResults).find((mediaType) => mediaType.includes(baseMediaType))] - ?.direction || 'inactive'; - audioSender.common.common.isMain = !baseMediaType.includes('-share'); - audioSender.common.common.multistreamEnabled = isMultistream; - audioSender.common.transportType = statsResults.connectionType.local.transport; - - audioSender.common.maxRemoteJitter = max(meanRemoteJitter) * 1000 || 0; - audioSender.common.meanRemoteJitter = mean(meanRemoteJitter) * 1000 || 0; - - audioSender.common.rtpPackets = totalPacketsSent - lastPacketsSent || 0; - // audioSender.streams[0].common.rtpPackets = audioSender.common.rtpPackets; - // From candidate-pair - audioSender.common.availableBitrate = getTotalValueFromBaseType( - statsResults, - sendrecvType, - baseMediaType, - 'availableOutgoingBitrate' - ); - - // Calculate based on how much packets lost of received compated to how to the client sent - const totalPacketsLostForaMin = totalPacketsLostOnReceiver - lastPacketsLostTotal; - audioSender.common.maxRemoteLossRate = - totalPacketsSent - lastPacketsSent > 0 - ? (totalPacketsLostForaMin * 100) / (totalPacketsSent - lastPacketsSent) - : 0; // This is the packets sent with in last min - - audioSender.common.maxRoundTripTime = max(meanRoundTripTime) * 1000 || 0; - audioSender.common.meanRoundTripTime = mean(meanRoundTripTime) * 1000 || 0; - audioSender.common.roundTripTime = audioSender.common.maxRoundTripTime; - - // Calculate the outgoing bitrate - const totalBytesSentInaMin = - getTotalValueFromBaseType(statsResults, sendrecvType, baseMediaType, 'totalBytesSent') - - getTotalValueFromBaseType(lastMqaDataSent, sendrecvType, baseMediaType, 'totalBytesSent'); - - audioSender.common.rtpBitrate = totalBytesSentInaMin ? (totalBytesSentInaMin * 8) / 60 : 0; -}; - -export const getAudioSenderStreamMqa = ({ - audioSenderStream, - statsResults, - lastMqaDataSent, - mediaType, -}) => { - const sendrecvType = STATS.SEND_DIRECTION; - - const lastBytesSent = lastMqaDataSent[mediaType]?.[sendrecvType].totalBytesSent || 0; - const lastFramesEncoded = lastMqaDataSent[mediaType]?.[sendrecvType].totalKeyFramesEncoded || 0; - const lastFirCount = lastMqaDataSent[mediaType]?.[sendrecvType].totalFirCount || 0; - const lastPacketsSent = lastMqaDataSent[mediaType]?.[sendrecvType].totalPacketsSent || 0; - - const {csi} = statsResults[mediaType]; - if (csi && !audioSenderStream.common.csi.includes(csi)) { - audioSenderStream.common.csi.push(csi); - } - - audioSenderStream.common.rtpPackets = - statsResults[mediaType][sendrecvType].totalPacketsSent - lastPacketsSent || 0; - - const totalBytesSentInaMin = statsResults[mediaType][sendrecvType].totalBytesSent - lastBytesSent; - audioSenderStream.common.transmittedBitrate = totalBytesSentInaMin - ? (totalBytesSentInaMin * 8) / 60 - : 0; - - audioSenderStream.transmittedKeyFrames = - statsResults[mediaType][sendrecvType].totalKeyFramesEncoded - lastFramesEncoded || 0; - audioSenderStream.requestedKeyFrames = - statsResults[mediaType][sendrecvType].totalFirCount - lastFirCount || 0; - - audioSenderStream.requestedBitrate = statsResults[mediaType][sendrecvType].requestedBitrate || 0; -}; - -export const getVideoReceiverMqa = ({ - videoReceiver, - statsResults, - lastMqaDataSent, - baseMediaType, - isMultistream, -}) => { - const sendrecvType = STATS.RECEIVE_DIRECTION; - - const getLastTotalValue = (value: string) => - getTotalValueFromBaseType(lastMqaDataSent, sendrecvType, baseMediaType, value); - const getTotalValue = (value: string) => - getTotalValueFromBaseType(statsResults, sendrecvType, baseMediaType, value); - - const lastPacketsReceived = getLastTotalValue('totalPacketsReceived'); - const lastPacketsLost = getLastTotalValue('totalPacketsLost'); - const lastBytesReceived = getLastTotalValue('totalBytesReceived'); - - const lastRtxPacketsReceived = getLastTotalValue('totalRtxPacketsReceived'); - const lastRtxBytesReceived = getLastTotalValue('totalRtxBytesReceived'); - - const packetsLost = getTotalValue('totalPacketsLost'); - const totalPacketsReceived = getTotalValue('totalPacketsReceived'); - const totalBytesReceived = getTotalValue('totalBytesReceived'); - - const totalRtxPacketsReceived = getTotalValue('totalRtxPacketsReceived'); - const totalRtxBytesReceived = getTotalValue('totalRtxBytesReceived'); - - const meanRemoteJitter = Object.keys(statsResults) - .filter((mt) => mt.includes(baseMediaType)) - .reduce((acc, mt) => acc.concat(statsResults[mt][sendrecvType].meanRemoteJitter), []); - - videoReceiver.common.common.direction = - statsResults[Object.keys(statsResults).find((mediaType) => mediaType.includes(baseMediaType))] - ?.direction || 'inactive'; - videoReceiver.common.common.multistreamEnabled = isMultistream; - videoReceiver.common.common.isMain = !baseMediaType.includes('-share'); - videoReceiver.common.transportType = statsResults.connectionType.local.transport; - - // collect the packets received for the last min - videoReceiver.common.rtpPackets = totalPacketsReceived - lastPacketsReceived || 0; - - // Hop by hop are numbers and not percentage so we compare on what we sent the last min - // this is including packet lost - const totalPacketsLost = packetsLost - lastPacketsLost; - videoReceiver.common.mediaHopByHopLost = totalPacketsLost; - videoReceiver.common.rtpHopByHopLost = totalPacketsLost; - - // calculate this values - videoReceiver.common.maxRemoteJitter = max(meanRemoteJitter) * 1000 || 0; - videoReceiver.common.meanRemoteJitter = mean(meanRemoteJitter) * 1000 || 0; - - // Calculate the outgoing bitrate - const totalBytesReceivedInaMin = totalBytesReceived - lastBytesReceived; - const totalRtxBytesReceivedInaMin = totalRtxBytesReceived - lastRtxBytesReceived; - - videoReceiver.common.rtpBitrate = totalBytesReceivedInaMin - ? (totalBytesReceivedInaMin * 8) / 60 - : 0; - videoReceiver.common.rtxPackets = totalRtxPacketsReceived - lastRtxPacketsReceived; - videoReceiver.common.rtxBitrate = totalRtxBytesReceivedInaMin - ? (totalRtxBytesReceivedInaMin * 8) / 60 - : 0; -}; - -export const getVideoReceiverStreamMqa = ({ - videoReceiverStream, - statsResults, - lastMqaDataSent, - mediaType, -}) => { - const sendrecvType = STATS.RECEIVE_DIRECTION; - - const lastPacketsReceived = lastMqaDataSent[mediaType]?.[sendrecvType].totalPacketsReceived || 0; - const lastPacketsLost = lastMqaDataSent[mediaType]?.[sendrecvType].totalPacketsLost || 0; - const lastBytesReceived = lastMqaDataSent[mediaType]?.[sendrecvType].totalBytesReceived || 0; - const lastFramesReceived = lastMqaDataSent[mediaType]?.[sendrecvType].framesReceived || 0; - const lastFramesDecoded = lastMqaDataSent[mediaType]?.[sendrecvType].framesDecoded || 0; - const lastFramesDropped = lastMqaDataSent[mediaType]?.[sendrecvType].framesDropped || 0; - const lastKeyFramesDecoded = lastMqaDataSent[mediaType]?.[sendrecvType].keyFramesDecoded || 0; - const lastPliCount = lastMqaDataSent[mediaType]?.[sendrecvType].totalPliCount || 0; - - const {csi} = statsResults[mediaType]; - if (csi && !videoReceiverStream.common.csi.includes(csi)) { - videoReceiverStream.common.csi.push(csi); - } - - videoReceiverStream.common.rtpPackets = - statsResults[mediaType][sendrecvType].totalPacketsReceived - lastPacketsReceived || 0; - - const totalPacketLoss = - statsResults[mediaType][sendrecvType].totalPacketsLost - lastPacketsLost || 0; - - // End to end packetloss is after recovery - videoReceiverStream.common.rtpEndToEndLost = totalPacketLoss; - - videoReceiverStream.common.rtpJitter = - // @ts-ignore - max(statsResults[mediaType][sendrecvType].meanRemoteJitter) * 1000 || 0; - - const totalBytesReceivedInaMin = - statsResults[mediaType][sendrecvType].totalBytesReceived - lastBytesReceived; - videoReceiverStream.common.receivedBitrate = totalBytesReceivedInaMin - ? (totalBytesReceivedInaMin * 8) / 60 - : 0; - - const totalFrameReceivedInaMin = - statsResults[mediaType][sendrecvType].framesReceived - lastFramesReceived; - const totalFrameDecodedInaMin = - statsResults[mediaType][sendrecvType].framesDecoded - lastFramesDecoded; - - videoReceiverStream.common.receivedFrameRate = Math.round( - totalFrameReceivedInaMin ? (totalFrameReceivedInaMin * 100) / 60 : 0 - ); - videoReceiverStream.common.renderedFrameRate = Math.round( - totalFrameDecodedInaMin ? (totalFrameDecodedInaMin * 100) / 60 : 0 - ); - - videoReceiverStream.common.framesDropped = - statsResults[mediaType][sendrecvType].framesDropped - lastFramesDropped || 0; - videoReceiverStream.receivedHeight = statsResults[mediaType][sendrecvType].height || 0; - videoReceiverStream.receivedWidth = statsResults[mediaType][sendrecvType].width || 0; - videoReceiverStream.receivedFrameSize = - (videoReceiverStream.receivedHeight * videoReceiverStream.receivedWidth) / 256; - - videoReceiverStream.receivedKeyFrames = - statsResults[mediaType][sendrecvType].keyFramesDecoded - lastKeyFramesDecoded || 0; - videoReceiverStream.requestedKeyFrames = - statsResults[mediaType][sendrecvType].totalPliCount - lastPliCount || 0; - - videoReceiverStream.isActiveSpeaker = - statsResults[mediaType][sendrecvType].isActiveSpeaker || - ((statsResults[mediaType][sendrecvType].lastActiveSpeakerTimestamp ?? 0) > 0 && - performance.now() + - performance.timeOrigin - - (statsResults[mediaType][sendrecvType].lastActiveSpeakerTimestamp ?? 0) < - MQA_INTERVAL); -}; - -export const getVideoSenderMqa = ({ - videoSender, - statsResults, - lastMqaDataSent, - baseMediaType, - isMultistream, -}) => { - const sendrecvType = STATS.SEND_DIRECTION; - - const getLastTotalValue = (value: string) => - getTotalValueFromBaseType(lastMqaDataSent, sendrecvType, baseMediaType, value); - const getTotalValue = (value: string) => - getTotalValueFromBaseType(statsResults, sendrecvType, baseMediaType, value); - - const lastPacketsSent = getLastTotalValue('totalPacketsSent'); - const lastBytesSent = getLastTotalValue('totalBytesSent'); - const lastPacketsLostTotal = getLastTotalValue('totalPacketsLostOnReceiver'); - const lastRtxPacketsSent = getLastTotalValue('totalRtxPacketsSent'); - const lastRtxBytesSent = getLastTotalValue('totalRtxBytesSent'); - - const totalPacketsLostOnReceiver = getTotalValue('totalPacketsLostOnReceiver'); - const totalPacketsSent = getTotalValue('totalPacketsSent'); - const totalBytesSent = getTotalValue('totalBytesSent'); - const availableOutgoingBitrate = getTotalValue('availableOutgoingBitrate'); - const totalRtxPacketsSent = getTotalValue('totalRtxPacketsSent'); - const totalRtxBytesSent = getTotalValue('totalRtxBytesSent'); - - videoSender.common.common.direction = - statsResults[Object.keys(statsResults).find((mediaType) => mediaType.includes(baseMediaType))] - ?.direction || 'inactive'; - videoSender.common.common.multistreamEnabled = isMultistream; - videoSender.common.common.isMain = !baseMediaType.includes('-share'); - videoSender.common.transportType = statsResults.connectionType.local.transport; - - const meanRemoteJitter = Object.keys(statsResults) - .filter((mt) => mt.includes(baseMediaType)) - .reduce((acc, mt) => acc.concat(statsResults[mt][sendrecvType].meanRemoteJitter), []); - const meanRoundTripTime = Object.keys(statsResults) - .filter((mt) => mt.includes(baseMediaType)) - .reduce((acc, mt) => acc.concat(statsResults[mt][sendrecvType].meanRoundTripTime), []); - - // @ts-ignore - videoSender.common.maxRemoteJitter = max(meanRemoteJitter) * 1000 || 0; - videoSender.common.meanRemoteJitter = mean(meanRemoteJitter) * 1000 || 0; - - videoSender.common.rtpPackets = totalPacketsSent - lastPacketsSent; - videoSender.common.availableBitrate = availableOutgoingBitrate; - - // Calculate based on how much packets lost of received compated to how to the client sent - const totalPacketsLostForaMin = totalPacketsLostOnReceiver - lastPacketsLostTotal; - - videoSender.common.maxRemoteLossRate = - totalPacketsSent - lastPacketsSent > 0 - ? (totalPacketsLostForaMin * 100) / (totalPacketsSent - lastPacketsSent) - : 0; // This is the packets sent with in last min || 0; - - videoSender.common.maxRoundTripTime = max(meanRoundTripTime) * 1000 || 0; - videoSender.common.meanRoundTripTime = mean(meanRoundTripTime) * 1000 || 0; - videoSender.common.roundTripTime = videoSender.common.maxRoundTripTime; - - // Calculate the outgoing bitrate - const totalBytesSentInaMin = totalBytesSent - lastBytesSent; - const totalRtxBytesSentInaMin = totalRtxBytesSent - lastRtxBytesSent; - - videoSender.common.rtpBitrate = totalBytesSentInaMin ? (totalBytesSentInaMin * 8) / 60 : 0; - videoSender.common.rtxPackets = totalRtxPacketsSent - lastRtxPacketsSent; - videoSender.common.rtxBitrate = totalRtxBytesSentInaMin ? (totalRtxBytesSentInaMin * 8) / 60 : 0; -}; - -export const getVideoSenderStreamMqa = ({ - videoSenderStream, - statsResults, - lastMqaDataSent, - mediaType, -}) => { - const sendrecvType = STATS.SEND_DIRECTION; - - const lastPacketsSent = lastMqaDataSent[mediaType]?.[sendrecvType].totalPacketsSent || 0; - const lastBytesSent = lastMqaDataSent[mediaType]?.[sendrecvType].totalBytesSent || 0; - const lastKeyFramesEncoded = - lastMqaDataSent[mediaType]?.[sendrecvType].totalKeyFramesEncoded || 0; - const lastFirCount = lastMqaDataSent[mediaType]?.[sendrecvType].totalFirCount || 0; - const lastFramesSent = lastMqaDataSent[mediaType]?.[sendrecvType].framesSent || 0; - - const {csi} = statsResults[mediaType]; - if (csi && !videoSenderStream.common.csi.includes(csi)) { - videoSenderStream.common.csi.push(csi); - } - - videoSenderStream.common.rtpPackets = - statsResults[mediaType][sendrecvType].totalPacketsSent - lastPacketsSent || 0; - - // Calculate the outgoing bitrate - const totalBytesSentInaMin = statsResults[mediaType][sendrecvType].totalBytesSent - lastBytesSent; - - videoSenderStream.common.transmittedBitrate = totalBytesSentInaMin - ? (totalBytesSentInaMin * 8) / 60 - : 0; - - videoSenderStream.transmittedKeyFrames = - statsResults[mediaType][sendrecvType].totalKeyFramesEncoded - lastKeyFramesEncoded || 0; - videoSenderStream.requestedKeyFrames = - statsResults[mediaType][sendrecvType].totalFirCount - lastFirCount || 0; - - // From tracks //TODO: calculate a proper one - const totalFrameSentInaMin = - statsResults[mediaType][sendrecvType].framesSent - (lastFramesSent || 0); - - videoSenderStream.common.transmittedFrameRate = Math.round( - totalFrameSentInaMin ? (totalFrameSentInaMin * 100) / 60 : 0 - ); - videoSenderStream.transmittedHeight = statsResults[mediaType][sendrecvType].height || 0; - videoSenderStream.transmittedWidth = statsResults[mediaType][sendrecvType].width || 0; - videoSenderStream.transmittedFrameSize = - (videoSenderStream.transmittedHeight * videoSenderStream.transmittedWidth) / 256; - videoSenderStream.requestedBitrate = statsResults[mediaType][sendrecvType].requestedBitrate || 0; - videoSenderStream.requestedFrameSize = - statsResults[mediaType][sendrecvType].requestedFrameSize || 0; -}; - -/** - * Checks if stream stats should be updated based on request status and elapsed time. - * - * @param {Object} statsResults - Stats results object. - * @param {string} mediaType - Media type (e.g., 'audio', 'video'). - * @param {string} direction - Stats direction (e.g., 'send', 'receive'). - * @returns {boolean} Whether stats should be updated. - */ -export const isStreamRequested = ( - statsResults: any, - mediaType: string, - direction: string -): boolean => { - const now = performance.timeOrigin + performance.now(); - const lastUpdateTimestamp = statsResults[mediaType][direction]?.lastRequestedUpdateTimestamp; - const isRequested = statsResults[mediaType][direction]?.isRequested; - - return isRequested || (lastUpdateTimestamp && now - lastUpdateTimestamp < MQA_INTERVAL); -}; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/media/MediaConnectionAwaiter.ts b/packages/@webex/plugin-meetings/test/unit/spec/media/MediaConnectionAwaiter.ts index de4d308dfff..e4e554e9961 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/media/MediaConnectionAwaiter.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/media/MediaConnectionAwaiter.ts @@ -1,6 +1,6 @@ import {assert} from '@webex/test-helper-chai'; import sinon from 'sinon'; -import {ConnectionState, Event} from '@webex/internal-media-core'; +import {ConnectionState, MediaConnectionEventNames} from '@webex/internal-media-core'; import testUtils from '../../../utils/testUtils'; import {ICE_AND_DTLS_CONNECTION_TIMEOUT} from '@webex/plugin-meetings/src/constants'; import MediaConnectionAwaiter from '../../../../src/media/MediaConnectionAwaiter'; @@ -67,9 +67,9 @@ describe('MediaConnectionAwaiter', () => { // check the right listener was registered assert.calledThrice(mockMC.on); - assert.equal(mockMC.on.getCall(0).args[0], Event.PEER_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(1).args[0], Event.ICE_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(2).args[0], Event.ICE_GATHERING_STATE_CHANGED); + assert.equal(mockMC.on.getCall(0).args[0], MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(1).args[0], MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(2).args[0], MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED); const iceGatheringListener = mockMC.on.getCall(2).args[1]; mockMC.getIceGatheringState.returns('complete'); @@ -109,9 +109,9 @@ describe('MediaConnectionAwaiter', () => { // check the right listener was registered assert.calledThrice(mockMC.on); - assert.equal(mockMC.on.getCall(0).args[0], Event.PEER_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(1).args[0], Event.ICE_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(2).args[0], Event.ICE_GATHERING_STATE_CHANGED); + assert.equal(mockMC.on.getCall(0).args[0], MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(1).args[0], MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(2).args[0], MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED); const iceConnectionListener = mockMC.on.getCall(1).args[1]; mockMC.getConnectionState.returns(ConnectionState.Failed); @@ -150,12 +150,12 @@ describe('MediaConnectionAwaiter', () => { // check the right listener was registered assert.calledThrice(mockMC.on); - assert.equal(mockMC.on.getCall(0).args[0], Event.PEER_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(1).args[0], Event.ICE_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(2).args[0], Event.ICE_GATHERING_STATE_CHANGED); + assert.equal(mockMC.on.getCall(0).args[0], MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(1).args[0], MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(2).args[0], MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED); const listener = mockMC.on.getCall(1).args[1]; const iceConnectionListener = mockMC.on.getCall(1).args[1]; - + mockMC.getIceGatheringState.returns('complete'); listener(); @@ -193,9 +193,9 @@ describe('MediaConnectionAwaiter', () => { // check the right listener was registered assert.calledThrice(mockMC.on); - assert.equal(mockMC.on.getCall(0).args[0], Event.PEER_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(1).args[0], Event.ICE_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(2).args[0], Event.ICE_GATHERING_STATE_CHANGED); + assert.equal(mockMC.on.getCall(0).args[0], MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(1).args[0], MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(2).args[0], MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED); mockMC.getConnectionState.returns(ConnectionState.Connected); @@ -232,9 +232,9 @@ describe('MediaConnectionAwaiter', () => { // check the right listener was registered assert.calledThrice(mockMC.on); - assert.equal(mockMC.on.getCall(0).args[0], Event.PEER_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(1).args[0], Event.ICE_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(2).args[0], Event.ICE_GATHERING_STATE_CHANGED); + assert.equal(mockMC.on.getCall(0).args[0], MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(1).args[0], MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(2).args[0], MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED); const listener = mockMC.on.getCall(0).args[1]; // call the listener and pretend we are now connected @@ -275,9 +275,9 @@ describe('MediaConnectionAwaiter', () => { // check the right listener was registered assert.calledThrice(mockMC.on); - assert.equal(mockMC.on.getCall(0).args[0], Event.PEER_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(1).args[0], Event.ICE_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(2).args[0], Event.ICE_GATHERING_STATE_CHANGED); + assert.equal(mockMC.on.getCall(0).args[0], MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(1).args[0], MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(2).args[0], MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED); const listener = mockMC.on.getCall(1).args[1]; // call the listener and pretend we are now connected @@ -325,9 +325,9 @@ describe('MediaConnectionAwaiter', () => { // check the right listener was registered assert.calledThrice(mockMC.on); - assert.equal(mockMC.on.getCall(0).args[0], Event.PEER_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(1).args[0], Event.ICE_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(2).args[0], Event.ICE_GATHERING_STATE_CHANGED); + assert.equal(mockMC.on.getCall(0).args[0], MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(1).args[0], MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(2).args[0], MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED); const listener = mockMC.on.getCall(2).args[1]; // call the listener and pretend we are now connected @@ -374,9 +374,9 @@ describe('MediaConnectionAwaiter', () => { // check the right listener was registered assert.calledThrice(mockMC.on); - assert.equal(mockMC.on.getCall(0).args[0], Event.PEER_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(1).args[0], Event.ICE_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(2).args[0], Event.ICE_GATHERING_STATE_CHANGED); + assert.equal(mockMC.on.getCall(0).args[0], MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(1).args[0], MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(2).args[0], MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED); await clock.tickAsync(ICE_AND_DTLS_CONNECTION_TIMEOUT * 2); await testUtils.flushPromises(); @@ -416,9 +416,9 @@ describe('MediaConnectionAwaiter', () => { // check the right listener was registered assert.calledThrice(mockMC.on); - assert.equal(mockMC.on.getCall(0).args[0], Event.PEER_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(1).args[0], Event.ICE_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(2).args[0], Event.ICE_GATHERING_STATE_CHANGED); + assert.equal(mockMC.on.getCall(0).args[0], MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(1).args[0], MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(2).args[0], MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED); const connectionStateListener = mockMC.on.getCall(0).args[1]; const iceGatheringListener = mockMC.on.getCall(2).args[1]; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/media/index.ts b/packages/@webex/plugin-meetings/test/unit/spec/media/index.ts index 52f62cc9c30..4c5e7acea08 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/media/index.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/media/index.ts @@ -1,9 +1,8 @@ -import * as internalMediaModule from '@webex/internal-media-core'; +import * as InternalMediaCoreModule from '@webex/internal-media-core'; import Media from '@webex/plugin-meetings/src/media/index'; import {assert} from '@webex/test-helper-chai'; import sinon from 'sinon'; import StaticConfig from '@webex/plugin-meetings/src/common/config'; -import {forEach} from 'lodash'; import MockWebex from '@webex/test-helper-mock-webex'; describe('createMediaConnection', () => { @@ -54,7 +53,7 @@ describe('createMediaConnection', () => { it('creates a RoapMediaConnection when multistream is disabled', () => { const roapMediaConnectionConstructorStub = sinon - .stub(internalMediaModule, 'RoapMediaConnection') + .stub(InternalMediaCoreModule, 'RoapMediaConnection') .returns(fakeRoapMediaConnection); StaticConfig.set({bandwidth: {audio: 123, video: 456, startBitrate: 999}}); @@ -132,7 +131,7 @@ describe('createMediaConnection', () => { it('creates a MultistreamRoapMediaConnection when multistream is enabled', () => { const multistreamRoapMediaConnectionConstructorStub = sinon - .stub(internalMediaModule, 'MultistreamRoapMediaConnection') + .stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection') .returns(fakeRoapMediaConnection); Media.createMediaConnection(true, 'some debug id', webex, 'meeting id', 'correlationId', { @@ -176,9 +175,9 @@ describe('createMediaConnection', () => { ].forEach(({testCase, turnServerInfo}) => { it(`passes empty ICE servers array to MultistreamRoapMediaConnection if ${testCase} (multistream enabled)`, () => { const multistreamRoapMediaConnectionConstructorStub = sinon - .stub(internalMediaModule, 'MultistreamRoapMediaConnection') + .stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection') .returns(fakeRoapMediaConnection); - + Media.createMediaConnection(true, 'debug string', webex, 'meeting id', 'correlationId', { mediaProperties: { mediaDirection: { @@ -202,10 +201,10 @@ describe('createMediaConnection', () => { ); }); }); - + it('does not pass bundlePolicy to MultistreamRoapMediaConnection if bundlePolicy is undefined', () => { const multistreamRoapMediaConnectionConstructorStub = sinon - .stub(internalMediaModule, 'MultistreamRoapMediaConnection') + .stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection') .returns(fakeRoapMediaConnection); Media.createMediaConnection(true, 'debug string', webex, 'meeting id', 'correlationId', { @@ -237,7 +236,7 @@ describe('createMediaConnection', () => { ].forEach(({testCase, turnServerInfo}) => { it(`passes empty ICE servers array to RoapMediaConnection if ${testCase} (multistream disabled)`, () => { const roapMediaConnectionConstructorStub = sinon - .stub(internalMediaModule, 'RoapMediaConnection') + .stub(InternalMediaCoreModule, 'RoapMediaConnection') .returns(fakeRoapMediaConnection); StaticConfig.set({bandwidth: {audio: 123, video: 456, startBitrate: 999}}); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/connectionStateHandler.ts b/packages/@webex/plugin-meetings/test/unit/spec/meeting/connectionStateHandler.ts index 729654fd0a9..a4f0460be35 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/connectionStateHandler.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/connectionStateHandler.ts @@ -4,7 +4,7 @@ import { ConnectionStateHandler, ConnectionStateEvent, } from '@webex/plugin-meetings/src/meeting/connectionStateHandler'; -import {Event, ConnectionState} from '@webex/internal-media-core'; +import {ConnectionState, MediaConnectionEventNames} from '@webex/internal-media-core'; describe('ConnectionStateHandler', () => { let connectionStateHandler: ConnectionStateHandler; @@ -26,8 +26,8 @@ describe('ConnectionStateHandler', () => { // check the right listener was registered assert.calledTwice(mockMC.on); - assert.equal(mockMC.on.getCall(0).args[0], Event.PEER_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(1).args[0], Event.ICE_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(0).args[0], MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(1).args[0], MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED); const listener = mockMC.on.getCall(0).args[1]; listener(); @@ -51,8 +51,8 @@ describe('ConnectionStateHandler', () => { // check the right listener was registered assert.calledTwice(mockMC.on); - assert.equal(mockMC.on.getCall(0).args[0], Event.PEER_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(1).args[0], Event.ICE_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(0).args[0], MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(1).args[0], MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED); const listener = mockMC.on.getCall(1).args[1]; listener(); @@ -76,8 +76,8 @@ describe('ConnectionStateHandler', () => { // check the right listener was registered assert.calledTwice(mockMC.on); - assert.equal(mockMC.on.getCall(0).args[0], Event.PEER_CONNECTION_STATE_CHANGED); - assert.equal(mockMC.on.getCall(1).args[0], Event.ICE_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(0).args[0], MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED); + assert.equal(mockMC.on.getCall(1).args[0], MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED); const peerConnectionListener = mockMC.on.getCall(0).args[1]; const iceConnectionListener = mockMC.on.getCall(1).args[1]; 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 6d0b3a7bcca..24e352c815f 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js @@ -4,11 +4,11 @@ import 'jsdom-global/register'; import {cloneDeep, forEach, isEqual, isUndefined} from 'lodash'; import sinon from 'sinon'; -import * as internalMediaModule from '@webex/internal-media-core'; +import * as InternalMediaCoreModule from '@webex/internal-media-core'; import StateMachine from 'javascript-state-machine'; import uuid from 'uuid'; import {assert, expect} from '@webex/test-helper-chai'; -import {Credentials, Token, WebexPlugin} from '@webex/webex-core'; +import {Credentials, WebexPlugin} from '@webex/webex-core'; import Support from '@webex/internal-plugin-support'; import MockWebex from '@webex/test-helper-mock-webex'; import StaticConfig from '@webex/plugin-meetings/src/common/config'; @@ -28,24 +28,21 @@ import { DISPLAY_HINTS, SELF_POLICY, IP_VERSION, - ERROR_DICTIONARY, NETWORK_STATUS, ONLINE, OFFLINE, - RECONNECTION, ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT, } from '@webex/plugin-meetings/src/constants'; -import * as InternalMediaCoreModule from '@webex/internal-media-core'; import { ConnectionState, - Event, + MediaConnectionEventNames, + StatsAnalyzerEventNames, Errors, ErrorType, RemoteTrackType, MediaType, } from '@webex/internal-media-core'; import {LocalStreamEventNames} from '@webex/media-helpers'; -import * as StatsAnalyzerModule from '@webex/plugin-meetings/src/statsAnalyzer'; import EventsScope from '@webex/plugin-meetings/src/common/events/events-scope'; import Meetings, {CONSTANTS} from '@webex/plugin-meetings'; import Meeting from '@webex/plugin-meetings/src/meeting'; @@ -2288,7 +2285,7 @@ describe('plugin-meetings', () => { Media.createMediaConnection = sinon.stub().returns({ initiateOffer: sinon.stub().callsFake(() => { // simulate offer being generated - eventListeners[Event.LOCAL_SDP_OFFER_GENERATED](); + eventListeners[MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED](); return Promise.resolve(); }), @@ -2488,7 +2485,7 @@ describe('plugin-meetings', () => { it('should reject if waitForMediaConnectionConnected() rejects after turn server retry', async () => { const FAKE_ERROR = {fatal: true}; const getErrorPayloadForClientErrorCodeStub = - + (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode = sinon.stub().returns(FAKE_ERROR)); webex.meetings.reachability = { @@ -2991,7 +2988,7 @@ describe('plugin-meetings', () => { }), }; meeting.iceCandidatesCount = 3; - + await meeting.addMedia({ mediaSettings: {}, }); @@ -3155,7 +3152,7 @@ describe('plugin-meetings', () => { statsAnalyzerStub = new EventsScope(); // mock the StatsAnalyzer constructor - sinon.stub(StatsAnalyzerModule, 'StatsAnalyzer').returns(statsAnalyzerStub); + sinon.stub(InternalMediaCoreModule, 'StatsAnalyzer').returns(statsAnalyzerStub); await meeting.addMedia({ mediaSettings: {}, @@ -3169,8 +3166,8 @@ describe('plugin-meetings', () => { it('LOCAL_MEDIA_STARTED triggers "meeting:media:local:start" event and sends metrics', async () => { statsAnalyzerStub.emit( {file: 'test', function: 'test'}, - StatsAnalyzerModule.EVENTS.LOCAL_MEDIA_STARTED, - {type: 'audio'} + StatsAnalyzerEventNames.LOCAL_MEDIA_STARTED, + {mediaType: 'audio'} ); assert.calledWith( @@ -3182,7 +3179,7 @@ describe('plugin-meetings', () => { }, EVENT_TRIGGERS.MEETING_MEDIA_LOCAL_STARTED, { - type: 'audio', + mediaType: 'audio', } ); assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, { @@ -3197,8 +3194,8 @@ describe('plugin-meetings', () => { it('LOCAL_MEDIA_STOPPED triggers the right metrics', async () => { statsAnalyzerStub.emit( {file: 'test', function: 'test'}, - StatsAnalyzerModule.EVENTS.LOCAL_MEDIA_STOPPED, - {type: 'video'} + StatsAnalyzerEventNames.LOCAL_MEDIA_STOPPED, + {mediaType: 'video'} ); assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, { @@ -3213,8 +3210,8 @@ describe('plugin-meetings', () => { it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics', async () => { statsAnalyzerStub.emit( {file: 'test', function: 'test'}, - StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STARTED, - {type: 'video'} + StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED, + {mediaType: 'video'} ); assert.calledWith( @@ -3226,7 +3223,7 @@ describe('plugin-meetings', () => { }, EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED, { - type: 'video', + mediaType: 'video', } ); assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, { @@ -3241,8 +3238,8 @@ describe('plugin-meetings', () => { it('REMOTE_MEDIA_STOPPED triggers the right metrics', async () => { statsAnalyzerStub.emit( {file: 'test', function: 'test'}, - StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STOPPED, - {type: 'audio'} + StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED, + {mediaType: 'audio'} ); assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, { @@ -3257,8 +3254,8 @@ describe('plugin-meetings', () => { it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics for share', async () => { statsAnalyzerStub.emit( {file: 'test', function: 'test'}, - StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STARTED, - {type: 'share'} + StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED, + {mediaType: 'share'} ); assert.calledWith( @@ -3270,7 +3267,7 @@ describe('plugin-meetings', () => { }, EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED, { - type: 'share', + mediaType: 'share', } ); assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, { @@ -3293,8 +3290,8 @@ describe('plugin-meetings', () => { it('REMOTE_MEDIA_STOPPED triggers the right metrics for share', async () => { statsAnalyzerStub.emit( {file: 'test', function: 'test'}, - StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STOPPED, - {type: 'share'} + StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED, + {mediaType: 'share'} ); assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, { @@ -3315,19 +3312,18 @@ describe('plugin-meetings', () => { }); it('calls submitMQE correctly', async () => { - const fakeData = {intervalMetadata: {bla: 'bla'}}; + const fakeData = {intervalMetadata: {bla: 'bla'}, networkType: 'wifi'}; statsAnalyzerStub.emit( {file: 'test', function: 'test'}, - StatsAnalyzerModule.EVENTS.MEDIA_QUALITY, - {data: fakeData, networkType: 'wifi'} + StatsAnalyzerEventNames.MEDIA_QUALITY, + {data: fakeData} ); assert.calledWithMatch(webex.internal.newMetrics.submitMQE, { name: 'client.mediaquality.event', options: { meetingId: meeting.id, - networkType: 'wifi', }, payload: { intervals: [fakeData], @@ -3384,7 +3380,7 @@ describe('plugin-meetings', () => { it('succeeds even if getDevices() throws', async () => { meeting.meetingState = 'ACTIVE'; - sinon.stub(internalMediaModule, 'getDevices').rejects(new Error('fake error')); + sinon.stub(InternalMediaCoreModule, 'getDevices').rejects(new Error('fake error')); await meeting.addMedia(); }); @@ -3653,11 +3649,11 @@ describe('plugin-meetings', () => { }; roapMediaConnectionConstructorStub = sinon - .stub(internalMediaModule, 'RoapMediaConnection') + .stub(InternalMediaCoreModule, 'RoapMediaConnection') .returns(fakeRoapMediaConnection); multistreamRoapMediaConnectionConstructorStub = sinon - .stub(internalMediaModule, 'MultistreamRoapMediaConnection') + .stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection') .returns(fakeMultistreamRoapMediaConnection); locusMediaRequestStub = sinon @@ -3705,13 +3701,13 @@ describe('plugin-meetings', () => { for (let idx = 0; idx < roapMediaConnectionToCheck.on.callCount; idx += 1) { if ( - roapMediaConnectionToCheck.on.getCall(idx).args[0] === Event.ROAP_MESSAGE_TO_SEND + roapMediaConnectionToCheck.on.getCall(idx).args[0] === MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND ) { return roapMediaConnectionToCheck.on.getCall(idx).args[1]; } } assert.fail( - 'listener for "roap:messageToSend" (Event.ROAP_MESSAGE_TO_SEND) was not registered' + 'listener for "roap:messageToSend" (MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND) was not registered' ); }; @@ -7487,7 +7483,7 @@ describe('plugin-meetings', () => { }; const simulateConnectionStateChange = (newState) => { meeting.mediaProperties.webrtcMediaConnection.getConnectionState = sinon.stub().returns(newState); - eventListeners[Event.PEER_CONNECTION_STATE_CHANGED](); + eventListeners[MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED](); } beforeEach(() => { @@ -7505,20 +7501,20 @@ describe('plugin-meetings', () => { it('should register for all the correct RoapMediaConnection events', () => { meeting.setupMediaConnectionListeners(); - assert.isFunction(eventListeners[Event.ROAP_STARTED]); - assert.isFunction(eventListeners[Event.ROAP_DONE]); - assert.isFunction(eventListeners[Event.ROAP_FAILURE]); - assert.isFunction(eventListeners[Event.ROAP_MESSAGE_TO_SEND]); - assert.isFunction(eventListeners[Event.REMOTE_TRACK_ADDED]); - assert.isFunction(eventListeners[Event.PEER_CONNECTION_STATE_CHANGED]); - assert.isFunction(eventListeners[Event.ICE_CONNECTION_STATE_CHANGED]); - assert.isFunction(eventListeners[Event.ICE_CANDIDATE]); - assert.isFunction(eventListeners[Event.ICE_CANDIDATE_ERROR]); + assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_STARTED]); + assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_DONE]); + assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_FAILURE]); + assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]); + assert.isFunction(eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]); + assert.isFunction(eventListeners[MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED]); + assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED]); + assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]); + assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]); }); it('should trigger a media:ready event when REMOTE_TRACK_ADDED is fired', () => { meeting.setupMediaConnectionListeners(); - eventListeners[Event.REMOTE_TRACK_ADDED]({ + eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({ track: 'track', type: RemoteTrackType.AUDIO, }); @@ -7528,7 +7524,7 @@ describe('plugin-meetings', () => { stream: fakeStream, }); - eventListeners[Event.REMOTE_TRACK_ADDED]({ + eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({ track: 'track', type: RemoteTrackType.VIDEO, }); @@ -7538,7 +7534,7 @@ describe('plugin-meetings', () => { stream: fakeStream, }); - eventListeners[Event.REMOTE_TRACK_ADDED]({ + eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({ track: 'track', type: RemoteTrackType.SCREENSHARE_VIDEO, }); @@ -7555,13 +7551,13 @@ describe('plugin-meetings', () => { }); it('should collect ice candidates', () => { - eventListeners[Event.ICE_CANDIDATE]({candidate: 'candidate'}); + eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: 'candidate'}); assert.equal(meeting.iceCandidatesCount, 1); }); it('should not collect null ice candidates', () => { - eventListeners[Event.ICE_CANDIDATE]({candidate: null}); + eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: null}); assert.equal(meeting.iceCandidatesCount, 0); }); @@ -7573,23 +7569,23 @@ describe('plugin-meetings', () => { }); it('should not collect skipped ice candidates error', () => { - eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 600, errorText: 'Address not associated with the desired network interface.' }}); + eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({error: { errorCode: 600, errorText: 'Address not associated with the desired network interface.' }}); assert.equal(meeting.iceCandidateErrors.size, 0); }); it('should collect valid ice candidates error', () => { - eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: '' }}); + eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: '' }}); assert.equal(meeting.iceCandidateErrors.size, 1); assert.equal(meeting.iceCandidateErrors.has('701_'), true); }); it('should increment counter if same valid ice candidates error collected', () => { - eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: '' }}); + eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: '' }}); - eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: 'STUN host lookup received error.' }}); - eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: 'STUN host lookup received error.' }}); + eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: 'STUN host lookup received error.' }}); + eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: 'STUN host lookup received error.' }}); assert.equal(meeting.iceCandidateErrors.size, 2); assert.equal(meeting.iceCandidateErrors.has('701_'), true); @@ -7885,7 +7881,7 @@ describe('plugin-meetings', () => { cause: {name: fakeRootCauseName}, }); - eventListeners[Event.ROAP_FAILURE](fakeError); + eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError); checkMetricSent('client.media-engine.local-sdp-generated', fakeError); checkBehavioralMetricSent( @@ -7902,7 +7898,7 @@ describe('plugin-meetings', () => { cause: {name: fakeRootCauseName}, }); - eventListeners[Event.ROAP_FAILURE](fakeError); + eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError); checkMetricSent('client.media-engine.remote-sdp-received', fakeError); checkBehavioralMetricSent( @@ -7919,7 +7915,7 @@ describe('plugin-meetings', () => { cause: {name: fakeRootCauseName}, }); - eventListeners[Event.ROAP_FAILURE](fakeError); + eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError); checkMetricSent('client.media-engine.remote-sdp-received', fakeError); checkBehavioralMetricSent( @@ -7934,7 +7930,7 @@ describe('plugin-meetings', () => { // SdpError is usually without a cause const fakeError = new Errors.SdpError(fakeErrorMessage, {name: fakeErrorName}); - eventListeners[Event.ROAP_FAILURE](fakeError); + eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError); checkMetricSent('client.media-engine.local-sdp-generated', fakeError); // expectedMetadataType is the error name in this case @@ -7952,7 +7948,7 @@ describe('plugin-meetings', () => { name: fakeErrorName, }); - eventListeners[Event.ROAP_FAILURE](fakeError); + eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError); checkMetricSent('client.media-engine.local-sdp-generated', fakeError); // expectedMetadataType is the error name in this case @@ -7978,7 +7974,7 @@ describe('plugin-meetings', () => { }; meeting.sdpResponseTimer = '1234'; - eventListeners[Event.REMOTE_SDP_ANSWER_PROCESSED](); + eventListeners[MediaConnectionEventNames.REMOTE_SDP_ANSWER_PROCESSED](); assert.calledOnce(webex.internal.newMetrics.submitClientEvent); assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, { @@ -8006,7 +8002,7 @@ describe('plugin-meetings', () => { it('handles LOCAL_SDP_OFFER_GENERATED correctly', () => { assert.equal(meeting.deferSDPAnswer, undefined); - eventListeners[Event.LOCAL_SDP_OFFER_GENERATED](); + eventListeners[MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED](); assert.calledOnce(webex.internal.newMetrics.submitClientEvent); assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, { @@ -8018,7 +8014,7 @@ describe('plugin-meetings', () => { }); it('handles LOCAL_SDP_ANSWER_GENERATED correctly', () => { - eventListeners[Event.LOCAL_SDP_ANSWER_GENERATED](); + eventListeners[MediaConnectionEventNames.LOCAL_SDP_ANSWER_GENERATED](); assert.calledOnce(webex.internal.newMetrics.submitClientEvent); assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, { @@ -8028,7 +8024,7 @@ describe('plugin-meetings', () => { }); }); - describe('handles Event.ROAP_MESSAGE_TO_SEND correctly', () => { + describe('handles MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND correctly', () => { let sendRoapOKStub; let sendRoapMediaRequestStub; let sendRoapAnswerStub; @@ -8046,7 +8042,7 @@ describe('plugin-meetings', () => { }); it('handles OK message correctly', () => { - eventListeners[Event.ROAP_MESSAGE_TO_SEND]({ + eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({ roapMessage: {messageType: 'OK', seq: 1}, }); @@ -8061,7 +8057,7 @@ describe('plugin-meetings', () => { it('handles OFFER message correctly (no answer in the http response)', async () => { sinon.stub(meeting, 'roapMessageReceived'); - eventListeners[Event.ROAP_MESSAGE_TO_SEND]({ + eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({ roapMessage: { messageType: 'OFFER', seq: 1, @@ -8087,7 +8083,7 @@ describe('plugin-meetings', () => { sendRoapMediaRequestStub.resolves({roapAnswer: fakeAnswer}); sinon.stub(meeting, 'roapMessageReceived'); - eventListeners[Event.ROAP_MESSAGE_TO_SEND]({ + eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({ roapMessage: { messageType: 'OFFER', seq: 1, @@ -8122,7 +8118,7 @@ describe('plugin-meetings', () => { .stub() .callsFake(({clientErrorCode}) => ({errorCode: clientErrorCode, fatal: true}))); - eventListeners[Event.ROAP_MESSAGE_TO_SEND]({ + eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({ roapMessage: { messageType: 'OFFER', seq: 1, @@ -8163,7 +8159,7 @@ describe('plugin-meetings', () => { }); it('handles ANSWER message correctly', () => { - eventListeners[Event.ROAP_MESSAGE_TO_SEND]({ + eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({ roapMessage: { messageType: 'ANSWER', seq: 10, @@ -8184,7 +8180,7 @@ describe('plugin-meetings', () => { it('sends metrics if fails to send roap ANSWER message', async () => { sendRoapAnswerStub.rejects(new Error('sending answer failed')); - await eventListeners[Event.ROAP_MESSAGE_TO_SEND]({ + await eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({ roapMessage: { messageType: 'ANSWER', seq: 10, @@ -8208,7 +8204,7 @@ describe('plugin-meetings', () => { [ErrorType.CONFLICT, ErrorType.DOUBLECONFLICT].forEach((errorType) => it(`handles ERROR message indicating glare condition correctly (errorType=${errorType})`, () => { - eventListeners[Event.ROAP_MESSAGE_TO_SEND]({ + eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({ roapMessage: { messageType: 'ERROR', seq: 10, @@ -8239,7 +8235,7 @@ describe('plugin-meetings', () => { ); it('handles ERROR message indicating other errors correctly', () => { - eventListeners[Event.ROAP_MESSAGE_TO_SEND]({ + eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({ roapMessage: { messageType: 'ERROR', seq: 10, @@ -8267,8 +8263,8 @@ describe('plugin-meetings', () => { }); it('registers for audio and video source count changed', () => { - assert.isFunction(eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED]); - assert.isFunction(eventListeners[Event.AUDIO_SOURCES_COUNT_CHANGED]); + assert.isFunction(eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED]); + assert.isFunction(eventListeners[MediaConnectionEventNames.AUDIO_SOURCES_COUNT_CHANGED]); }); it('forwards the VIDEO_SOURCES_COUNT_CHANGED event as "media:remoteVideoSourceCountChanged"', () => { @@ -8278,7 +8274,7 @@ describe('plugin-meetings', () => { sinon.stub(meeting.mediaRequestManagers.video, 'setNumCurrentSources'); - eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED]( + eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED]( numTotalSources, numLiveSources, mediaContent @@ -8302,7 +8298,7 @@ describe('plugin-meetings', () => { const numLiveSources = 2; const mediaContent = 'MAIN'; - eventListeners[Event.AUDIO_SOURCES_COUNT_CHANGED]( + eventListeners[MediaConnectionEventNames.AUDIO_SOURCES_COUNT_CHANGED]( numTotalSources, numLiveSources, mediaContent @@ -8330,7 +8326,7 @@ describe('plugin-meetings', () => { 'setNumCurrentSources' ); - eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED]( + eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED]( numTotalSources, numLiveSources, 'MAIN' @@ -8348,7 +8344,7 @@ describe('plugin-meetings', () => { 'setNumCurrentSources' ); - eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED]( + eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED]( numTotalSources, numLiveSources, 'SLIDES' diff --git a/packages/@webex/plugin-meetings/test/unit/spec/multistream/mediaRequestManager.ts b/packages/@webex/plugin-meetings/test/unit/spec/multistream/mediaRequestManager.ts index b1f33a31089..3f0c70170f5 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/multistream/mediaRequestManager.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/multistream/mediaRequestManager.ts @@ -4,7 +4,7 @@ import sinon from 'sinon'; import {assert} from '@webex/test-helper-chai'; import {getMaxFs} from '@webex/plugin-meetings/src/multistream/remoteMedia'; import FakeTimers from '@sinonjs/fake-timers'; -import * as mediaCore from '@webex/internal-media-core'; +import * as InternalMediaCoreModule from '@webex/internal-media-core'; import { expect } from 'chai'; type ExpectedActiveSpeaker = { @@ -978,7 +978,7 @@ describe('MediaRequestManager', () => { beforeEach(() => { sendMediaRequestsCallback.resetHistory(); getRecommendedMaxBitrateForFrameSizeSpy = sinon.spy( - mediaCore, + InternalMediaCoreModule, 'getRecommendedMaxBitrateForFrameSize' ); }); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/rtcMetrics/index.ts b/packages/@webex/plugin-meetings/test/unit/spec/rtcMetrics/index.ts index 91e3431a94c..7a0ff4eb3c4 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/rtcMetrics/index.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/rtcMetrics/index.ts @@ -1,3 +1,4 @@ +import 'jsdom-global/register'; import RtcMetrics from '@webex/plugin-meetings/src/rtcMetrics'; import MockWebex from '@webex/test-helper-mock-webex'; import {assert} from '@webex/test-helper-chai'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/stats-analyzer/index.js b/packages/@webex/plugin-meetings/test/unit/spec/stats-analyzer/index.js deleted file mode 100644 index 5acaba12b18..00000000000 --- a/packages/@webex/plugin-meetings/test/unit/spec/stats-analyzer/index.js +++ /dev/null @@ -1,2147 +0,0 @@ -import 'jsdom-global/register'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import sinon from 'sinon'; -import {ConnectionState} from '@webex/internal-media-core'; - -import {StatsAnalyzer, EVENTS} from '../../../../src/statsAnalyzer'; -import NetworkQualityMonitor from '../../../../src/networkQualityMonitor'; -import testUtils from '../../../utils/testUtils'; -import {MEDIA_DEVICES, MQA_INTERVAL, _UNKNOWN_} from '@webex/plugin-meetings/src/constants'; -import LoggerProxy from '../../../../src/common/logs/logger-proxy'; -import LoggerConfig from '../../../../src/common/logs/logger-config'; -import {CpuInfo} from '@webex/web-capabilities'; - -const {assert} = chai; - -chai.use(chaiAsPromised); -sinon.assert.expose(chai.assert, {prefix: ''}); - -const startStatsAnalyzer = async ({statsAnalyzer, mediaStatus, lastEmittedEvents = {}, pc}) => { - statsAnalyzer.updateMediaStatus(mediaStatus); - statsAnalyzer.startAnalyzer(pc); - statsAnalyzer.lastEmittedStartStopEvent = lastEmittedEvents; - - await testUtils.flushPromises(); -}; - -describe('plugin-meetings', () => { - describe('StatsAnalyzer', () => { - describe('parseStatsResult', () => { - const sandbox = sinon.createSandbox(); - let statsAnalyzer; - - const initialConfig = {}; - const defaultStats = {}; - - beforeEach(() => { - const networkQualityMonitor = new NetworkQualityMonitor(initialConfig); - - statsAnalyzer = new StatsAnalyzer( - { - config: initialConfig, - receiveSlotCallback: () => ({}), - networkQualityMonitor, - statsResults: defaultStats, - }, - ); - }); - - afterEach(() => { - sandbox.reset(); - }); - - it('should call processOutboundRTPResult', () => { - const calledSpy = sandbox.spy(statsAnalyzer, 'processOutboundRTPResult'); - statsAnalyzer.parseGetStatsResult({type: 'outbound-rtp'}, 'video-send'); - assert(calledSpy.calledOnce); - }); - - it('should call processInboundRTPResult', () => { - const calledSpy = sandbox.spy(statsAnalyzer, 'processInboundRTPResult'); - statsAnalyzer.parseGetStatsResult({type: 'inbound-rtp'}, 'video-recv'); - assert(calledSpy.calledOnce); - }); - - it('should call compareSentAndReceived', () => { - const calledSpy = sandbox.spy(statsAnalyzer, 'compareSentAndReceived'); - statsAnalyzer.parseGetStatsResult({type: 'remote-outbound-rtp'}, 'video-send'); - assert(calledSpy.calledOnce); - }); - - it('should call parseCandidate', () => { - const calledSpy = sandbox.spy(statsAnalyzer, 'parseCandidate'); - statsAnalyzer.parseGetStatsResult({type: 'local-candidate'}, 'video-send'); - assert(calledSpy.calledOnce); - }); - - it('processOutboundRTPResult should create the correct stats results for audio', () => { - // establish the `statsResults` object. - statsAnalyzer.parseGetStatsResult({type: 'none'}, 'audio-send', true); - - statsAnalyzer.processOutboundRTPResult( - { - bytesSent: 50000, - codecId: 'RTCCodec_1_Outbound_111', - headerBytesSent: 25000, - id: 'RTCOutboundRTPAudioStream_123456789', - kind: 'audio', - mediaSourceId: 'RTCAudioSource_2', - mediaType: 'audio', - nackCount: 1, - packetsSent: 3600, - remoteId: 'RTCRemoteInboundRtpAudioStream_123456789', - ssrc: 123456789, - targetBitrate: 256000, - timestamp: 1707341489336, - trackId: 'RTCMediaStreamTrack_sender_2', - transportId: 'RTCTransport_0_1', - type: 'outbound-rtp', - requestedBitrate: 10000, - }, - 'audio-send', - true, - ); - - assert.strictEqual(statsAnalyzer.statsResults['audio-send'].send.headerBytesSent, 25000); - assert.strictEqual(statsAnalyzer.statsResults['audio-send'].send.totalBytesSent, 50000); - assert.strictEqual(statsAnalyzer.statsResults['audio-send'].send.totalNackCount, 1); - assert.strictEqual(statsAnalyzer.statsResults['audio-send'].send.totalPacketsSent, 3600); - assert.strictEqual(statsAnalyzer.statsResults['audio-send'].send.requestedBitrate, 10000); - }); - - it('processOutboundRTPResult should create the correct stats results for video', () => { - // establish the `statsResults` object for video. - statsAnalyzer.parseGetStatsResult({type: 'none'}, 'video-send', true); - - statsAnalyzer.processOutboundRTPResult( - { - bytesSent: 250000, - codecId: 'RTCCodec_1_Outbound_107', - headerBytesSent: 50000, - id: 'RTCOutboundRTPVideoStream_987654321', - kind: 'video', - mediaSourceId: 'RTCVideoSource_3', - mediaType: 'video', - nackCount: 5, - packetsSent: 15000, - remoteId: 'RTCRemoteInboundRtpVideoStream_987654321', - retransmittedBytesSent: 500, - retransmittedPacketsSent: 10, - ssrc: 987654321, - targetBitrate: 1024000, - timestamp: 1707341489336, - trackId: 'RTCMediaStreamTrack_sender_3', - transportId: 'RTCTransport_0_2', - type: 'outbound-rtp', - requestedBitrate: 50000, - }, - 'video-send', - true, - ); - - assert.strictEqual(statsAnalyzer.statsResults['video-send'].send.headerBytesSent, 50000); - assert.strictEqual(statsAnalyzer.statsResults['video-send'].send.totalBytesSent, 250000); - assert.strictEqual(statsAnalyzer.statsResults['video-send'].send.totalNackCount, 5); - assert.strictEqual(statsAnalyzer.statsResults['video-send'].send.totalPacketsSent, 15000); - assert.strictEqual(statsAnalyzer.statsResults['video-send'].send.requestedBitrate, 50000); - assert.strictEqual( - statsAnalyzer.statsResults['video-send'].send.totalRtxPacketsSent, - 10, - ); - assert.strictEqual( - statsAnalyzer.statsResults['video-send'].send.totalRtxBytesSent, - 500, - ); - }); - - it('processInboundRTPResult should create the correct stats results for audio', () => { - // establish the `statsResults` object. - statsAnalyzer.parseGetStatsResult({type: 'none'}, 'audio-recv-1', false); - - statsAnalyzer.processInboundRTPResult( - { - audioLevel: 0, - bytesReceived: 509, - codecId: 'RTCCodec_6_Inbound_111', - concealedSamples: 200000, - concealmentEvents: 13, - fecPacketsDiscarded: 1, - fecPacketsReceived: 1, - headerBytesReceived: 250, - id: 'RTCInboundRTPAudioStream_123456789', - insertedSamplesForDeceleration: 0, - jitter: 0.012, - jitterBufferDelay: 1000, - jitterBufferEmittedCount: 10000, - kind: 'audio', - lastPacketReceivedTimestamp: 1707341488529, - mediaType: 'audio', - packetsDiscarded: 0, - packetsLost: 0, - packetsReceived: 12, - remoteId: 'RTCRemoteOutboundRTPAudioStream_123456789', - removedSamplesForAcceleration: 0, - silentConcealedSamples: 200000, - ssrc: 123456789, - timestamp: 1707341489419, - totalAudioEnergy: 133, - totalSamplesDuration: 7, - totalSamplesReceived: 300000, - trackId: 'RTCMediaStreamTrack_receiver_76', - transportId: 'RTCTransport_0_1', - type: 'inbound-rtp', - requestedBitrate: 10000, - }, - 'audio-recv-1', - false, - ); - - assert.strictEqual( - statsAnalyzer.statsResults['audio-recv-1'].recv.totalPacketsReceived, - 12, - ); - assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.fecPacketsDiscarded, 1); - assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.fecPacketsReceived, 1); - assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.totalBytesReceived, 509); - assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.requestedBitrate, 10000); - assert.strictEqual( - statsAnalyzer.statsResults['audio-recv-1'].recv.headerBytesReceived, - 250, - ); - assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.audioLevel, 0); - assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.totalAudioEnergy, 133); - assert.strictEqual( - statsAnalyzer.statsResults['audio-recv-1'].recv.totalSamplesReceived, - 300000, - ); - assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.totalSamplesDecoded, 0); - assert.strictEqual( - statsAnalyzer.statsResults['audio-recv-1'].recv.concealedSamples, - 200000, - ); - }); - - it('processInboundRTPResult should create the correct stats results for video', () => { - // establish the `statsResults` object for video. - statsAnalyzer.parseGetStatsResult({type: 'none'}, 'video-recv', false); - - statsAnalyzer.processInboundRTPResult( - { - bytesReceived: 100000, - codecId: 'RTCCodec_6_Inbound_107', - fecPacketsDiscarded: 2, - fecPacketsReceived: 2, - headerBytesReceived: 10000, - id: 'RTCInboundRTPVideoStream_987654321', - jitter: 0.05, - jitterBufferDelay: 5000, - jitterBufferEmittedCount: 50000, - kind: 'video', - lastPacketReceivedTimestamp: 1707341488529, - mediaType: 'video', - packetsDiscarded: 5, - packetsLost: 10, - packetsReceived: 1500, - remoteId: 'RTCRemoteOutboundRTPVideoStream_987654321', - ssrc: 987654321, - timestamp: 1707341489419, - trackId: 'RTCMediaStreamTrack_receiver_3', - transportId: 'RTCTransport_0_2', - type: 'inbound-rtp', - requestedBitrate: 50000, - retransmittedBytesReceived: 500, - retransmittedPacketsReceived: 10, - }, - 'video-recv', - false, - ); - - assert.strictEqual(statsAnalyzer.statsResults['video-recv'].recv.totalPacketsReceived, 1500); - assert.strictEqual(statsAnalyzer.statsResults['video-recv'].recv.fecPacketsDiscarded, 2); - assert.strictEqual(statsAnalyzer.statsResults['video-recv'].recv.fecPacketsReceived, 2); - assert.strictEqual(statsAnalyzer.statsResults['video-recv'].recv.totalBytesReceived, 100000); - assert.strictEqual(statsAnalyzer.statsResults['video-recv'].recv.requestedBitrate, 50000); - assert.strictEqual(statsAnalyzer.statsResults['video-recv'].recv.headerBytesReceived, 10000); - assert.strictEqual(statsAnalyzer.statsResults['video-recv'].recv.totalRtxBytesReceived, 500); - assert.strictEqual(statsAnalyzer.statsResults['video-recv'].recv.totalRtxPacketsReceived, 10); - }); - - - it('parseAudioSource should create the correct stats results', () => { - // establish the `statsResults` object. - statsAnalyzer.parseGetStatsResult({type: 'none'}, 'audio-send', true); - - statsAnalyzer.parseAudioSource( - { - audioLevel: 0.03, - echoReturnLoss: -30, - echoReturnLossEnhancement: 0.17, - id: 'RTCAudioSource_2', - kind: 'audio', - timestamp: 1707341488160.012, - totalAudioEnergy: 0.001, - totalSamplesDuration: 4.5, - trackIdentifier: '2207e5bf-c595-4301-93f7-283994d8143f', - type: 'media-source', - }, - 'audio-send', - true, - ); - - assert.strictEqual(statsAnalyzer.statsResults['audio-send'].send.audioLevel, 0.03); - assert.strictEqual(statsAnalyzer.statsResults['audio-send'].send.totalAudioEnergy, 0.001); - }); - }); - - describe('compareSentAndReceived()', () => { - let statsAnalyzer; - let sandBoxSpy; - - const initialConfig = { - videoPacketLossRatioThreshold: 9, - }; - - const defaultStats = { - resolutions: {}, - internal: { - 'video-send-1': { - send: { - totalPacketsLostOnReceiver: 10, - }, - }, - }, - 'video-send-1': { - send: { - packetsSent: 2, - meanRemoteJitter: [], - meanRoundTripTime: [], - }, - }, - }; - - const statusResult = { - type: 'remote-inbound-rtp', - packetsLost: 11, - rttThreshold: 501, - jitterThreshold: 501, - }; - - const sandbox = sinon.createSandbox(); - - beforeEach(() => { - const networkQualityMonitor = new NetworkQualityMonitor(initialConfig); - - statsAnalyzer = new StatsAnalyzer( - { - config: initialConfig, - receiveSlotCallback: () => ({}), - networkQualityMonitor, - statsResults: defaultStats, - }, - ); - - sandBoxSpy = sandbox.spy( - statsAnalyzer.networkQualityMonitor, - 'determineUplinkNetworkQuality', - ); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it('should trigger determineUplinkNetworkQuality with specific arguments', async () => { - await statsAnalyzer.parseGetStatsResult(statusResult, 'video-send-1', true); - - assert.calledOnce(statsAnalyzer.networkQualityMonitor.determineUplinkNetworkQuality); - assert( - sandBoxSpy.calledWith({ - mediaType: 'video-send-1', - remoteRtpResults: statusResult, - statsAnalyzerCurrentStats: statsAnalyzer.statsResults, - }), - ); - }); - }); - - describe('startAnalyzer', () => { - let clock; - let pc; - let networkQualityMonitor; - let statsAnalyzer; - let mqeData; - let loggerSpy; - let receiveSlot; - - let receivedEventsData = { - local: {}, - remote: {}, - }; - - const initialConfig = { - analyzerInterval: 1000, - }; - - let fakeStats; - - const sandbox = sinon.createSandbox(); - - const resetReceivedEvents = () => { - receivedEventsData = { - local: {}, - remote: {}, - }; - }; - - const registerStatsAnalyzerEvents = (statsAnalyzer) => { - statsAnalyzer.on(EVENTS.LOCAL_MEDIA_STARTED, (data) => { - receivedEventsData.local.started = data; - }); - statsAnalyzer.on(EVENTS.LOCAL_MEDIA_STOPPED, (data) => { - receivedEventsData.local.stopped = data; - }); - statsAnalyzer.on(EVENTS.REMOTE_MEDIA_STARTED, (data) => { - receivedEventsData.remote.started = data; - }); - statsAnalyzer.on(EVENTS.REMOTE_MEDIA_STOPPED, (data) => { - receivedEventsData.remote.stopped = data; - }); - statsAnalyzer.on(EVENTS.MEDIA_QUALITY, ({data}) => { - mqeData = data; - }); - }; - - before(() => { - LoggerConfig.set({enable: false}); - LoggerProxy.set(); - loggerSpy = sandbox.spy(LoggerProxy.logger, 'info'); - }); - - beforeEach(() => { - clock = sinon.useFakeTimers(); - receiveSlot = undefined; - - resetReceivedEvents(); - - // bytesReceived and bytesSent need to be non-zero in order for StatsAnalyzer to parse any other values - fakeStats = { - audio: { - senders: [ - { - localTrackLabel: 'fake-microphone', - report: [ - { - type: 'outbound-rtp', - bytesSent: 1, - packetsSent: 0, - isRequested: true, - }, - { - type: 'remote-inbound-rtp', - packetsLost: 0, - }, - { - type: 'candidate-pair', - state: 'succeeded', - localCandidateId: 'fake-candidate-id', - }, - { - type: 'candidate-pair', - state: 'failed', - localCandidateId: 'bad-candidate-id', - }, - { - type: 'local-candidate', - id: 'fake-candidate-id', - protocol: 'tcp', - }, - ], - }, - ], - receivers: [ - { - report: [ - { - type: 'inbound-rtp', - bytesReceived: 1, - fecPacketsDiscarded: 0, - fecPacketsReceived: 0, - packetsLost: 0, - packetsReceived: 0, - isRequested: true, - lastRequestedUpdateTimestamp: 0, - }, - { - type: 'remote-outbound-rtp', - }, - { - type: 'candidate-pair', - state: 'succeeded', - localCandidateId: 'fake-candidate-id', - }, - { - type: 'candidate-pair', - state: 'failed', - localCandidateId: 'bad-candidate-id', - }, - { - type: 'local-candidate', - id: 'fake-candidate-id', - protocol: 'tcp', - }, - ], - }, - ], - }, - video: { - senders: [ - { - localTrackLabel: 'fake-camera', - report: [ - { - type: 'outbound-rtp', - bytesSent: 1, - framesSent: 0, - packetsSent: 0, - isRequested: true, - lastRequestedUpdateTimestamp: 0, - }, - { - type: 'remote-inbound-rtp', - packetsLost: 0, - }, - { - type: 'candidate-pair', - state: 'succeeded', - localCandidateId: 'fake-candidate-id', - }, - { - type: 'candidate-pair', - state: 'failed', - localCandidateId: 'bad-candidate-id', - }, - { - type: 'local-candidate', - id: 'fake-candidate-id', - protocol: 'tcp', - }, - ], - }, - ], - receivers: [ - { - report: [ - { - type: 'inbound-rtp', - bytesReceived: 1, - frameHeight: 720, - frameWidth: 1280, - framesDecoded: 0, - framesReceived: 0, - packetsLost: 0, - packetsReceived: 0, - isRequested: true, - lastRequestedUpdateTimestamp: 0, - isActiveSpeaker: false, - lastActiveSpeakerUpdateTimestamp: 0, - }, - { - type: 'remote-outbound-rtp', - }, - { - type: 'candidate-pair', - state: 'succeeded', - localCandidateId: 'fake-candidate-id', - }, - { - type: 'candidate-pair', - state: 'failed', - localCandidateId: 'bad-candidate-id', - }, - { - type: 'local-candidate', - id: 'fake-candidate-id', - protocol: 'tcp', - }, - ], - }, - ], - }, - share: { - senders: [ - { - localTrackLabel: 'fake-share', - report: [ - { - type: 'outbound-rtp', - bytesSent: 1, - framesSent: 0, - packetsSent: 0, - isRequested: true, - lastRequestedUpdateTimestamp: 0, - encoderImplementation: 'fake-encoder', - }, - { - type: 'remote-inbound-rtp', - packetsLost: 0, - }, - { - type: 'candidate-pair', - state: 'succeeded', - localCandidateId: 'fake-candidate-id', - }, - { - type: 'candidate-pair', - state: 'failed', - localCandidateId: 'bad-candidate-id', - }, - { - type: 'local-candidate', - id: 'fake-candidate-id', - protocol: 'tcp', - }, - ], - }, - ], - receivers: [ - { - report: [ - { - type: 'inbound-rtp', - bytesReceived: 1, - frameHeight: 720, - frameWidth: 1280, - framesDecoded: 0, - framesReceived: 0, - packetsLost: 0, - packetsReceived: 0, - isRequested: true, - lastRequestedUpdateTimestamp: 0, - }, - { - type: 'remote-outbound-rtp', - }, - { - type: 'candidate-pair', - state: 'succeeded', - localCandidateId: 'fake-candidate-id', - }, - { - type: 'candidate-pair', - state: 'failed', - localCandidateId: 'bad-candidate-id', - }, - { - type: 'local-candidate', - id: 'fake-candidate-id', - protocol: 'tcp', - }, - ], - }, - ], - }, - }; - - pc = { - getConnectionState: sinon.stub().returns(ConnectionState.Connected), - getTransceiverStats: sinon.stub().resolves({ - audio: { - senders: [fakeStats.audio.senders[0]], - receivers: [fakeStats.audio.receivers[0]], - }, - video: { - senders: [fakeStats.video.senders[0]], - receivers: [fakeStats.video.receivers[0]], - }, - screenShareAudio: { - senders: [fakeStats.audio.senders[0]], - receivers: [fakeStats.audio.receivers[0]], - }, - screenShareVideo: { - senders: [fakeStats.share.senders[0]], - receivers: [fakeStats.share.receivers[0]], - }, - }), - }; - - networkQualityMonitor = new NetworkQualityMonitor(initialConfig); - - statsAnalyzer = new StatsAnalyzer({ - config: initialConfig, receiveSlotCallback: () => receiveSlot, networkQualityMonitor, - }); - - registerStatsAnalyzerEvents(statsAnalyzer); - }); - - afterEach(() => { - sandbox.reset(); - clock.restore(); - }); - - const mergeProperties = ( - target, - properties, - keyValue = 'fake-candidate-id', - matchKey = 'type', - matchValue = 'local-candidate', - ) => { - for (let key in target) { - if (target.hasOwnProperty(key)) { - if (typeof target[key] === 'object') { - mergeProperties(target[key], properties, keyValue, matchKey, matchValue); - } - if (key === 'id' && target[key] === keyValue && target[matchKey] === matchValue) { - Object.assign(target, properties); - } - } - } - }; - - const progressTime = async (time = initialConfig.analyzerInterval) => { - await clock.tickAsync(time); - await testUtils.flushPromises(); - }; - - const checkReceivedEvent = ({expected}) => { - // check that we got the REMOTE_MEDIA_STARTED event for audio - assert.deepEqual(receivedEventsData.local.started, expected.local?.started); - assert.deepEqual(receivedEventsData.local.stopped, expected.local?.stopped); - assert.deepEqual(receivedEventsData.remote.started, expected.remote?.started); - assert.deepEqual(receivedEventsData.remote.stopped, expected.remote?.stopped); - }; - - const checkMqeData = () => { - for (const data of [ - mqeData.audioTransmit, - mqeData.audioReceive, - mqeData.videoTransmit, - mqeData.videoReceive, - ]) { - assert.strictEqual(data.length, 2); - assert.strictEqual(data[0].common.common.isMain, true); - assert.strictEqual(data[1].common.common.isMain, false); - } - - assert.strictEqual(mqeData.videoReceive[0].streams[0].receivedFrameSize, 3600); - assert.strictEqual(mqeData.videoReceive[0].streams[0].receivedHeight, 720); - assert.strictEqual(mqeData.videoReceive[0].streams[0].receivedWidth, 1280); - }; - - it('emits LOCAL_MEDIA_STARTED and LOCAL_MEDIA_STOPPED events for audio', async () => { - await startStatsAnalyzer({ - statsAnalyzer, - pc, - mediaStatus: { - expected: { - sendAudio: true, - }, - }, - }); - - // check that we haven't received any events yet - checkReceivedEvent({expected: {}}); - - // setup a mock to return some values higher the previous ones - fakeStats.audio.senders[0].report[0].packetsSent += 10; - - await progressTime(); - - // check that we got the LOCAL_MEDIA_STARTED event for audio - checkReceivedEvent({expected: {local: {started: {type: 'audio'}}}}); - - // now advance the clock and the mock still returns same values, so only "stopped" event should be triggered - resetReceivedEvents(); - await progressTime(); - checkReceivedEvent({expected: {local: {stopped: {type: 'audio'}}}}); - }); - - it('emits LOCAL_MEDIA_STARTED and LOCAL_MEDIA_STOPPED events for video', async () => { - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {sendVideo: true}}}); - - // check that we haven't received any events yet - checkReceivedEvent({expected: {}}); - - // setup a mock to return some values higher the previous ones - fakeStats.video.senders[0].report[0].framesSent += 1; - - await progressTime(); - - // check that we got the LOCAL_MEDIA_STARTED event for audio - checkReceivedEvent({expected: {local: {started: {type: 'video'}}}}); - - // now advance the clock and the mock still returns same values, so only "stopped" event should be triggered - resetReceivedEvents(); - await progressTime(); - checkReceivedEvent({expected: {local: {stopped: {type: 'video'}}}}); - }); - - it('emits LOCAL_MEDIA_STARTED and LOCAL_MEDIA_STOPPED events for share', async () => { - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {sendShare: true}}}); - - // check that we haven't received any events yet - checkReceivedEvent({expected: {}}); - - // setup a mock to return some values higher the previous ones - fakeStats.share.senders[0].report[0].framesSent += 1; - - await progressTime(); - - // check that we got the LOCAL_MEDIA_STARTED event for audio - checkReceivedEvent({expected: {local: {started: {type: 'share'}}}}); - - // now advance the clock and the mock still returns same values, so only "stopped" event should be triggered - resetReceivedEvents(); - await progressTime(); - checkReceivedEvent({expected: {local: {stopped: {type: 'share'}}}}); - }); - - it('emits REMOTE_MEDIA_STARTED and REMOTE_MEDIA_STOPPED events for audio', async () => { - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveAudio: true}}}); - - // check that we haven't received any events yet - checkReceivedEvent({expected: {}}); - - // setup a mock to return some values higher the previous ones - fakeStats.audio.receivers[0].report[0].packetsReceived += 5; - - await progressTime(); - // check that we got the REMOTE_MEDIA_STARTED event for audio - checkReceivedEvent({expected: {remote: {started: {type: 'audio'}}}}); - - // now advance the clock and the mock still returns same values, so only "stopped" event should be triggered - resetReceivedEvents(); - await progressTime(); - - checkReceivedEvent({expected: {remote: {stopped: {type: 'audio'}}}}); - }); - - it('emits REMOTE_MEDIA_STARTED and REMOTE_MEDIA_STOPPED events for video', async () => { - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}}); - - // check that we haven't received any events yet - checkReceivedEvent({expected: {}}); - - // setup a mock to return some values higher the previous ones - fakeStats.video.receivers[0].report[0].framesDecoded += 1; - - await progressTime(); - // check that we got the REMOTE_MEDIA_STARTED event for video - checkReceivedEvent({expected: {remote: {started: {type: 'video'}}}}); - - // now advance the clock and the mock still returns same values, so only "stopped" event should be triggered - resetReceivedEvents(); - await progressTime(); - - checkReceivedEvent({expected: {remote: {stopped: {type: 'video'}}}}); - }); - - it('emits REMOTE_MEDIA_STARTED and REMOTE_MEDIA_STOPPED events for share', async () => { - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveShare: true}}}); - - // check that we haven't received any events yet - checkReceivedEvent({expected: {}}); - - // setup a mock to return some values higher the previous ones - fakeStats.share.receivers[0].report[0].framesDecoded += 1; - - await progressTime(); - // check that we got the REMOTE_MEDIA_STARTED event for video - checkReceivedEvent({expected: {remote: {started: {type: 'share'}}}}); - - // now advance the clock and the mock still returns same values, so only "stopped" event should be triggered - resetReceivedEvents(); - await progressTime(); - - checkReceivedEvent({expected: {remote: {stopped: {type: 'share'}}}}); - }); - - it('emits the correct MEDIA_QUALITY events', async () => { - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}}); - - await progressTime(); - - // Check that the mqe data has been emitted and is correctly computed. - checkMqeData(); - }); - - it('emits the correct transportType in MEDIA_QUALITY events', async () => { - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}}); - - await progressTime(); - - assert.strictEqual(mqeData.audioTransmit[0].common.transportType, 'TCP'); - assert.strictEqual(mqeData.videoReceive[0].common.transportType, 'TCP'); - }); - - it('emits the correct transportType in MEDIA_QUALITY events when using a TURN server', async () => { - fakeStats.audio.senders[0].report[4].relayProtocol = 'tls'; - fakeStats.video.senders[0].report[4].relayProtocol = 'tls'; - fakeStats.audio.receivers[0].report[4].relayProtocol = 'tls'; - fakeStats.video.receivers[0].report[4].relayProtocol = 'tls'; - - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}}); - - await progressTime(); - - assert.strictEqual(mqeData.audioTransmit[0].common.transportType, 'TLS'); - assert.strictEqual(mqeData.videoReceive[0].common.transportType, 'TLS'); - }); - - it('emits the correct peripherals in MEDIA_QUALITY events', async () => { - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}}); - - await progressTime(); - - assert.strictEqual( - mqeData.intervalMetadata.peripherals.find((val) => val.name === MEDIA_DEVICES.MICROPHONE) - .information, - 'fake-microphone', - ); - assert.strictEqual( - mqeData.intervalMetadata.peripherals.find((val) => val.name === MEDIA_DEVICES.CAMERA) - .information, - 'fake-camera', - ); - }); - - it('emits the correct peripherals in MEDIA_QUALITY events when localTrackLabel is undefined', async () => { - fakeStats.audio.senders[0].localTrackLabel = undefined; - fakeStats.video.senders[0].localTrackLabel = undefined; - - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}}); - - await progressTime(); - - assert.strictEqual( - mqeData.intervalMetadata.peripherals.find((val) => val.name === MEDIA_DEVICES.MICROPHONE) - .information, - _UNKNOWN_, - ); - assert.strictEqual( - mqeData.intervalMetadata.peripherals.find((val) => val.name === MEDIA_DEVICES.CAMERA) - .information, - _UNKNOWN_, - ); - }); - - describe('frame rate reporting in stats analyzer', () => { - beforeEach(async () => { - await startStatsAnalyzer({pc, statsAnalyzer}); - }); - - it('should report a zero frame rate for both transmitted and received video at the start', async () => { - assert.strictEqual(mqeData.videoTransmit[0].streams[0].common.transmittedFrameRate, 0); - assert.strictEqual(mqeData.videoReceive[0].streams[0].common.receivedFrameRate, 0); - }); - - it('should accurately report the transmitted and received frame rate after video frames are processed', async () => { - fakeStats.video.senders[0].report[0].framesSent += 300; - fakeStats.video.receivers[0].report[0].framesReceived += 300; - await progressTime(MQA_INTERVAL); - - // 300 frames in 60 seconds = 5 frames per second - assert.strictEqual(mqeData.videoTransmit[0].streams[0].common.transmittedFrameRate, 500); - assert.strictEqual(mqeData.videoReceive[0].streams[0].common.receivedFrameRate, 500); - }); - }); - - describe('RTP packets count in stats analyzer', () => { - beforeEach(async () => { - await startStatsAnalyzer({pc, statsAnalyzer}); - }); - - it('should report zero RTP packets for all streams at the start of the stats analyzer', async () => { - assert.strictEqual(mqeData.audioTransmit[0].common.rtpPackets, 0); - assert.strictEqual(mqeData.audioTransmit[0].streams[0].common.rtpPackets, 0); - assert.strictEqual(mqeData.audioReceive[0].common.rtpPackets, 0); - assert.strictEqual(mqeData.audioReceive[0].streams[0].common.rtpPackets, 0); - assert.strictEqual(mqeData.videoTransmit[0].common.rtpPackets, 0); - assert.strictEqual(mqeData.videoTransmit[0].streams[0].common.rtpPackets, 0); - assert.strictEqual(mqeData.videoReceive[0].common.rtpPackets, 0); - assert.strictEqual(mqeData.videoReceive[0].streams[0].common.rtpPackets, 0); - }); - - it('should update the RTP packets count correctly after audio and video packets are sent', async () => { - fakeStats.audio.senders[0].report[0].packetsSent += 5; - fakeStats.video.senders[0].report[0].packetsSent += 5; - await progressTime(MQA_INTERVAL); - - assert.strictEqual(mqeData.audioTransmit[0].common.rtpPackets, 5); - assert.strictEqual(mqeData.audioTransmit[0].streams[0].common.rtpPackets, 5); - assert.strictEqual(mqeData.videoTransmit[0].common.rtpPackets, 5); - assert.strictEqual(mqeData.videoTransmit[0].streams[0].common.rtpPackets, 5); - }); - - it('should update the RTP packets count correctly after audio and video packets are received', async () => { - fakeStats.audio.senders[0].report[0].packetsSent += 10; - fakeStats.video.senders[0].report[0].packetsSent += 10; - fakeStats.audio.receivers[0].report[0].packetsReceived += 10; - fakeStats.video.receivers[0].report[0].packetsReceived += 10; - await progressTime(MQA_INTERVAL); - - assert.strictEqual(mqeData.audioReceive[0].common.rtpPackets, 10); - assert.strictEqual(mqeData.audioReceive[0].streams[0].common.rtpPackets, 10); - assert.strictEqual(mqeData.videoReceive[0].common.rtpPackets, 10); - assert.strictEqual(mqeData.videoReceive[0].streams[0].common.rtpPackets, 10); - }); - }); - - describe('FEC packet reporting in stats analyzer', () => { - beforeEach(async () => { - await startStatsAnalyzer({pc, statsAnalyzer}); - }); - - it('should initially report zero FEC packets at the start of the stats analyzer', async () => { - assert.strictEqual(mqeData.audioReceive[0].common.fecPackets, 0); - }); - - it('should accurately report the count of FEC packets received', async () => { - fakeStats.audio.receivers[0].report[0].fecPacketsReceived += 5; - await progressTime(MQA_INTERVAL); - - assert.strictEqual(mqeData.audioReceive[0].common.fecPackets, 5); - }); - - it('should accurately update and reset the FEC packet count based on received packets over MQA intervals', async () => { - fakeStats.audio.receivers[0].report[0].fecPacketsReceived += 15; - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.audioReceive[0].common.fecPackets, 15); - - fakeStats.audio.receivers[0].report[0].fecPacketsReceived += 45; - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.audioReceive[0].common.fecPackets, 45); - - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.audioReceive[0].common.fecPackets, 0); - }); - }); - - describe('RTP recovered packets emission', async() => { - beforeEach(async() => { - await startStatsAnalyzer({pc, statsAnalyzer}); - }); - - it('should initially report zero RTP recovered packets', async() => { - assert.strictEqual(mqeData.audioReceive[0].common.rtpRecovered, 0); - }) - - it('should report RTP recovered packets equal to FEC packets received', async() => { - fakeStats.audio.receivers[0].report[0].fecPacketsReceived += 10; - - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.audioReceive[0].common.rtpRecovered, 10); - }) - - it('should reset RTP recovered packets count after each interval', async () => { - fakeStats.audio.receivers[0].report[0].fecPacketsReceived += 100; - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.audioReceive[0].common.rtpRecovered, 100); - - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.audioReceive[0].common.rtpRecovered, 0); - }) - - it('should correctly calculate RTP recovered packets after discarding FEC packets', async () => { - fakeStats.audio.receivers[0].report[0].fecPacketsReceived += 100; - fakeStats.audio.receivers[0].report[0].fecPacketsDiscarded += 20; - - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.audioReceive[0].common.rtpRecovered, 80); - }) - }) - - describe('packet loss metrics reporting in stats analyzer', () => { - beforeEach(async () => { - await startStatsAnalyzer({pc, statsAnalyzer}); - }); - - it('should report zero packet loss for both audio and video at the start of the stats analyzer', async () => { - assert.strictEqual(mqeData.audioReceive[0].common.mediaHopByHopLost, 0); - assert.strictEqual(mqeData.audioReceive[0].common.rtpHopByHopLost, 0); - assert.strictEqual(mqeData.videoReceive[0].common.mediaHopByHopLost, 0); - assert.strictEqual(mqeData.videoReceive[0].common.rtpHopByHopLost, 0); - }); - - it('should update packet loss metrics correctly for both audio and video after packet loss is detected', async () => { - fakeStats.audio.receivers[0].report[0].packetsLost += 5; - fakeStats.video.receivers[0].report[0].packetsLost += 5; - await progressTime(MQA_INTERVAL); - - assert.strictEqual(mqeData.audioReceive[0].common.mediaHopByHopLost, 5); - assert.strictEqual(mqeData.audioReceive[0].common.rtpHopByHopLost, 5); - assert.strictEqual(mqeData.videoReceive[0].common.mediaHopByHopLost, 5); - assert.strictEqual(mqeData.videoReceive[0].common.rtpHopByHopLost, 5); - }); - }); - - describe('maximum remote loss rate reporting in stats analyzer', () => { - beforeEach(async () => { - await startStatsAnalyzer({pc, statsAnalyzer}); - }); - - it('should report a zero maximum remote loss rate for both audio and video at the start', async () => { - assert.strictEqual(mqeData.audioTransmit[0].common.maxRemoteLossRate, 0); - assert.strictEqual(mqeData.videoTransmit[0].common.maxRemoteLossRate, 0); - }); - - it('should maintain a zero maximum remote loss rate for both audio and video after packets are sent without loss', async () => { - fakeStats.audio.senders[0].report[0].packetsSent += 100; - fakeStats.video.senders[0].report[0].packetsSent += 100; - await progressTime(MQA_INTERVAL); - - assert.strictEqual(mqeData.audioTransmit[0].common.maxRemoteLossRate, 0); - assert.strictEqual(mqeData.videoTransmit[0].common.maxRemoteLossRate, 0); - }); - - it('should accurately calculate the maximum remote loss rate for both audio and video after packet loss is detected', async () => { - fakeStats.audio.senders[0].report[0].packetsSent += 200; - fakeStats.audio.senders[0].report[1].packetsLost += 10; - fakeStats.video.senders[0].report[0].packetsSent += 200; - fakeStats.video.senders[0].report[1].packetsLost += 10; - await progressTime(MQA_INTERVAL); - - assert.strictEqual(mqeData.audioTransmit[0].common.maxRemoteLossRate, 5); - assert.strictEqual(mqeData.videoTransmit[0].common.maxRemoteLossRate, 5); - }); - - it('should reset the maximum remote loss rate across MQA intervals', async() => { - fakeStats.audio.senders[0].report[0].packetsSent += 100; - fakeStats.audio.senders[0].report[1].packetsLost += 10; - fakeStats.video.senders[0].report[0].packetsSent += 50; - fakeStats.video.senders[0].report[1].packetsLost += 5; - await progressTime(MQA_INTERVAL); - - assert.strictEqual(mqeData.audioTransmit[0].common.maxRemoteLossRate, 10); - assert.strictEqual(mqeData.videoTransmit[0].common.maxRemoteLossRate, 10); - - await progressTime(MQA_INTERVAL); - - assert.strictEqual(mqeData.audioTransmit[0].common.maxRemoteLossRate, 0); - assert.strictEqual(mqeData.videoTransmit[0].common.maxRemoteLossRate, 0); - - }) - }); - - it('has the correct localIpAddress set when the candidateType is host', async () => { - await startStatsAnalyzer({pc, statsAnalyzer}); - - await progressTime(); - assert.strictEqual(statsAnalyzer.getLocalIpAddress(), ''); - mergeProperties(fakeStats, {address: 'test', candidateType: 'host'}); - await progressTime(); - assert.strictEqual(statsAnalyzer.getLocalIpAddress(), 'test'); - }); - - it('has the correct localIpAddress set when the candidateType is prflx and relayProtocol is set', async () => { - await startStatsAnalyzer({pc, statsAnalyzer}); - - await progressTime(); - assert.strictEqual(statsAnalyzer.getLocalIpAddress(), ''); - mergeProperties(fakeStats, { - relayProtocol: 'test', - address: 'test2', - candidateType: 'prflx', - }); - await progressTime(); - assert.strictEqual(statsAnalyzer.getLocalIpAddress(), 'test2'); - }); - - it('has the correct localIpAddress set when the candidateType is prflx and relayProtocol is not set', async () => { - await startStatsAnalyzer({pc, statsAnalyzer}); - - await progressTime(); - assert.strictEqual(statsAnalyzer.getLocalIpAddress(), ''); - mergeProperties(fakeStats, { - relatedAddress: 'relatedAddress', - address: 'test2', - candidateType: 'prflx', - }); - await progressTime(); - assert.strictEqual(statsAnalyzer.getLocalIpAddress(), 'relatedAddress'); - }); - - it('has no localIpAddress set when the candidateType is invalid', async () => { - await startStatsAnalyzer({pc, statsAnalyzer}); - - await progressTime(); - assert.strictEqual(statsAnalyzer.getLocalIpAddress(), ''); - mergeProperties(fakeStats, {candidateType: 'invalid'}); - await progressTime(); - assert.strictEqual(statsAnalyzer.getLocalIpAddress(), ''); - }); - - it('has the correct share video encoder implementation as provided by the stats', async () => { - await startStatsAnalyzer({pc, statsAnalyzer}); - - await progressTime(); - assert.strictEqual(statsAnalyzer.shareVideoEncoderImplementation, 'fake-encoder'); - }); - - it('logs a message when audio send packets do not increase', async () => { - await startStatsAnalyzer( - { - statsAnalyzer, pc, mediaStatus: {expected: {sendAudio: true}}, - lastEmittedEvents: {audio: {local: EVENTS.LOCAL_MEDIA_STARTED}}, - }, - ); - - // don't increase the packets when time progresses. - await progressTime(); - - assert( - loggerSpy.calledWith( - 'StatsAnalyzer:index#compareLastStatsResult --> No audio RTP packets sent', - ), - ); - }); - - it('does not log a message when audio send packets increase', async () => { - await startStatsAnalyzer({ - statsAnalyzer, pc, - mediaStatus: {expected: {sendAudio: true}}, - lastEmittedEvents: {audio: {local: EVENTS.LOCAL_MEDIA_STOPPED}}, - }, - ); - - fakeStats.audio.senders[0].report[0].packetsSent += 5; - await progressTime(); - - assert( - loggerSpy.neverCalledWith( - 'StatsAnalyzer:index#compareLastStatsResult --> No audio RTP packets sent', - ), - ); - }); - - it('logs a message when video send packets do not increase', async () => { - await startStatsAnalyzer({ - statsAnalyzer, pc, mediaStatus: {expected: {sendVideo: true}}, - lastEmittedEvents: {video: {local: EVENTS.LOCAL_MEDIA_STARTED}}, - }, - ); - - // don't increase the packets when time progresses. - await progressTime(); - - assert( - loggerSpy.calledWith( - 'StatsAnalyzer:index#compareLastStatsResult --> No video RTP packets sent', - ), - ); - }); - - it('does not log a message when video send packets increase', async () => { - await startStatsAnalyzer( - { - statsAnalyzer, pc, - mediaStatus: { - expected: { - sendVideo: true, - }, - }, - lastEmittedEvents: { - video: { - local: EVENTS.LOCAL_MEDIA_STOPPED, - }, - }, - }); - - fakeStats.video.senders[0].report[0].packetsSent += 5; - await progressTime(); - - assert( - loggerSpy.neverCalledWith( - 'StatsAnalyzer:index#compareLastStatsResult --> No video RTP packets sent', - ), - ); - }); - - it('logs a message when share send packets do not increase', async () => { - await startStatsAnalyzer({ - pc, mediaStatus: {expected: {sendShare: true}}, - lastEmittedEvents: {share: {local: EVENTS.LOCAL_MEDIA_STARTED}}, statsAnalyzer, - }, - ); - - // don't increase the packets when time progresses. - await progressTime(); - - assert( - loggerSpy.calledWith( - 'StatsAnalyzer:index#compareLastStatsResult --> No share RTP packets sent', - ), - ); - }); - - it('does not log a message when share send packets increase', async () => { - await startStatsAnalyzer({ - pc, statsAnalyzer, mediaStatus: {expected: {sendShare: true}}, - lastEmittedEvents: {share: {local: EVENTS.LOCAL_MEDIA_STOPPED}}, - }, - ); - - fakeStats.share.senders[0].report[0].packetsSent += 5; - await progressTime(); - - assert( - loggerSpy.neverCalledWith( - 'StatsAnalyzer:index#compareLastStatsResult --> No share RTP packets sent', - ), - ); - }); - - ['avatar', 'invalid', 'no source', 'bandwidth limited', 'policy violation'].forEach( - (sourceState) => { - it(`does not log a message when no packets are recieved for a receive slot with sourceState "${sourceState}"`, async () => { - receiveSlot = { - sourceState, - csi: 2, - id: '4', - }; - - await startStatsAnalyzer({pc, statsAnalyzer}); - - // don't increase the packets when time progresses. - await progressTime(); - - assert.neverCalledWith( - loggerSpy, - 'StatsAnalyzer:index#processInboundRTPResult --> No packets received for receive slot id: "4" and csi: 2. Total packets received on slot: ', - 0, - ); - }); - }, - ); - - it(`logs a message if no packets are sent`, async () => { - receiveSlot = { - sourceState: 'live', - csi: 2, - id: '4', - }; - await startStatsAnalyzer({pc, statsAnalyzer}); - - // don't increase the packets when time progresses. - await progressTime(); - - assert.calledWith( - loggerSpy, - 'StatsAnalyzer:index#processInboundRTPResult --> No packets received for mediaType: video-recv-0, receive slot id: "4" and csi: 2. Total packets received on slot: ', - 0, - ); - - assert.calledWith( - loggerSpy, - 'StatsAnalyzer:index#processInboundRTPResult --> No frames received for mediaType: video-recv-0, receive slot id: "4" and csi: 2. Total frames received on slot: ', - 0, - ); - - assert.calledWith( - loggerSpy, - 'StatsAnalyzer:index#processInboundRTPResult --> No frames decoded for mediaType: video-recv-0, receive slot id: "4" and csi: 2. Total frames decoded on slot: ', - 0, - ); - - assert.calledWith( - loggerSpy, - 'StatsAnalyzer:index#processInboundRTPResult --> No packets received for mediaType: audio-recv-0, receive slot id: "4" and csi: 2. Total packets received on slot: ', - 0, - ); - - assert.calledWith( - loggerSpy, - 'StatsAnalyzer:index#processInboundRTPResult --> No packets received for mediaType: video-share-recv-0, receive slot id: "4" and csi: 2. Total packets received on slot: ', - 0, - ); - - assert.calledWith( - loggerSpy, - 'StatsAnalyzer:index#processInboundRTPResult --> No frames received for mediaType: video-share-recv-0, receive slot id: "4" and csi: 2. Total frames received on slot: ', - 0, - ); - assert.calledWith( - loggerSpy, - 'StatsAnalyzer:index#processInboundRTPResult --> No frames decoded for mediaType: video-share-recv-0, receive slot id: "4" and csi: 2. Total frames decoded on slot: ', - 0, - ); - assert.calledWith( - loggerSpy, - 'StatsAnalyzer:index#processInboundRTPResult --> No packets received for mediaType: audio-share-recv-0, receive slot id: "4" and csi: 2. Total packets received on slot: ', - 0, - ); - }); - - it(`does not log a message if receiveSlot is undefined`, async () => { - await startStatsAnalyzer({pc, statsAnalyzer}); - - // don't increase the packets when time progresses. - await progressTime(); - - assert.neverCalledWith( - loggerSpy, - 'StatsAnalyzer:index#processInboundRTPResult --> No packets received for receive slot "". Total packets received on slot: ', - 0, - ); - }); - - it('has the correct number of senders and receivers (2)', async () => { - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}}); - - await progressTime(); - - assert.lengthOf(mqeData.audioTransmit, 2); - assert.lengthOf(mqeData.audioReceive, 2); - assert.lengthOf(mqeData.videoTransmit, 2); - assert.lengthOf(mqeData.videoReceive, 2); - }); - - it('has one stream per sender/reciever', async () => { - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}}); - - await progressTime(); - - assert.deepEqual(mqeData.audioTransmit[0].streams, [ - { - common: { - codec: 'opus', - csi: [], - requestedBitrate: 0, - requestedFrames: 0, - rtpPackets: 0, - ssci: 0, - transmittedBitrate: 0.13333333333333333, - transmittedFrameRate: 0, - }, - transmittedKeyFrames: 0, - requestedKeyFrames: 0, - requestedBitrate: 0, - }, - ]); - assert.deepEqual(mqeData.audioTransmit[1].streams, [ - { - common: { - codec: 'opus', - csi: [], - requestedBitrate: 0, - requestedFrames: 0, - rtpPackets: 0, - ssci: 0, - transmittedBitrate: 0.13333333333333333, - transmittedFrameRate: 0, - }, - transmittedKeyFrames: 0, - requestedKeyFrames: 0, - requestedBitrate: 0, - }, - ]); - assert.deepEqual(mqeData.audioReceive[0].streams, [ - { - common: { - codec: 'opus', - concealedFrames: 0, - csi: [], - maxConcealRunLength: 0, - optimalBitrate: 0, - optimalFrameRate: 0, - receivedBitrate: 0.13333333333333333, - receivedFrameRate: 0, - renderedFrameRate: 0, - requestedBitrate: 0, - requestedFrameRate: 0, - rtpEndToEndLost: 0, - maxRtpJitter: 0, - meanRtpJitter: 0, - rtpPackets: 0, - ssci: 0, - rtpJitter: 0, - framesDropped: 0, - framesReceived: 0, - }, - }, - ]); - assert.deepEqual(mqeData.audioReceive[1].streams, [ - { - common: { - codec: 'opus', - concealedFrames: 0, - csi: [], - maxConcealRunLength: 0, - optimalBitrate: 0, - optimalFrameRate: 0, - receivedBitrate: 0.13333333333333333, - receivedFrameRate: 0, - renderedFrameRate: 0, - requestedBitrate: 0, - requestedFrameRate: 0, - rtpEndToEndLost: 0, - maxRtpJitter: 0, - meanRtpJitter: 0, - rtpPackets: 0, - ssci: 0, - rtpJitter: 0, - framesDropped: 0, - framesReceived: 0, - }, - }, - ]); - assert.deepEqual(mqeData.videoTransmit[0].streams, [ - { - common: { - codec: 'H264', - csi: [], - duplicateSsci: 0, - requestedBitrate: 0, - requestedFrames: 0, - rtpPackets: 0, - ssci: 0, - transmittedBitrate: 0.13333333333333333, - transmittedFrameRate: 0, - }, - h264CodecProfile: 'BP', - isAvatar: false, - isHardwareEncoded: false, - localConfigurationChanges: 2, - maxFrameQp: 0, - maxNoiseLevel: 0, - minRegionQp: 0, - remoteConfigurationChanges: 0, - requestedFrameSize: 0, - requestedKeyFrames: 0, - transmittedFrameSize: 0, - transmittedHeight: 0, - transmittedKeyFrames: 0, - transmittedKeyFramesClient: 0, - transmittedKeyFramesConfigurationChange: 0, - transmittedKeyFramesFeedback: 0, - transmittedKeyFramesLocalDrop: 0, - transmittedKeyFramesOtherLayer: 0, - transmittedKeyFramesPeriodic: 0, - transmittedKeyFramesSceneChange: 0, - transmittedKeyFramesStartup: 0, - transmittedKeyFramesUnknown: 0, - transmittedWidth: 0, - requestedBitrate: 0, - }, - ]); - assert.deepEqual(mqeData.videoTransmit[1].streams, [ - { - common: { - codec: 'H264', - csi: [], - duplicateSsci: 0, - requestedBitrate: 0, - requestedFrames: 0, - rtpPackets: 0, - ssci: 0, - transmittedBitrate: 0.13333333333333333, - transmittedFrameRate: 0, - }, - h264CodecProfile: 'BP', - isAvatar: false, - isHardwareEncoded: false, - localConfigurationChanges: 2, - maxFrameQp: 0, - maxNoiseLevel: 0, - minRegionQp: 0, - remoteConfigurationChanges: 0, - requestedBitrate: 0, - requestedFrameSize: 0, - requestedKeyFrames: 0, - transmittedFrameSize: 0, - transmittedHeight: 0, - transmittedKeyFrames: 0, - transmittedKeyFramesClient: 0, - transmittedKeyFramesConfigurationChange: 0, - transmittedKeyFramesFeedback: 0, - transmittedKeyFramesLocalDrop: 0, - transmittedKeyFramesOtherLayer: 0, - transmittedKeyFramesPeriodic: 0, - transmittedKeyFramesSceneChange: 0, - transmittedKeyFramesStartup: 0, - transmittedKeyFramesUnknown: 0, - transmittedWidth: 0, - }, - ]); - assert.deepEqual(mqeData.videoReceive[0].streams, [ - { - common: { - codec: 'H264', - concealedFrames: 0, - csi: [], - maxConcealRunLength: 0, - optimalBitrate: 0, - optimalFrameRate: 0, - receivedBitrate: 0.13333333333333333, - receivedFrameRate: 0, - renderedFrameRate: 0, - requestedBitrate: 0, - requestedFrameRate: 0, - rtpEndToEndLost: 0, - rtpJitter: 0, - rtpPackets: 0, - ssci: 0, - framesDropped: 0, - }, - h264CodecProfile: 'BP', - isActiveSpeaker: false, - optimalFrameSize: 0, - receivedFrameSize: 3600, - receivedHeight: 720, - receivedKeyFrames: 0, - receivedKeyFramesForRequest: 0, - receivedKeyFramesSourceChange: 0, - receivedKeyFramesUnknown: 0, - receivedWidth: 1280, - requestedFrameSize: 0, - requestedKeyFrames: 0, - }, - ]); - assert.deepEqual(mqeData.videoReceive[1].streams, [ - { - common: { - codec: 'H264', - concealedFrames: 0, - csi: [], - maxConcealRunLength: 0, - optimalBitrate: 0, - optimalFrameRate: 0, - receivedBitrate: 0.13333333333333333, - receivedFrameRate: 0, - renderedFrameRate: 0, - requestedBitrate: 0, - requestedFrameRate: 0, - rtpEndToEndLost: 0, - rtpJitter: 0, - rtpPackets: 0, - ssci: 0, - framesDropped: 0, - }, - h264CodecProfile: 'BP', - isActiveSpeaker: false, - optimalFrameSize: 0, - receivedFrameSize: 3600, - receivedHeight: 720, - receivedKeyFrames: 0, - receivedKeyFramesForRequest: 0, - receivedKeyFramesSourceChange: 0, - receivedKeyFramesUnknown: 0, - receivedWidth: 1280, - requestedFrameSize: 0, - requestedKeyFrames: 0, - }, - ]); - }); - - it('has three streams for video receivers when three exist', async () => { - pc.getTransceiverStats = sinon.stub().resolves({ - audio: { - senders: [fakeStats.audio.senders[0]], - receivers: [fakeStats.audio.receivers[0]], - }, - video: { - senders: [fakeStats.video.senders[0]], - receivers: [ - fakeStats.video.receivers[0], - fakeStats.video.receivers[0], - fakeStats.video.receivers[0], - ], - }, - screenShareAudio: { - senders: [fakeStats.audio.senders[0]], - receivers: [fakeStats.audio.receivers[0]], - }, - screenShareVideo: { - senders: [fakeStats.video.senders[0]], - receivers: [fakeStats.video.receivers[0]], - }, - }); - - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}}); - - await progressTime(); - - assert.deepEqual(mqeData.videoReceive[0].streams, [ - { - common: { - codec: 'H264', - concealedFrames: 0, - csi: [], - maxConcealRunLength: 0, - optimalBitrate: 0, - optimalFrameRate: 0, - receivedBitrate: 0.13333333333333333, - receivedFrameRate: 0, - renderedFrameRate: 0, - requestedBitrate: 0, - requestedFrameRate: 0, - rtpEndToEndLost: 0, - rtpJitter: 0, - rtpPackets: 0, - ssci: 0, - framesDropped: 0, - }, - h264CodecProfile: 'BP', - isActiveSpeaker: false, - optimalFrameSize: 0, - receivedFrameSize: 3600, - receivedHeight: 720, - receivedKeyFrames: 0, - receivedKeyFramesForRequest: 0, - receivedKeyFramesSourceChange: 0, - receivedKeyFramesUnknown: 0, - receivedWidth: 1280, - requestedFrameSize: 0, - requestedKeyFrames: 0, - }, - { - common: { - codec: 'H264', - concealedFrames: 0, - csi: [], - maxConcealRunLength: 0, - optimalBitrate: 0, - optimalFrameRate: 0, - receivedBitrate: 0.13333333333333333, - receivedFrameRate: 0, - renderedFrameRate: 0, - requestedBitrate: 0, - requestedFrameRate: 0, - rtpEndToEndLost: 0, - rtpJitter: 0, - rtpPackets: 0, - ssci: 0, - framesDropped: 0, - }, - h264CodecProfile: 'BP', - isActiveSpeaker: false, - optimalFrameSize: 0, - receivedFrameSize: 3600, - receivedHeight: 720, - receivedKeyFrames: 0, - receivedKeyFramesForRequest: 0, - receivedKeyFramesSourceChange: 0, - receivedKeyFramesUnknown: 0, - receivedWidth: 1280, - requestedFrameSize: 0, - requestedKeyFrames: 0, - }, - { - common: { - codec: 'H264', - concealedFrames: 0, - csi: [], - maxConcealRunLength: 0, - optimalBitrate: 0, - optimalFrameRate: 0, - receivedBitrate: 0.13333333333333333, - receivedFrameRate: 0, - renderedFrameRate: 0, - requestedBitrate: 0, - requestedFrameRate: 0, - rtpEndToEndLost: 0, - rtpJitter: 0, - rtpPackets: 0, - ssci: 0, - framesDropped: 0, - }, - h264CodecProfile: 'BP', - isActiveSpeaker: false, - optimalFrameSize: 0, - receivedFrameSize: 3600, - receivedHeight: 720, - receivedKeyFrames: 0, - receivedKeyFramesForRequest: 0, - receivedKeyFramesSourceChange: 0, - receivedKeyFramesUnknown: 0, - receivedWidth: 1280, - requestedFrameSize: 0, - requestedKeyFrames: 0, - }, - ]); - }); - - describe('stream count for simulcast', async () => { - it('has three streams for video senders for simulcast', async () => { - pc.getTransceiverStats = sinon.stub().resolves({ - audio: { - senders: [fakeStats.audio.senders[0]], - receivers: [fakeStats.audio.receivers[0]], - }, - video: { - senders: [ - { - localTrackLabel: 'fake-camera', - report: [ - { - type: 'outbound-rtp', - bytesSent: 1, - framesSent: 0, - packetsSent: 0, - isRequested: true, - }, - { - type: 'outbound-rtp', - bytesSent: 1, - framesSent: 0, - packetsSent: 1, - isRequested: true, - }, - { - type: 'outbound-rtp', - bytesSent: 1000, - framesSent: 1, - packetsSent: 0, - isRequested: true, - }, - { - type: 'remote-inbound-rtp', - packetsLost: 0, - }, - { - type: 'candidate-pair', - state: 'succeeded', - localCandidateId: 'fake-candidate-id', - }, - { - type: 'candidate-pair', - state: 'failed', - localCandidateId: 'bad-candidate-id', - }, - { - type: 'local-candidate', - id: 'fake-candidate-id', - protocol: 'tcp', - }, - ], - }, - ], - receivers: [fakeStats.video.receivers[0]], - }, - screenShareAudio: { - senders: [fakeStats.audio.senders[0]], - receivers: [fakeStats.audio.receivers[0]], - }, - screenShareVideo: { - senders: [fakeStats.video.senders[0]], - receivers: [fakeStats.video.receivers[0]], - }, - }); - - await startStatsAnalyzer({ - pc, - statsAnalyzer, - mediaStatus: { - expected: { - receiveVideo: true, - }, - }, - }); - - await progressTime(); - - assert.deepEqual(mqeData.videoTransmit[0].streams, [ - { - common: { - codec: 'H264', - csi: [], - duplicateSsci: 0, - requestedBitrate: 0, - requestedFrames: 0, - rtpPackets: 0, - ssci: 0, - transmittedBitrate: 0.13333333333333333, - transmittedFrameRate: 0, - }, - h264CodecProfile: 'BP', - isAvatar: false, - isHardwareEncoded: false, - localConfigurationChanges: 2, - maxFrameQp: 0, - maxNoiseLevel: 0, - minRegionQp: 0, - remoteConfigurationChanges: 0, - requestedFrameSize: 0, - requestedKeyFrames: 0, - transmittedFrameSize: 0, - transmittedHeight: 0, - transmittedKeyFrames: 0, - transmittedKeyFramesClient: 0, - transmittedKeyFramesConfigurationChange: 0, - transmittedKeyFramesFeedback: 0, - transmittedKeyFramesLocalDrop: 0, - transmittedKeyFramesOtherLayer: 0, - transmittedKeyFramesPeriodic: 0, - transmittedKeyFramesSceneChange: 0, - transmittedKeyFramesStartup: 0, - transmittedKeyFramesUnknown: 0, - transmittedWidth: 0, - requestedBitrate: 0, - }, - { - common: { - codec: 'H264', - csi: [], - duplicateSsci: 0, - requestedBitrate: 0, - requestedFrames: 0, - rtpPackets: 1, - ssci: 0, - transmittedBitrate: 0.13333333333333333, - transmittedFrameRate: 0, - }, - h264CodecProfile: 'BP', - isAvatar: false, - isHardwareEncoded: false, - localConfigurationChanges: 2, - maxFrameQp: 0, - maxNoiseLevel: 0, - minRegionQp: 0, - remoteConfigurationChanges: 0, - requestedFrameSize: 0, - requestedKeyFrames: 0, - transmittedFrameSize: 0, - transmittedHeight: 0, - transmittedKeyFrames: 0, - transmittedKeyFramesClient: 0, - transmittedKeyFramesConfigurationChange: 0, - transmittedKeyFramesFeedback: 0, - transmittedKeyFramesLocalDrop: 0, - transmittedKeyFramesOtherLayer: 0, - transmittedKeyFramesPeriodic: 0, - transmittedKeyFramesSceneChange: 0, - transmittedKeyFramesStartup: 0, - transmittedKeyFramesUnknown: 0, - transmittedWidth: 0, - requestedBitrate: 0, - }, - { - common: { - codec: 'H264', - csi: [], - duplicateSsci: 0, - requestedBitrate: 0, - requestedFrames: 0, - rtpPackets: 0, - ssci: 0, - transmittedBitrate: 133.33333333333334, - transmittedFrameRate: 2, - }, - h264CodecProfile: 'BP', - isAvatar: false, - isHardwareEncoded: false, - localConfigurationChanges: 2, - maxFrameQp: 0, - maxNoiseLevel: 0, - minRegionQp: 0, - remoteConfigurationChanges: 0, - requestedFrameSize: 0, - requestedKeyFrames: 0, - transmittedFrameSize: 0, - transmittedHeight: 0, - transmittedKeyFrames: 0, - transmittedKeyFramesClient: 0, - transmittedKeyFramesConfigurationChange: 0, - transmittedKeyFramesFeedback: 0, - transmittedKeyFramesLocalDrop: 0, - transmittedKeyFramesOtherLayer: 0, - transmittedKeyFramesPeriodic: 0, - transmittedKeyFramesSceneChange: 0, - transmittedKeyFramesStartup: 0, - transmittedKeyFramesUnknown: 0, - transmittedWidth: 0, - requestedBitrate: 0, - }, - ]); - }); - }); - describe('active speaker status emission', async () => { - beforeEach(async () => { - await startStatsAnalyzer({pc, statsAnalyzer}); - performance.timeOrigin = 1; - }); - - it('reports active speaker as true when the participant has been speaking', async () => { - fakeStats.video.receivers[0].report[0].isActiveSpeaker = true; - await progressTime(5 * MQA_INTERVAL); - assert.strictEqual(mqeData.videoReceive[0].streams[0].isActiveSpeaker, true); - }); - - it('reports active speaker as false when the participant has not spoken', async () => { - fakeStats.video.receivers[0].report[0].isActiveSpeaker = false; - await progressTime(5 * MQA_INTERVAL); - assert.strictEqual(mqeData.videoReceive[0].streams[0].isActiveSpeaker, false); - }); - - it('defaults to false when active speaker status is indeterminate', async () => { - fakeStats.video.receivers[0].report[0].isActiveSpeaker = undefined; - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.videoReceive[0].streams[0].isActiveSpeaker, false); - }); - - it('updates active speaker to true following a recent status change to speaking', async () => { - fakeStats.video.receivers[0].report[0].isActiveSpeaker = false; - fakeStats.video.receivers[0].report[0].lastActiveSpeakerUpdateTimestamp = performance.timeOrigin + performance.now() + (30 * 1000); - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.videoReceive[0].streams[0].isActiveSpeaker, true); - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.videoReceive[0].streams[0].isActiveSpeaker, false); - }); - }); - describe('sends streams according to their is requested flag', async () => { - - beforeEach(async () => { - performance.timeOrigin = 0; - await startStatsAnalyzer({pc, statsAnalyzer}); - }); - - it('should send a stream if it is requested', async () => { - fakeStats.audio.senders[0].report[0].isRequested = true; - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.audioTransmit[0].streams.length, 1); - }); - - it('should not sent a stream if its is requested flag is undefined', async () => { - fakeStats.audio.senders[0].report[0].isRequested = undefined; - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.audioTransmit[0].streams.length, 0); - }); - - it('should not send a stream if it is not requested', async () => { - fakeStats.audio.receivers[0].report[0].isRequested = false; - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.audioReceive[0].streams.length, 0); - }); - - it('should send the stream if it was recently requested', async () => { - fakeStats.audio.receivers[0].report[0].lastRequestedUpdateTimestamp = performance.timeOrigin + performance.now() + (30 * 1000); - fakeStats.audio.receivers[0].report[0].isRequested = false; - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.audioReceive[0].streams.length, 1); - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.audioReceive[0].streams.length, 0); - }); - }); - - describe('window and screen size emission', async () => { - beforeEach(async () => { - await startStatsAnalyzer({pc, statsAnalyzer}); - }); - - it('should record the screen size from window.screen properties', async () => { - sinon.stub(window.screen, 'width').get(() => 1280); - sinon.stub(window.screen, 'height').get(() => 720); - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.intervalMetadata.screenWidth, 1280); - assert.strictEqual(mqeData.intervalMetadata.screenHeight, 720); - assert.strictEqual(mqeData.intervalMetadata.screenResolution, 3600); - }); - - it('should record the initial app window size from window properties', async () => { - sinon.stub(window, 'innerWidth').get(() => 720); - sinon.stub(window, 'innerHeight').get(() => 360); - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.intervalMetadata.appWindowWidth, 720); - assert.strictEqual(mqeData.intervalMetadata.appWindowHeight, 360); - assert.strictEqual(mqeData.intervalMetadata.appWindowSize, 1013); - - sinon.stub(window, 'innerWidth').get(() => 1080); - sinon.stub(window, 'innerHeight').get(() => 720); - await progressTime(MQA_INTERVAL); - assert.strictEqual(mqeData.intervalMetadata.appWindowWidth, 1080); - assert.strictEqual(mqeData.intervalMetadata.appWindowHeight, 720); - assert.strictEqual(mqeData.intervalMetadata.appWindowSize, 3038); - }); - }); - - describe('sends multistreamEnabled', async () => { - it('false if StatsAnalyzer initialized with default value for isMultistream', async () => { - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}}); - - await progressTime(); - - for (const data of [ - mqeData.audioTransmit, - mqeData.audioReceive, - mqeData.videoTransmit, - mqeData.videoReceive, - ]) { - assert.strictEqual(data[0].common.common.multistreamEnabled, false); - } - }); - - it('false if StatsAnalyzer initialized with false', async () => { - statsAnalyzer = new StatsAnalyzer({ - config: initialConfig, - receiveSlotCallback: () => receiveSlot, - networkQualityMonitor, - isMultistream: false, - }); - registerStatsAnalyzerEvents(statsAnalyzer); - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: false}}}); - - await progressTime(); - - for (const data of [ - mqeData.audioTransmit, - mqeData.audioReceive, - mqeData.videoTransmit, - mqeData.videoReceive, - ]) { - assert.strictEqual(data[0].common.common.multistreamEnabled, false); - } - }); - - it('true if StatsAnalyzer initialized with multistream', async () => { - statsAnalyzer = new StatsAnalyzer({ - config: initialConfig, - receiveSlotCallback: () => receiveSlot, - networkQualityMonitor, - isMultistream: true, - }); - registerStatsAnalyzerEvents(statsAnalyzer); - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}}); - - await progressTime(); - - for (const data of [ - mqeData.audioTransmit, - mqeData.audioReceive, - mqeData.videoTransmit, - mqeData.videoReceive, - ]) { - assert.strictEqual(data[0].common.common.multistreamEnabled, true); - } - }); - }); - - describe('CPU Information Reporting', async () => { - let getNumLogicalCoresStub; - - beforeEach(async () => { - getNumLogicalCoresStub = sinon.stub(CpuInfo, 'getNumLogicalCores'); - }); - - afterEach(() => { - getNumLogicalCoresStub.restore(); - }); - - it('reports 1 of logical CPU cores when not available', async () => { - getNumLogicalCoresStub.returns(undefined); - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}}); - - await progressTime(); - assert.equal(mqeData.intervalMetadata.cpuInfo.numberOfCores, 1); - }); - - it('reports the number of logical CPU cores', async () => { - getNumLogicalCoresStub.returns(12); - await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}}); - - await progressTime(); - assert.equal(mqeData.intervalMetadata.cpuInfo.numberOfCores, 12); - }); - }); - }) - }); -}); diff --git a/packages/calling/package.json b/packages/calling/package.json index 041dd513f63..189d9d55095 100644 --- a/packages/calling/package.json +++ b/packages/calling/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "@types/platform": "1.3.4", - "@webex/internal-media-core": "2.8.0", + "@webex/internal-media-core": "2.9.0", "@webex/media-helpers": "workspace:*", "async-mutex": "0.4.0", "buffer": "6.0.3", diff --git a/packages/calling/src/CallingClient/calling/call.test.ts b/packages/calling/src/CallingClient/calling/call.test.ts index 62cc39e4626..ce43512f806 100644 --- a/packages/calling/src/CallingClient/calling/call.test.ts +++ b/packages/calling/src/CallingClient/calling/call.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable dot-notation */ /* eslint-disable @typescript-eslint/no-shadow */ -import * as MediaSDK from '@webex/internal-media-core'; +import * as InternalMediaCoreModule from '@webex/internal-media-core'; import {EffectEvent} from '@webex/web-media-effects'; import {ERROR_TYPE, ERROR_LAYER} from '../../Errors/types'; import * as Utils from '../../common/Utils'; @@ -31,7 +31,9 @@ jest.mock('@webex/internal-media-core'); const webex = getTestUtilsWebex(); -const mockMediaSDK = MediaSDK as jest.Mocked; +const mockInternalMediaCoreModule = InternalMediaCoreModule as jest.Mocked< + typeof InternalMediaCoreModule +>; const defaultServiceIndicator = ServiceIndicator.CALLING; const activeUrl = 'FakeActiveUrl'; @@ -215,7 +217,7 @@ describe('Call Tests', () => { setUserMuted: jest.fn(), }; - const localAudioStream = mockStream as unknown as MediaSDK.LocalMicrophoneStream; + const localAudioStream = mockStream as unknown as InternalMediaCoreModule.LocalMicrophoneStream; const call = callManager.createCall(dest, CallDirection.OUTBOUND, deviceId, mockLineId); @@ -321,7 +323,7 @@ describe('Call Tests', () => { }), }; - const localAudioStream = mockStream as unknown as MediaSDK.LocalMicrophoneStream; + const localAudioStream = mockStream as unknown as InternalMediaCoreModule.LocalMicrophoneStream; const warnSpy = jest.spyOn(log, 'warn'); const call = createCall( @@ -340,7 +342,7 @@ describe('Call Tests', () => { call.dial(localAudioStream); expect(mockTrack.enabled).toEqual(true); - expect(mockMediaSDK.RoapMediaConnection).toBeCalledOnceWith( + expect(mockInternalMediaCoreModule.RoapMediaConnection).toBeCalledOnceWith( roapMediaConnectionConfig, roapMediaConnectionOptions, expect.any(String) @@ -375,7 +377,7 @@ describe('Call Tests', () => { }), }; - const localAudioStream = mockStream as unknown as MediaSDK.LocalMicrophoneStream; + const localAudioStream = mockStream as unknown as InternalMediaCoreModule.LocalMicrophoneStream; const warnSpy = jest.spyOn(log, 'warn'); const call = createCall( @@ -394,7 +396,7 @@ describe('Call Tests', () => { call.answer(localAudioStream); expect(mockTrack.enabled).toEqual(true); - expect(mockMediaSDK.RoapMediaConnection).toBeCalledOnceWith( + expect(mockInternalMediaCoreModule.RoapMediaConnection).toBeCalledOnceWith( roapMediaConnectionConfig, roapMediaConnectionOptions, expect.any(String) @@ -428,7 +430,7 @@ describe('Call Tests', () => { getEffectByKind: jest.fn(), }; - const localAudioStream = mockStream as unknown as MediaSDK.LocalMicrophoneStream; + const localAudioStream = mockStream as unknown as InternalMediaCoreModule.LocalMicrophoneStream; const onStreamSpy = jest.spyOn(localAudioStream, 'on'); const onEffectSpy = jest.spyOn(mockEffect, 'on'); const offStreamSpy = jest.spyOn(localAudioStream, 'off'); @@ -448,7 +450,7 @@ describe('Call Tests', () => { call.dial(localAudioStream); expect(mockTrack.enabled).toEqual(true); - expect(mockMediaSDK.RoapMediaConnection).toBeCalledOnceWith( + expect(mockInternalMediaCoreModule.RoapMediaConnection).toBeCalledOnceWith( roapMediaConnectionConfig, roapMediaConnectionOptions, expect.any(String) @@ -464,11 +466,11 @@ describe('Call Tests', () => { /* Checking if listeners on the localAudioStream have been registered */ expect(onStreamSpy).toBeCalledTimes(2); expect(onStreamSpy).toBeCalledWith( - MediaSDK.LocalStreamEventNames.OutputTrackChange, + InternalMediaCoreModule.LocalStreamEventNames.OutputTrackChange, expect.any(Function) ); expect(onStreamSpy).toBeCalledWith( - MediaSDK.LocalStreamEventNames.EffectAdded, + InternalMediaCoreModule.LocalStreamEventNames.EffectAdded, expect.any(Function) ); @@ -514,11 +516,11 @@ describe('Call Tests', () => { /* Checks for switching off the listeners on call disconnect */ expect(offStreamSpy).toBeCalledTimes(2); expect(offStreamSpy).toBeCalledWith( - MediaSDK.LocalStreamEventNames.OutputTrackChange, + InternalMediaCoreModule.LocalStreamEventNames.OutputTrackChange, expect.any(Function) ); expect(offStreamSpy).toBeCalledWith( - MediaSDK.LocalStreamEventNames.EffectAdded, + InternalMediaCoreModule.LocalStreamEventNames.EffectAdded, expect.any(Function) ); expect(offEffectSpy).toBeCalledWith(EffectEvent.Enabled, expect.any(Function)); @@ -535,7 +537,7 @@ describe('Call Tests', () => { getEffectByKind: jest.fn(), }; - const localAudioStream = mockStream as unknown as MediaSDK.LocalMicrophoneStream; + const localAudioStream = mockStream as unknown as InternalMediaCoreModule.LocalMicrophoneStream; webex.request.mockReturnValue({ statusCode: 200, body: { @@ -584,7 +586,7 @@ describe('Call Tests', () => { on: jest.fn(), }; - const localAudioStream = mockStream as unknown as MediaSDK.LocalMicrophoneStream; + const localAudioStream = mockStream as unknown as InternalMediaCoreModule.LocalMicrophoneStream; const warnSpy = jest.spyOn(log, 'warn'); const call = createCall( @@ -624,7 +626,7 @@ describe('Call Tests', () => { getEffectByKind: jest.fn(), }; - const localAudioStream = mockStream as unknown as MediaSDK.LocalMicrophoneStream; + const localAudioStream = mockStream as unknown as InternalMediaCoreModule.LocalMicrophoneStream; const onStream1Spy = jest.spyOn(localAudioStream, 'on'); const offStream1Spy = jest.spyOn(localAudioStream, 'off'); @@ -636,11 +638,11 @@ describe('Call Tests', () => { expect(mockTrack.enabled).toEqual(true); expect(onStream1Spy).toBeCalledTimes(2); expect(onStream1Spy).toBeCalledWith( - MediaSDK.LocalStreamEventNames.OutputTrackChange, + InternalMediaCoreModule.LocalStreamEventNames.OutputTrackChange, expect.any(Function) ); expect(onStream1Spy).toBeCalledWith( - MediaSDK.LocalStreamEventNames.EffectAdded, + InternalMediaCoreModule.LocalStreamEventNames.EffectAdded, expect.any(Function) ); @@ -656,7 +658,8 @@ describe('Call Tests', () => { getEffectByKind: jest.fn(), }; - const localAudioStream2 = mockStream2 as unknown as MediaSDK.LocalMicrophoneStream; + const localAudioStream2 = + mockStream2 as unknown as InternalMediaCoreModule.LocalMicrophoneStream; const onStream2Spy = jest.spyOn(localAudioStream2, 'on'); call.updateMedia(localAudioStream2); @@ -664,15 +667,15 @@ describe('Call Tests', () => { expect(call['mediaConnection'].updateLocalTracks).toBeCalledOnceWith({audio: mockTrack2}); expect(call['localAudioStream']).toEqual(localAudioStream2); expect(offStream1Spy).toBeCalledWith( - MediaSDK.LocalStreamEventNames.EffectAdded, + InternalMediaCoreModule.LocalStreamEventNames.EffectAdded, expect.any(Function) ); expect(onStream2Spy).toBeCalledWith( - MediaSDK.LocalStreamEventNames.OutputTrackChange, + InternalMediaCoreModule.LocalStreamEventNames.OutputTrackChange, expect.any(Function) ); expect(onStream2Spy).toBeCalledWith( - MediaSDK.LocalStreamEventNames.EffectAdded, + InternalMediaCoreModule.LocalStreamEventNames.EffectAdded, expect.any(Function) ); }); @@ -689,7 +692,7 @@ describe('Call Tests', () => { getEffectByKind: jest.fn(), }; - const localAudioStream = mockStream as unknown as MediaSDK.LocalMicrophoneStream; + const localAudioStream = mockStream as unknown as InternalMediaCoreModule.LocalMicrophoneStream; const call = callManager.createCall(dest, CallDirection.OUTBOUND, deviceId, mockLineId); @@ -703,7 +706,8 @@ describe('Call Tests', () => { }, }; - const localAudioStream2 = errorStream as unknown as MediaSDK.LocalMicrophoneStream; + const localAudioStream2 = + errorStream as unknown as InternalMediaCoreModule.LocalMicrophoneStream; call.updateMedia(localAudioStream2); diff --git a/packages/calling/src/CallingClient/calling/call.ts b/packages/calling/src/CallingClient/calling/call.ts index 0978508ad1b..9fc68949cba 100644 --- a/packages/calling/src/CallingClient/calling/call.ts +++ b/packages/calling/src/CallingClient/calling/call.ts @@ -1,5 +1,5 @@ import { - Event, + MediaConnectionEventNames, LocalMicrophoneStream, LocalStreamEventNames, RoapMediaConnection, @@ -2399,7 +2399,7 @@ export class Call extends Eventing implements ICall { */ private mediaRoapEventsListener() { this.mediaConnection.on( - Event.ROAP_MESSAGE_TO_SEND, + MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND, // eslint-disable-next-line @typescript-eslint/no-explicit-any async (event: any) => { log.info( @@ -2467,7 +2467,7 @@ export class Call extends Eventing implements ICall { */ private mediaTrackListener() { // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.mediaConnection.on(Event.REMOTE_TRACK_ADDED, (e: 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); } diff --git a/yarn.lock b/yarn.lock index 09e9b39ff6d..8f9cd5fa7ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2034,12 +2034,22 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.18.9": - version: 7.24.1 - resolution: "@babel/runtime@npm:7.24.1" +"@babel/runtime-corejs2@npm:^7.25.0": + version: 7.25.4 + resolution: "@babel/runtime-corejs2@npm:7.25.4" dependencies: - regenerator-runtime: "npm:^0.14.0" - checksum: 5c8f3b912ba949865f03b3cf8395c60e1f4ebd1033fbd835bdfe81b6cac8a87d85bc3c7aded5fcdf07be044c9ab8c818f467abe0deca50020c72496782639572 + core-js: ^2.6.12 + regenerator-runtime: ^0.14.0 + checksum: 5c543575e3eef559a81657e696ee7c2b71da159905b59eaf1b9b9a2efbd8c5259edb4f79d5a86c97326fa47a7dc447f60133e00ca4977ac5244fefdf34fc5921 + languageName: node + linkType: hard + +"@babel/runtime@npm:^7.18.9, @babel/runtime@npm:^7.25.0": + version: 7.25.4 + resolution: "@babel/runtime@npm:7.25.4" + dependencies: + regenerator-runtime: ^0.14.0 + checksum: 5c2aab03788e77f1f959d7e6ce714c299adfc9b14fb6295c2a17eb7cad0dd9c2ebfb2d25265f507f68c43d5055c5cd6f71df02feb6502cea44b68432d78bcbbe languageName: node linkType: hard @@ -5901,7 +5911,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^20.1.0, @types/node@npm:^20.11.28, @types/node@npm:^20.14.1": +"@types/node@npm:^20.1.0, @types/node@npm:^20.11.28": version: 20.14.9 resolution: "@types/node@npm:20.14.9" dependencies: @@ -5910,6 +5920,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.14.1": + version: 20.16.1 + resolution: "@types/node@npm:20.16.1" + dependencies: + undici-types: ~6.19.2 + checksum: 2b8f30f416f5c1851ffa8a13ef6c464a5e355edfd763713c22813a7839f6419a64e27925f9e89c972513d78432263179332f0bffb273d16498233bfdf495d096 + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0, @types/normalize-package-data@npm:^2.4.1": version: 2.4.4 resolution: "@types/normalize-package-data@npm:2.4.4" @@ -7400,7 +7419,7 @@ __metadata: "@typescript-eslint/eslint-plugin": 5.38.1 "@typescript-eslint/parser": 5.38.1 "@web/dev-server": 0.4.5 - "@webex/internal-media-core": 2.8.0 + "@webex/internal-media-core": 2.9.0 "@webex/media-helpers": "workspace:*" async-mutex: 0.4.0 buffer: 6.0.3 @@ -7691,19 +7710,21 @@ __metadata: languageName: unknown linkType: soft -"@webex/internal-media-core@npm:2.8.0": - version: 2.8.0 - resolution: "@webex/internal-media-core@npm:2.8.0" +"@webex/internal-media-core@npm:2.9.0": + version: 2.9.0 + resolution: "@webex/internal-media-core@npm:2.9.0" dependencies: "@babel/runtime": ^7.18.9 + "@babel/runtime-corejs2": ^7.25.0 "@webex/ts-sdp": 1.7.0 + "@webex/web-capabilities": ^1.4.1 "@webex/web-client-media-engine": 3.23.1 events: ^3.3.0 typed-emitter: ^2.1.0 uuid: ^8.3.2 webrtc-adapter: ^8.1.2 xstate: ^4.30.6 - checksum: a27ddbd484404dc99b627879df10ff97212fddb3bf94829aa41408d719436b33d6bce824b2a33a30a5dd83eabc9c3d70cbb4fe086fc00d5c1aca5da918503e3d + checksum: 59543f7e194242b0122eecb8ffdc1276887f820fa43d97add8a041fa78741a4296e491e8b1346bb892d0810e045d6304659f7be16b67cd2fac6d3dc7a5dc3227 languageName: node linkType: hard @@ -8377,15 +8398,6 @@ __metadata: languageName: node linkType: hard -"@webex/ladon-ts@npm:^4.2.4": - version: 4.2.4 - resolution: "@webex/ladon-ts@npm:4.2.4" - dependencies: - onnxruntime-web: "npm:^1.15.1" - checksum: 60715e517f5dc000bf8c1eb47c013de139f1a4c0cefc814c4667434ca83ba54f1638187345af9a78746ad4da06e3115164a6faed523c815506fffb05a43427c6 - languageName: node - linkType: hard - "@webex/ladon-ts@npm:^4.3.0": version: 4.3.0 resolution: "@webex/ladon-ts@npm:4.3.0" @@ -8471,7 +8483,7 @@ __metadata: "@babel/preset-typescript": 7.22.11 "@webex/babel-config-legacy": "workspace:*" "@webex/eslint-config-legacy": "workspace:*" - "@webex/internal-media-core": 2.8.0 + "@webex/internal-media-core": 2.9.0 "@webex/jest-config-legacy": "workspace:*" "@webex/legacy-tools": "workspace:*" "@webex/test-helper-chai": "workspace:*" @@ -8707,7 +8719,7 @@ __metadata: "@webex/babel-config-legacy": "workspace:*" "@webex/common": "workspace:*" "@webex/eslint-config-legacy": "workspace:*" - "@webex/internal-media-core": 2.8.0 + "@webex/internal-media-core": 2.9.0 "@webex/internal-plugin-conversation": "workspace:*" "@webex/internal-plugin-device": "workspace:*" "@webex/internal-plugin-llm": "workspace:*" @@ -9000,12 +9012,12 @@ __metadata: linkType: soft "@webex/rtcstats@npm:^1.3.2": - version: 1.3.2 - resolution: "@webex/rtcstats@npm:1.3.2" + version: 1.3.3 + resolution: "@webex/rtcstats@npm:1.3.3" dependencies: "@types/node": ^20.14.1 uuid: ^8.3.2 - checksum: dab5bbd1310e549e0e60dce4f7f5e67cbc8729ded894581ff196af189e9884fdb89ac7ef19f9a125ddefd1a9d57d228a34c9a9f4cfb792d939d07d11c0cfc604 + checksum: 7ac73b2f6bf8bf44bfaff7a5e904b175509632b80bf4cbdb09c5570c940d7e02445ba0c95f713067b03770eee49162d964b98a8d1e05f42e3f80b5bfd503352c languageName: node linkType: hard @@ -9401,16 +9413,7 @@ __metadata: languageName: unknown linkType: soft -"@webex/web-capabilities@npm:^1.1.0": - version: 1.2.0 - resolution: "@webex/web-capabilities@npm:1.2.0" - dependencies: - bowser: "npm:^2.11.0" - checksum: 69bf1521485c0a29f10edc85c06fee33a983f1363639e6662bd65389602f4e2ded8e37be6d3cf845bd6efef6712680fce7592e175d5e1e33291dc2769ac66c4c - languageName: node - linkType: hard - -"@webex/web-capabilities@npm:^1.4.0": +"@webex/web-capabilities@npm:^1.1.0, @webex/web-capabilities@npm:^1.4.0, @webex/web-capabilities@npm:^1.4.1": version: 1.4.1 resolution: "@webex/web-capabilities@npm:1.4.1" dependencies: @@ -9452,15 +9455,16 @@ __metadata: linkType: hard "@webex/web-media-effects@npm:^2.15.6": - version: 2.15.6 - resolution: "@webex/web-media-effects@npm:2.15.6" + version: 2.20.8 + resolution: "@webex/web-media-effects@npm:2.20.8" dependencies: - "@webex/ladon-ts": "npm:^4.2.4" - events: "npm:^3.3.0" - js-logger: "npm:^1.6.1" - typed-emitter: "npm:^1.4.0" - uuid: "npm:^9.0.1" - checksum: cd37e0c7b37a7a6611ae14ca1926453cd536c3259bfffc6b1ca2d3a804caab97c44101c31cccdb8b91a7d03cc4e0fc60d0b3785e4c6fcde0f937ceb055fcf977 + "@webex/ladon-ts": ^4.3.0 + events: ^3.3.0 + js-logger: ^1.6.1 + typed-emitter: ^1.4.0 + uuid: ^9.0.1 + worker-timers: ^8.0.2 + checksum: 97d51ff86bcb7880f18b3615dcd745df826758bb93cfd1c076fd60357ff57c2dd00b7ba4a609b0c71fa5b7caf39f5443c0fc371f1ed4486fa2aafe42a4b50d0d languageName: node linkType: hard @@ -16972,6 +16976,16 @@ __metadata: languageName: node linkType: hard +"fast-unique-numbers@npm:^9.0.8": + version: 9.0.8 + resolution: "fast-unique-numbers@npm:9.0.8" + dependencies: + "@babel/runtime": ^7.25.0 + tslib: ^2.6.3 + checksum: 27840ed4ada274f6391cc81a977ab068ef0931a835711415794010ff8775f1ba2ffc4ae890b00b28fdee2e54a929efec6cf9e154ae958f55d16d55cc4023f6cc + languageName: node + linkType: hard + "fast-url-parser@npm:1.1.3": version: 1.1.3 resolution: "fast-url-parser@npm:1.1.3" @@ -31130,6 +31144,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.6.3": + version: 2.7.0 + resolution: "tslib@npm:2.7.0" + checksum: 1606d5c89f88d466889def78653f3aab0f88692e80bb2066d090ca6112ae250ec1cfa9dbfaab0d17b60da15a4186e8ec4d893801c67896b277c17374e36e1d28 + languageName: node + linkType: hard + "tslib@npm:~2.3.0": version: 2.3.1 resolution: "tslib@npm:2.3.1" @@ -31600,6 +31621,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.19.2": + version: 6.19.8 + resolution: "undici-types@npm:6.19.8" + checksum: de51f1b447d22571cf155dfe14ff6d12c5bdaec237c765085b439c38ca8518fc360e88c70f99469162bf2e14188a7b0bcb06e1ed2dc031042b984b0bb9544017 + languageName: node + linkType: hard + "unicode-canonical-property-names-ecmascript@npm:^2.0.0": version: 2.0.0 resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.0" @@ -33279,6 +33307,40 @@ __metadata: languageName: node linkType: hard +"worker-timers-broker@npm:^7.1.1": + version: 7.1.1 + resolution: "worker-timers-broker@npm:7.1.1" + dependencies: + "@babel/runtime": ^7.25.0 + fast-unique-numbers: ^9.0.8 + tslib: ^2.6.3 + worker-timers-worker: ^8.0.3 + checksum: baa4a7bf49efb8ade9be7cb7865ac27e8030b278d7698ce70a09972ab92a5a7a3e6aaf5dbb3ebecc2d9f94562f10643dada43159382b40a2b9d257f82f473071 + languageName: node + linkType: hard + +"worker-timers-worker@npm:^8.0.3": + version: 8.0.3 + resolution: "worker-timers-worker@npm:8.0.3" + dependencies: + "@babel/runtime": ^7.25.0 + tslib: ^2.6.3 + checksum: af06dc1df2eb5b45ed5d0b5d56de621411f9354633eb9d4368effa00c856301d02c83c632c77645f2753167f0168136d17c2b8cbf986b041ba5b08cf8c10ad83 + languageName: node + linkType: hard + +"worker-timers@npm:^8.0.2": + version: 8.0.4 + resolution: "worker-timers@npm:8.0.4" + dependencies: + "@babel/runtime": ^7.25.0 + tslib: ^2.6.3 + worker-timers-broker: ^7.1.1 + worker-timers-worker: ^8.0.3 + checksum: 51bcc64f01ea143ac644b88d10b211afd0dd05991ae2a747066f25456f9e84eaa48e530c01d71998a1bc2a00303d8ea40ea97f070bea5e7ec84890140f7f1cb6 + languageName: node + linkType: hard + "workerpool@npm:6.2.1": version: 6.2.1 resolution: "workerpool@npm:6.2.1"