Client Observability: Web
The Vonage Video SDK exposes detailed stream-quality metrics through a high-level statistics API—recommended for most use cases—which provides audio, video, network, and sender-side statistics in a unified, session-aware form that remains stable across peer-connection transitions. For advanced debugging, the SDK also offers access to the raw WebRTC stats report, which reflects unprocessed peer-connection data.
The SDK also exposes network condition metrics that provide a high-level assessment of connection health for both publishers and subscribers. These metrics include a network condition score, the reason driving that score, and—for subscribers—a degradation source indicating which side of the connection is responsible for any observed issues. See Network condition and degradation source for details.
Audio, Video and Media Link Statistics API
The Vonage Video Web SDK sends periodic audio, video, and media link statistics for both publishers and subscribers. These include packet counts, bitrates, frame rate data, pause/freeze metrics, codec information, and transport-level network metrics such as bandwidth estimation and network condition scoring.
Getting Statistics for a Publisher
The Publisher.getStats() method provides you with an array
of objects defining the current audio-video statistics for the publisher.
For a publisher in a routed session (one that uses the
OpenTok
Media Router), this array includes one object, defining the statistics for
the single audio-video stream that is sent to the Vonage Video Media Router. In a relayed session,
the array includes an object for each subscriber to the published stream.
The following code logs some metrics for the publisher's stream every second:
window.setInterval(() => {
publisher.getStats((error, statsArray) => {
if (error) {
console.error(error);
return;
}
statsArray.forEach(statsContainer => {
const stats = statsContainer.stats;
const connectionId = stats.connectionId || 'routed';
console.log(`\nStats for ${connectionId}`);
if (stats.video) {
const video = stats.video;
if (video.layers && video.layers.length > 0) {
console.log(`Video layers: ${video.layers.length}`);
video.layers.forEach((layer, index) => {
console.log(` Layer ${index}: ${layer.width}x${layer.height}`);
console.log(` encodedFrameRate: ${layer.encodedFrameRate} fps`);
console.log(` bitrate: ${layer.bitrate} bps`);
console.log(` totalBitrate: ${layer.totalBitrate} bps`);
console.log(` codec: ${layer.codec}`);
console.log(` scalabilityMode: ${layer.scalabilityMode}`);
if (layer.qualityLimitationReason) {
console.log(` qualityLimitationReason: ${layer.qualityLimitationReason}`);
}
});
}
console.log('transport estimated bandwidth:', stats.mediaLink.transport.connectionEstimatedBandwidth, 'bps');
console.log('network condition:', stats.mediaLink.transport.networkCondition);
console.log('network condition reason:', stats.mediaLink.transport.networkConditionReason);
}
});
});
}, 1000);
Receiving Video Quality Events on the Publishers
In addition to polling statistics with Publisher.getStats(), you can listen for real-time notifications when the publisher detects a meaningful change in video quality by subscribing to the videoQualityChanged event:
publisher.on('videoQualityChanged', ({ reason, statsContainer }) => {
console.log('Video quality change reason:', reason);
const { stats } = statsContainer;
if (stats.video && stats.video.layers) {
stats.video.layers.forEach((layer) => {
console.log(
`Resolution: ${layer.width}x${layer.height}, FPS: ${layer.frameRate}`
);
});
}
});
Receiving Network Condition Events on the Publishers
To receive network condition change events for the publisher, listen to the networkConditionChanged event:
publisher.on('networkConditionChanged', ({ reason, statsContainer }) => {
const { stats } = statsContainer;
console.log('Network condition changed.');
console.log(`Network Condition: ${stats.mediaLink.transport.networkCondition}, Reason: ${stats.mediaLink.transport.networkConditionReason}`);
});
This event is triggered when a significant change in network condition is detected for the publisher. The statsContainer object includes the media link statistics for the impacted subscriber. In relayed sessions, only the statistics for the impacted subscriber are included in the event.
Getting Statistics for a Subscriber
The getStats() method of a subscriber object provides you with information about the
subscriber's stream.
The following code logs several metrics for subscriber's stream every second:
window.setInterval(() => {
subscriber.getStats((error, stats) => {
if (error) {
console.error('Error getting subscriber stats: ', error.message);
return;
}
const video = stats.video;
if (video) {
console.log('video bitrate:', video.bitrate, 'bps');
console.log('video totalBitrate:', video.totalBitrate, 'bps');
console.log('decoded frame rate:', video.decodedFrameRate, 'fps');
console.log('codec:', video.codec);
console.log('res:', `${video.width}x${video.height}`);
console.log('freezeCount:', video.freezeCount);
console.log('totalFreezesDuration:', video.totalFreezesDuration, 'ms');
console.log('pauseCount:', video.pauseCount);
console.log('totalPausesDuration:', video.totalPausesDuration, 'ms');
}
});
}, 1000);
Receiving Video Quality Events on the Subscribers
Subscribers can listen for the videoQualityChanged event to be notified when significant video quality interruptions or changes are detected.
subscriber.on('videoQualityChanged', ({ reason, stats }) => {
if (reason === 'videoInterruption') {
console.warn('Video playback was interrupted');
if (stats.video.freezeCount > 0) {
console.log(`Freeze count: ${stats.video.freezeCount}`);
}
if (stats.video.pauseCount > 0) {
console.log(`Pause count: ${stats.video.pauseCount}`);
}
}
});
Receiving Network Condition Events on the Subscribers
To receive network condition change events for the subscriber, listen to the networkConditionChanged event:
subscriber.on('networkConditionChanged', ({ reason, stats }) => {
console.log('Network condition changed.');
console.log(`Degradation source: ${stats.mediaLink.networkDegradationSource}`);
if (stats.mediaLink.networkDegradationSource === 'local') {
console.log(`Network Condition: ${stats.mediaLink.transport.networkCondition}, Reason: ${stats.mediaLink.transport.networkConditionReason}`);
} else if (stats.mediaLink.networkDegradationSource === 'remote') {
console.log(`Network Condition: ${stats.mediaLink.remotePublisherTransport.networkCondition}, Reason: ${stats.mediaLink.remotePublisherTransport.networkConditionReason}`);
}
});
This event is triggered when a significant change in network condition is detected for the subscriber or the remote publisher. The stats object includes the media link statistics with local and remote transport metrics and the degradation source.
Known Issues
The actual values and conditions that trigger quality limitations are implementation-specific and may vary between browsers and platforms. For example:
- Screen sharing video streams never trigger the
videoQualityChangedevent. - Firefox does not support
qualityLimitationReason, so this property is not present in the stats of the publisher. Also,videoQualityChangedevents with reasonsbandwidth,cpuandotherare not supported in this browser. - Hardware-accelerated video encoding and dedicated video encoders prevent macOS to trigger
cpulimitations.
Statistics Data Structures
This section outlines the structs and properties provided by the Web audio and video statistics API. While all Video SDK platforms expose the same set of statistics, there may be minor differences in how each platform structures or names individual fields. These variations reflect platform-specific SDK design conventions rather than differences in the underlying metrics.
For a platform-independent explanation of the available statistics and what they represent, refer to client observability overview.
Publisher Statistics (stats)
Provides statistics about a publisher.
connectionId— The unique ID of the client's connection, which matches the id property of theconnectionproperty of theconnectionCreatedevent that the Session object dispatched for the remote client (only available in relayed sessions).subscriberId— The unique ID of the subscriber, which matches the id property of theSubscriberobject in the subscribing client's app (only available in relayed sessions).
Publisher Audio Statistics (stats.audio)
Provides statistics about a publisher’s audio track.
bytesSent— Total audio bytes sent.packetsLost— Total audio packets that did not reach the subscriber or Media Router.packetsSent— Total audio packets sent.timestamp— Unix timestamp (ms) when the stats were gathered.
Publisher Video Statistics (stats.video)
These fields represent the publisher's current video performance:
bytesSent— Total video bytes sent.packetsLost— Total video packets that did not reach the subscriber or Media Router.packetsSent— Total video packets sent.layers— An ordered list of active video encoding layers from highest to lowest resolution.
Publisher Video Layer Statistics (stats.video.layers)
Represents one simulcast layer or SVC layer.
width— Encoded width in pixels.height— Encoded height in pixels.encodedFrameRate— Actual encoding frame rate for this layer.bitrate— Payload bitrate (bps).totalBitrate— Bitrate including RTP headers and padding (bps).scalabilityMode— Scalability configuration (e.g., "L1T3" for SVC or "L3T3" for simulcast).codec— Codec used for this layer.qualityLimitationReason— Indicates why the encoder adjusted quality ('bandwidth', 'cpu', 'other').
Publisher Media Link Statistics (stats.mediaLink.transport)
The transport object provides peer-connection–level network estimation metrics that apply to the overall audio-video transport, rather than to individual tracks or layers.
connectionEstimatedBandwidth— Estimated available uplink bandwidth for the connection (bps).networkCondition— Current network condition score ("unknown","critical","warning","fair","good", or"excellent").networkConditionReason— Primary reason impacting the network condition ("none","unknown","bandwidth", or"packetLoss").
Subscriber Video Statistics (stats.video)
These fields describe the subscriber’s real-time video reception and decoding performance:
bytesReceived— Total video bytes received.packetsLost— Total video packets that did not reach the subscriber.packetsReceived— Total video packets received.timestamp— Unix timestamp (ms) when the stats were gathered.decodedFrameRate— Actual frame rate produced by the decoder (fps).bitrate— Payload bitrate in bits per second.totalBitrate— Bitrate including RTP headers and padding (bps).codec— Codec used for this subscriber.pauseCount— Number of pauses where no frame was rendered for ≥5 seconds.totalPausesDuration— Cumulative duration (ms) of all pauses.freezeCount— Number of short freezes (from the WebRTC stats definition).totalFreezesDuration— Cumulative duration (ms) of all freezes.
Subscriber Sender-Side Estimation (stats.senderStats)
These metrics provide bandwidth estimates reported for the sender’s outbound connection:
connectionMaxAllocatedBitrate— Maximum allocated bitrate estimated for the sender (bps).connectionEstimatedBandwidth— Current estimated uplink bandwidth for the sender (bps).
Subscriber Media Link Statistics (stats.mediaLink)
The mediaLink object provides transport-level and network degradation information for a subscriber's connection. It has the same structure as the publisher's mediaLink.transport for the local and remote transports, plus a degradation source indicator:
transport— Local transport statistics for this subscriber's downlink connection (same structure as publisher'sstats.mediaLink.transport). May be limited if sender-side stats and/or audio fallback are disabled.remotePublisherTransport— Remote publisher transport statistics for the uplink connection (same structure as publisher'sstats.mediaLink.transport). May be limited if sender-side stats and/or audio fallback are disabled.networkDegradationSource— Indicates which side caused network degradation, if any. Possible values:"none","local","remote", or"bothOrUnclear". May be limited if sender-side stats and/or audio fallback are disabled.
Call Quality Monitoring
Beyond the core statistics APIs, OpenTok.js provides additional capabilities for monitoring and responding to call quality changes. These features help applications optimize performance by adapting to device constraints and network conditions.
CPU Performance Monitoring
Applications may run on various mobile and desktop devices on different platforms. Additionally, the hardware specifications for the devices aren't homogeneous. For example, some mobile devices may have better CPU performance than many desktop devices and vice-versa.
The large number of possible hardware configurations—CPU(s), GPU, RAM, hardware encoders/decoders, etc.—means that some tuning may be required. Less capable devices can be configured to disable more CPU intensive features, while more capable devices can default to a more immersive experience.
Detecting CPU Performance Changes
You can detect changes in the device's CPU load by monitoring the Session cpuPerformanceChanged
event. The event contains a cpuPerformanceState property, which is set to one of the following values:
'nominal'— The device can take on additional work.'fair'— The device can still take on additional work, however battery-life may get reduced; additionally, for devices with fans, the fans may become active and audible.'serious'— The device is stressed, so throttling of resources (e.g., CPU) may occur.'critical'— The device is extremely stressed; if not alleviated, issues may occur.
For more details, see this W3C specification.
Optimizing an Application Based on CPU Performance Changes
In response to this event, an application can notify users about resource consumption or disable computationally expensive processes, such as video transformers. See the Session cpuPerformanceChanged.
The following code disables video capture when the CPU enters a 'critical' performance state and
reenables it once the state returns to 'fair' or better:
let isVideoDisabledByCPU = false;
session.on('cpuPerformanceChanged', (event) => {
if (event.cpuPerformanceState === 'critical') {
// The application should alert the user why their video is being disabled
publisher.publishVideo(false);
isVideoDisabledByCPU = true;
} else if (event.cpuPerformanceState === 'nominal' || event.cpuPerformanceState === 'fair') {
if (isVideoDisabledByCPU) {
publisher.publishVideo(true);
isVideoDisabledByCPU = false;
}
}
})
Mean Opinion Score (MOS)
The quality of experience that a user perceives from a service can be rated using Mean Opinion Score (MOS).
The Rating System
MOS is expressed as a positive number. The score can range between 1 (the worst quality) to 5 (the best quality):
- 5 (Excellent) — A hypothetical upper limit to the best quality a user can experience.
- 4 (Good) — A more attainable rating. Vonage users can expect to receive this level of quality.
- 3 (Fair) — Quality is OK.
- 2 (Poor) — Quality is unacceptable.
- 1 (Bad) — Quality is horrible.
The Algorithm
The MOS rating takes into account several factors, all of which impact (and can degrade) the user experience. These factors include (but are not limited to) the following:
- Packet loss — lost packets degrade media quality
- Bitrate — the higher the bitrate, the higher the potential fidelity of the media
- Network latency - packets that arrive too late may be dropped, which can lead to missing audio and/or jumpy video
Optimizing an Application Based on Call Quality Changes
Use the Subscriber qualityScoreChanged event to monitor changes in audio and video quality. Observing changes in media quality is insufficient, though. Given the real-time nature of call applications, responding to the observed changes by tuning your application to continually deliver the best user experience is necessary as well.
A simple heuristic is shown below. An application is dynamically optimized, based on resource constraints.
// We want to know if the CPU is overloaded. If it is, then we
// can disable certain features so that the best call possible
// can still take place.
let isCpuOverloaded = false;
session.on('cpuPerformanceChanged', (event) => {
isCpuOverloaded = event.cpuPerformanceState === 'critical';
});
// We monitor for changes in call quality. This allows us to
// tune our application, taking into account multiple factors
// (inlined below)
subscriber.on('qualityScoreChanged', (event) => {
const { qualityScore } = event;
const isVideoQualityBad = qualityScore.video < 2;
if (!isVideoQualityBad && !isCpuOverloaded) {
// Subscribe to the highest quality video since the CPU isn't taxed
// and the quality received is good
subscriber.setPreferredResolution('1280x720');
subscriber.setPreferredFrameRate(30);
}
else if (isVideoQualityBad && !isCpuOverloaded) {
// Even though the CPU isn't taxed, the video quality received is
// bad. This might be due to (hopefully) intermittent network issues, so
// we subscribe to lower quality video.
subscriber.setPreferredResolution('320x180');
subscriber.setPreferredFrameRate(7);
}
else if (isVideoQualityBad && isCpuOverloaded) {
// The video quality received is bad and the CPU not being overloaded.
// Let's disable video for now.
// We can enable video once conditions improve. See statement below.
subscriber.subscribeToVideo(false);
}
else {
// Enable video
subscriber.subscribeToVideo(true);
}
});
Estimating Call Quality in a Pre-Call Test
You can use the Vonage Video Network test library for Web to see if the client will support publishing audio and video and report the estimated audio and video MOS scores for a client's published stream. This library uses the Publisher.getRtcStatsReport() and Subscriber.subscriber.getStats() methods to calculate the MOS score.
Sender-Side Statistics
See the sender-side statistics overview.
Enabling Sender-Side Statistics
Sender-side statistics are received on the subscribers. To receive sender-side statistics, enable them for the stream’s publisher by passing the publishSenderStats property set to true in the OT.initPublisher call:
const publisher = OT.initPublisher({
publishSenderStats: true
});
If publishSenderStats is not enabled, no sender statistics channel will be published for this publisher. The default value is false.
Receiving Sender-Side Statistics
If the publisher has enabled sender-side statistics, subscribers receive them automatically via Subscriber.getStats() described above. The senderStats property on the returned stats object provides two metrics:
connectionMaxAllocatedBitrate— The maximum bitrate that can be estimated for the connection (bps)connectionEstimatedBandwidth— The current estimated bandwidth for the connection (bps)
These metrics are calculated per audio-video bundle, so the same values appear in both video and audio statistics. Note that the first call to getStats may not include sender statistics due to network latency.
subscriber.getStats((stats) => {
if (stats.senderStats) {
console.log(`Connection max allocated bitrate: ${stats.senderStats.connectionMaxAllocatedBitrate} bps`);
console.log(`Connection current estimated bandwidth: ${stats.senderStats.connectionEstimatedBandwidth} bps`);
}
});
Known Issues
In some cases, when the session is relayed —or in certain routed setups with only two participants— and the Publisher uses Firefox, sender-side statistics may not be available due to browser limitations.
Network Condition and Degradation Source
The SDK provides real-time network condition metrics for both publishers and subscribers, including a condition score, the reason driving that score, and a degradation source for subscribers. For a full explanation of the network condition model, scores, reasons, and how to enable it, see the client observability overview.
Network condition data is available through two channels:
- Periodic statistics: The media link stats events include transport metrics with
connectionEstimatedBandwidth,networkConditionandnetworkConditionReason. Subscriber stats also exposeremotePublisherTransportandnetworkDegradationSource. - Network condition changed events: Dedicated events on both publisher and subscriber are triggered when a significant change in network condition is detected.
The following example shows how to use the subscriber network condition data to identify the source of degradation:
subscriber.on('networkConditionChanged', ({ reason, stats }) => {
console.log('Network condition changed.');
console.log(`Degradation source: ${stats.mediaLink.networkDegradationSource}`);
if (stats.mediaLink.networkDegradationSource === 'local') {
console.log(`Network Condition: ${stats.mediaLink.transport.networkCondition}, Reason: ${stats.mediaLink.transport.networkConditionReason}`);
} else if (stats.mediaLink.networkDegradationSource === 'remote') {
console.log(`Network Condition: ${stats.mediaLink.remotePublisherTransport.networkCondition}, Reason: ${stats.mediaLink.remotePublisherTransport.networkConditionReason}`);
});
RTC Stats Report
To get low-level peer connection statistics for a publisher, use the Publisher.getRtcStatsReport() method. It returns a promise that, on success, resolves with an RtcStatsReport object for the subscribed stream:
publisher.getRtcStatsReport()
.then((stats) => stats.forEach(console.log))
.catch(console.log);
To get low-level peer connection statistics for a subscriber, use the Subscriber.getRtcStatsReport() method. It returns a promise that, on success, resolves with an RtcStatsReport object for the subscribed stream:
subscriber.getRtcStatsReport()
.then((stats) => stats.forEach(console.log))
.catch(console.log);