onNetworkQualityChanged and onMediaMetric are delivered passively through RTCMediaEvent—just subscribe, no polling needed. getMetric() is for pulling on demand.
For the full data dictionary of Metric fields, see Media quality.
1. Subscribe to quality level changes (recommended)
Most apps only need “a network status indicator + poor-network prompts”, so preferonNetworkQualityChanged. It’s driven by the server’s quality reports (about one every 5 seconds) and fires on every report, even when the level hasn’t changed:
- No debouncing: the SDK doesn’t check for tier crossings or apply hysteresis, so when the level hovers around a boundary, the callback results flip back and forth with it. A status indicator can show
currentLeveldirectly; for actions that interrupt the user or change the call, such as poor-network prompts and automatic degradation, debounce yourself (see 5.1). trendonly compares with the previous callback:INITIALthe first time,DEGRADEDwhen worse,RECOVEREDwhen better, andSTABLEwhen unchanged (or when this level can’t be recognized).- Per direction: uplink and downlink are evaluated and reported independently; each callback represents one direction only (
direction).
RTCMediaSimpleEvent, override only the callbacks you need, and register it with setRtcMediaEvent:
onNetworkQualityChanged is called on a background thread of the signaling channel. Switch to the main thread (for example, runOnUiThread / View.post) before updating the UI.
:::
NetworkQualityChange structure:
2. Periodic quality snapshots with onMediaMetric
When you need a detailed diagnostic panel (bitrate, packet loss, frame rate, and so on for each track), subscribe to onMediaMetric. After joining a channel, the SDK delivers a full MediaMetric.Metric about every 5 seconds:
qualityReport is computed and delivered by the server. After you first join a channel, it may be null until the server’s first quality report arrives; the example handles this with nullable access.
:::
3. Actively query with getMetric
Pull the most recently collected full quality snapshot (including qualityReport) at any time. This suits on-demand cases such as a diagnostic panel that only computes when the user opens the details:
getStats. The sampling period is about 5 seconds, so it may be null right after media starts.
:::
4. Connection quality assessment with qualityReport
All three methods return the same qualityReport: the server has already combined packet loss, RTT, jitter, bitrate, and other metrics to compute a level and MOS, so you can use it directly without computing anything yourself. It distinguishes two directions: uplink (from this client to the server) and downlink (from the server to this client).
level values:
BesidesonNetworkQualityChanged,onMediaMetric.qualityReport, andgetMetric().qualityReportshare the same data source—the same quality report delivered by the server. They differ only in how it’s delivered (per report / periodically / on demand).
qualityReport, Metric also contains overall network statistics networkStats, uplink/downlink aggregates localUploadStats / remoteDownloadStats, and track-level statistics localAudios / localVideos / remoteAudios / remoteVideos. For all fields, see Media quality.
5. Handling poor networks
To handle a poor network, first separate two things: direction and response.- A poor uplink means you can’t send; you can proactively degrade (turn off the camera, unpublish video). A poor downlink means you can’t receive; the server usually optimizes this automatically, and you can unsubscribe from video if needed. So look at both
uplinkanddownlink, not just one side. onNetworkQualityChangedfires on every quality report, and the SDK doesn’t debounce it. A status indicator can simply followcurrentLevel; for actions like poor-network prompts and degradation, debounce yourself and act only after the network has stayed poor for a while (see 5.1). The same applies if you readonMediaMetricperiodically.
5.1 Show network status
Map the quality level to a status indicator. Don’t show raw data like packet loss or RTT to end users:
A poor uplink and a poor downlink need different prompt messages. The callback isn’t debounced, so when the level hovers around the
good / poor boundary, it keeps reporting a drop. Prompt only after the network has stayed poor beyond a threshold, and only once per poor-network episode:
5.2 Poor uplink
When the uplink level stays atpoor / lost, degrade step by step from light to heavy:
1. Turn off the camera and keep only audio. Audio bitrate is low and almost always survives a poor network. Stopping camera capture significantly reduces uplink usage while keeping the publishing path, so you can restart capture as soon as the network recovers:
qualityLimitationReason on the local video tracks:
5.3 Poor downlink
When the downlink level stays atpoor:
1. First check whether the problem is on the other side. Only if this client has downlink packet loss (downlink.loss clearly > 0) is your receiving path poor. If this client has almost no packet loss and only one person’s video stutters, it’s usually that person’s poor uplink; unsubscribing won’t help, so just show “The other user’s network is poor”.
2. When the publisher sends simulcast, the server switches layers automatically. Based on each subscriber’s own downlink bandwidth and packet loss, the server automatically switches it to the appropriate high or low stream; subscribers don’t need to switch manually (when subscribing, the candidate layer list tells the server which layers it can switch to). With a single stream, there’s nothing to degrade to automatically.
3. Unsubscribe from video and receive only audio.
onActiveSpeakersChanged:
5.4 Disconnects and reconnects
Connection state notifications are inRTCClientEvent. The initial listener is passed in through join(..., clientEvent, ...); the SDK reconnects automatically when the network drops, and you only need to update the UI accordingly:
5.5 Quick reference
6. Complete example
Putting it all together: useonNetworkQualityChanged to drive the status indicator and per-direction prompts (prompt only on DEGRADED, so STABLE doesn’t repeat it; a level hovering around a boundary can still repeat it, so debounce as in 5.1 if needed), degrade and recover as needed, and handle disconnects and reconnects.
After creatingNetworkQualityHandler, callattach()first, then passhandler.clientEventtoRTCEngine.join(...). For degrading actions such asstopCapturein the example, we recommend letting the user confirm via a button instead of silently changing the nature of the call.