The LiveKit Swift SDK provides comprehensive statistics for monitoring track quality, performance, and network conditions. Statistics are based on the WebRTC stats specification and include detailed metrics for audio, video, and transport layers.
Overview
Track statistics provide insight into:
- Codec information and configuration
- Bandwidth usage and bitrates
- Packet loss and jitter
- Frame rates and resolutions
- Quality limitations (CPU, bandwidth)
- Network transport metrics
Enabling Statistics
Enable statistics reporting in RoomOptions:
import LiveKit
let roomOptions = RoomOptions(
reportRemoteTrackStatistics: true
)
let room = Room(roomOptions: roomOptions)
Statistics collection has minimal overhead but may impact battery life on mobile devices. Enable only when needed for monitoring or debugging.
Receiving Statistics
Implement TrackDelegate to receive statistics updates:
class MyTrackDelegate: TrackDelegate {
func track(
_ track: Track,
didUpdateStatistics statistics: TrackStatistics,
simulcastStatistics: [VideoCodec: TrackStatistics]
) {
// Process statistics
print("Track statistics updated")
}
}
// Attach delegate
track.add(delegate: MyTrackDelegate())
TrackStatistics
The main statistics container:
public class TrackStatistics {
public let codec: [CodecStatistics]
public let transportStats: TransportStatistics?
public let videoSource: [VideoSourceStatistics]
public let certificate: [CertificateStatistics]
public let iceCandidatePair: [IceCandidatePairStatistics]
public let localIceCandidate: LocalIceCandidateStatistics?
public let remoteIceCandidate: RemoteIceCandidateStatistics?
public let inboundRtpStream: [InboundRtpStreamStatistics]
public let outboundRtpStream: [OutboundRtpStreamStatistics]
public let remoteInboundRtpStream: [RemoteInboundRtpStreamStatistics]
public let remoteOutboundRtpStream: [RemoteOutboundRtpStreamStatistics]
}
Common Statistics
Codec Statistics
func track(_ track: Track, didUpdateStatistics statistics: TrackStatistics, simulcastStatistics: [VideoCodec: TrackStatistics]) {
for codec in statistics.codec {
print("Codec: \(codec.mimeType ?? "unknown")")
print("Payload type: \(codec.payloadType ?? 0)")
print("Clock rate: \(codec.clockRate ?? 0)")
}
}
Inbound RTP Stream (Receiving)
for stream in statistics.inboundRtpStream {
// Video metrics
if let width = stream.frameWidth, let height = stream.frameHeight {
print("Resolution: \(width)×\(height)")
}
if let fps = stream.framesPerSecond {
print("FPS: \(fps)")
}
// Network metrics
if let bytesReceived = stream.bytesReceived {
print("Bytes received: \(bytesReceived)")
}
if let packetsLost = stream.packetsLost {
print("Packets lost: \(packetsLost)")
}
if let jitter = stream.jitter {
print("Jitter: \(jitter)s")
}
// Decoder info
if let decoder = stream.decoderImplementation {
print("Decoder: \(decoder)")
}
}
Outbound RTP Stream (Sending)
for stream in statistics.outboundRtpStream {
// Video metrics
if let width = stream.frameWidth, let height = stream.frameHeight {
print("Encoding: \(width)×\(height)")
}
if let fps = stream.framesPerSecond {
print("FPS: \(fps)")
}
// Network metrics
if let bytesSent = stream.bytesSent {
print("Bytes sent: \(bytesSent)")
}
if let targetBitrate = stream.targetBitrate {
print("Target bitrate: \(targetBitrate / 1000) kbps")
}
// Quality limitations
if let reason = stream.qualityLimitationReason {
switch reason {
case .bandwidth:
print("Quality limited by bandwidth")
case .cpu:
print("Quality limited by CPU")
case .none:
print("No quality limitation")
case .other:
print("Quality limited by other factors")
}
}
// Encoder info
if let encoder = stream.encoderImplementation {
print("Encoder: \(encoder)")
}
}
Transport Statistics
if let transport = statistics.transportStats {
print("Packets sent: \(transport.packetsSent ?? 0)")
print("Packets received: \(transport.packetsReceived ?? 0)")
print("Bytes sent: \(transport.bytesSent ?? 0)")
print("Bytes received: \(transport.bytesReceived ?? 0)")
if let dtlsState = transport.dtlsState {
print("DTLS state: \(dtlsState)")
}
if let iceState = transport.iceState {
print("ICE state: \(iceState)")
}
}
Simulcast Statistics
When simulcast is enabled, statistics are provided per codec/layer:
func track(
_ track: Track,
didUpdateStatistics statistics: TrackStatistics,
simulcastStatistics: [VideoCodec: TrackStatistics]
) {
// Main track statistics
print("Main track: \(statistics)")
// Per-codec/layer statistics
for (codec, stats) in simulcastStatistics {
print("Codec \(codec):")
for stream in stats.outboundRtpStream {
let rid = stream.rid ?? "unknown"
let active = stream.active ?? false
let bytesSent = stream.bytesSent ?? 0
print(" Layer \(rid): active=\(active), bytes=\(bytesSent)")
}
}
}
Sorting Simulcast Layers
// Sort outbound streams by RID index (high to low)
let sortedStreams = statistics.outboundRtpStream.sortedByRidIndex()
for stream in sortedStreams {
print("RID: \(stream.rid ?? "unknown")")
print("Resolution: \(stream.frameWidth ?? 0)×\(stream.frameHeight ?? 0)")
}
Bitrate Calculation
Calculate bitrate from cumulative byte counts:
class BitrateCalculator: TrackDelegate {
private var previousStats: OutboundRtpStreamStatistics?
private var previousTime: Date?
func track(
_ track: Track,
didUpdateStatistics statistics: TrackStatistics,
simulcastStatistics: [VideoCodec: TrackStatistics]
) {
guard let stream = statistics.outboundRtpStream.first else {
return
}
defer {
previousStats = stream
previousTime = Date()
}
guard let previous = previousStats,
let prevTime = previousTime,
let bytesSent = stream.bytesSent,
let prevBytesSent = previous.bytesSent else {
return
}
let deltaBytes = bytesSent - prevBytesSent
let deltaTime = Date().timeIntervalSince(prevTime)
let bitrate = Double(deltaBytes * 8) / deltaTime
print("Bitrate: \(bitrate / 1_000_000) Mbps")
}
}
Quality Limitation Durations
for stream in statistics.outboundRtpStream {
if let durations = stream.qualityLimitationDurations {
print("Quality limitation durations:")
if let none = durations.none {
print(" None: \(none)s")
}
if let cpu = durations.cpu {
print(" CPU: \(cpu)s")
}
if let bandwidth = durations.bandwidth {
print(" Bandwidth: \(bandwidth)s")
}
if let other = durations.other {
print(" Other: \(other)s")
}
}
}
Network Metrics
ICE Candidate Pair
for pair in statistics.iceCandidatePair {
if let state = pair.state {
print("ICE pair state: \(state)")
}
if let rtt = pair.currentRoundTripTime {
print("RTT: \(rtt * 1000) ms")
}
if let availableBitrate = pair.availableOutgoingBitrate {
print("Available bitrate: \(availableBitrate / 1_000_000) Mbps")
}
}
Local ICE Candidate
if let local = statistics.localIceCandidate {
print("Local candidate type: \(local.candidateType?.rawValue ?? "unknown")")
print("Local address: \(local.address ?? "unknown")")
print("Local port: \(local.port ?? 0)")
}
Remote ICE Candidate
if let remote = statistics.remoteIceCandidate {
print("Remote candidate type: \(remote.candidateType?.rawValue ?? "unknown")")
print("Remote address: \(remote.address ?? "unknown")")
print("Remote port: \(remote.port ?? 0)")
}
Audio Metrics
Audio Source (Outbound)
for source in statistics.videoSource {
if let audioSource = source as? AudioSourceStatistics {
if let audioLevel = audioSource.audioLevel {
print("Audio level: \(audioLevel)")
}
if let echoReturnLoss = audioSource.echoReturnLoss {
print("Echo return loss: \(echoReturnLoss) dB")
}
}
}
Audio Reception (Inbound)
for stream in statistics.inboundRtpStream {
if let jitterBufferDelay = stream.jitterBufferDelay {
print("Jitter buffer delay: \(jitterBufferDelay)s")
}
if let concealedSamples = stream.concealedSamples {
print("Concealed samples: \(concealedSamples)")
}
}
Video Source Metrics
for source in statistics.videoSource {
if let width = source.width, let height = source.height {
print("Source resolution: \(width)×\(height)")
}
if let fps = source.framesPerSecond {
print("Source FPS: \(fps)")
}
}
Advanced Monitoring
Frame Drop Detection
for stream in statistics.inboundRtpStream {
if let framesReceived = stream.framesReceived,
let framesDecoded = stream.framesDecoded,
let framesDropped = stream.framesDropped {
let dropRate = Double(framesDropped) / Double(framesReceived)
print("Frame drop rate: \(dropRate * 100)%")
}
}
Freeze Detection
for stream in statistics.inboundRtpStream {
if let freezeCount = stream.freezeCount,
let totalFreezesDuration = stream.totalFreezesDuration {
print("Freezes: \(freezeCount) (total duration: \(totalFreezesDuration)s)")
}
}
Encoder/Decoder Implementation
for stream in statistics.outboundRtpStream {
if let encoder = stream.encoderImplementation {
print("Encoder: \(encoder)")
}
if let powerEfficient = stream.powerEfficientEncoder {
print("Power efficient encoder: \(powerEfficient)")
}
}
for stream in statistics.inboundRtpStream {
if let decoder = stream.decoderImplementation {
print("Decoder: \(decoder)")
}
if let powerEfficient = stream.powerEfficientDecoder {
print("Power efficient decoder: \(powerEfficient)")
}
}
SwiftUI Integration
Use TrackDelegateObserver for SwiftUI:
import SwiftUI
import LiveKit
struct TrackStatsView: View {
@StateObject private var observer: TrackDelegateObserver
init(track: Track) {
_observer = StateObject(wrappedValue: TrackDelegateObserver(track: track))
}
var body: some View {
VStack {
if let stream = observer.statistics?.inboundRtpStream.first {
Text("Resolution: \(stream.frameWidth ?? 0)×\(stream.frameHeight ?? 0)")
Text("FPS: \(stream.framesPerSecond ?? 0, specifier: "%.1f")")
Text("Jitter: \(stream.jitter ?? 0, specifier: "%.3f")s")
}
ForEach(observer.allStatistics, id: \.id) { stat in
Text("Stat: \(stat.type.rawValue)")
}
}
}
}
Statistics Types
All available statistics types:
public enum StatisticsType: String {
case codec
case inboundRtp = "inbound-rtp"
case outboundRtp = "outbound-rtp"
case remoteInboundRtp = "remote-inbound-rtp"
case remoteOutboundRtp = "remote-outbound-rtp"
case mediaSource = "media-source"
case mediaPlayout = "media-playout"
case peerConnection = "peer-connection"
case dataChannel = "data-channel"
case transport
case candidatePair = "candidate-pair"
case localCandidate = "local-candidate"
case remoteCandidate = "remote-candidate"
case certificate
}
Best Practices
- Enable only when needed: Statistics collection has minimal but measurable overhead
- Process on background queue: Don’t block the delegate callback
- Use aggregation: Compute averages over time windows for smoother metrics
- Monitor key metrics: Focus on bitrate, packet loss, and quality limitations
- Alert on anomalies: Set thresholds for packet loss, RTT, and frame drops
Reference
- WebRTC Stats Specification
- Source code:
Sources/LiveKit/Types/Statistics.swift
- Source code:
Sources/LiveKit/Types/TrackStatistics.swift