On doing some tests, I feel there is a bug in iOS 12 and below
with the AVAssetDownloadDelegate
When doing some tests, I noticed the following when trying to download media over HLS using AVAssetDownloadTask
:
iOS 13 and above
- When going into the background, the download continues
- When coming into the foreground from the background, the
AVAssetDownloadDelegate
still triggers assetDownloadTask didLoad totalTimeRangesLoaded
and the progress can be updated
- After suspending or quitting an app, reinitializing an
AVAssetDownloadURLSession
with the same URLSessionConfiguration identifier
, the download resumes automatically from where it last left off
iOS 12 and below
Everything still almost holds true except point 2, for some reason the assetDownloadTask didLoad totalTimeRangesLoaded
no longer gets triggered when coming into the foreground from the background and so the progress no longer gets updated.
One workaround I got was from this answer https://stackoverflow.com/a/55847387/1619193
was that in the past, downloads had to be resumed manually after the app was suspended for AVAssetDownloadTask
by providing it a location to the partially downloaded file on disk.
As per the documentation:
AVAssetDownloadTask provides the ability to resume previously stopped
downloads under certain circumstances. To do so, simply instantiate a
new AVAssetDownloadTask with an AVURLAsset instantiated with a file
NSURL pointing to the partially downloaded bundle with the desired
download options, and the download will continue restoring any
previously downloaded data.
Interestingly, you cannot find this on the official documentation anymore and also it seems like setting the destinationURL
has been deprecated so it seems like there has been some refactoring in how things work.
My solution:
- Subscribe to the
UIApplication.willEnterForegroundNotification
notification
- In the call back for the
UIApplication.willEnterForegroundNotification
, check if the device is running iOS 12 and below
- If it does, cancel the current
AVAssetDownloadTask
- This should trigger the
AVAssetDownloadDelegate
callback assetDownloadTask didFinishDownloadingTo
which will give you the location of the partially downloaded file
- Reconfigure the
AVAssetDownloadTask
but do not configure it with the HLS url, instead configure it with the URL to the partially downloaded asset
- Resume the download and the progress
AVAssetDownloadDelegate
will seem to start firing again
You can download an example of this here
Here are some small snippets of the above steps:
private let downloadButton = UIButton(type: .system)
private let downloadTaskIdentifier = "com.mindhyve.HLSDOWNLOADER"
private var backgroundConfiguration: URLSessionConfiguration?
private var assetDownloadURLSession: AVAssetDownloadURLSession!
private var downloadTask: AVAssetDownloadTask!
override func viewDidLoad()
{
super.viewDidLoad()
// UI configuration left out intentionally
subscribeToNotifications()
initializeDownloadSession()
}
private func initializeDownloadSession()
{
// This will create a new configuration if the identifier does not exist
// Otherwise, it will reuse the existing identifier which is how a download
// task resumes
backgroundConfiguration
= URLSessionConfiguration.background(withIdentifier: downloadTaskIdentifier)
// Resume will happen automatically when this configuration is made
assetDownloadURLSession
= AVAssetDownloadURLSession(configuration: backgroundConfiguration!,
assetDownloadDelegate: self,
delegateQueue: OperationQueue.main)
}
private func resumeDownloadTask()
{
var sourceURL = getHLSSourceURL(.large)
// Now Check if we have any previous download tasks to resume
if let destinationURL = destinationURL
{
sourceURL = destinationURL
}
if let sourceURL = sourceURL
{
let urlAsset = AVURLAsset(url: sourceURL)
downloadTask = assetDownloadURLSession.makeAssetDownloadTask(asset: urlAsset,
assetTitle: "Movie",
assetArtworkData: nil,
options: nil)
downloadTask.resume()
}
}
func cancelDownloadTask()
{
downloadTask.cancel()
}
private func getHLSSourceURL(_ size: HLSSampleSize) -> URL?
{
if size == .large
{
return URL(string: "https://video.film.belet.me/45505/480/ff27c84a-6a13-4429-b830-02385592698b.m3u8")
}
return URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8")
}
// MARK: INTENTS
@objc
private func downloadButtonTapped()
{
print("\(downloadButton.titleLabel!.text!) tapped")
if downloadTask != nil,
downloadTask.state == .running
{
cancelDownloadTask()
}
else
{
resumeDownloadTask()
}
}
@objc
private func didEnterForeground()
{
if #available(iOS 13.0, *) { return }
// In iOS 12 and below, there seems to be a bug with AVAssetDownloadDelegate.
// It will not give you progress when coming from the background so we cancel
// the task and resume it and you should see the progress in maybe 5-8 seconds
if let downloadTask = downloadTask
{
downloadTask.cancel()
initializeDownloadSession()
resumeDownloadTask()
}
}
private func subscribeToNotifications()
{
NotificationCenter.default.addObserver(self,
selector: #selector(didEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil)
}
// MARK: AVAssetDownloadDelegate
func urlSession(_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?)
{
guard error != nil else
{
// download complete, do what you want
return
}
// something went wrong, handle errors
}
func urlSession(_ session: URLSession,
assetDownloadTask: AVAssetDownloadTask,
didFinishDownloadingTo location: URL)
{
// Save the download path of the task to resume downloads
destinationURL = location
}
If something seems out of place, I recommend checking out the full working example here