
Security News
npm Adopts OIDC for Trusted Publishing in CI/CD Workflows
npm now supports Trusted Publishing with OIDC, enabling secure package publishing directly from CI/CD workflows without relying on long-lived tokens.
@observertc/client-monitor-js
Advanced tools
JavaScript library to monitor WebRTC applications
@observertc/client-monitor-js is a client-side library to monitor WebRTCStats and integrate your app with ObserveRTC components.
npm install @observertc/client-monitor-js
or
yarn add @observertc/client-monitor-js
import { ClientMonitor } from "@observertc/client-monitor-js";
// Create a monitor with default configuration
const monitor = new ClientMonitor({
clientId: "my-client-id",
callId: "my-call-id",
collectingPeriodInMs: 2000,
samplingPeriodInMs: 4000,
});
// Add a peer connection to monitor
monitor.addSource(peerConnection);
// Listen for samples
monitor.on("sample-created", (sample) => {
console.log("Sample created:", sample);
// Send sample to your analytics backend
});
// Listen for issues
monitor.on("issue", (issue) => {
console.log("Issue detected:", issue);
});
// Close when done
monitor.close();
Direct integration with native WebRTC PeerConnections:
import { ClientMonitor } from "@observertc/client-monitor-js";
const peerConnection = new RTCPeerConnection();
const monitor = new ClientMonitor();
// Add the peer connection for monitoring
monitor.addSource(peerConnection);
import { ClientMonitor } from "@observertc/client-monitor-js";
import mediasoup from "mediasoup-client";
const device = new mediasoup.Device();
const monitor = new ClientMonitor();
// Monitor the mediasoup device
monitor.addSource(device);
// The monitor will automatically detect new transports created after adding the device
const transport = device.createSendTransport(/* ... */);
// For transports created before adding the device, add them manually:
monitor.addSource(transport);
Important: When adding a mediasoup device, the monitor automatically hooks into the newtransport
event to detect newly created transports. However, transports created before adding the device must be added manually.
Customize logging behavior by providing your own logger:
import { setLogger, Logger } from "@observertc/client-monitor-js";
const customLogger: Logger = {
trace: (...args) => console.trace(...args),
debug: (...args) => console.debug(...args),
info: (...args) => console.info(...args),
warn: (...args) => console.warn(...args),
error: (...args) => console.error(...args),
};
setLogger(customLogger);
The ClientMonitor
accepts a comprehensive configuration object. All configuration options are optional except when specifically noted:
import { ClientMonitor } from "@observertc/client-monitor-js";
const monitor = new ClientMonitor({
// Basic configuration (all optional)
clientId: "unique-client-id",
callId: "unique-call-id",
collectingPeriodInMs: 2000, // Default: 2000ms
samplingPeriodInMs: 4000, // Optional, no default
// Integration settings (optional with defaults)
integrateNavigatorMediaDevices: true, // Default: true
addClientJointEventOnCreated: true, // Default: true
addClientLeftEventOnClose: true, // Default: true
bufferingEventsForSamples: false, // Default: false
// Detector configurations (all optional with defaults)
audioDesyncDetector: {
disabled: false,
createIssue: true,
fractionalCorrectionAlertOnThreshold: 0.1,
fractionalCorrectionAlertOffThreshold: 0.05,
},
congestionDetector: {
disabled: false,
createIssue: true,
sensitivity: "medium", // 'low', 'medium', 'high'
},
cpuPerformanceDetector: {
disabled: false,
createIssue: true,
fpsVolatilityThresholds: {
lowWatermark: 0.1,
highWatermark: 0.3,
},
durationOfCollectingStatsThreshold: {
lowWatermark: 5000,
highWatermark: 10000,
},
},
dryInboundTrackDetector: {
disabled: false,
createIssue: true,
thresholdInMs: 5000,
},
dryOutboundTrackDetector: {
disabled: false,
createIssue: true,
thresholdInMs: 5000,
},
videoFreezesDetector: {
disabled: false,
createIssue: true,
},
playoutDiscrepancyDetector: {
disabled: false,
createIssue: true,
lowSkewThreshold: 2,
highSkewThreshold: 5,
},
syntheticSamplesDetector: {
disabled: false,
createIssue: true,
minSynthesizedSamplesDuration: 1000,
},
longPcConnectionEstablishmentDetector: {
disabled: false,
createIssue: true,
thresholdInMs: 5000,
},
// Application data (optional)
appData: {
userId: "user-123",
roomId: "room-456",
},
});
Important: You can create a monitor with minimal configuration or even no configuration at all:
// Minimal configuration
const monitor = new ClientMonitor({
clientId: "my-client",
collectingPeriodInMs: 1000,
});
// No configuration (uses all defaults)
const monitor = new ClientMonitor();
The ClientMonitor
is the main class that orchestrates WebRTC monitoring, statistics collection, and anomaly detection.
addSource(source: RTCPeerConnection | MediasoupDevice | MediasoupTransport)
: Adds a source for monitoringclose()
: Closes the monitor and stops all monitoring activitiescollect()
: Manually collects stats from all monitored sourcescreateSample()
: Creates a client sample with current statesetCollectingPeriod(periodInMs: number)
: Updates the stats collection intervalsetSamplingPeriod(periodInMs: number)
: Updates the sampling intervalsetScore(score: number, reasons?: Record<string, number>)
: Manually sets the client scoreaddEvent(event: ClientEvent)
: Adds a custom client eventaddIssue(issue: ClientIssue)
: Adds a custom client issueaddMetaData(metaData: ClientMetaData)
: Adds metadataaddExtensionStats(stats: ExtensionStat)
: Adds custom extension statsgetTrackMonitor(trackId: string)
: Retrieves a track monitor by IDwatchMediaDevices()
: Integrates with navigator.mediaDevicesfetchUserAgentData()
: Fetches browser user agent informationscore
: Current client performance score (0.0-5.0)scoreReasons
: Detailed score calculation reasonsclosed
: Whether the monitor is closedconfig
: Current configurationdetectors
: Detector management instancepeerConnections
: Array of monitored peer connectionstracks
: Array of monitored tracksDetectors are specialized components that monitor for specific anomalies and issues in WebRTC connections. Each detector focuses on a particular aspect of the connection quality.
Detects audio synchronization issues by monitoring sample corrections.
Triggers on:
Configuration:
audioDesyncDetector: {
disabled: false,
createIssue: true,
fractionalCorrectionAlertOnThreshold: 0.1, // 10% correction rate triggers alert
fractionalCorrectionAlertOffThreshold: 0.05, // 5% correction rate clears alert
}
Monitors network congestion by analyzing available bandwidth vs. usage.
Triggers on:
Configuration:
congestionDetector: {
disabled: false,
createIssue: true,
sensitivity: 'medium', // 'low', 'medium', 'high'
}
Detects CPU performance issues affecting media processing.
Triggers on:
Configuration:
cpuPerformanceDetector: {
disabled: false,
createIssue: true,
fpsVolatilityThresholds: {
lowWatermark: 0.1,
highWatermark: 0.3,
},
durationOfCollectingStatsThreshold: {
lowWatermark: 5000,
highWatermark: 10000,
},
}
Detects inbound tracks that stop receiving data.
Triggers on:
Configuration:
dryInboundTrackDetector: {
disabled: false,
createIssue: true,
thresholdInMs: 5000,
}
Detects outbound tracks that stop sending data.
Triggers on:
Configuration:
dryOutboundTrackDetector: {
disabled: false,
createIssue: true,
thresholdInMs: 5000,
}
Detects frozen video tracks.
Triggers on:
Configuration:
videoFreezesDetector: {
disabled: false,
createIssue: true,
}
Detects discrepancies between received and rendered frames.
Triggers on:
Configuration:
playoutDiscrepancyDetector: {
disabled: false,
createIssue: true,
lowSkewThreshold: 2,
highSkewThreshold: 5,
}
Detects when audio playout synthesizes samples due to missing data.
Triggers on:
Configuration:
syntheticSamplesDetector: {
disabled: false,
createIssue: true,
minSynthesizedSamplesDuration: 1000,
}
Detects slow peer connection establishment.
Triggers on:
Configuration:
longPcConnectionEstablishmentDetector: {
disabled: false,
createIssue: true,
thresholdInMs: 5000,
}
Create custom detectors by implementing the Detector
interface:
import { Detector } from "@observertc/client-monitor-js";
class CustomDetector implements Detector {
public readonly name = 'custom-detector';
constructor(private monitor: any) {}
public update() {
// Custom detection logic
if (this.detectCustomCondition()) {
this.monitor.parent.emit('custom-issue', {
type: 'custom-issue',
payload: { reason: 'Custom condition detected' }
});
}
}
private detectCustomCondition(): boolean {
// Your detection logic here
return false;
}
}
// Add to monitor
const detector = new CustomDetector(someMonitor);
monitor.detectors.add(detector);
// Remove detector
monitor.detectors.remove(detector);
The scoring system provides quantitative quality assessment ranging from 0.0 (worst) to 5.0 (best). The library includes a DefaultScoreCalculator
implementation and allows custom score calculators via the ScoreCalculator
interface.
interface ScoreCalculator {
update(): void;
encodeClientScoreReasons?<T extends Record<string, number>>(reasons?: T): string;
encodePeerConnectionScoreReasons?<T extends Record<string, number>>(reasons?: T): string;
encodeInboundAudioScoreReasons?<T extends Record<string, number>>(reasons?: T): string;
encodeInboundVideoScoreReasons?<T extends Record<string, number>>(reasons?: T): string;
encodeOutboundAudioScoreReasons?<T extends Record<string, number>>(reasons?: T): string;
encodeOutboundVideoScoreReasons?<T extends Record<string, number>>(reasons?: T): string;
}
The default implementation calculates scores using a hierarchical weighted average approach:
The client score is calculated as a weighted average of:
Client Score = Σ(PC_Score × PC_Weight) / Σ(PC_Weight)
Where PC_Score = Track_Score_Avg × PC_Stability_Score
Based on Round Trip Time (RTT) and packet loss:
RTT Penalties:
Packet Loss Penalties:
20% loss: -5.0 points
Inbound Audio Track Score:
normalizedBitrate = log10(max(bitrate, MIN_AUDIO_BITRATE) / MIN_AUDIO_BITRATE) / NORMALIZATION_FACTOR;
lossPenalty = exp(-packetLoss / 2);
score = min(MAX_SCORE, 5 * normalizedBitrate * lossPenalty);
Inbound Video Track Score:
Outbound Audio Track Score:
Outbound Video Track Score:
Each score calculation includes detailed reasons for penalties:
monitor.on("score", (event) => {
console.log("Client Score:", event.clientScore);
console.log("Score Reasons:", event.scoreReasons);
// Example reasons:
// {
// "high-rtt": 1.0,
// "high-packetloss": 2.0,
// "cpu-limitation": 2.0,
// "dropped-video-frames": 1.0
// }
});
Implement your own scoring logic by implementing the ScoreCalculator
interface:
import { ScoreCalculator } from "@observertc/client-monitor-js";
class CustomScoreCalculator {
constructor(clientMonitor) {
this.clientMonitor = clientMonitor;
}
update() {
// Calculate peer connection scores
for (const pcMonitor of this.clientMonitor.peerConnections) {
this.calculatePeerConnectionScore(pcMonitor);
}
// Calculate track scores
for (const track of this.clientMonitor.tracks) {
this.calculateTrackScore(track);
}
// Calculate final client score
this.calculateClientScore();
}
calculatePeerConnectionScore(pcMonitor) {
const rttMs = (pcMonitor.avgRttInSec ?? 0) * 1000;
const fractionLost = pcMonitor.inboundRtps.reduce((acc, rtp) => acc + (rtp.fractionLost ?? 0), 0);
let score = 5.0;
const reasons = {};
// Custom RTT penalties
if (rttMs > 200) {
score -= 1.5;
reasons["custom-high-rtt"] = 1.5;
}
// Custom packet loss penalties
if (fractionLost > 0.02) {
score -= 2.0;
reasons["custom-packet-loss"] = 2.0;
}
pcMonitor.calculatedStabilityScore.value = Math.max(0, score);
pcMonitor.calculatedStabilityScore.reasons = reasons;
}
calculateTrackScore(trackMonitor) {
let score = 5.0;
const reasons = {};
if (trackMonitor.direction === "inbound" && trackMonitor.kind === "video") {
// Custom video quality scoring
const fps = trackMonitor.ewmaFps ?? 0;
if (fps < 15) {
score -= 2.0;
reasons["low-fps"] = 2.0;
}
}
trackMonitor.calculatedScore.value = Math.max(0, score);
trackMonitor.calculatedScore.reasons = reasons;
}
calculateClientScore() {
let totalScore = 0;
let totalWeight = 0;
const combinedReasons = {};
for (const pcMonitor of this.clientMonitor.peerConnections) {
if (pcMonitor.calculatedStabilityScore.value !== undefined) {
totalScore += pcMonitor.calculatedStabilityScore.value;
totalWeight += 1;
// Combine reasons
Object.assign(combinedReasons, pcMonitor.calculatedStabilityScore.reasons || {});
}
}
const clientScore = totalWeight > 0 ? totalScore / totalWeight : 5.0;
this.clientMonitor.setScore(clientScore, combinedReasons);
}
// Optional: Custom encoding for reasons
encodeClientScoreReasons(reasons) {
return JSON.stringify(reasons || {});
}
}
// Apply custom calculator
const monitor = new ClientMonitor();
monitor.scoreCalculator = new CustomScoreCalculator(monitor);
The monitor collects WebRTC statistics periodically and adapts them for consistent processing across different browsers and integrations.
collectingPeriodInMs
getStats()
on peer connectionsStats adapters handle browser-specific differences and integration requirements:
Add custom adaptation logic:
monitor.statsAdapters.add((stats) => {
// Custom adaptation logic
return stats.map((stat) => {
if (stat.type === "inbound-rtp" && stat.trackIdentifier) {
// Custom track identifier handling
stat.trackIdentifier = stat.trackIdentifier.replace(/[{}]/g, "");
}
return stat;
});
});
The monitor collects and processes all standard WebRTC statistics:
Sampling creates periodic snapshots (ClientSample
) containing the complete state of the monitored client.
A ClientSample
includes:
Enable automatic sampling by setting samplingPeriodInMs
:
const monitor = new ClientMonitor({
collectingPeriodInMs: 2000,
samplingPeriodInMs: 4000, // Create sample every 4 seconds
});
monitor.on("sample-created", (sample) => {
console.log("Sample created:", sample);
// Send to analytics backend
sendToAnalytics(sample);
});
Create samples on demand:
const monitor = new ClientMonitor({
collectingPeriodInMs: 2000,
bufferingEventsForSamples: true, // Required for manual sampling
});
// Create sample manually
const sample = monitor.createSample();
if (sample) {
console.log("Manual sample:", sample);
}
For efficient data transmission and storage, ObserveRTC provides dedicated compression packages for ClientSample
objects:
@observertc/samples-encoder - Compresses ClientSample objects for transmission:
import { SamplesEncoder } from "@observertc/samples-encoder";
const encoder = new SamplesEncoder();
const sample = monitor.createSample();
// Encode the sample for efficient transmission
const encodedSample = encoder.encode(sample);
// Send compressed data over the network
fetch("/api/samples", {
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
},
body: encodedSample,
});
@observertc/samples-decoder - Decompresses received ClientSample objects:
import { SamplesDecoder } from "@observertc/samples-decoder";
const decoder = new SamplesDecoder();
// Receive compressed sample data
const compressedData = await response.arrayBuffer();
// Decode back to ClientSample object
const decodedSample = decoder.decode(compressedData);
// Process the restored sample
console.log("Decoded sample:", decodedSample);
Benefits of Using Compression:
Installation:
# For encoding (client-side)
npm install @observertc/samples-encoder
# For decoding (server-side)
npm install @observertc/samples-decoder
# Both packages (if needed)
npm install @observertc/samples-encoder @observertc/samples-decoder
Integration with ObserveRTC Stack: These compression packages are part of the broader ObserveRTC ecosystem and are designed to work seamlessly with:
The compression format maintains full compatibility with the ObserveRTC schema definitions and can be used with any transport mechanism (WebSocket, HTTP REST, etc.).
The monitor generates events for WebRTC state changes and issues for detected problems.
Automatically generated events include:
Issues are generated by detectors:
Add custom events and issues:
// Custom event
monitor.addEvent({
type: "user-action",
payload: { action: "mute-audio" },
timestamp: Date.now(),
});
// Custom issue
monitor.addIssue({
type: "custom-problem",
payload: { severity: "high", description: "Custom issue detected" },
timestamp: Date.now(),
});
Listen for real-time events:
// Sample created
monitor.on("sample-created", (sample) => {
console.log("New sample:", sample);
});
// Issue detected
monitor.on("issue", (issue) => {
console.log("Issue:", issue.type, issue.payload);
});
// Score updated
monitor.on("score", ({ clientScore, scoreReasons }) => {
console.log("Score:", clientScore, "Reasons:", scoreReasons);
});
// Congestion detected
monitor.on("congestion", ({ peerConnectionMonitor, availableIncomingBitrate }) => {
console.log("Congestion detected on PC:", peerConnectionMonitor.peerConnectionId);
});
// Stats collected
monitor.on("stats-collected", ({ durationOfCollectingStatsInMs, collectedStats }) => {
console.log("Stats collection took:", durationOfCollectingStatsInMs, "ms");
});
The monitor creates specialized monitor objects for each WebRTC statistics type, providing navigation, derived fields, and lifecycle management.
ClientMonitor
├── PeerConnectionMonitor[]
│ ├── InboundRtpMonitor[]
│ ├── OutboundRtpMonitor[]
│ ├── RemoteInboundRtpMonitor[]
│ ├── RemoteOutboundRtpMonitor[]
│ ├── MediaSourceMonitor[]
│ ├── CodecMonitor[]
│ ├── IceTransportMonitor[]
│ ├── IceCandidateMonitor[]
│ ├── IceCandidatePairMonitor[]
│ ├── CertificateMonitor[]
│ ├── DataChannelMonitor[]
│ └── MediaPlayoutMonitor[]
├── InboundTrackMonitor[]
└── OutboundTrackMonitor[]
Monitors incoming media tracks with attached detectors:
Properties:
score
: Calculated quality scorebitrate
: Receiving bitratejitter
: Network jitterfractionLost
: Packet loss fractiondtxMode
: Discontinuous transmission modedetectors
: Attached detectorsDetectors:
Monitors outgoing media tracks:
Properties:
score
: Calculated quality scorebitrate
: Aggregate sending bitratesendingPacketRate
: Packet sending rateremoteReceivedPacketRate
: Remote receiving ratedetectors
: Attached detectorsMethods:
getHighestLayer()
: Gets highest bitrate layergetOutboundRtps()
: Gets all outbound RTP monitorsExtended inbound RTP statistics with derived fields:
Derived Fields:
bitrate
: Calculated receiving bitratepacketRate
: Packet receiving ratedeltaPacketsLost
: Packets lost since last collectiondeltaJitterBufferDelay
: Jitter buffer delay changeewmaFps
: Exponentially weighted moving average FPSExtended outbound RTP statistics:
Derived Fields:
bitrate
: Calculated sending bitratepayloadBitrate
: Payload-only bitratepacketRate
: Packet sending rateretransmissionRate
: Retransmission rateNavigation:
getRemoteInboundRtp()
: Navigate to corresponding remote statsgetMediaSource()
: Navigate to media sourceICE candidate pair with derived metrics:
Derived Fields:
availableIncomingBitrate
: Calculated available bandwidthavailableOutgoingBitrate
: Calculated available bandwidthICE transport layer monitoring:
Properties:
selectedCandidatePair
: Currently selected candidate pairEvery monitor supports two types of additional data properties that serve different purposes:
attachments
- Data shipped with ClientSample:
ClientSample
when createSample()
is calledappData
- Application-specific data (not shipped):
ClientSample
creation// Set application data (not shipped with samples)
trackMonitor.appData = {
userId: "user-123",
internalTrackId: "track-abc",
localProcessingFlags: { enableProcessing: true },
};
// Set attachments (shipped with samples)
trackMonitor.attachments = {
roomId: "room-456",
participantRole: "presenter",
mediaType: "screen-share",
customMetrics: { quality: "high" },
};
Every monitor in the hierarchy supports both properties:
ClientMonitor.attachments
/ ClientMonitor.appData
PeerConnectionMonitor.appData
(attachments set via tracks)InboundTrackMonitor
, OutboundTrackMonitor
InboundRtpMonitor
, OutboundRtpMonitor
, etc.IceCandidatePairMonitor
, IceTransportMonitor
, etc.Use Cases:
attachments for:
appData for:
Stats adapters provide a powerful mechanism to customize how WebRTC statistics are processed before being consumed by monitors. They handle browser-specific differences and allow custom preprocessing logic.
The library includes several built-in adapters that are automatically applied based on browser detection:
mediaType
to kind
field for RTP statsStats adapters are automatically added based on detected browser:
// Automatically applied for Firefox
if (browser.name === "firefox") {
pcMonitor.statsAdapters.add(new Firefox94StatsAdapter());
pcMonitor.statsAdapters.add(new FirefoxTransportStatsAdapter());
}
Create custom adapters by implementing the StatsAdapter
interface:
import { StatsAdapter } from "@observertc/client-monitor-js";
class CustomStatsAdapter {
name = "custom-stats-adapter";
adapt(stats) {
// Pre-processing: runs before monitor updates
return stats.map((stat) => {
if (stat.type === "inbound-rtp" && stat.trackIdentifier) {
// Custom track identifier normalization
stat.trackIdentifier = stat.trackIdentifier.replace(/[{}]/g, "");
}
if (stat.type === "outbound-rtp" && stat.mediaSourceId) {
// Add custom metadata
stat.customQualityFlag = this.calculateQualityFlag(stat);
}
return stat;
});
}
postAdapt(stats) {
// Post-processing: runs after initial monitor updates
// Useful for cross-stat calculations
const inboundStats = stats.filter((s) => s.type === "inbound-rtp");
const outboundStats = stats.filter((s) => s.type === "outbound-rtp");
// Add custom correlation stats
if (inboundStats.length > 0 && outboundStats.length > 0) {
stats.push({
type: "custom-correlation",
id: "correlation-metrics",
timestamp: Date.now(),
totalStreams: inboundStats.length + outboundStats.length,
avgBitrate: this.calculateAvgBitrate(inboundStats, outboundStats),
});
}
return stats;
}
calculateQualityFlag(stat) {
// Custom quality assessment logic
return stat.bitrate > 1000000 ? "high" : "standard";
}
calculateAvgBitrate(inbound, outbound) {
// Custom correlation calculation
const totalBitrate = [...inbound, ...outbound].reduce((sum, stat) => sum + (stat.bitrate || 0), 0);
return totalBitrate / (inbound.length + outbound.length);
}
}
// Add to peer connection monitor
const adapter = new CustomStatsAdapter();
pcMonitor.statsAdapters.add(adapter);
// Remove adapter
pcMonitor.statsAdapters.remove(adapter);
// or by name
pcMonitor.statsAdapters.remove("custom-stats-adapter");
Adapters are processed in a specific order during stats collection:
getStats()
called on peer connectionadapt()
method called on all adapters in orderpostAdapt()
method called for advanced cross-stat processingclass MediasoupProbatorFilter {
name = "mediasoup-probator-filter";
adapt(stats) {
// Filter out mediasoup probator tracks
return stats.filter((stat) => {
if (stat.type === "inbound-rtp" || stat.type === "outbound-rtp") {
return stat.trackIdentifier !== "probator";
}
return true;
});
}
}
class BandwidthEstimationAdapter {
name = "bandwidth-estimation-adapter";
postAdapt(stats) {
const candidatePairs = stats.filter((s) => s.type === "candidate-pair");
const selectedPair = candidatePairs.find((p) => p.state === "succeeded");
if (selectedPair && selectedPair.availableIncomingBitrate) {
// Add custom bandwidth metrics
stats.push({
type: "custom-bandwidth",
id: "bandwidth-estimation",
timestamp: Date.now(),
estimatedBandwidth: selectedPair.availableIncomingBitrate,
bandwidthUtilization: this.calculateUtilization(stats, selectedPair),
});
}
return stats;
}
calculateUtilization(stats, selectedPair) {
const totalBitrate = stats
.filter((s) => s.type === "inbound-rtp")
.reduce((sum, s) => sum + (s.bitrate || 0), 0);
return totalBitrate / selectedPair.availableIncomingBitrate;
}
}
The library automatically calculates numerous derived metrics from raw WebRTC statistics, providing enhanced insights into connection quality and performance. These metrics are computed during stats processing and are available on monitor objects.
Available on ClientMonitor
:
const monitor = new ClientMonitor();
// Aggregated bitrates across all peer connections
console.log(monitor.sendingAudioBitrate); // Total audio sending bitrate (bps)
console.log(monitor.sendingVideoBitrate); // Total video sending bitrate (bps)
console.log(monitor.receivingAudioBitrate); // Total audio receiving bitrate (bps)
console.log(monitor.receivingVideoBitrate); // Total video receiving bitrate (bps)
// Network capacity metrics
console.log(monitor.totalAvailableIncomingBitrate); // Available bandwidth for receiving
console.log(monitor.totalAvailableOutgoingBitrate); // Available bandwidth for sending
// Connection quality
console.log(monitor.avgRttInSec); // Average RTT across connections (seconds)
console.log(monitor.score); // Calculated quality score (0.0-5.0)
console.log(monitor.durationOfCollectingStatsInMs); // Time to collect stats (performance indicator)
Available on PeerConnectionMonitor
:
const pcMonitor = /* get from monitor.peerConnections */;
// Bitrate metrics by media type
console.log(pcMonitor.sendingAudioBitrate); // Audio sending bitrate (bps)
console.log(pcMonitor.sendingVideoBitrate); // Video sending bitrate (bps)
console.log(pcMonitor.receivingAudioBitrate); // Audio receiving bitrate (bps)
console.log(pcMonitor.receivingVideoBitrate); // Video receiving bitrate (bps)
// Packet loss rates
console.log(pcMonitor.outboundFractionLost); // Outbound packet loss fraction
console.log(pcMonitor.inboundFractionalLost); // Inbound packet loss fraction
// Delta metrics (change since last collection)
console.log(pcMonitor.deltaInboundPacketsLost); // Packets lost in period
console.log(pcMonitor.deltaInboundPacketsReceived); // Packets received in period
console.log(pcMonitor.deltaOutboundPacketsSent); // Packets sent in period
console.log(pcMonitor.deltaAudioBytesSent); // Audio bytes sent in period
console.log(pcMonitor.deltaVideoBytesSent); // Video bytes sent in period
console.log(pcMonitor.deltaDataChannelBytesSent); // Data channel bytes sent
// Connection timing and RTT
console.log(pcMonitor.avgRttInSec); // Current average RTT (seconds)
console.log(pcMonitor.ewmaRttInSec); // EWMA smoothed RTT (seconds)
console.log(pcMonitor.connectingStartedAt); // Connection start timestamp
console.log(pcMonitor.connectedAt); // Connection established timestamp
// Network topology detection
console.log(pcMonitor.usingTURN); // Boolean: using TURN relay
console.log(pcMonitor.usingTCP); // Boolean: using TCP transport
console.log(pcMonitor.iceState); // ICE connection state
// Historical peaks
console.log(pcMonitor.highestSeenSendingBitrate); // Peak sending bitrate seen
console.log(pcMonitor.highestSeenReceivingBitrate); // Peak receiving bitrate seen
console.log(pcMonitor.highestSeenAvailableIncomingBitrate); // Peak available incoming
console.log(pcMonitor.highestSeenAvailableOutgoingBitrate); // Peak available outgoing
Available on InboundTrackMonitor
:
const inboundTrack = /* get from monitor.tracks */;
console.log(inboundTrack.bitrate); // Receiving bitrate (bps)
console.log(inboundTrack.jitter); // Network jitter (seconds)
console.log(inboundTrack.fractionLost); // Packet loss fraction
console.log(inboundTrack.score); // Track quality score (0.0-5.0)
Available on OutboundTrackMonitor
:
const outboundTrack = /* get from monitor.tracks */;
console.log(outboundTrack.bitrate); // Sending bitrate (bps)
console.log(outboundTrack.sendingPacketRate); // Packets sent per second
console.log(outboundTrack.remoteReceivedPacketRate); // Remote packets received per second
console.log(outboundTrack.jitter); // Remote reported jitter
console.log(outboundTrack.fractionLost); // Remote reported packet loss
console.log(outboundTrack.score); // Track quality score (0.0-5.0)
Available on InboundRtpMonitor
:
const inboundRtp = /* get from pcMonitor.mappedInboundRtpMonitors */;
// Bitrate and packet metrics
console.log(inboundRtp.bitrate); // Calculated receiving bitrate (bps)
console.log(inboundRtp.packetRate); // Packets received per second
console.log(inboundRtp.fractionLost); // Calculated packet loss fraction
console.log(inboundRtp.bitPerPixel); // Video: bits per pixel efficiency
// Video-specific derived metrics
console.log(inboundRtp.avgFramesPerSec); // Average FPS over recent samples
console.log(inboundRtp.ewmaFps); // EWMA smoothed FPS
console.log(inboundRtp.fpsVolatility); // FPS stability (lower is better)
console.log(inboundRtp.isFreezed); // Boolean: video appears frozen
// Audio-specific metrics
console.log(inboundRtp.receivingAudioSamples); // Audio samples received in period
console.log(inboundRtp.desync); // Boolean: audio desync detected
// Delta metrics (change since last collection)
console.log(inboundRtp.deltaPacketsLost); // Packets lost in period
console.log(inboundRtp.deltaPacketsReceived); // Packets received in period
console.log(inboundRtp.deltaBytesReceived); // Bytes received in period
console.log(inboundRtp.deltaJitterBufferDelay); // Jitter buffer delay change
console.log(inboundRtp.deltaFramesDecoded); // Video frames decoded in period
console.log(inboundRtp.deltaFramesReceived); // Video frames received in period
console.log(inboundRtp.deltaFramesRendered); // Video frames rendered in period
console.log(inboundRtp.deltaCorruptionProbability); // Frame corruption change
console.log(inboundRtp.deltaTime); // Elapsed time for calculations (ms)
Available on OutboundRtpMonitor
:
const outboundRtp = /* get from pcMonitor.mappedOutboundRtpMonitors */;
// Bitrate metrics
console.log(outboundRtp.bitrate); // Total sending bitrate (bps)
console.log(outboundRtp.payloadBitrate); // Payload-only bitrate (excluding headers/retransmissions)
console.log(outboundRtp.packetRate); // Packets sent per second
console.log(outboundRtp.bitPerPixel); // Video: bits per pixel efficiency
// Delta metrics
console.log(outboundRtp.deltaPacketsSent); // Packets sent in period
console.log(outboundRtp.deltaBytesSent); // Bytes sent in period
Remote Inbound RTP (remote peer's receiving stats):
const remoteInboundRtp = /* get from pcMonitor.mappedRemoteInboundRtpMonitors */;
console.log(remoteInboundRtp.packetRate); // Remote receiving packet rate
console.log(remoteInboundRtp.deltaPacketsLost); // Remote packets lost in period
Remote Outbound RTP (remote peer's sending stats):
const remoteOutboundRtp = /* get from pcMonitor.mappedRemoteOutboundRtpMonitors */;
console.log(remoteOutboundRtp.bitrate); // Remote sending bitrate
Available on IceTransportMonitor
and IceCandidatePairMonitor
:
const iceTransport = /* get from pcMonitor.mappedIceTransportMonitors */;
// Transport-level bitrates
console.log(iceTransport.sendingBitrate); // Transport sending bitrate
console.log(iceTransport.receivingBitrate); // Transport receiving bitrate
// Delta metrics
console.log(iceTransport.deltaPacketsSent); // Packets sent in period
console.log(iceTransport.deltaPacketsReceived); // Packets received in period
console.log(iceTransport.deltaBytesSent); // Bytes sent in period
console.log(iceTransport.deltaBytesReceived); // Bytes received in period
// ICE candidate pair specific
const candidatePair = /* get from pcMonitor.mappedIceCandidatePairMonitors */;
console.log(candidatePair.availableIncomingBitrate); // Bandwidth estimation for receiving
console.log(candidatePair.availableOutgoingBitrate); // Bandwidth estimation for sending
Available on DataChannelMonitor
:
const dataChannel = /* get from pcMonitor.mappedDataChannelMonitors */;
console.log(dataChannel.deltaBytesSent); // Bytes sent in period
console.log(dataChannel.deltaBytesReceived); // Bytes received in period
Media Source derived metrics (local media):
const mediaSource = /* get from pcMonitor.mappedMediaSourceMonitors */;
// Media source stats are mostly raw WebRTC stats
// Derived metrics are primarily calculated at RTP level
Media Playout derived metrics (audio playout):
const mediaPlayout = /* get from pcMonitor.mappedMediaPlayoutMonitors */;
console.log(mediaPlayout.deltaSynthesizedSamplesDuration); // Synthesized audio duration in period
console.log(mediaPlayout.deltaSamplesDuration); // Total samples duration in period
// Access derived metrics through monitor hierarchy
monitor.on("stats-collected", () => {
// Client-level aggregates
console.log("Total sending bitrate:", monitor.sendingAudioBitrate + monitor.sendingVideoBitrate);
// Per-connection metrics
monitor.peerConnections.forEach((pc) => {
console.log(`PC ${pc.peerConnectionId} RTT:`, pc.avgRttInSec * 1000, "ms");
// Per-track metrics
pc.mappedInboundTracks.forEach((track) => {
if (track.kind === "video") {
const inboundRtp = track.getInboundRtp();
console.log(`Video FPS: ${inboundRtp?.ewmaFps}, Volatility: ${inboundRtp?.fpsVolatility}`);
}
});
});
});
// Manual access to specific metrics
const videoTrack = monitor.tracks.find((t) => t.kind === "video" && t.direction === "inbound");
if (videoTrack) {
const rtp = videoTrack.getInboundRtp();
console.log("Video quality metrics:", {
bitrate: rtp.bitrate,
fps: rtp.ewmaFps,
volatility: rtp.fpsVolatility,
packetLoss: rtp.fractionLost,
});
}
The main sample structure containing complete client state:
type ClientSample = {
timestamp: number;
clientId?: string;
callId?: string;
score?: number;
scoreReasons?: string;
attachments?: Record<string, unknown>;
peerConnections?: PeerConnectionSample[];
clientEvents?: ClientEvent[];
clientIssues?: ClientIssue[];
clientMetaItems?: ClientMetaData[];
extensionStats?: ExtensionStat[];
};
Per-peer-connection statistics:
type PeerConnectionSample = {
peerConnectionId: string;
score?: number;
scoreReasons?: string;
attachments?: Record<string, unknown>;
inboundTracks?: InboundTrackSample[];
outboundTracks?: OutboundTrackSample[];
codecs?: CodecStats[];
inboundRtps?: InboundRtpStats[];
outboundRtps?: OutboundRtpStats[];
remoteInboundRtps?: RemoteInboundRtpStats[];
remoteOutboundRtps?: RemoteOutboundRtpStats[];
mediaSources?: MediaSourceStats[];
mediaPlayouts?: MediaPlayoutStats[];
dataChannels?: DataChannelStats[];
iceTransports?: IceTransportStats[];
iceCandidates?: IceCandidateStats[];
iceCandidatePairs?: IceCandidatePairStats[];
certificates?: CertificateStats[];
};
All stats types include standard WebRTC fields plus:
timestamp
: When the stats were collectedid
: Unique identifierattachments
: Additional data for samplingKey Stats Types:
InboundRtpStats
: Receiving stream statisticsOutboundRtpStats
: Sending stream statisticsIceCandidatePairStats
: ICE candidate pair informationCodecStats
: Codec configurationMediaSourceStats
: Local media source statsimport { ClientMonitor } from "@observertc/client-monitor-js";
const monitor = new ClientMonitor({
clientId: "client-123",
callId: "call-456",
collectingPeriodInMs: 2000,
samplingPeriodInMs: 5000,
});
// Add peer connection
const pc = new RTCPeerConnection();
monitor.addSource(pc);
// Handle samples
monitor.on("sample-created", (sample) => {
// Send to analytics
fetch("/analytics", {
method: "POST",
body: JSON.stringify(sample),
headers: { "Content-Type": "application/json" },
});
});
// Handle issues
monitor.on("issue", (issue) => {
console.warn("Issue detected:", issue.type, issue.payload);
});
const monitor = new ClientMonitor({
clientId: "advanced-client",
collectingPeriodInMs: 1000,
samplingPeriodInMs: 3000,
// Sensitive congestion detection
congestionDetector: {
sensitivity: "high",
createIssue: true,
},
// Strict CPU monitoring
cpuPerformanceDetector: {
fpsVolatilityThresholds: {
lowWatermark: 0.05,
highWatermark: 0.2,
},
durationOfCollectingStatsThreshold: {
lowWatermark: 3000,
highWatermark: 6000,
},
},
// Quick dry track detection
dryInboundTrackDetector: {
thresholdInMs: 3000,
},
appData: {
version: "1.0.0",
feature: "screen-share",
},
});
import mediasoup from "mediasoup-client";
const device = new mediasoup.Device();
const monitor = new ClientMonitor({
clientId: "mediasoup-client",
});
// Load device capabilities
await device.load({ routerRtpCapabilities });
// Add device for monitoring
monitor.addSource(device);
// Create transport
const sendTransport = device.createSendTransport({
// transport options
});
// The monitor automatically detects the new transport
// For existing transports, add manually:
// monitor.addSource(sendTransport);
// Produce media
const producer = await sendTransport.produce({
track: videoTrack,
codecOptions: {},
});
// Track is automatically monitored
class NetworkLatencyDetector {
name = "network-latency-detector";
constructor(pcMonitor) {
this.pcMonitor = pcMonitor;
this.highLatencyThreshold = 200; // ms
}
update() {
const rttMs = (this.pcMonitor.avgRttInSec || 0) * 1000;
if (rttMs > this.highLatencyThreshold) {
this.pcMonitor.parent.emit("high-latency", {
peerConnectionId: this.pcMonitor.peerConnectionId,
rttMs,
});
this.pcMonitor.parent.addIssue({
type: "high-latency",
payload: { rttMs, threshold: this.highLatencyThreshold },
});
}
}
}
// Add to peer connection monitor
monitor.on("peer-connection-opened", ({ peerConnectionMonitor }) => {
const detector = new NetworkLatencyDetector(peerConnectionMonitor);
peerConnectionMonitor.detectors.add(detector);
});
class MonitoringDashboard {
constructor(monitor) {
this.monitor = monitor;
this.setupEventListeners();
}
setupEventListeners() {
this.monitor.on("score", ({ clientScore, scoreReasons }) => {
this.updateScoreDisplay(clientScore, scoreReasons);
});
this.monitor.on("congestion", ({ availableIncomingBitrate, availableOutgoingBitrate }) => {
this.showCongestionAlert(availableIncomingBitrate, availableOutgoingBitrate);
});
this.monitor.on("stats-collected", ({ durationOfCollectingStatsInMs }) => {
this.updatePerformanceMetrics(durationOfCollectingStatsInMs);
});
this.monitor.on("issue", (issue) => {
this.addIssueToLog(issue);
});
}
updateScoreDisplay(score, reasons) {
document.getElementById("score").textContent = score.toFixed(1);
document.getElementById("score-reasons").textContent = JSON.stringify(reasons, null, 2);
}
showCongestionAlert(incoming, outgoing) {
const alert = document.createElement("div");
alert.className = "congestion-alert";
alert.textContent = `Congestion detected! Available: ${incoming}/${outgoing} kbps`;
document.body.appendChild(alert);
}
updatePerformanceMetrics(duration) {
document.getElementById("collection-time").textContent = `${duration}ms`;
}
addIssueToLog(issue) {
const log = document.getElementById("issue-log");
const entry = document.createElement("div");
entry.textContent = `${new Date().toISOString()}: ${issue.type} - ${JSON.stringify(issue.payload)}`;
log.appendChild(entry);
}
}
// Initialize dashboard
const dashboard = new MonitoringDashboard(monitor);
// Limit stored scores history
monitor.scoreCalculator.constructor.lastNScoresMaxLength = 5;
// Disable unnecessary detectors
monitor.config.audioDesyncDetector.disabled = true;
// Reduce collection frequency
monitor.setCollectingPeriod(5000);
// Check if source is properly added
console.log("Peer connections:", monitor.peerConnections.length);
// Verify stats collection
monitor.on("stats-collected", ({ collectedStats }) => {
console.log("Collected stats from PCs:", collectedStats.length);
});
// Check for adaptation issues
monitor.statsAdapters.add((stats) => {
console.log("Raw stats count:", stats.length);
return stats;
});
// Check browser support
if (!window.RTCPeerConnection) {
console.error("WebRTC not supported");
}
// Handle browser-specific issues
monitor.on("stats-collected", ({ collectedStats }) => {
if (collectedStats.length === 0) {
console.warn("No stats collected - possible browser issue");
}
});
Enable debug logging:
import { setLogger } from "@observertc/client-monitor-js";
setLogger({
trace: console.trace,
debug: console.debug,
info: console.info,
warn: console.warn,
error: console.error,
});
// Optimize for large numbers of tracks
const monitor = new ClientMonitor({
collectingPeriodInMs: 3000, // Reduce frequency
samplingPeriodInMs: 10000, // Less frequent sampling
// Disable resource-intensive detectors
cpuPerformanceDetector: { disabled: true },
audioDesyncDetector: { disabled: true },
});
// Manual garbage collection
setInterval(() => {
// Clear old data periodically
monitor.scoreCalculator.totalReasons = {};
}, 60000);
// Configuration
type ClientMonitorConfig = {
/* ... */
};
// Core types
type ClientSample = {
/* ... */
};
type ClientEvent = { type: string; payload?: any; timestamp: number };
type ClientIssue = { type: string; payload?: any; timestamp: number };
// Monitor types
class InboundTrackMonitor {
/* ... */
}
class OutboundTrackMonitor {
/* ... */
}
class PeerConnectionMonitor {
/* ... */
}
// Detector interface
interface Detector {
readonly name: string;
update(): void;
}
interface ClientMonitorEvents {
"sample-created": (sample: ClientSample) => void;
"stats-collected": (data: {
durationOfCollectingStatsInMs: number;
collectedStats: [string, RTCStats[]][];
}) => void;
score: (data: { clientScore: number; scoreReasons?: Record<string, number> }) => void;
issue: (issue: ClientIssue) => void;
congestion: (data: CongestionEvent) => void;
close: () => void;
// ... detector-specific events
}
A: The default 2-second interval (2000ms) works well for most applications. For real-time applications or debugging, you might use 1 second. For low-bandwidth situations, 5 seconds is acceptable.
A:
collectingPeriod
: How often to collect WebRTC stats from browser APIssamplingPeriod
: How often to create complete client samples (includes events, issues, metadata)A:
A: The library is designed for web browsers with WebRTC support. For React Native, you'd need WebRTC polyfills and may encounter platform-specific issues.
A: Just add each peer connection as a source:
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
monitor.addSource(pc1);
monitor.addSource(pc2);
A: The monitor automatically cleans up associated resources and emits appropriate events. You don't need to manually remove closed connections.
A: Scores are based on standard WebRTC metrics and industry best practices. They provide good relative quality assessment but should be calibrated based on your specific use case and user feedback.
A: Yes, you can filter events before sampling or add custom logic in event handlers to control what gets included.
A: Use the attachments
property to tag tracks:
// When adding a screen share track
trackMonitor.attachments = { mediaType: "screen-share" };
A: The library is designed to be lightweight. Typical overhead is <1% CPU usage. The main cost is the periodic getStats()
calls, which is why the collection period is configurable.
https://www.npmjs.com/package/@observertc/client-monitor-js
Schema definitions are available at https://github.com/observertc/schemas
Client-monitor is made with the intention to provide an open-source monitoring solution for WebRTC developers. If you are interested in getting involved, please read our contribution guidelines.
Apache-2.0
FAQs
ObserveRTC Client Integration Javascript Library
The npm package @observertc/client-monitor-js receives a total of 9,386 weekly downloads. As such, @observertc/client-monitor-js popularity was classified as popular.
We found that @observertc/client-monitor-js demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
npm now supports Trusted Publishing with OIDC, enabling secure package publishing directly from CI/CD workflows without relying on long-lived tokens.
Research
/Security News
A RubyGems malware campaign used 60 malicious packages posing as automation tools to steal credentials from social media and marketing tool users.
Security News
The CNA Scorecard ranks CVE issuers by data completeness, revealing major gaps in patch info and software identifiers across thousands of vulnerabilities.