The basics of handling HLS downloads

Now that we have addressed creating an AVAssetDownloadURLSession and both types of download tasks, it is time to cover how to handle downloads through its various stages.

AVFoundation informs us through a couple of methods defined in AVAssetDownloadDelegate. These callbacks are slightly different for the two types of download tasks. Since AVAggregateAssetDownloadTask is slightly more complex and provides us with more options, I will focus on callbacks for this type of download task in this blogpost.

The three most important method AVAssetDownloadDelegate has to offer are:

// 1. Tells the delegate the location this asset will be downloaded to.
func urlSession(_ session: URLSession,
                aggregateAssetDownloadTask: AVAggregateAssetDownloadTask,
                willDownloadTo location: URL)

// 2. Report progress updates for the aggregate download task
func urlSession(_ session: URLSession,
                aggregateAssetDownloadTask: AVAggregateAssetDownloadTask,
                didLoad timeRange: CMTimeRange,
                totalTimeRangesLoaded loadedTimeRanges: [NSValue],
                timeRangeExpectedToLoad: CMTimeRange,
                for mediaSelection: AVMediaSelection)

// 3. Tells the delegate that the task finished transferring data, either successfully or with an error
func urlSession(_ session: URLSession,
                task: URLSessionTask,
                didCompleteWithError error: Error?) 

Keeping track of your downloaded asset

The first callback you will receive provides you with the position the HLS video will be downloaded to. This callback is:

func urlSession(_ session: URLSession,
                aggregateAssetDownloadTask: AVAggregateAssetDownloadTask,
                willDownloadTo location: URL)

You should keep track of this position for later use. The HLS video should not be moved from this location to allow the system to optimise playback or to free up space when necessary.

Progress updates of AVAggregateAssetDownloadTask

Progress updates of AVAggregateAssetDownloadTask are different from regular download tasks. It does not provide bytesWritten and totalBytesWritten, instead, it returns didLoad: CMTimeRange, loadedTimeRanges: [NSValue] and timeRangeExpectedToLoad: CMTimeRange.AVAssetDownloadURLSession will provide progress updates periodically through:

func urlSession(_ session: URLSession,
                aggregateAssetDownloadTask: AVAggregateAssetDownloadTask,
                didLoad timeRange: CMTimeRange,
                totalTimeRangesLoaded loadedTimeRanges: [NSValue],
                timeRangeExpectedToLoad: CMTimeRange,
                for mediaSelection: AVMediaSelection)

To calculcate progress from this values, we need to iterate through loadedTimeRanges to calculate the progress, like so:

var progress = 0.0
for value in loadedTimeRanges where timeRangeExpectedToLoad.duration.seconds > 0 {
    let loadedTimeRange = value.timeRangeValue
    progress += loadedTimeRange.duration.seconds / timeRangeExpectedToLoad.duration.seconds
}

Completion and error handling

When a download has completed or when a download fails we will receive a callback on:

func urlSession(_ session: URLSession,
                task: URLSessionTask,
                didCompleteWithError error: Error?) 

On receiving this callback for a completed download, we want to identify which download was completed to inform our user. Easiest way is by checking the remote url, but to get this first we’ll need to type cast the generic URLSessionTask to AVAggregateAssetDownloadTask, like so:

guard let task = task as? AVAggregateAssetDownloadTask else { return }

let url = task.urlAsset.url

To know wether a download has failed, we can simply check for the error not being nil. After which we should identify the download to handle it accordingly.

There is a small catch here. AVAggregateAssetDownloadTask.urlAsset.url is a non-optional property and should always have a value. However, I identified some cases in which this property does not have a value and by accessing the property it will crash your app.

Luckily there is also a way to retrieve the remote url from the error:

guard let task = task as? AVAggregateAssetDownloadTask else { return }

// Check wether there is an error
if let error = error {
    let remoteURL: URL

    // Retrieve the remote url from the error
    if let urlString = (error as NSError).userInfo[NSURLErrorFailingURLStringErrorKey] as? String,
        let url = URL(string: urlString) {
        remoteURL = url
        } else {
        // Fallback to retrieving the remote url from `AVAggregateAssetDownloadTask` as a fallback.
        remoteURL = task.urlAsset.url
    }

    // Handle error here
}