Skip to content

Statistics

Track.statistics holds the latest TrackStatistics of a track: what the camera or microphone captures, what is sent or received, and how the connection to the other side is doing. It is nil until the first collection, and it stops updating when reporting is off. The numbers are grouped in five buckets, each nil where it does not apply:

Bucket What it describes
TrackStatistics.videoSource The frames a local video track captures, before encoding: VideoSourceStatistics.width, VideoSourceStatistics.height and VideoSourceStatistics.fps
TrackStatistics.outboundRtp What is sent: OutboundStatistics.width, OutboundStatistics.height and OutboundStatistics.fps for video, OutboundStatistics.audioFrameRate for audio
TrackStatistics.codec The video codec in use, CodecStatistics.mimeType, as video/VP8 or video/H264
TrackStatistics.inboundRtp What arrives on a remote track: size, rate, frames received and rendered, freezes, audio rate and level
TrackStatistics.iceCandidatePair The connection to the participant the remote track comes from

The track itself is on Tracks; which copy of a track goes to whom is on Track publications.

Turn reporting on

Statistics are off by default. Set Room.reportStatistics to true and the SDK collects them once a second for every track you send and receive; set it back to false and collection stops, which also stops the work it costs. You can flip it at any time, during a call included.

Each collection does two things: it writes Track.statistics, and it calls didUpdateStatistics on your RoomDelegate and on the participant's ParticipantDelegate, with the participant and the publication the numbers belong to. The event fires only when the numbers changed since the last one, and always on the main thread. Observe Track.statistics when you render one track; use the event when you keep a table of them.

import RevueSDK

/// Keep the latest statistics of every publication, yours and the ones you receive. The event names the
/// publication, so one local track sent to three participants gives three entries, one per recipient, and
/// the numbers in each event belong to that publication. Reporting is off until you turn it on.
final class StatisticsObserver: RoomDelegate {
    private(set) var latest: [String: TrackStatistics] = [:]   // keyed by publication.sid

    init() {
        Room.shared.reportStatistics = true
        Room.shared.delegates.add(delegate: self)
    }

    func room(_ room: Room, participant: Participant, trackPublication publication: TrackPublication,
              didUpdateStatistics statistics: TrackStatistics) {
        latest[publication.sid] = statistics
    }

    /// Frames per second you send in total: the sum over the publications of your video tracks.
    @MainActor
    var totalVideoFpsSent: Double {
        Room.shared.localParticipant.videoTracks
            .compactMap { latest[$0.sid]?.outboundRtp?.fps }
            .reduce(0, +)
    }
}

Two levels: the track and each publication

A local track is sent to each participant separately, so there are two kinds of numbers on your side, and they live in two places.

The track's own Track.statistics is the capture amount: videoSource is what the camera captures, its real width, height and frames per second, no matter how many participants you send it to. It does not change when you publish the track to a fourth participant or unpublish it from one, and it is the same whether the track is muted for some of them or not.

What you send to one participant is that participant's publication: the statistics on the LocalTrackPublication that goes to them hold the sent width, height, frames per second and codec for that recipient. They differ from one recipient to the next; a recipient whose publication is muted gets the placeholder picture, not your camera frames. The numbers of a publication are what didUpdateStatistics gives you with that publication: one event per publication, with its own numbers.

There is no total on the track. To know how much video you send in all, add up the publications yourself; to know how much this device uploads in all, read bytesSent of the connection statistics of each participant (see below), which count everything sent to them.

Bucket Local track Local publication Remote track and its publication
videoSource Capture size and rate nil nil
outboundRtp nil Sent to that recipient nil
codec nil Codec sent to that recipient Codec received
inboundRtp nil nil What arrives from that participant
iceCandidatePair nil nil The connection to that participant

Known issue

On a local track sent to several participants, Track.statistics still holds the numbers of whichever recipient was written last, and the sent width, height and fps in every didUpdateStatistics are the total over all recipients, not that recipient's share. The statistics property on LocalTrackPublication does not exist yet. Until this is fixed, read the capture size and rate from videoSource only, and treat outboundRtp on a local track as a total.

On the receiving side there is nothing to split: a remote track is one participant's track sent to you, so it has one publication and Track.statistics of the remote track is the whole picture.

What you receive

inboundRtp on a remote track says how the data from that participant arrives. For video, InboundStatistics.width, InboundStatistics.height and InboundStatistics.fps are the decoded size and rate. InboundStatistics.framesReceived counts every frame that arrived, including the placeholder frames of a muted publication; InboundStatistics.framesRendered counts the frames handed to your view, which excludes them. When framesReceived keeps growing and framesRendered stops, the sender muted you; when both stop, nothing is arriving. InboundStatistics.freezeCount and InboundStatistics.totalFreezesDuration count the gaps of more than a second between frames.

For audio, InboundStatistics.audioFrameRate is the number of audio segments arriving per second and InboundStatistics.audioLevel the raw level of the last segment. That level is a diagnostic value, not the smoothed Participant.audioLevel used for speaking detection.

Connection statistics: iceCandidatePair

iceCandidatePair is the connection to that participant, not a property of the track: the SDK measures it once per participant, and the audio and video tracks from the same participant share one IceCandidatePairStatistics. It is nil on your local tracks, because a local track goes to several participants and has no single connection; read it from any track you receive from the participant you are interested in.

Field Meaning
IceCandidatePairStatistics.rttMs Round-trip time in milliseconds; -1 until the first round trip has been measured, a few seconds into the call
IceCandidatePairStatistics.jitterMs, IceCandidatePairStatistics.packetLossRate Jitter in milliseconds and packet loss in percent
IceCandidatePairStatistics.uploadBitrateKbps, IceCandidatePairStatistics.downloadBitrateKbps, IceCandidatePairStatistics.remoteSuggestedBitrateKbps Available bitrate each way, and the bitrate the other side says it can take
IceCandidatePairStatistics.bytesSent, IceCandidatePairStatistics.bytesReceived, IceCandidatePairStatistics.packetsSent, IceCandidatePairStatistics.packetsReceived Totals since the connection started; take the difference between two readings for a rate
IceCandidatePairStatistics.connectionType 0 no connection, 1 relayed over TCP, 2 relayed over UDP, 3 direct UDP
IceCandidatePairStatistics.isReliable True when there is a connection and the round trip has been measured; before that the other fields read 0 for lack of data

Check isReliable before you show the other fields, and show the round trip as unknown while rttMs is -1. Participant.connectionQuality is the SDK's own rating of these numbers as a ConnectionQuality level, updated once a second and reported only when the level changes; see Connection quality.

import RevueSDK

/// The connection to one participant, read from any track you receive from them: their audio and video
/// tracks carry the same iceCandidatePair. rttMs is -1 until the first round trip is measured, and
/// connectionType 0 means no connection, so check isReliable before showing the other numbers.
func roundTripMilliseconds(to friendId: String) -> Int32? {
    guard let participant = Room.shared.remoteParticipants[friendId] else { return nil }
    let publication = participant.audioTracks.first ?? participant.videoTracks.first
    guard let pair = publication?.track?.statistics?.iceCandidatePair, pair.isReliable else { return nil }
    return pair.rttMs
}

Fields that do not apply are nil

The same OutboundStatistics and InboundStatistics serve audio and video tracks. A field that has no meaning for the kind of track is nil, never 0, so a 0 you read is a measured zero:

Track Fields that are nil
Audio track width, height and fps of outboundRtp and inboundRtp; videoSource and codec are nil too
Video track audioFrameRate of outboundRtp and inboundRtp, and audioLevel of inboundRtp