Let’s say we want to play a track from the server using AVKit’s AVPlayer. However, it’d be quite expensive to download the track over and over again. To reduce network traffic and preserve iPhone’s battery it’s better to cache the downloaded track on the user’s device after it’s downloaded in the first time. As we know, reading from disk is much cheaper in terms of resources than downloading a file over the network.

Unfortunately, AVPlayerItem doesn’t cache the downloaded data out of the box, so we need to do some work. Luckily, there are open-source repos covering that. We’ll use https://github.com/neekeetab/CachingPlayerItem for accessing the track’s data and https://github.com/hyperoslo/Cache for caching. In short, CachingPlayerItem is a simple subclass of AVPlayerItem exposing methods for retrieving the track’s data.

Here’s what we’d need to do:

  1. Get raw track data from CachingPlayerItem when it’s downloaded.
  2. Store raw data to the disk cache.
  3. When starting playing a track first check if it’s cached. If yes, play from cache. If no, stream it from the server using CachingPlayerItem.

I’ve prepared a simple class doing that:

import AVKit
import Cache

class AudioPlayerWorker {
    private var player: AVPlayer!

    let diskConfig = DiskConfig(name: "DiskCache")
    let memoryConfig = MemoryConfig(expiry: .never, countLimit: 10, totalCostLimit: 10)

    lazy var storage: Cache.Storage? = {
        return try? Cache.Storage(diskConfig: diskConfig, memoryConfig: memoryConfig)
    }()

    // MARK: - Logic

    /// Plays a track either from the network if it's not cached or from the cache.
    func play(with url: URL) {
        // Trying to retrieve a track from cache asynchronously.
        storage?.async.entry(ofType: Data.self, forKey: url.absoluteString, completion: { result in
            let playerItem: CachingPlayerItem
            switch result {
            case .error:
                // The track is not cached.
                playerItem = CachingPlayerItem(url: url)
            case .value(let entry):
                // The track is cached.
                playerItem = CachingPlayerItem(data: entry.object, mimeType: "audio/mpeg", fileExtension: "mp3")
            }
            playerItem.delegate = self
            self.player = AVPlayer(playerItem: playerItem)
            self.player.automaticallyWaitsToMinimizeStalling = false
            self.player.play()
        })
    }

}

// MARK: - CachingPlayerItemDelegate
extension AudioPlayerWorker: CachingPlayerItemDelegate {
    func playerItem(_ playerItem: CachingPlayerItem, didFinishDownloadingData data: Data) {
        // A track is downloaded. Saving it to the cache asynchronously.
        storage?.async.setObject(data, forKey: playerItem.url.absoluteString, completion: { _ in })
    }
}

Feel free to use this code in your projects!