From 3b996a8baca0d7dc7ec888e1c5a91ab27abcb65e Mon Sep 17 00:00:00 2001 From: balazskreith Date: Fri, 14 Feb 2025 17:08:54 +0200 Subject: [PATCH] save --- docs/main.md | 212 ++++++++++++++++++-- src/ClientMonitor.ts | 24 ++- src/ClientMonitorEvents.ts | 14 +- src/detectors/SynthesizedSamplesDetector.ts | 2 +- src/index.ts | 22 +- src/scores/DefaultScoreCalculator.ts | 173 +++++++++------- 6 files changed, 349 insertions(+), 98 deletions(-) diff --git a/docs/main.md b/docs/main.md index fb98601..d306de0 100644 --- a/docs/main.md +++ b/docs/main.md @@ -14,17 +14,16 @@ client-monitor / [Monitors](./monitors.md) / [Detectors](./detectors.md) / [Samp 3. [Configuration](#configuration) 4. [ClientMonitor](#client-monitor) 5. [Detectors](#detectors) -5. [Sources](#sources) 5. [Collecting and Adapting Stats](#collecting-and-adapting-stats) 6. [Sampling](#sampling) 6. [Scores](#scores) 8. [Events and Issues](#events-and-issues) -6. [WebRTC Monitors](#stats-monitors) -6. [Examples](#examples) -7. [Best Practices](#best-practices) -8. [Troubleshooting](#troubleshooting) -9. [Schema compatibility Table](#hamokmessage-compatibility-table) -10. [FAQ](#faq) +6. [WebRTC Monitors](#webrtc-monitors) +7. [Examples](#examples) +8. [Best Practices](#best-practices) +9. [Troubleshooting](#troubleshooting) +10. [Schema compatibility Table](#hamokmessage-compatibility-table) +11. [FAQ](#faq) ## Installation @@ -502,7 +501,7 @@ The configuration of built-in detectors can be adjusted via the `ClientMonitor` * `LongPcConnectionEstablishmentDetector` is a built-in detector that identifies when a peer connection takes too long to establish. It helps detect connection issues that result in delays in establishing a connection. * `DryInboundTrackDetector` is a built-in detector that identifies when an inbound track (audio or video) does not flow any data from the moment it's activated. It is attached to an `InboundTrackMonitor` and helps detect issues with track transmission. * `DryOutboundTrackDetector` is a built-in detector that identifies when an outbound track (audio or video) does not flow any data from the moment it's activated. It is attached to an `OutboundTrackMonitor` and helps detect issues with track transmission. - * `PlayoutDiscrepancyDetector` is a built-in detector that identifies when there is a discrepancy between the received and rendered media playouts. It is attached to a `MediaPlayoutMonitor` and helps detect issues with media playout synchronization. + * `PlayoutDiscrepancyDetector` is a built-in detector that identifies when there is a discrepancy between the received and rendered media playouts. It is attached to a `MediaPlayoutMonitor` and helps detect issues with media playout synchronization. ### Adding / Removing Custom Detectors @@ -731,6 +730,12 @@ Each WebRTC stats-based monitor contains all fields from the corresponding stats In addition to `PeerConnectionMonitor` and related WebRTC Stats Monitors, `TrackMonitor` instances are also created and tracked. +### `appData` and `attachments` + +Every Monitor have an `appData` property that can be used to store custom metadata. This property can be used to store additional information about the monitor, such as custom fields, flags, or other data important +**for the application** uses the monitor. The `attachments` property is used to store additional data that should be shipped **with the sample**. This data is not used by the monitor itself, but it is included in the sample +when the sample is created. The `appData` won't be included in the sample, `attachments` will be included. + ### `CertificateMonitor` The `CertificateMonitor` is an extension of the `certificate` [stats](https://www.w3.org/TR/webrtc-stats/#certificatestats-dict*) collected from the `PeerConnection`. In addition to the fields contained within the stats, it includes calculated properties and navigational methods. @@ -740,6 +745,7 @@ The `CertificateMonitor` is an extension of the `certificate` [stats](https://ww Contains all the fields of the [certificate](https://www.w3.org/TR/webrtc-stats/#certificatestats-dict*) stats, plus the following: * **`appData?`** (`Record` or `undefined`): Optional application-specific metadata associated with the certificate. This data is included in the sample. +* **`attachments?`** (`Record` or `undefined`): Optional additional data to be included in the sample. This data is not used by the monitor but is included in the sample. #### **Methods:** @@ -764,6 +770,7 @@ The `CodecMonitor` is an extension of the `CodecStats` collected from the `PeerC Contains all the fields of the [CodecStats](https://www.w3.org/TR/webrtc-stats/#codecstats-dict*) stats, plus the following: * **`appData?`** (`Record` or `undefined`): Optional application-specific metadata associated with the codec. This data is included in the sample. +* **`attachments?`** (`Record` or `undefined`): Optional additional data to be included in the sample. This data is not used by the monitor but is included in the sample. #### **Methods:** @@ -786,6 +793,7 @@ The `IceCandidateMonitor` is an extension of the `IceCandidateStats` collected f Contains all the fields of the [IceCandidateStats](https://www.w3.org/TR/webrtc-stats/#icecandidatestats-dict*) stats, plus the following: * **`appData?`** (`Record` or `undefined`): Optional application-specific metadata associated with the ICE candidate. This data is included in the sample. +* **`attachments?`** (`Record` or `undefined`): Optional additional data to be included in the sample. This data is not used by the monitor but is included in the sample. #### **Methods:** @@ -807,6 +815,7 @@ The `IceCandidatePairMonitor` tracks information related to pairs of ICE candida Contains all the fields of the [IceCandidatePairStats](https://www.w3.org/TR/webrtc-stats/#icecandidatepairstats-dict*) stats, plus the following: * **`appData?`** (`Record` or `undefined`): Optional application-specific metadata associated with the ICE candidate pair. This data is included in the sample. +* **`attachments?`** (`Record` or `undefined`): Optional additional data to be included in the sample. This data is not used by the monitor but is included in the sample. #### **Methods:** @@ -831,6 +840,7 @@ The `IceTransportMonitor` tracks information related to the ICE transport layer Contains all the fields of the [IceTransportStats](https://www.w3.org/TR/webrtc-stats/#icetransportstats-dict*) stats, plus the following: * **`appData?`** (`Record` or `undefined`): Optional application-specific metadata associated with the ICE transport. This data is included in the sample. +* **`attachments?`** (`Record` or `undefined`): Optional additional data to be included in the sample. This data is not used by the monitor but is included in the sample. #### **Methods:** @@ -852,6 +862,7 @@ The `InboundRtpMonitor` tracks information related to inbound RTP streams in a W Contains all the fields of the [InboundRtpStats](https://www.w3.org/TR/webrtc-stats/#inboundrtpstats-dict*) stats, plus the following: * **`appData?`** (`Record` or `undefined`): Optional application-specific metadata associated with the inbound RTP. This data is included in the sample. +* **`attachments?`** (`Record` or `undefined`): Optional additional data to be included in the sample. This data is not used by the monitor but is included in the sample. * **`bitrate?`** (`number` or `undefined`): The calculated bitrate for the inbound RTP stream, in bits per second. * **`isFreezed?`** (`boolean` or `undefined`): Indicates whether the video track is frozen. * **`desync?`** (`boolean` or `undefined`): Indicates whether there is audio desynchronization. @@ -863,7 +874,6 @@ Contains all the fields of the [InboundRtpStats](https://www.w3.org/TR/webrtc-st #### **Methods:** -* **`visited`** (`boolean`): A getter that returns whether the `InboundRtpMonitor` has been visited. It is used to manage the lifecycle of stats and is reset after each access. * **`createSample(): InboundRtpStats`**: Used by the `ClientMonitor` to create a complete `ClientSample`. More details on sampling can be found in the [sampling](#sampling) section. * **`accept(stats: Omit): void`**: Method called by the `PeerConnectionMonitor` to accept new inbound RTP stats and update the internal fields of the `InboundRtpMonitor` instance. The method ensures that only valid stats with a positive time difference are applied. @@ -883,6 +893,7 @@ The `InboundTrackMonitor` tracks and monitors the inbound media track in a WebRT #### **Properties:** +* **`attachments?`** (`Record` or `undefined`): Optional additional data to be included in the sample. This data is not used by the monitor but is included in the sample. * **`direction`** (`'inbound'`): Indicates the direction of the media track. For this class, the direction is always inbound. * **`detectors`** (`Detectors`): An instance of the `Detectors` class, which manages different detection mechanisms for the track (e.g., stuck tracks, audio desynchronization, and frozen video). * **`contentType`** (`'lowmotion' | 'highmotion' | 'standard'`): Defines the type of content in the track based on motion characteristics. Defaults to `'standard'`. @@ -913,6 +924,7 @@ The `MediaPlayoutMonitor` tracks information related to media playout in a WebRT Contains all the fields of the [MediaPlayout](https://www.w3.org/TR/webrtc-stats/#dom-rtcstatstype-media-playout) stats, plus the following: * **`appData`** (`Record | undefined`): Optional application-specific metadata associated with the media playout. +* **`attachments?`** (`Record` or `undefined`): Optional additional data to be included in the sample. This data is not used by the monitor but is included in the sample. #### **Methods:** @@ -933,6 +945,7 @@ The `MediaSourceMonitor` extends the `MediaSourceStats` collected from the `Peer Contains all the fields of the [MediaSourceStats](https://www.w3.org/TR/webrtc-stats/#mediasourcestats-dict*) stats, plus the following: * **`appData?`** (`Record` or `undefined`): Optional application-specific metadata associated with the media source. This data is included in the sample. +* **`attachments?`** (`Record` or `undefined`): Optional additional data to be included in the sample. This data is not used by the monitor but is included in the sample. #### **Methods:** @@ -955,6 +968,7 @@ The `OutboundRtpMonitor` extends the `OutboundRtpStats` collected from the `Peer Contains all the fields of the [OutboundRtpStats](https://www.w3.org/TR/webrtc-stats/#outboundrtpstats-dict*) stats, plus the following: * **`appData?`** (`Record` or `undefined`): Optional application-specific metadata associated with the outbound RTP stats. This data is included in the sample. +* **`attachments?`** (`Record` or `undefined`): Optional additional data to be included in the sample. This data is not used by the monitor but is included in the sample. * **`bitrate?`** (`number` or `undefined`): Derived field representing the bitrate of the outbound RTP stream. * **`packetRate?`** (`number` or `undefined`): Derived field representing the packet rate (packets per second) of the outbound RTP stream. @@ -980,6 +994,7 @@ The `OutboundTrackMonitor` tracks and manages statistics related to an outbound #### **Properties:** * **`direction`** (`'outbound'`): The direction of the track, indicating that this is an outbound monitor. +* **`attachments?`** (`Record` or `undefined`): Optional additional data to be included in the sample. This data is not used by the monitor but is included in the sample. * **`detectors`** (`Detectors`): Instance of the `Detectors` class used for performance monitoring and detecting issues on the track. * **`mappedOutboundRtp`** (`Map`): A map of `OutboundRtpMonitor` instances, keyed by the SSRC, to monitor the RTP stats associated with the track. * **`contentType`** (`'lowmotion' | 'highmotion' | 'standard'`): The type of content being transmitted on the track, which could impact performance. @@ -998,14 +1013,18 @@ The `OutboundTrackMonitor` tracks and manages statistics related to an outbound * **`getMediaSource()`**: Retrieves the `MediaSourceMonitor` associated with the track identifier. -## `PeerConnectionMonitor` +### `PeerConnectionMonitor` The `PeerConnectionMonitor` class manages and tracks WebRTC peer connection statistics and metrics. It integrates with various other monitors, such as `OutboundRtpMonitor`, `InboundRtpMonitor`, and `DataChannelMonitor`, to collect and analyze connection data. It is responsible for detecting performance issues, calculating stability scores, and providing a comprehensive view of the peer connection's health. -### Properties: +#### Properties: + +- **`appData`** (`Record | undefined`): Custom application data associated with the peer connection. +- **`attachments?`** (`Record` or `undefined`): Optional additional data to be included in the sample. This data is not used by the monitor but is included in the sample. - **`peerConnectionId`** (`string`): The unique identifier for the peer connection being monitored. - **`detectors`** (`Detectors`): An instance of the `Detectors` class used for tracking performance trends, such as connection establishment time and congestion. +* **`attachments?`** (`Record` or `undefined`): Optional additional data to be included in the sample. This data is not used by the monitor but is included in the sample. - **`mappedCodecMonitors`** (`Map`): A map of `CodecMonitor` instances, keyed by codec name, to monitor the RTP codec statistics for the connection. - **`mappedInboundRtpMonitors`** (`Map`): A map of `InboundRtpMonitor` instances, keyed by SSRC, to monitor incoming RTP streams for the connection. - **`mappedRemoteOutboundRtpMonitors`** (`Map`): A map of `RemoteOutboundRtpMonitor` instances to track remote outbound RTP statistics. @@ -1072,9 +1091,7 @@ The `PeerConnectionMonitor` class manages and tracks WebRTC peer connection stat - **`mediaSources`** (`MediaSourceMonitor[]`): Returns an array of all `MediaSourceMonitor` instances -- **`appData`** (`Record | undefined`): Custom application data associated with the peer connection. - -### Methods: +#### Methods: * **`getStats()`**: Retrieves and returns the statistics for the peer connection. It emits the `stats` event and updates internal performance metrics. * **`accept(stats: W3C.RtcStats[])`**: Accepts an array of `RtcStats` objects and updates the internal state based on the data. It recalculates various metrics like RTT, available bitrates, and packet loss. @@ -1090,6 +1107,8 @@ The `PeerConnectionTransportMonitor` class tracks transport layer statistics for Contains all the fields of the [PeerConnectionTransportStats](https://www.w3.org/TR/webrtc-stats/#peerconnectiontransportstats-dict*) stats, plus the following: - **`appData?`** (`Record` or `undefined`): Optional custom application data associated with the transport stats. +- **`attachments?`** (`Record` or `undefined`): Optional additional data to be included in the sample. This data is not used by the monitor but is included in the sample. + #### **Methods:** @@ -1111,6 +1130,8 @@ The `RemoteInboundRtpMonitor` class tracks the remote inbound RTP statistics for Contains all the fields of the [RemoteInboundRtpStats](https://www.w3.org/TR/webrtc-stats/#remoteinboundrtpstats-dict*) stats, plus the following: - **`appData?`** (`Record` or `undefined`): Optional custom application data associated with the remote inbound RTP stats. +- **`attachments?`** (`Record` or `undefined`): Optional additional data to be included in the sample. This data is not used by the monitor but is included in the sample. + - **`packetRate?`** (`number` or `undefined`): The packet rate (packets per second) for the remote inbound RTP stream. - **`deltaPacketsLost?`** (`number` or `undefined`): The change in the number of packets lost since the last update. - **`visited`** (`boolean`): A getter that returns whether the monitor has been visited. Initially `true`, it is reset to `false` after being accessed once. @@ -1135,6 +1156,8 @@ Contains all the fields of the [RemoteOutboundRtpStats](https://www.w3.org/TR/we #### **Properties:** - **`appData?`** (`Record` or `undefined`): Optional custom application data associated with the outbound RTP stats. +- **`attachments?`** (`Record` or `undefined`): Optional additional data to be included in the sample. This data is not used by the monitor but is included in the sample. + - **`bitrate?`** (`number` or `undefined`): Derived field representing the bitrate of the remote outbound RTP stream. - **`visited`** (`boolean`): A getter that returns whether the `RemoteOutboundRtpMonitor` has been visited. It is used to manage the lifecycle of stats and is reset after each access. @@ -1149,3 +1172,164 @@ Contains all the fields of the [RemoteOutboundRtpStats](https://www.w3.org/TR/we - **`getInboundRtp()`**: Retrieves the corresponding inbound RTP stats for the remote peer identified by the `ssrc`. - **`getCodec()`**: Retrieves the `CodecMonitor` associated with the current `codecId`. + +## Examples + +### Show key metrics on a call + +An instantiated client monitor can be used to access key metrics for a call, +such as audio and video bitrate, packet loss, and round-trip time (RTT). +The following example demonstrates how to access these metrics using the client monitor: + +```javascript +const monitor = new ClientMonitor(); + +monitor.on('stats-collected', (event) => { + const { clientMonitor } = event; + + const audioBitrate = clientMonitor.sendingAudioBitrate; + const videoBitrate = clientMonitor.sendingVideoBitrate; + const packetLoss = clientMonitor.sendingFractionLost; + const rtt = clientMonitor.avgRttInSec; + + console.log(`Audio Bitrate: ${audioBitrate} bps`); + console.log(`Video Bitrate: ${videoBitrate} bps`); + console.log(`Packet Loss: ${packetLoss}%`); + console.log(`Round-Trip Time: ${rtt} seconds`); +}) +``` + +Each monitor instance provides access to stats related to its specific area of monitoring. +For example `InboundRtpMonitor` provides access to inbound RTP stats, additionally there +are derivated metrics calculated each time new stats assigned to the monitor, +For example `bitrate` is calculated between two inbound rtp stats `bytesReceived` property. + +### Detect network issues + +ClientMonitor provides a set of detectors that can be used to detect network issues, +such as congestion. The following example demonstrates how to use the detectors to +detect network congestion: + +```javascript +const monitor = new ClientMonitor(); + +monitor.on('congestion', (event) => { + const { + peerConnectionMonitor, + availableOutgoingBitrate, + availableIncomingBitrate, + maxAvailableOutgoingBitrate, + maxAvailableIncomingBitrate, + maxSendingBitrate, + maxReceivingBitrate, + } = event; + + console.log( + ` + Network congestion detected on peer connection ${peerConnectionMonitor.peerConnectionId}. + The highest estimated available outgoing bitrate was ${maxAvailableOutgoingBitrate} bps. + The highest estimated available incoming bitrate was ${maxAvailableIncomingBitrate} bps. + The highest sending bitrate was ${maxSendingBitrate} bps caused congestion. + The highest receiving bitrate was ${maxReceivingBitrate} bps caused congestion. + The current available outgoing bitrate is ${availableOutgoingBitrate} bps. + The current available incoming bitrate is ${availableIncomingBitrate} bps. + ` + ); +}); +``` + +### LifeCycle of monitors + +The lifecycle of monitors is managed by the `ClientMonitor`, and `PeerConnectionMonitor` instances. +Each monitor instance is created when new stats are received, and the monitor is updated with the new stats. +The monitor is considered visited once it has been accessed, and the visited flag is reset after each access. +If `visited` is `false`, the underlying media entity is considered to be outdated. + +The `ClientMonitor` emits events when new monitor is added. + + + +```javascript +import { ClientMonitor } from 'webrtc-monitor'; + +const monitor = new ClientMonitor(); + +monitor.on('new-codec-monitor', (event) => { + const { codecMonitor } = event; + console.log(`New codec monitor added: ${codecMonitor.id}`); +}); + +monitor.on('new-inbound-rtp-monitor', (event) => { + const { inboundRtpMonitor } = event; + console.log(`New inbound RTP monitor added: ${inboundRtpMonitor.id}`); +}); + +monitor.on('new-inbound-track-monitor', (event) => { + const { inboundTrackMonitor } = event; + console.log(`New inbound Track monitor added. trackId: ${inboundTrackMonitor.track.id}`); +}); +``` + +### Add custom detector + +You can add custom detectors to the `Detectors` instance of the `ClientMonitor`, `PeerConnectionMonitor`, `InboundTrackMonitor`, or `OutboundTrackMonitor`. +The following example demonstrates how to add a custom detector to a track added to the `ClientMonitor`: + +```javascript +monitor.on('new-inbound-track-monitor', (event) => { + const { clientMonitor, inboundTrackMonitor } = event; + + inboundTrackMonitor.detectors.addDetector(new class implemets Detector { + constructor() { + this.name = 'custom-detector'; + } + + update() { + if (inboundTrackMonitor.bitrate < 1000) { + clientMonitor.addIssue({ + type: 'low-bitrate', + payload: { + trackId: inboundTrackMonitor.track.id, + bitrate: inboundTrackMonitor.bitrate, + } + }); + } + // Custom logic to detect issues + + } + }()); +}); +``` + +The above example demonstrates how to add a custom detector to an `InboundTrackMonitor` instance. +The detector checks if the bitrate of the track is below a certain threshold and adds an issue to the `ClientMonitor` if the condition is met. + + +### Show reasons for a low score + +### Add `appData` and `attachments` + +```javascript +monitor.on('new-inbound-track-monitor', (event) => { + const { clientMonitor, inboundTrackMonitor } = event; + + // Add custom data to the inbound track monitor used by the application + inboundTrackMonitor.appData = { + mediaElement: MediaElementRef + }; + + // Add attachments to the inbound track monitor will be attached to the sample + inboundTrackMonitor.attachments = { + customData: 'customValue' + }; +}); +``` + +## Best Practices + +## Troubleshooting + +## Schema compatibility Table + +## FAQ + diff --git a/src/ClientMonitor.ts b/src/ClientMonitor.ts index 3c9c04b..ef92bc7 100644 --- a/src/ClientMonitor.ts +++ b/src/ClientMonitor.ts @@ -320,11 +320,18 @@ export class ClientMonitor extends EventEmitter { if (this.closed) return; if (!this.bufferingSampleData) return; + const timestamp = event.timestamp ?? Date.now(); const payload = event.payload ? JSON.stringify(event.payload) : undefined; this._clientEvents.push({ ...event, payload, - timestamp: event.timestamp ?? Date.now(), + timestamp, + }); + + this.emit('client-event', { + ...event, + payload: event.payload, + timestamp, }); } @@ -351,11 +358,19 @@ export class ClientMonitor extends EventEmitter { if (this.closed) return; if (!this.bufferingSampleData) return; + const timestamp = metaData.timestamp ?? Date.now(); + this._clientMetaItems.push({ type: metaData.type, payload: metaData.payload ? JSON.stringify(metaData.payload) : undefined, - timestamp: metaData.timestamp ?? Date.now(), + timestamp, }); + + this.emit('meta', { + ...metaData, + payload: metaData.payload, + timestamp, + }) } public addExtensionStats(stats: { type: string, payload?: Record}): void { @@ -367,6 +382,11 @@ export class ClientMonitor extends EventEmitter { type: stats.type, payload, }); + + this.emit('extension-stats', { + ...stats, + payload: stats.payload, + }); } public addSource(source: unknown): void { diff --git a/src/ClientMonitorEvents.ts b/src/ClientMonitorEvents.ts index ba80666..200d9e2 100644 --- a/src/ClientMonitorEvents.ts +++ b/src/ClientMonitorEvents.ts @@ -36,6 +36,11 @@ export type ClientMetaData = { timestamp: number, } +export type ExtensionStat = { + type: string, + payload?: Record | boolean | string | number, +} + export type ClientMonitorBaseEvent = { clientMonitor: ClientMonitor, } @@ -64,7 +69,7 @@ export type AudioDesyncTrackEventPayload = ClientMonitorBaseEvent & { trackMonitor: InboundTrackMonitor, } -export type SynthesizedSamplesEventPayload = ClientMonitorBaseEvent & { +export type SynthesizedAudioEventPayload = ClientMonitorBaseEvent & { mediaPlayoutMonitor: MediaPlayoutMonitor, } @@ -168,12 +173,15 @@ export type ClientMonitorEvents = { "stats-collected": [StatsCollectedEventPayload], 'close': [], 'issue': [ClientIssue], - 'added-client-event': [ClientEvent], + 'client-event': [ClientEvent], + 'meta': [ClientMetaData], + 'extension-stats': [ExtensionStat], + // detector events 'congestion': [CongestionEventPayload], 'cpulimitation': [ClientMonitorBaseEvent], 'audio-desync-track': [AudioDesyncTrackEventPayload], - 'synthesized-samples': [SynthesizedSamplesEventPayload], + 'synthesized-audio': [SynthesizedAudioEventPayload], 'freezed-video-track': [FreezedVideoTrackEventPayload], 'dry-inbound-track': [DryInboundTrackEventPayload], 'dry-outbound-track': [DryOutboundTrackEventPayload], diff --git a/src/detectors/SynthesizedSamplesDetector.ts b/src/detectors/SynthesizedSamplesDetector.ts index 343d347..dc25de7 100644 --- a/src/detectors/SynthesizedSamplesDetector.ts +++ b/src/detectors/SynthesizedSamplesDetector.ts @@ -27,7 +27,7 @@ export class SynthesizedSamplesDetector implements Detector { const clientMonitor = this.peerConnection.parent; - clientMonitor.emit('synthesized-samples', { + clientMonitor.emit('synthesized-audio', { mediaPlayoutMonitor: this.mediaPlayout, clientMonitor: clientMonitor, }); diff --git a/src/index.ts b/src/index.ts index d5218ac..a5e2d25 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,6 @@ export { PeerConnectionTransportMonitor } from "./monitors/PeerConnectionTranspo export { RemoteInboundRtpMonitor } from "./monitors/RemoteInboundRtpMonitor"; export { RemoteOutboundRtpMonitor } from "./monitors/RemoteOutboundRtpMonitor"; export type { TrackMonitor } from "./monitors/TrackMonitor"; - export { ClientMonitor, } from "./ClientMonitor"; @@ -28,13 +27,34 @@ export type { ClientMetaData, SampleCreatedEventPayload, StatsCollectedEventPayload, + SynthesizedAudioEventPayload, + InboundVideoPlayoutDiscrepancyEventPayload, CongestionEventPayload, AudioDesyncTrackEventPayload, FreezedVideoTrackEventPayload, DryInboundTrackEventPayload, + DryOutboundTrackEventPayload, + IceTupleChangedEventPayload, TooLongPcConnectionEstablishmentEventPayload, ScoreEventPayload, ClientMonitorEvents, + + NewCertificateMonitorEventPayload, + NewCodecMonitorEventPayload, + NewDataChannelMonitorEventPayload, + NewIceCandidateMonitorPayload, + NewIceCandidatePairMonitorEventPayload, + NewIceTransportMonitorEventPayload, + NewInboundRtpMonitorEventPayload, + NewInboundTrackMonitorEventPayload, + NewMediaPlayoutMonitorEventPayload, + NewMediaSourceMonitorEventPayload, + NewOutboundRtpMonitorEventPayload, + NewOutboundTrackMonitorEventPayload, + NewPeerConnectionTransportMonitorEventPayload, + NewRemoteInboundRtpMonitorEventPayload, + NewRemoteOutboundRtpMonitorEventPayload, + NewPeerConnectionMonitorEventPayload, } from "./ClientMonitorEvents"; export type { Detector, diff --git a/src/scores/DefaultScoreCalculator.ts b/src/scores/DefaultScoreCalculator.ts index 27624e1..57ee38f 100644 --- a/src/scores/DefaultScoreCalculator.ts +++ b/src/scores/DefaultScoreCalculator.ts @@ -10,38 +10,54 @@ export type DefaultScoreCalculatorOutboundVideoTrackScoreAppData = { diffBitrateSquares: number[]; lastBitrate?: number; ewmaBitrate?: number; - lastScoreDetails: { - targetDeviatioPenalty: number, - cpuLimitationPenalty: number, - bitrateVolatilityPenalty: number, - } + subtractions: DefaultScoreCalculatorSubtractions; +} + +export type DefaultScoreCalculatorSubtractionReason = + 'high-rtt' | + 'high-packetloss' | + 'low-fps' | + 'volatile-fps' | + 'dropped-frames' | + 'frame-corruptions' | + 'low-bitrate' | + 'cpu-limitation' | + 'high-volatile-bitrate'; + +export type DefaultScoreCalculatorSubtractions = { + [x in DefaultScoreCalculatorSubtractionReason]?: number; } export type DefaultScoreCalculatorOutboundAudioTrackScoreAppData = { lastNScores: number[]; - lastScoreDetails: { - targetDeviatioPenalty: number, - cpuLimitationPenalty: number, - bitrateVolatilityPenalty: number, - } + subtractions: DefaultScoreCalculatorSubtractions; + // lastScoreDetails: { + // targetDeviatioPenalty: number, + // cpuLimitationPenalty: number, + // bitrateVolatilityPenalty: number, + // } } export type DefaultScoreCalculatorInboundVideoTrackScoreAppData = { lastNScores: number[]; ewmaFps?: number; - lastScoreDetails: { - fpsPenalty: number; - fractionOfDroppedFramesPenalty: number; - corruptionProbabilityPenalty: number; - } + subtractions: DefaultScoreCalculatorSubtractions; + + // lastScoreDetails: { + // fpsPenalty: number; + // fractionOfDroppedFramesPenalty: number; + // corruptionProbabilityPenalty: number; + // } } export type DefaultScoreCalculatorPeerConnectionScoreAppData = { lastNScores: number[]; - lastScoreDetails: { - rttPenalty: number; - fractionLostPenalty: number; - } + subtractions: DefaultScoreCalculatorSubtractions; + + // lastScoreDetails: { + // rttPenalty: number; + // fractionLostPenalty: number; + // } } @@ -65,6 +81,8 @@ export class DefaultScoreCalculator { public static readonly MIN_AUDIO_BITRATE = 6000; // 6 kbps is the lowest usable bitrate private static readonly NORMALIZATION_FACTOR = Math.log10(this.TARGET_AUDIO_BITRATE / this.MIN_AUDIO_BITRATE); + public subtractions: DefaultScoreCalculatorSubtractions = {}; + public constructor( private readonly clientMonitor: ClientMonitor, @@ -85,6 +103,7 @@ export class DefaultScoreCalculator { const clientMonitor: ClientMonitor = this.clientMonitor; let clientTotalScore = 0; let clientTotalWeight = 0; + this.subtractions = {}; for (const pcMonitor of clientMonitor.peerConnections) { if (pcMonitor.calculatedStabilityScore.value === undefined) continue; @@ -101,6 +120,8 @@ export class DefaultScoreCalculator { trackTotalScore += trackScore.value * trackScore.weight; trackTotalWeight += trackScore.weight; noTrack = false; + + this._accumulateSubtraction(trackScore.appData?.subtractions ?? {}); } @@ -115,13 +136,14 @@ export class DefaultScoreCalculator { clientTotalScore += totalPcScore * pcMonitor.calculatedStabilityScore.weight; clientTotalWeight += pcMonitor.calculatedStabilityScore.weight; + + this._accumulateSubtraction(pcMonitor.calculatedStabilityScore?.appData?.subtractions ?? {}); } const clientScore = clientTotalScore / Math.max(clientTotalWeight, 1); clientMonitor.setScore(clientScore); } - private _calculatePeerConnectionStabilityScore(pcMonitor: PeerConnectionMonitor) { // Packet Jitter measured in seconds // we use RTT and lost packets to calculate the base score for the connection @@ -133,42 +155,41 @@ export class DefaultScoreCalculator { let scoreValue = 5.0; let appData = score.appData as DefaultScoreCalculatorPeerConnectionScoreAppData | undefined; + const subtractions: DefaultScoreCalculatorSubtractions = {}; if (!appData) { appData = { lastNScores: [], - lastScoreDetails: { - rttPenalty: 0, - fractionLostPenalty: 0, - } + subtractions, + // lastScoreDetails: { + // rttPenalty: 0, + // fractionLostPenalty: 0, + // } } score.appData = appData; + } else { + appData.subtractions = subtractions; } - const lastScoreDetails = appData.lastScoreDetails; - if (300 < rttInMs) { - lastScoreDetails.rttPenalty = 2.0; + subtractions["high-rtt"] = 2.0; } else if (150 < rttInMs) { - lastScoreDetails.rttPenalty = 1.0; + subtractions["high-rtt"] = 1.0; } if (0.01 < fractionLost) { if (fractionLost < 0.05) { - lastScoreDetails.fractionLostPenalty = 1.0; + subtractions["high-packetloss"] = 1.0; } else if (fractionLost < 0.2) { - lastScoreDetails.fractionLostPenalty = 2.0; + subtractions["high-packetloss"] = 2.0; } else { - lastScoreDetails.fractionLostPenalty = 5.0; + subtractions["high-packetloss"] = 5.0; } } scoreValue = Math.max( DefaultScoreCalculator.MIN_SCORE, - DefaultScoreCalculator.MAX_SCORE - ( - lastScoreDetails.rttPenalty + - lastScoreDetails.fractionLostPenalty - ) + DefaultScoreCalculator.MAX_SCORE - this._getTotalSubtraction(subtractions) ); appData.lastNScores.push(scoreValue); @@ -224,25 +245,18 @@ export class DefaultScoreCalculator { return; } let appData = trackMonitor.calculatedScore.appData as DefaultScoreCalculatorInboundVideoTrackScoreAppData | undefined; + const subtractions: DefaultScoreCalculatorSubtractions = {}; if (!appData) { appData = { lastNScores: [], - lastScoreDetails: { - fpsPenalty: 0, - fractionOfDroppedFramesPenalty: 0, - corruptionProbabilityPenalty: 0, - } + subtractions, } trackMonitor.calculatedScore.appData = appData; + } else { + appData.subtractions = subtractions; } - const lastScoreDetails = appData.lastScoreDetails; - - lastScoreDetails.fpsPenalty = 0; - lastScoreDetails.fractionOfDroppedFramesPenalty = 0; - lastScoreDetails.corruptionProbabilityPenalty = 0; - if (inboundRtp.framesPerSecond) { inboundRtp.lastNFramesPerSec.push(inboundRtp.framesPerSecond); @@ -259,9 +273,9 @@ export class DefaultScoreCalculator { // console.warn('volatility', volatility, 'stdDev', stdDev, 'avgFpsSqueres', avgFpsSqueres); if (1.1 < volatility && volatility < 1.2) { - lastScoreDetails.fpsPenalty = 1.0; + subtractions["volatile-fps"] = 1.0; } else if (1.2 < volatility) { - lastScoreDetails.fpsPenalty = 2.0; + subtractions["volatile-fps"] = 2.0; } } @@ -269,23 +283,19 @@ export class DefaultScoreCalculator { const fractionOfDroppedFrames = inboundRtp.framesDropped / (inboundRtp.framesDropped + inboundRtp.framesRendered); if (0.1 < fractionOfDroppedFrames && fractionOfDroppedFrames < 0.2) { - lastScoreDetails.fractionOfDroppedFramesPenalty = 1.0; + subtractions['dropped-frames'] = 1.0; } else if (0.2 < fractionOfDroppedFrames) { - lastScoreDetails.fractionOfDroppedFramesPenalty = 2.0; + subtractions['dropped-frames'] = 2.0; } } if (inboundRtp.deltaCorruptionProbability) { - appData.lastScoreDetails.corruptionProbabilityPenalty = 2.0 * inboundRtp.deltaCorruptionProbability; + subtractions['frame-corruptions'] = 2.0 * inboundRtp.deltaCorruptionProbability; } const scoreValue = Math.max( DefaultScoreCalculator.MIN_SCORE, - DefaultScoreCalculator.MAX_SCORE - ( - lastScoreDetails.fpsPenalty + - lastScoreDetails.fractionOfDroppedFramesPenalty + - lastScoreDetails.corruptionProbabilityPenalty - ) + DefaultScoreCalculator.MAX_SCORE - this._getTotalSubtraction(subtractions) ); appData.lastNScores.push(scoreValue); @@ -348,26 +358,19 @@ export class DefaultScoreCalculator { } const score = trackMonitor.calculatedScore; let appData = score.appData as DefaultScoreCalculatorOutboundVideoTrackScoreAppData | undefined; + const subtractions: DefaultScoreCalculatorSubtractions = {}; if (!appData) { appData = { lastNScores: [], diffBitrateSquares: [], - lastScoreDetails: { - targetDeviatioPenalty: 0, - cpuLimitationPenalty: 0, - bitrateVolatilityPenalty: 0, - } + subtractions, } score.appData = appData; + } else { + appData.subtractions = subtractions; } - const lastScoreDetails = appData.lastScoreDetails; - - lastScoreDetails.targetDeviatioPenalty = 0; - lastScoreDetails.cpuLimitationPenalty = 0; - lastScoreDetails.bitrateVolatilityPenalty = 0; - // max score: 5 // target deviation penalty: 0-2 // cpu limitation penalty: 0-1 @@ -389,16 +392,16 @@ export class DefaultScoreCalculator { if (0 < deviation && lowThreshold < deviation) { if (0.05 <= percentage && percentage < 0.15) { - lastScoreDetails.targetDeviatioPenalty = 1.0; + subtractions['low-bitrate'] = 1.0; } else if (0.15 <= percentage) { - lastScoreDetails.targetDeviatioPenalty = 2.0; + subtractions['low-bitrate'] = 2.0; } } } } if (outboundRtp.qualityLimitationReason === 'cpu') { - lastScoreDetails.cpuLimitationPenalty = 1.0; + subtractions['cpu-limitation'] = 2.0; } if (outboundRtp.bitrate) { @@ -423,9 +426,9 @@ export class DefaultScoreCalculator { // console.warn('volatility', volatility, 'stdDev', stdDev, 'avgBitrateSquare', avgBitrateSquare); if (0.1 < volatility && volatility < 0.2) { - appData.lastScoreDetails.bitrateVolatilityPenalty = 1.0; + subtractions['high-volatile-bitrate'] = 1.0; } else if (0.2 < volatility) { - appData.lastScoreDetails.bitrateVolatilityPenalty = 2.0; + subtractions['high-volatile-bitrate'] = 2.0; } } appData.lastBitrate = outboundRtp.bitrate; @@ -433,11 +436,7 @@ export class DefaultScoreCalculator { const scoreValue = Math.max( DefaultScoreCalculator.MIN_SCORE, - DefaultScoreCalculator.MAX_SCORE - ( - lastScoreDetails.targetDeviatioPenalty + - lastScoreDetails.cpuLimitationPenalty + - lastScoreDetails.bitrateVolatilityPenalty - ) + DefaultScoreCalculator.MAX_SCORE - this._getTotalSubtraction(subtractions) ); appData.lastNScores.push(scoreValue); @@ -503,4 +502,24 @@ export class DefaultScoreCalculator { private _getRoundedScore(score: number) { return Math.round(score * 100) / 100; } + + private _getTotalSubtraction(subtractions: DefaultScoreCalculatorSubtractions) { + let result = 0; + for (const value in Object.values(subtractions)) { + if (typeof value !== 'number') continue; + + result += value; + } + + return result; + } + + private _accumulateSubtraction(subtractions: DefaultScoreCalculatorSubtractions) { + for (const [_key, value] of Object.entries(subtractions)) { + if (typeof value !== 'number') continue; + const key = _key as DefaultScoreCalculatorSubtractionReason; + + this.subtractions[key] = (this.subtractions[key] ?? 0) + value; + } + } } \ No newline at end of file