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

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

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