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.

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 videoQualityChanged event.
  • Firefox does not support qualityLimitationReason, so this property is not present in the stats of the publisher. Also, videoQualityChanged events with reasons bandwidth, cpu and other are not supported in this browser.
  • Hardware-accelerated video encoding and dedicated video encoders prevent macOS to trigger cpu limitations.

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 the connection property of the connectionCreated event 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 the Subscriber object 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').

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).

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's stats.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's stats.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, networkCondition and networkConditionReason. Subscriber stats also expose remotePublisherTransport and networkDegradationSource.
  • 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);