Client Observability: Android

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

Statistics are delivered through separate listeners:

Publisher:

  • PublisherKit.VideoStatsListener — video statistics and video quality events
  • PublisherKit.AudioStatsListener — audio statistics
  • PublisherKit.MediaLinkStatsListener — media link (transport-level) statistics

Subscriber:

  • SubscriberKit.VideoStatsListener — video statistics and video quality events
  • SubscriberKit.AudioStatsListener — audio statistics
  • SubscriberKit.MediaLinkStatsListener — media link (transport-level) statistics

Enabling Statistics for Publishers

Video Statistics

publisher.setVideoStatsListener(new PublisherKit.VideoStatsListener() {
    @Override
    public void onVideoStats(PublisherKit publisher, PublisherKit.PublisherVideoStats[] statsArray) {
        if (statsArray != null && statsArray.length > 0) {
            // For routed sessions, first element is sufficient.
            // For relayed sessions, iterate all statsArray elements to get per-subscriber info.
            PublisherKit.PublisherVideoStats stats = statsArray[0];

            String connectionId = (stats.connectionId != null && !stats.connectionId.isEmpty())
                    ? stats.connectionId
                    : "<none>";
            String subscriberId = (stats.subscriberId != null && !stats.subscriberId.isEmpty())
                    ? stats.subscriberId
                    : "<none>";

            Log.d("VideoStats", "Publisher Video Stats for connectionId: " + connectionId
                    + ", subscriberId: " + subscriberId);

            Log.d("VideoStats", "Video bytes sent: " + stats.videoBytesSent);
            Log.d("VideoStats", "Video packets sent: " + stats.videoPacketsSent);
            Log.d("VideoStats", "Video packets lost: " + stats.videoPacketsLost);
            Log.d("VideoStats", "Stats timestamp: " + stats.timeStamp + " ms");

            if (stats.videoLayers != null) {
                for (PublisherKit.VideoLayerStats layer : stats.videoLayers) {
                    Log.d("VideoStats", "Layer: " + layer.width + "x" + layer.height);
                    Log.d("VideoStats", "  Encoded FPS: " + layer.encodedFrameRate);
                    Log.d("VideoStats", "  Bitrate: " + layer.bitrate + " bps");
                    Log.d("VideoStats", "  Total bitrate (incl. RTP overhead): " + layer.totalBitrate + " bps");
                    Log.d("VideoStats", "  Codec: " + (layer.codec != null ? layer.codec : "unknown"));
                    Log.d("VideoStats", "  Scalability mode: " + (layer.scalabilityMode != null ? layer.scalabilityMode : "none"));
                    Log.d("VideoStats", "  Quality limitation: " + layer.qualityLimitationReason);
                }
            }
        }
    }

    @Override
    public void onVideoQualityChanged(PublisherKit publisher, PublisherKit.PublisherVideoStats stats, String reason) {
        Log.d("Stats", "Publisher video quality event: " + reason);
    }
});

Audio Statistics

publisher.setAudioStatsListener(new PublisherKit.AudioStatsListener() {
    @Override
    public void onAudioStats(PublisherKit publisher, PublisherKit.PublisherAudioStats[] statsArray) {
        if (statsArray != null && statsArray.length > 0) {
            // For routed sessions, first element is sufficient.
            // For relayed sessions, iterate all statsArray elements to get per-subscriber info.
            PublisherKit.PublisherAudioStats stats = statsArray[0];

            String connectionId = (stats.connectionId != null && !stats.connectionId.isEmpty())
                    ? stats.connectionId
                    : "<none>";
            String subscriberId = (stats.subscriberId != null && !stats.subscriberId.isEmpty())
                    ? stats.subscriberId
                    : "<none>";

            Log.d("AudioStats", "Publisher Audio Stats for connectionId: " + connectionId
                    + ", subscriberId: " + subscriberId);

            Log.d("AudioStats", "Audio bytes sent: " + stats.audioBytesSent);
            Log.d("AudioStats", "Audio packets sent: " + stats.audioPacketsSent);
            Log.d("AudioStats", "Audio packets lost: " + stats.audioPacketsLost);
            Log.d("AudioStats", "Stats timestamp: " + stats.timeStamp + " ms");
        }
    }
});
publisher.setMediaLinkStatsListener(new PublisherKit.MediaLinkStatsListener() {
    @Override
    public void onMediaLinkStats(PublisherKit publisher, PublisherKit.PublisherMediaLinkStats[] mediaLinkStats) {
        if (mediaLinkStats != null && mediaLinkStats.length > 0) {
            PublisherKit.PublisherMediaLinkStats stats = mediaLinkStats[0];
            if (stats.transport != null) {
                Log.d("MediaLinkStats", "Uplink bandwidth: " + stats.transport.getConnectionEstimatedBandwidth() + " bps");
                Log.d("MediaLinkStats", "Network condition: " + stats.transport.networkCondition);
                Log.d("MediaLinkStats", "Condition reason: " + stats.transport.networkConditionReason);
            }
        }
    }

    @Override
    public void onNetworkConditionChanged(PublisherKit publisher, PublisherKit.PublisherMediaLinkStats mediaLinkStats, String reason) {
        if (mediaLinkStats.transport != null) {
            Log.d("Stats", "Publisher network condition: " + mediaLinkStats.transport.networkCondition);
            Log.d("Stats", "Reason: " + mediaLinkStats.transport.networkConditionReason);
        }
    }
});

See Network condition and degradation source for details on interpreting network condition scores and reasons.

For a publisher in a routed session (one that uses the Vonage Video Media Router), the stats array includes one object, defining the statistics for the single audio or video media stream that is sent to the Vonage Video Media Router. In a relayed session, the stats array includes an object for each subscriber to the published stream.

Enabling statistics for subscribers

Video Statistics

subscriber.setVideoStatsListener(new SubscriberKit.VideoStatsListener() {
    @Override
    public void onVideoStats(SubscriberKit subscriber, SubscriberKit.SubscriberVideoStats stats) {
        Log.d("Stats", "Video bytes received: " + stats.videoBytesReceived);
    }

    @Override
    public void onVideoQualityChanged(SubscriberKit subscriber, SubscriberKit.SubscriberVideoStats stats, String reason) {
        Log.d("Stats", "Subscriber video quality event: " + reason);
    }
});

Audio Statistics

subscriber.setAudioStatsListener(new SubscriberKit.AudioStatsListener() {
    @Override
    public void onAudioStats(SubscriberKit subscriber, SubscriberKit.SubscriberAudioStats stats) {
        Log.d("Stats", "Audio packets received: " + stats.audioPacketsReceived);
    }
});
subscriber.setMediaLinkStatsListener(new SubscriberKit.MediaLinkStatsListener() {
    @Override
    public void onMediaLinkStats(SubscriberKit subscriber, SubscriberKit.SubscriberMediaLinkStats mediaLinkStats) {
        if (mediaLinkStats.transport != null) {
            Log.d("MediaLinkStats", "Downlink bandwidth: " + mediaLinkStats.transport.getConnectionEstimatedBandwidth() + " bps");
        }
        if (mediaLinkStats.remotePublisherTransport != null) {
            Log.d("MediaLinkStats", "Remote publisher uplink bandwidth: " + mediaLinkStats.remotePublisherTransport.getConnectionEstimatedBandwidth() + " bps");
        }
        Log.d("MediaLinkStats", "Degradation source: " + mediaLinkStats.networkDegradationSource);
    }

    @Override
    public void onNetworkConditionChanged(SubscriberKit subscriber, SubscriberKit.SubscriberMediaLinkStats mediaLinkStats, String reason) {
        if (mediaLinkStats.transport != null) {
            Log.d("Stats", "Local network condition: " + mediaLinkStats.transport.networkCondition);
        }
        if (mediaLinkStats.remotePublisherTransport != null) {
            Log.d("Stats", "Remote publisher network condition: " + mediaLinkStats.remotePublisherTransport.networkCondition);
        }
        Log.d("Stats", "Degradation source: " + mediaLinkStats.networkDegradationSource);
    }
});

See Network condition and degradation source for details on interpreting network condition scores and reasons.

Statistics data structures

This section outlines the structs and properties provided by the Android 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.

TransportStats

Represents shared transport-level metrics.

  • connectionEstimatedBandwidth – Estimated available connection bandwidth (bps).
  • networkCondition – Current network condition score (NETWORK_CONDITION_UNKNOWN, NETWORK_CONDITION_CRITICAL, NETWORK_CONDITION_WARNING, NETWORK_CONDITION_FAIR, NETWORK_CONDITION_GOOD, or NETWORK_CONDITION_EXCELLENT).
  • networkConditionReason – Primary reason impacting the network condition (NETWORK_CONDITION_REASON_NONE, NETWORK_CONDITION_REASON_UNKNOWN, NETWORK_CONDITION_REASON_BANDWIDTH, or NETWORK_CONDITION_REASON_PACKET_LOSS).

PublisherKit.PublisherVideoStats

Provides statistics about a publisher’s video track. It includes:

  • connectionId – In a relayed session, the connection ID of the client subscribing to the stream. Undefined in a routed session.
  • subscriberId – In a relayed session, the subscribed ID of the client subscribing to the stream. Undefined in a routed session.
  • videoPacketsLost – Estimated video packets lost.
  • videoPacketsSent – Video packets sent.
  • videoBytesSent – Video bytes sent.
  • timeStamp – Unix timestamp in milliseconds when stats were gathered.
  • startTime – The timestamp, in milliseconds since the Unix epoch, from which the cumulative totals started accumulating.
  • videoLayers – The array of video layer statistics (see OTPublisherKitVideoLayerStats).

PublisherKit.PublisherAudioStats

Provides statistics about a publisher’s audio track. It includes:

  • connectionId – In a relayed session, the connection ID of the client subscribing to the stream. Undefined in a routed session.
  • subscriberId – In a relayed session, the subscribed ID of the client subscribing to the stream. Undefined in a routed session.
  • audioPacketsLost – Estimated packets lost.
  • audioPacketsSent – Audio packets sent.
  • audioBytesSent – Audio bytes sent.
  • timeStamp – Unix timestamp in milliseconds.
  • startTime – The timestamp, in milliseconds since the Unix epoch, from which the cumulative totals started accumulating.

PublisherKit.VideoLayerStats

Represents one simulcast layer or SVC layer.

  • width – Encoded frame width.
  • height – Encoded frame height.
  • encodedFrameRate – Encoded frames per second.
  • bitrate – Layer bitrate (bps).
  • totalBitrate – Layer bitrate including RTP overhead (bps).
  • scalabilityMode – SVC/scalability descriptor (e.g., "L3T3").
  • qualityLimitationReason – Reason for quality limitation (bandwidth, CPU, codec, resolution, or layer change).
  • codec – The codec used by this video layer.

SubscriberKit.SenderStats

Sender-side estimation metrics (mirrored on both audio and video).

  • connectionMaxAllocatedBitrate – Maximum bitrate estimated for the sender connection.
  • connectionEstimatedBandwidth – Current bandwidth estimation (bps).

SubscriberKit.SubscriberVideoStats

Provides statistics about a subscriber’s video track. It includes:

  • videoPacketsLost – Estimated video packets lost.
  • videoPacketsReceived – Video packets received.
  • videoBytesReceived – Video bytes received.
  • timeStamp – Unix timestamp in milliseconds when stats were gathered.
  • senderStats – Sender-side metrics (optional).
  • width – Decoded frame width in pixels.
  • height – Decoded frame height in pixels.
  • decodedFrameRate – Decoded frames per second.
  • bitrate – Video bitrate (bps).
  • totalBitrate – Bitrate including RTP overhead (bps).
  • pauseCount – Number of pauses (>5s since last frame). Includes intentional disables and audio-fallback cases.
  • totalPausesDuration – Total pause duration (ms).
  • freezeCount – Freeze count (WebRTC-defined freeze event).
  • totalFreezesDuration – Total freeze duration (ms).
  • codec – Current decoder codec.

SubscriberKit.SubscriberAudioStats

Provides statistics about a subscriber's audio track. It includes:

  • audioPacketsLost – Estimated packets lost.
  • audioPacketsReceived – Packets received.
  • audioBytesReceived – Bytes received.
  • timeStamp – Unix timestamp in milliseconds.
  • senderStats – Sender-side metrics (optional).

PublisherKit.PublisherMediaLinkStats

Provides transport-level statistics for a publisher's connection.

  • transport – Transport statistics for this publisher (see TransportStats)

SubscriberKit.SubscriberMediaLinkStats

Provides transport-level statistics for a subscriber's connections, including visibility into the remote publisher's network performance. This enables applications to diagnose whether connection issues originate from the subscriber's downlink or the publisher's uplink.

  • transport – Local transport and network statistics for this subscriber. May be limited if sender-side statistics and/or audio fallback are disabled.
  • remotePublisherTransport – Remote publisher transport and network statistics. May be limited if sender-side statistics and/or audio fallback are disabled.
  • networkDegradationSource – Indicates the source of network degradation, if any (NetworkDegradationSource.NONE, NetworkDegradationSource.LOCAL, NetworkDegradationSource.REMOTE, or NetworkDegradationSource.BOTH_OR_UNCLEAR).

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 setting the senderStatisticsTrack property to true when building the publisher:

Publisher publisher = new Publisher.Builder(context)
        .senderStatsTrack(true) // Enable sender-side stats
        .build();

If senderStatsTrack 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 the video and audio stats callbacks described above. The senderStats property on both SubscriberKit.SubscriberVideoStats and SubscriberKit.SubscriberAudioStats provides two metrics:

  • connectionMaxAllocatedBitrate — The maximum bitrate that can be estimated for the connection
  • connectionEstimatedBandwidth — The current estimated bandwidth for the connection

These metrics are calculated per audio-video bundle, so the same values appear in both video and audio statistics.

subscriber.setVideoStatsListener((subscriber, stats) -> {
    if (stats.senderStats != null) {
        Log.d(TAG, "Connection max allocated bitrate: " + stats.senderStats.connectionMaxAllocatedBitrate);
        Log.d(TAG, "Connection current estimated bandwidth: " + stats.senderStats.connectionEstimatedBandwidth);
    }
});

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 networkCondition and networkConditionReason. For subscribers, media link stats also include remotePublisherTransport and networkDegradationSource.
  • Network condition changed events: Dedicated callbacks 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:

@Override
public void onNetworkConditionChanged(SubscriberKit subscriber, SubscriberKit.SubscriberMediaLinkStats mediaLinkStats, String reason) {

    int localCondition = mediaLinkStats.transport != null ? mediaLinkStats.transport.networkCondition : TransportStats.NETWORK_CONDITION_UNKNOWN;
    int remoteCondition = mediaLinkStats.remotePublisherTransport != null ? mediaLinkStats.remotePublisherTransport.networkCondition : TransportStats.NETWORK_CONDITION_UNKNOWN;
    int source = mediaLinkStats.networkDegradationSource;

    if (source == NetworkDegradationSource.LOCAL) {
        Log.d("Stats", "Local network is degraded (condition: " + localCondition + ")");
    } else if (source == NetworkDegradationSource.REMOTE) {
        Log.d("Stats", "Remote publisher network is degraded (condition: " + remoteCondition + ")");
    } else if (source == NetworkDegradationSource.BOTH_OR_UNCLEAR) {
        Log.d("Stats", "Degradation source unclear — local: " + localCondition + ", remote: " + remoteCondition);
    }
}

RTC Stats Report

To get low-level peer connection statistics for a publisher, use the PublisherKit.getRtcStatsReport() method. This provides RTC stats reports for the media stream. This is an asynchronous operation. Call the PublisherKit.setRtcStatsReportListener(PublisherKit.PublisherRtcStatsReportListener listener) method, and then implement the PublisherKit.PublisherRtcStatsReportListener.onRtcStatsReport(PublisherKit publisher, PublisherKit.PublisherRtcStats[] stats) method prior to calling PublisherKit.getRtcStatsReport().

When the stats are available, the implementation of the PublisherKit.PublisherRtcStatsReportListener.onRtcStatsReport(PublisherKit publisher, PublisherKit.PublisherRtcStats[] stats) method is called.

An array of PublisherRtcStats objects is passed into that method. The PublisherRtcStats object includes a jsonArrayOfReports property. This is a JSON array of RTC stats reports, which are similar to the format of the RtcStatsReport object implemented in web browsers (see these Mozilla docs).

To get low-level peer connection statistics for a subscriber, use the SubscriberKit.getRtcStatsReport() method. This provides an RTC stats report for the media stream.

This is an asynchronous operation. Call the SubscriberKit.setRtcStatsReportListener(SubscriberKit.SubscriberRtcStatsReportListener listener) method, and then implement the SubscriberKit.SubscriberRtcStatsReportListener.onRtcStatsReport(SubscriberKit subscriber, java.lang.String jsonArrayOfReports) method prior to calling SubscriberKit.getRtcStatsReport().

When the stats are available, the implementation of the SubscriberKit.SubscriberRtcStatsReportListener.onRtcStatsReport(SubscriberKit subscriber, java.lang.String jsonArrayOfReports) method is called. The jsonArrayOfReports parameter is a JSON array of RTC stats reports, which are similar to the format of the RtcStatsReport object implemented in web browsers (see these Mozilla docs).

Also see this W3C documentation.