1

i have a problem described in title. you may see source code in my repository (https://github.com/Hudayberdyyev/custom_download_manager) . i will try to briefly explain the problem. I am trying to write a download manager based on this repo (https://github.com/r-plus/HLSion). and basically it consists of 3 parts:

  1. SessionManager (Which managed all of sessions)
  2. HLSData (HLSData model which initialized same as the code below. it is like an intermediary between the session manager )
public convenience init(url: URL, options: [String: Any]? = nil, name: String) {
        let urlAsset = AVURLAsset(url: url, options: options)
        self.init(asset: urlAsset, description: name)
}
  1. AssetStore (It's managed HLSData.plist file. Which contain name and path of each download session).

this is how the start of downloads is implemented:

var sources = [HLSData]()
@objc func startDownloadButtonTapped() {
        print(#function)
        let hlsData = sources[0]
        switch hlsData.state {
        case .notDownloaded:
            hlsData.download { (percent) in
                DispatchQueue.main.async {
                    print("percent = \(percent)")
                    self.percentLabel.text = "\(percent)"
                }
            }.finish { (relativePath) in
                DispatchQueue.main.async {
                    print("download completed relative path = \(relativePath)")
                }
            }.onError { (error) in
                print("Error finish. \(error)")
            }
        case .downloading:
            print("State is downloading")
            break
        case .downloaded:
            print(hlsData.localUrl ?? "localURL is nil")
        }
}

Before tapping state is notDownloaded. respectively app is start download when the button tapped and state is changed to downloading. Everything is works fine and progress tracked well. But when i go to the background and return back to app, state is still keep of downloading, but progress closure doesn't work anymore. How can i restore or reset this closures for tracking progress. Thanks in advance.

1 Answers1

1

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

  1. When going into the background, the download continues
  2. When coming into the foreground from the background, the AVAssetDownloadDelegate still triggers assetDownloadTask didLoad totalTimeRangesLoaded and the progress can be updated
  3. 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:

  1. Subscribe to the UIApplication.willEnterForegroundNotification notification
  2. In the call back for the UIApplication.willEnterForegroundNotification, check if the device is running iOS 12 and below
  3. If it does, cancel the current AVAssetDownloadTask
  4. This should trigger the AVAssetDownloadDelegate callback assetDownloadTask didFinishDownloadingTo which will give you the location of the partially downloaded file
  5. Reconfigure the AVAssetDownloadTask but do not configure it with the HLS url, instead configure it with the URL to the partially downloaded asset
  6. 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

Shawn Frank
  • 4,381
  • 2
  • 19
  • 29