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
}