Suggestions

close search

Add Messaging, Voice, and Authentication to your apps with Vonage Communications APIs

Visit the Vonage API Developer Portal
Developer Documentation moved on August 7, 2026. Future updates are available only at → https://developer.vonage.com/en/opentok/overview

Client observability — Web



The OpenTok Web SDK provides calls to access real-time network and media statistics in a video session. These calls report detailed stream quality metrics—such as packet loss, data received, and bandwidth—and can be used on any publisher or subscribed stream.

The OpenTok 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.

This guide includes the following sections:

The OpenTok 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 change events on the publisher

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 subscriber

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:

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.

Publisher audio statistics (stats.audio)

Provides statistics about a publisher’s audio track.

Publisher video statistics (stats.video)

These fields represent the publisher's current video performance:

Publisher video layer statistics (stats.video.layers)

Represents one simulcast layer or SVC layer.

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.

Subscriber video statistics (stats.video)

These fields describe the subscriber’s real-time video reception and decoding performance:

Subscriber sender-side estimation (stats.senderStats)

These metrics provide bandwidth estimates reported for the sender’s outbound connection:

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:

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:

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

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

For example:

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 is 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 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.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:

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:

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 array of RtcStatsReport objects:

publisher.getRtcStatsReport()
  .then(statArrays => statsArray.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);