Handle Video Stats
In this step you will handle subscriber video and media link callbacks to diagnose stuttering and surface network health in your app.
| What you'll monitor | How it can be used |
|---|---|
videoPacketsLost / videoBytesReceived |
Confirm the receive path is struggling (symptom: stutter, freezes) |
transport (local downlink bandwidth) |
Suggest “Your connection looks weak” when your downlink looks constrained |
remotePublisherTransport (remote uplink) |
Suggest “The other participant’s network may be unstable” when their send path looks constrained |
networkDegradationSource |
Choose between local vs remote messaging—or log both bandwidth values for support |
1. Handle Subscriber Video Stats
Implement SubscriberKit.VideoStatsListener to receive periodic video receive metrics:
private val videoStatsListener = SubscriberKit.VideoStatsListener { _, stats ->
Log.d(TAG, "videoBytesReceived=${stats.videoBytesReceived}")
Log.d(TAG, "videoPacketsLost=${stats.videoPacketsLost}")
Log.d(TAG, "videoPacketsReceived=${stats.videoPacketsReceived}")
Log.d(TAG, "timeStamp=${stats.timeStamp}")
mainHandler.post {
latestObservabilityStats = ObservabilityStats.fromVideoStats(
stats,
latestMediaLinkSnapshot,
)
}
}
Callbacks may arrive on a background thread. Post UI updates to the main thread (as above) if you drive Compose or Views from these listeners.
2. Handle Media Link Stats
Implement SubscriberKit.MediaLinkStatsListener.onMediaLinkStats & SubscriberKit.MediaLinkStatsListener.onNetworkConditionChanged for transport bandwidth and degradation source. Network condition scores are also present on transport and remotePublisherTransport in each periodic sample—see Network condition.
private val mediaLinkStatsListener = object : SubscriberKit.MediaLinkStatsListener {
//
// Triggers on regular intervals while subscribed
// Good place for: Overlay numbers, charts, steady telemetry
//
override fun onMediaLinkStats(
subscriber: SubscriberKit,
mediaLinkStats: SubscriberKit.SubscriberMediaLinkStats,
) {
applyMediaLinkStats(mediaLinkStats)
}
//
// Triggers when the SDK detects a **significant** network condition change
// Good place for: Toasts, banners, support logs, adaptive UI
//
override fun onNetworkConditionChanged(
subscriber: SubscriberKit,
mediaLinkStats: SubscriberKit.SubscriberMediaLinkStats,
reason: String,
) {
Log.d(TAG, "onNetworkConditionChanged reason=$reason")
applyMediaLinkStats(mediaLinkStats, networkConditionChangeReason = reason)
handleNetworkDegradation(mediaLinkStats)
}
}
Video stats and media link stats are delivered on separate callbacks. The sample keeps a MediaLinkSnapshot and merges it when building or updating ObservabilityStats:
private fun applyMediaLinkStats(
mediaLinkStats: SubscriberKit.SubscriberMediaLinkStats,
networkConditionChangeReason: String? = null,
) {
val localBandwidth = mediaLinkStats.transport?.connectionEstimatedBandwidth
val remoteBandwidth =
mediaLinkStats.remotePublisherTransport?.connectionEstimatedBandwidth
val localCondition = mediaLinkStats.transport?.networkCondition
val remoteCondition = mediaLinkStats.remotePublisherTransport?.networkCondition
val conditionReason = mediaLinkStats.transport?.networkConditionReason
mainHandler.post {
latestMediaLinkSnapshot = MediaLinkSnapshot(
localEstimatedBandwidth = localBandwidth,
remoteEstimatedBandwidth = remoteBandwidth,
networkDegradationSource = mediaLinkStats.networkDegradationSource,
localNetworkCondition = localCondition,
remoteNetworkCondition = remoteCondition,
lastNetworkConditionChangeReason = networkConditionChangeReason,
)
latestObservabilityStats?.let { current ->
latestObservabilityStats = current.copy(
localEstimatedBandwidth = localBandwidth,
remoteEstimatedBandwidth = remoteBandwidth,
networkDegradationSource = mediaLinkStats.networkDegradationSource,
localNetworkCondition = localCondition,
remoteNetworkCondition = remoteCondition,
lastNetworkConditionChangeReason = networkConditionChangeReason,
)
}
networkConditionChangeReason?.let { changeReason ->
Log.d(
TAG,
"network condition event: $changeReason local=$localCondition remote=$remoteCondition",
)
}
}
}
Extend MediaLinkSnapshot and ObservabilityStats with the new fields (or map condition integers to labels in the overlay in the next step).
3. React to degradation on network condition events
When video stutters, onNetworkConditionChanged fires on a significant change. Use it with networkDegradationSource to choose user-facing copy:
private fun handleNetworkDegradation(
mediaLinkStats: SubscriberKit.SubscriberMediaLinkStats,
) {
val localCondition = mediaLinkStats.transport?.networkCondition
?: TransportStats.NETWORK_CONDITION_UNKNOWN
val remoteCondition = mediaLinkStats.remotePublisherTransport?.networkCondition
?: TransportStats.NETWORK_CONDITION_UNKNOWN
when (mediaLinkStats.networkDegradationSource) {
NetworkDegradationSource.LOCAL -> {
Log.w(TAG, "Local network degraded (condition=$localCondition)")
// e.g. show: "Your connection looks weak"
}
NetworkDegradationSource.REMOTE -> {
Log.w(TAG, "Remote publisher network degraded (condition=$remoteCondition)")
// e.g. show: "The other participant's network may be unstable"
}
NetworkDegradationSource.BOTH_OR_UNCLEAR -> {
Log.w(TAG, "Degradation unclear — local=$localCondition remote=$remoteCondition")
}
else -> { /* NetworkDegradationSource.NONE — no action */ }
}
}
reason in onNetworkConditionChanged describes why the event fired (for example a shift in bandwidth or packet loss). Combine it with networkCondition and networkConditionReason on transport for analytics.
Define ObservabilityStats, MediaLinkSnapshot, and fromVideoStats() in your project. The sample ObservabilityStats.kt is a starting point.
Client Observability
Learn how to use client observability to monitor real-time quality metrics for a video call with Vonage Video SDK.