import AudioVideoController from '../audiovideocontroller/AudioVideoController' ; import BrowserBehavior from '../browserbehavior/BrowserBehavior' ; import Direction from '../clientmetricreport/ClientMetricReportDirection' ; import MediaType from '../clientmetricreport/ClientMetricReportMediaType' ; import DefaultClientMetricReport from '../clientmetricreport/DefaultClientMetricReport' ; import StreamMetricReport from '../clientmetricreport/StreamMetricReport' ; import Logger from '../logger/Logger' ; import MeetingSessionLifecycleEvent from '../meetingsession/MeetingSessionLifecycleEvent' ; import MeetingSessionLifecycleEventCondition from '../meetingsession/MeetingSessionLifecycleEventCondition' ; import MeetingSessionStatus from '../meetingsession/MeetingSessionStatus' ; import IntervalScheduler from '../scheduler/IntervalScheduler' ; import SignalingClient from '../signalingclient/SignalingClient' ; import { SdkClientMetricFrame , SdkMetric , SdkStreamMetricFrame , } from '../signalingprotocol/SignalingProtocol.js' ; import { Maybe } from '../utils/Types' ; import VideoStreamIndex from '../videostreamindex/VideoStreamIndex' ; import AudioLogEvent from './AudioLogEvent' ; import StatsCollector from './StatsCollector' ; import VideoLogEvent from './VideoLogEvent' ; type RawMetricReport = any ; type StatsReportItem = any ; export default class DefaultStatsCollector implements StatsCollector { private static readonly INTERVAL_MS = 1000 ; private static readonly FIREFOX_UPDATED_GET_STATS_VERSION = '66.0.0' ; private static readonly CLIENT_TYPE = 'amazon-chime-sdk-js' ; private intervalScheduler : IntervalScheduler | null = null ; private signalingClient : SignalingClient ; private videoStreamIndex : VideoStreamIndex ; private clientMetricReport : DefaultClientMetricReport ; constructor ( private audioVideoController , private logger , private browserBehavior , private readonly interval = DefaultStatsCollector . INTERVAL_MS ) { } toAttribute ( str ) { return this . toSuffix ( str ) . substring ( 1 ) ; } private toSuffix ( str ) { if ( str . toLowerCase ( ) === str ) { return `_${ str }` ; } else if ( str . toUpperCase ( ) === str ) { return `_${ str . toLowerCase ( ) }` ; } else { return str . replace ( /([A-Z][a-z]+)/g , function ( $1 ) { return `_${ $1 }` ; } ) . replace ( /([A-Z][A-Z]+)/g , function ( $1 ) { return `_${ $1 }` ; } ) . toLowerCase ( ) ; } } metricsAddTime = ( _name , _duration , _attributes ? ) : void => { } ; metricsLogEvent = ( _name , _attributes ) : void => { } ; logLatency ( eventName , timeMs , attributes ? ) { const event = this . toSuffix ( eventName ) ; this . logEventTime ( 'meeting' + event , timeMs , attributes ) ; } logStateTimeout ( stateName , attributes ? ) { const state = this . toSuffix ( stateName ) ; this . logEvent ( 'meeting_session_state_timeout' , { ... attributes , state : `state${ state }` , } ) ; } logAudioEvent ( eventName , attributes ? ) { const event = 'audio' + this . toSuffix ( AudioLogEvent [ eventName ] ) ; this . logEvent ( event , attributes ) ; } logVideoEvent ( eventName , attributes ? ) { const event = 'video' + this . toSuffix ( VideoLogEvent [ eventName ] ) ; this . logEvent ( event , attributes ) ; } private logEventTime ( eventName , timeMs , attributes = { } ) { const finalAttributes = { ... attributes , call_id : this . audioVideoController . configuration . meetingId , client_type : DefaultStatsCollector . CLIENT_TYPE , metric_type : 'latency' , } ; this . logger . debug ( ( ) => { return `[DefaultStatsCollector] ${ eventName }: ${ JSON . stringify ( finalAttributes ) }` ; } ) ; this . metricsAddTime ( eventName , timeMs , finalAttributes ) ; } logMeetingSessionStatus ( status ) { const statusEventName = `${ status . statusCode ( ) }` ; this . logEvent ( statusEventName ) ; const statusAttribute = { status : statusEventName , status_code : `${ status . statusCode ( ) }` , } ; this . logEvent ( 'meeting_session_status' , statusAttribute ) ; if ( status . isTerminal ( ) ) { this . logEvent ( 'meeting_session_stopped' , statusAttribute ) ; } if ( status . isAudioConnectionFailure ( ) ) { this . logEvent ( 'meeting_session_audio_failed' , statusAttribute ) ; } if ( status . isFailure ( ) ) { this . logEvent ( 'meeting_session_failed' , statusAttribute ) ; } } logLifecycleEvent ( lifecycleEvent , condition ) { const attributes = { lifecycle_event : `lifecycle${ this . toSuffix ( MeetingSessionLifecycleEvent [ lifecycleEvent ] ) }` , lifecycle_event_code : `${ lifecycleEvent }` , lifecycle_event_condition : `condition${ this . toSuffix ( MeetingSessionLifecycleEventCondition [ condition ] ) }` , lifecycle_event_condition_code : `${ condition }` , } ; this . logEvent ( 'meeting_session_lifecycle' , attributes ) ; } private logEvent ( eventName , attributes = { } ) { const finalAttributes = { ... attributes , call_id : this . audioVideoController . configuration . meetingId , client_type : DefaultStatsCollector . CLIENT_TYPE , } ; this . logger . debug ( ( ) => { return `[DefaultStatsCollector] ${ eventName }: ${ JSON . stringify ( finalAttributes ) }` ; } ) ; this . metricsLogEvent ( eventName , finalAttributes ) ; } /**
     * WEBRTC METRICS COLLECTION.
     */ start ( signalingClient , videoStreamIndex , clientMetricReport ? ) { if ( this . intervalScheduler ) { return false ; } this . logger . info ( 'Starting DefaultStatsCollector' ) ; this . signalingClient = signalingClient ; this . videoStreamIndex = videoStreamIndex ; if ( clientMetricReport ) { this . clientMetricReport = clientMetricReport ; } else { this . clientMetricReport = new DefaultClientMetricReport ( this . logger , this . videoStreamIndex , this . audioVideoController . configuration . credentials . attendeeId ) ; } this . intervalScheduler = new IntervalScheduler ( this . interval ) ; this . intervalScheduler . start ( ( ) => { this . getStatsWrapper ( ) ; } ) ; return true ; } stop ( ) { this . logger . info ( 'Stopping DefaultStatsCollector' ) ; if ( this . intervalScheduler ) { this . intervalScheduler . stop ( ) ; } this . intervalScheduler = null ; } /**
     * Convert raw metrics to client metric report.
     */ private updateMetricValues ( rawMetricReport , isStream ) { const metricReport = isStream ? this . clientMetricReport . streamMetricReports [ Number ( rawMetricReport . ssrc ) ] : this . clientMetricReport . globalMetricReport ; let metricMap ; if ( isStream ) { metricMap = this . clientMetricReport . getMetricMap ( ( metricReport as StreamMetricReport ) . mediaType , ( metricReport as StreamMetricReport ) . direction ) ; } else { metricMap = this . clientMetricReport . getMetricMap ( ) ; } for ( const rawMetric in rawMetricReport ) { if ( rawMetric in metricMap ) { metricReport . previousMetrics [ rawMetric ] = metricReport . currentMetrics [ rawMetric ] ; metricReport . currentMetrics [ rawMetric ] = rawMetricReport [ rawMetric ] ; } } } private processRawMetricReports ( rawMetricReports ) { this . clientMetricReport . currentSsrcs = { } ; const timeStamp = Date . now ( ) ; for ( const rawMetricReport of rawMetricReports ) { const isStream = this . isStreamRawMetricReport ( rawMetricReport . type ) ; if ( isStream ) { const existingStreamMetricReport = this . clientMetricReport . streamMetricReports [ Number ( rawMetricReport . ssrc ) ] ; if ( ! existingStreamMetricReport ) { const streamMetricReport = new StreamMetricReport ( ) ; streamMetricReport . mediaType = this . getMediaType ( rawMetricReport ) ; streamMetricReport . direction = this . getDirectionType ( rawMetricReport ) ; if ( ! this . videoStreamIndex . allStreams ( ) . empty ( ) ) { streamMetricReport . streamId = this . videoStreamIndex . streamIdForSSRC ( Number ( rawMetricReport . ssrc ) ) ; } this . clientMetricReport . streamMetricReports [ Number ( rawMetricReport . ssrc ) ] = streamMetricReport ; } else { existingStreamMetricReport . streamId = this . videoStreamIndex . streamIdForSSRC ( Number ( rawMetricReport . ssrc ) ) ; } this . clientMetricReport . currentSsrcs [ Number ( rawMetricReport . ssrc ) ] = 1 ; } this . updateMetricValues ( rawMetricReport , isStream ) ; } this . clientMetricReport . removeDestroyedSsrcs ( ) ; this . clientMetricReport . previousTimestampMs = this . clientMetricReport . currentTimestampMs ; this . clientMetricReport . currentTimestampMs = timeStamp ; this . clientMetricReport . print ( ) ; } /**
     * Protobuf packaging.
     */ private addMetricFrame ( metricName , clientMetricFrame , metricSpec , ssrc ? ) { const type = metricSpec . type ; const transform = metricSpec . transform ; const sourceMetric = metricSpec . source ; const streamMetricFramesLength = clientMetricFrame . streamMetricFrames . length ; const latestStreamMetricFrame = clientMetricFrame . streamMetricFrames [ streamMetricFramesLength - 1 ] ; if ( type ) { const metricFrame = SdkMetric . create ( ) ; metricFrame . type = type ; metricFrame . value = sourceMetric ? transform ( sourceMetric , ssrc ) : transform ( metricName , ssrc ) ; ssrc ? latestStreamMetricFrame . metrics . push ( metricFrame ) : clientMetricFrame . globalMetrics . push ( metricFrame ) ; } } private addGlobalMetricsToProtobuf ( clientMetricFrame ) { const metricMap = this . clientMetricReport . getMetricMap ( ) ; for ( const metricName in this . clientMetricReport . globalMetricReport . currentMetrics ) { this . addMetricFrame ( metricName , clientMetricFrame , metricMap [ metricName ] ) ; } } private addStreamMetricsToProtobuf ( clientMetricFrame ) { for ( const ssrc in this . clientMetricReport . streamMetricReports ) { const streamMetricReport = this . clientMetricReport . streamMetricReports [ ssrc ] ; const streamMetricFrame = SdkStreamMetricFrame . create ( ) ; streamMetricFrame . streamId = streamMetricReport . streamId ; streamMetricFrame . metrics = [ ] ; clientMetricFrame . streamMetricFrames . push ( streamMetricFrame ) ; const metricMap = this . clientMetricReport . getMetricMap ( streamMetricReport . mediaType , streamMetricReport . direction ) ; for ( const metricName in streamMetricReport . currentMetrics ) { this . addMetricFrame ( metricName , clientMetricFrame , metricMap [ metricName ] , Number ( ssrc ) ) ; } } } private makeClientMetricProtobuf ( ) { const clientMetricFrame = SdkClientMetricFrame . create ( ) ; clientMetricFrame . globalMetrics = [ ] ; clientMetricFrame . streamMetricFrames = [ ] ; this . addGlobalMetricsToProtobuf ( clientMetricFrame ) ; this . addStreamMetricsToProtobuf ( clientMetricFrame ) ; return clientMetricFrame ; } private sendClientMetricProtobuf ( clientMetricFrame ) { this . signalingClient . sendClientMetrics ( clientMetricFrame ) ; } /**
     * Helper functions.
     */ private isStreamRawMetricReport ( type ) { return type === 'ssrc' || type === 'inbound-rtp' || type === 'outbound-rtp' ; } private getMediaType ( rawMetricReport ) { return rawMetricReport . mediaType === 'audio' ? MediaType . AUDIO : MediaType . VIDEO ; } private getDirectionType ( rawMetricReport ) { return rawMetricReport . id . toLowerCase ( ) . indexOf ( 'send' ) !== - 1 || rawMetricReport . id . toLowerCase ( ) . indexOf ( 'outbound' ) !== - 1 || rawMetricReport . type === 'outbound-rtp' ? Direction . UPSTREAM : Direction . DOWNSTREAM ; } /**
     * Metric report filter.
     */ isValidChromeRawMetric ( rawMetricReport ) { return ( this . browserBehavior . hasChromiumWebRTC ( ) && ( rawMetricReport . type === 'ssrc' || rawMetricReport . type === 'VideoBwe' || ( rawMetricReport . type === 'googCandidatePair' && rawMetricReport . googWritable === 'true' && rawMetricReport . googReadable === 'true' ) ) ) ; } isValidStandardRawMetric ( rawMetricReport ) { const valid = rawMetricReport . type === 'inbound-rtp' || rawMetricReport . type === 'outbound-rtp' || ( rawMetricReport . type === 'candidate-pair' && rawMetricReport . state === 'succeeded' ) ; if ( this . browserBehavior . hasFirefoxWebRTC ( ) ) { if ( this . compareMajorVersion ( DefaultStatsCollector . FIREFOX_UPDATED_GET_STATS_VERSION ) === - 1 ) { return valid ; } else { return valid && rawMetricReport . isRemote === false ; } } return valid ; } isValidSsrc ( rawMetricReport ) { let validSsrc = true ; if ( this . isStreamRawMetricReport ( rawMetricReport . type ) && this . getDirectionType ( rawMetricReport ) === Direction . DOWNSTREAM && this . getMediaType ( rawMetricReport ) === MediaType . VIDEO ) { validSsrc = this . videoStreamIndex . streamIdForSSRC ( Number ( rawMetricReport . ssrc ) ) > 0 ; } return validSsrc ; } isValidRawMetricReport ( rawMetricReport ) { return ( ( this . isValidChromeRawMetric ( rawMetricReport ) || this . isValidStandardRawMetric ( rawMetricReport ) ) && this . isValidSsrc ( rawMetricReport ) ) ; } filterRawMetricReports ( rawMetricReports ) { const filteredRawMetricReports = [ ] ; for ( const rawMetricReport of rawMetricReports ) { if ( this . isValidRawMetricReport ( rawMetricReport ) ) { filteredRawMetricReports . push ( rawMetricReport ) ; } } return filteredRawMetricReports ; } private handleRawMetricReports ( rawMetricReports ) { const filteredRawMetricReports = this . filterRawMetricReports ( rawMetricReports ) ; this . logger . debug ( ( ) => { return `Filtered raw metrics : ${ JSON . stringify ( filteredRawMetricReports ) }` ; } ) ; this . processRawMetricReports ( filteredRawMetricReports ) ; const clientMetricFrame = this . makeClientMetricProtobuf ( ) ; this . sendClientMetricProtobuf ( clientMetricFrame ) ; this . audioVideoController . forEachObserver ( observer => { Maybe . of ( observer . metricsDidReceive ) . map ( f => f . bind ( observer ) ( this . clientMetricReport . clone ( ) ) ) ; } ) ; } /**
     * Get raw webrtc metrics.
     */ private getStatsWrapper ( ) { if ( ! this . audioVideoController . rtcPeerConnection ) { return ; } const rawMetricReports = [ ] ; if ( ! this . browserBehavior . requiresPromiseBasedWebRTCGetStats ( ) ) { this . audioVideoController . rtcPeerConnection . getStats ( ( res ) => { res . result ( ) . forEach ( ( report ) => { const item = { } ; report . names ( ) . forEach ( ( name ) => { item [ name ] = report . stat ( name ) ; } ) ; item . id = report . id ; item . type = report . type ; item . timestamp = report . timestamp ; rawMetricReports . push ( item ) ; } ) ; this . handleRawMetricReports ( rawMetricReports ) ; } , ( error ) => { this . logger . error ( error . message ) ; } ) ; } else { this . audioVideoController . rtcPeerConnection . getStats ( ) . then ( ( report ) => { report . forEach ( ( item ) => { rawMetricReports . push ( item ) ; } ) ; this . handleRawMetricReports ( rawMetricReports ) ; } ) . catch ( ( error ) => { this . logger . error ( error . message ) ; } ) ; } } private compareMajorVersion ( version ) { const currentMajorVersion = parseInt ( this . browserBehavior . version ( ) . split ( '.' ) [ 0 ] ) ; const expectedMajorVersion = parseInt ( version . split ( '.' ) [ 0 ] ) ; if ( expectedMajorVersion === currentMajorVersion ) { return 0 ; } if ( expectedMajorVersion > currentMajorVersion ) { return 1 ; } return - 1 ; } }