Skip to main content
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

  1. Enable only when needed: Statistics collection has minimal but measurable overhead
  2. Process on background queue: Don’t block the delegate callback
  3. Use aggregation: Compute averages over time windows for smoother metrics
  4. Monitor key metrics: Focus on bitrate, packet loss, and quality limitations
  5. 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

Build docs developers (and LLMs) love