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.
Audio and Video Statistics API
The Vonage Video Web SDK sends periodic audio and video network statistics for both publishers and subscribers. These include packet counts, bitrates, frame rate data, pause/freeze metrics, codec information, and optional sender-side network estimation.
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}`);
}
});
}
if (stats.transportStats) {
console.log(
'transport estimated bandwidth:',
stats.transportStats.connectionEstimatedBandwidth,
'bps'
);
}
}
});
});
}, 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}`
);
});
}
});
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}`);
}
}
});
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 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.
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 Transport Statistics (stats.transport)
The transportStats 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).
Subscriber Audio Statistics (stats.audio)
Provides statistics about a subscriber’s audio track.
bytesReceived— Total audio bytes received.packetsLost— Total audio packets that did not reach the subscriber.packetsReceived— Total audio packets successfully received.timestamp— Unix timestamp (ms) when these stats were gathered.
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).
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.
Subscribing to Sender-Side Statistics
Subscribers automatically receive sender statistics only if the publisher has enabled them and if the user calls Subscriber.getStats() at least once. Note that due to network latency, the first call to getStats may not include sender statistics. Subsequent calls are more likely to return this data.
No additional events or methods are required; the sender statistics are included in the existing stats object returned by getStats().
Receiving Statistics Events
Sender-side statistics are included as an optional senderStats object inside the stats object passed to the Subscriber.getStats() callback. The senderStats object contains two properties:
connectionMaxAllocatedBitrate— The maximum bitrate that can be estimated for the connection (in bits per second)connectionEstimatedBandwidth— The current estimated bandwidth for the connection (in bits per second)
These two metrics are calculated per audio-video bundle, so the same values appear in both video and audio statistics. Because they reflect the transport rather than individual tracks, the metrics are shared across both audio and video.
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`);
} else {
console.log("Sender stats not available yet.");
}
});
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.
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);