Skip to main content
The SDK offers three ways to get network quality, from “passively reacting to changes” to “actively querying”. Pick the one that fits: 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. Most apps only need “a network status indicator + poor-network prompts”, so prefer onNetworkQualityChanged. 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 currentLevel directly; for actions that interrupt the user or change the call, such as poor-network prompts and automatic degradation, debounce yourself (see 5.1).
  • trend only compares with the previous callback: INITIAL the first time, DEGRADED when worse, RECOVERED when better, and STABLE when 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).
Extend RTCMediaSimpleEvent, override only the callbacks you need, and register it with setRtcMediaEvent:
:::note 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:
:::note 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:
:::note Returns a thread-safe copy and doesn’t trigger the underlying 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:
onNetworkQualityChanged, onMediaMetric.qualityReport, and getMetric().qualityReport share 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).
Besides 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 uplink and downlink, not just one side.
  • onNetworkQualityChanged fires on every quality report, and the SDK doesn’t debounce it. A status indicator can simply follow currentLevel; 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 read onMediaMetric periodically.

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:
When the uplink level stays at poor / 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:
2. Unpublish video entirely. More thorough than turning off the camera; it frees uplink encoding and sending resources:
Turning off the camera changes the nature of the call, so prompt the user before doing it rather than acting silently—especially in scenarios like emergency command or law enforcement recording. To further pinpoint why the uplink is limited, read qualityLimitationReason on the local video tracks:
When the downlink level stays at poor: 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.
4. Multi-party calls: subscribe only to the current speaker’s video and receive only audio from everyone else. You can get the current speakers from onActiveSpeakersChanged:

5.4 Disconnects and reconnects

Connection state notifications are in RTCClientEvent. 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: use onNetworkQualityChanged 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 creating NetworkQualityHandler, call attach() first, then pass handler.clientEvent to RTCEngine.join(...). For degrading actions such as stopCapture in the example, we recommend letting the user confirm via a button instead of silently changing the nature of the call.