Service quality

You can use service quality callbacks to notify users about video, audio, and sharing quality during a session and unstable network conditions. You can also use these to measure or show latency, FPS, and resolution. Zoom Video SDK optimizes the FPS and resolution to the current device and network capabilities to optimize a good, smooth experience.

Additionally, you can build network strength indicators for your users or a real-time statistics dashboard of video, audio, and share quality. This is similar to how Zoom Meetings shares diagnostic information.

If you want near real time analytics to quickly diagnose and resolve network disruptions, consider purchasing a Quality of Service Subscription (QSS) plan to use our QSS API and webhooks for Video SDK.

Network status

The ZoomVideoSDKNetworkStatus enum includes values representing the network status.

ZoomVideoSDKNetworkStatus_None,
ZoomVideoSDKNetworkStatus_Bad,
ZoomVideoSDKNetworkStatus_Normal,
ZoomVideoSDKNetworkStatus_Good

To get a network status change callback, there must be at least two users in the session with their video on. You can use the network status returned from the callback to create an icon to show the current network quality.

func onUserVideoNetworkStatusChanged(_ status: ZoomVideoSDKNetworkStatus, user: ZoomVideoSDKUser) {
    if status == .bad {
      //Do something
    }
}
- (void)onUserVideoNetworkStatusChanged:(ZoomVideoSDKNetworkStatus)status user:(ZoomVideoSDKUser *)user {
    if (status == ZoomVideoSDKNetworkStatus_Bad) {
      //Do something
    }
}

QOS statistics

The onQOSStatisticsReceived callback provides real-time quality-of-service metrics for audio, video, and sharing streams. It fires for both send and receive directions, identified by the direction property on the statistics object.

The base ZoomVideoSDKQOSStatistics object contains fields common to all stream types and directions. Depending on the value of direction, cast the object to ZoomVideoSDKQOSSendStatistics or ZoomVideoSDKQOSRecvStatistics to specifically access send or receive fields.

func onQOSStatisticsReceived(_ statistics: ZoomVideoSDKQOSStatistics, user: ZoomVideoSDKUser) {
    // Common fields available on all statistics
    statistics.direction        // .send or .receive
    statistics.statisticsType   // .audio, .video, or .share
    statistics.codecName        // e.g. "h264", "av1", "opus" — valid only during this callback
    statistics.timestamp
    statistics.rtt
    statistics.jitter
    statistics.width
    statistics.height
    statistics.fps
    statistics.bps
    statistics.packetsLost
    statistics.packetsSent
    statistics.networkLevel
    statistics.avg_loss          // per-thousand, e.g. 100 = 10%
    statistics.max_loss          // per-thousand
    statistics.bandwidth
    if statistics.direction == .send, let sendStats = statistics as? ZoomVideoSDKQOSSendStatistics {
        sendStats.frameWidthInput
        sendStats.frameHeightInput
        sendStats.frameRateInput
        sendStats.bytesSent
        sendStats.headerBytesSent       // RTP header and padding bytes sent
        sendStats.packetsSent
        sendStats.totalPacketSendDelay
        sendStats.totalEncodeTime
        sendStats.framesEncoded
    } else if statistics.direction == .receive, let recvStats = statistics as? ZoomVideoSDKQOSRecvStatistics {
        recvStats.bytesReceived
        recvStats.headerBytesReceived   // RTP header and padding bytes received
        recvStats.packetsReceived
        recvStats.estimatedPlayoutTimestamp
        recvStats.totalDecodeTime
        recvStats.framesDecoded
        recvStats.jitterBufferDelay
        recvStats.jitterBufferEmittedCount
    }
}
- (void)onQOSStatisticsReceived:(ZoomVideoSDKQOSStatistics *)statistics user:(ZoomVideoSDKUser *)user {
    // Common fields available on all statistics
    statistics.direction;       // ZoomVideoSDKStatisticsDirection_Send or _Receive
    statistics.statisticsType;  // Audio, Video, or Share
    statistics.codecName;       // e.g. "h264", "av1", "opus" — valid only during this callback
    statistics.timestamp;
    statistics.rtt;
    statistics.jitter;
    statistics.width;
    statistics.height;
    statistics.fps;
    statistics.bps;
    statistics.packetsLost;
    statistics.packetsSent;
    statistics.networkLevel;
    statistics.avg_loss;         // per-thousand, e.g. 100 = 10%
    statistics.max_loss;         // per-thousand
    statistics.bandwidth;
    if (statistics.direction == ZoomVideoSDKStatisticsDirection_Send) {
        ZoomVideoSDKQOSSendStatistics *sendStats = (ZoomVideoSDKQOSSendStatistics *)statistics;
        sendStats.frameWidthInput;
        sendStats.frameHeightInput;
        sendStats.frameRateInput;
        sendStats.bytesSent;
        sendStats.headerBytesSent;      // RTP header and padding bytes sent
        sendStats.packetsSent;
        sendStats.totalPacketSendDelay;
        sendStats.totalEncodeTime;
        sendStats.framesEncoded;
    } else if (statistics.direction == ZoomVideoSDKStatisticsDirection_Receive) {
        ZoomVideoSDKQOSRecvStatistics *recvStats = (ZoomVideoSDKQOSRecvStatistics *)statistics;
        recvStats.bytesReceived;
        recvStats.headerBytesReceived;  // RTP header and padding bytes received
        recvStats.packetsReceived;
        recvStats.estimatedPlayoutTimestamp;
        recvStats.totalDecodeTime;
        recvStats.framesDecoded;
        recvStats.jitterBufferDelay;
        recvStats.jitterBufferEmittedCount;
    }
}

Video quality

When statisticsType is .video, the callback provides video-specific metrics such as frame dimensions, frame rate, and bitrate. To receive video statistics, there must be at least two users in the session with their video on.

Audio quality

When statisticsType is .audio, the callback provides audio-specific metrics such as codec, jitter, and packet loss. The width, height, and fps fields are not applicable for audio. To receive audio statistics, there must be at least two users actively sending audio to the session.

On the receive side, ZoomVideoSDKQOSRecvStatistics exposes additional audio-only metrics from the NetEQ jitter buffer and concealment engine. These are valid only when statisticsType is .audio and direction is .receive.

if statistics.statisticsType == .audio, statistics.direction == .receive,
   let recvStats = statistics as? ZoomVideoSDKQOSRecvStatistics {
    recvStats.packetsDiscarded               // non-FEC packets discarded by NetEQ
    recvStats.fecPacketsReceived             // total FEC packets received
    recvStats.fecPacketsDiscarded            // FEC packets discarded by NetEQ
    recvStats.jitterBufferTargetDelay        // accumulated target delay, in seconds
    recvStats.jitterBufferMinimumDelay       // accumulated minimum achievable delay, in seconds
    recvStats.totalSamplesReceived           // total samples generated by NetEQ
    recvStats.concealedSamples               // samples produced by concealment
    recvStats.silentConcealedSamples         // subset of concealedSamples that are silent
    recvStats.concealmentEvents              // number of concealment events
    recvStats.insertedSamplesForDeceleration // samples inserted to slow down playout
    recvStats.removedSamplesForAcceleration  // samples removed to speed up playout
    recvStats.totalSamplesDuration           // total duration of received samples, in seconds
    recvStats.audioLevel                     // most recent audio level, range [0.0, 1.0]
    recvStats.totalAudioEnergy               // accumulated audio energy
}
if (statistics.statisticsType == ZoomVideoSDKDataType_Audio &&
    statistics.direction == ZoomVideoSDKStatisticsDirection_Receive) {
    ZoomVideoSDKQOSRecvStatistics *recvStats = (ZoomVideoSDKQOSRecvStatistics *)statistics;
    recvStats.packetsDiscarded;               // non-FEC packets discarded by NetEQ
    recvStats.fecPacketsReceived;             // total FEC packets received
    recvStats.fecPacketsDiscarded;            // FEC packets discarded by NetEQ
    recvStats.jitterBufferTargetDelay;        // accumulated target delay, in seconds
    recvStats.jitterBufferMinimumDelay;       // accumulated minimum achievable delay, in seconds
    recvStats.totalSamplesReceived;           // total samples generated by NetEQ
    recvStats.concealedSamples;               // samples produced by concealment
    recvStats.silentConcealedSamples;         // subset of concealedSamples that are silent
    recvStats.concealmentEvents;              // number of concealment events
    recvStats.insertedSamplesForDeceleration; // samples inserted to slow down playout
    recvStats.removedSamplesForAcceleration;  // samples removed to speed up playout
    recvStats.totalSamplesDuration;           // total duration of received samples, in seconds
    recvStats.audioLevel;                     // most recent audio level, range [0.0, 1.0]
    recvStats.totalAudioEnergy;               // accumulated audio energy
}

Sharing quality

When statisticsType is .share, the callback provides sharing metrics such as frame dimensions, frame rate, and bitrate. To receive share statistics, there must be at least two users in the session with at least one sharing.