Bitrate Adaptation in iOS

3 minute read

Published:

In my previous post, I talked about video encoding and streaming in iOS. This post is a quick guide to implementing adaptive bitrate streaming in iOS.

Table of Contents

Introduction

Adaptive bitrate streaming (ABR) is crucial for delivering high-quality video experiences while efficiently managing network resources. This post explores various approaches to implement ABR in iOS applications.

Understanding Adaptive Bitrate Streaming

How ABR Works

  • Segments video into small chunks
  • Provides multiple quality variants of each chunk
  • Client dynamically selects quality based on:
    • Network conditions
    • Device capabilities
    • Buffer status
    • User preferences

Common ABR Protocols

  • HLS (HTTP Live Streaming) - Apple’s native solution
  • DASH (Dynamic Adaptive Streaming over HTTP)
  • Smooth Streaming
  • HDS (HTTP Dynamic Streaming)

Native Implementation with AVFoundation

Basic HLS Setup with AVPlayer

class AdaptiveStreamingPlayer {
    private var player: AVPlayer?
    private var playerItem: AVPlayerItem?
    
    func setupStream(masterPlaylistURL: URL) {
        // Create asset with automatic bitrate adaptation
        let asset = AVURLAsset(url: masterPlaylistURL)
        playerItem = AVPlayerItem(asset: asset)
        
        // Configure preferred bitrate range
        playerItem?.preferredPeakBitRate = 2_000_000 // 2 Mbps maximum
        
        // Configure preferred forward buffer duration
        playerItem?.preferredForwardBufferDuration = 10 // 10 seconds
        
        player = AVPlayer(playerItem: playerItem)
    }
}

Advanced Bitrate Monitoring

extension AdaptiveStreamingPlayer {
    func monitorBitrateAdaptation() {
        // Observe current bitrate
        playerItem?.addObserver(self, forKeyPath: "accessLog", options: [.new], context: nil)
        
        // Monitor playback statistics
        Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
            guard let accessLog = self?.playerItem?.accessLog(),
                  let lastEvent = accessLog.events.last else {
                return
            }
            
            print("Current bitrate: \(lastEvent.indicatedBitrate)")
            print("Observable bitrate: \(lastEvent.observedBitrate)")
            print("Switch bitrate: \(lastEvent.switchBitrate)")
        }
    }
}

Custom Adaptation Logic

extension AdaptiveStreamingPlayer {
    func configureBitrateAdaptation() {
        let preferredBitrateRange = BitrateRange(
            minimum: 300_000,   // 300 Kbps
            maximum: 5_000_000  // 5 Mbps
        )
        
        // Configure based on network type
        let networkType = getCurrentNetworkType()
        switch networkType {
        case .wifi:
            playerItem?.preferredPeakBitRate = preferredBitrateRange.maximum
        case .cellular:
            playerItem?.preferredPeakBitRate = 1_000_000 // 1 Mbps for cellular
        case .none:
            playerItem?.preferredPeakBitRate = preferredBitrateRange.minimum
        }
    }
}

Third-Party Solutions

GCDWebServer

  • GitHub Repository
  • Useful for implementing custom streaming servers
  • Can be used to create local HLS servers

VersaPlayer

PrxPlayer

Google IMA SDK

Best Practices

1. Variant Stream Generation

struct StreamVariant {
    let bitrate: Int
    let resolution: CGSize
    let codec: String
}

let recommendedVariants = [
    StreamVariant(bitrate: 300_000, resolution: CGSize(width: 480, height: 270), codec: "H.264"),
    StreamVariant(bitrate: 800_000, resolution: CGSize(width: 640, height: 360), codec: "H.264"),
    StreamVariant(bitrate: 1_500_000, resolution: CGSize(width: 1280, height: 720), codec: "H.264"),
    StreamVariant(bitrate: 3_000_000, resolution: CGSize(width: 1920, height: 1080), codec: "H.264")
]

2. Network Monitoring

class NetworkMonitor {
    private let monitor = NWPathMonitor()
    
    func startMonitoring(bitrateHandler: @escaping (Int) -> Void) {
        monitor.pathUpdateHandler = { path in
            let recommendedBitrate: Int
            
            if path.usesInterfaceType(.wifi) {
                recommendedBitrate = 3_000_000 // 3 Mbps for WiFi
            } else if path.usesInterfaceType(.cellular) {
                recommendedBitrate = 800_000 // 800 Kbps for cellular
            } else {
                recommendedBitrate = 300_000 // 300 Kbps for unknown
            }
            
            bitrateHandler(recommendedBitrate)
        }
        
        monitor.start(queue: DispatchQueue.global())
    }
}

Conclusion

Implementing effective bitrate adaptation in iOS requires a combination of AVFoundation’s built-in capabilities and possibly third-party solutions. The key is to balance quality with network efficiency while maintaining a smooth viewing experience.

Key Takeaways

  • Utilize AVFoundation’s native HLS support
  • Monitor network conditions actively
  • Implement appropriate bitrate ranges
  • Consider third-party solutions for advanced needs

Resources