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 |