-->

Error while implementing AVAssetDownloadURLSession

2020-07-23 04:38发布

问题:

I'm trying to implement an offline mode to a streaming application. The goal is to be able to download an HLS stream on the device of the user to make it possible to watch the stream even while the user is offline.

I have recently stumble on this tutorial. It seems to answer the exact requirements of what I was trying to implement but I'm facing a problem while trying to make it work.

I've created a little DownloadManager to apply the logic of the tutorial. Here is my singleton class:

import AVFoundation

class DownloadManager:NSObject {

static var shared = DownloadManager()
private var config: URLSessionConfiguration!
private var downloadSession: AVAssetDownloadURLSession!

override private init() {
    super.init()
    config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
    downloadSession = AVAssetDownloadURLSession(configuration: config, assetDownloadDelegate: self, delegateQueue: OperationQueue.main)
}

func setupAssetDownload(_ url: URL) {
    let options = [AVURLAssetAllowsCellularAccessKey: false]

    let asset = AVURLAsset(url: url, options: options)

    // Create new AVAssetDownloadTask for the desired asset
    let downloadTask = downloadSession.makeAssetDownloadTask(asset: asset,
                                                             assetTitle: "Test Download",
                                                             assetArtworkData: nil,
                                                             options: nil)
    // Start task and begin download
    downloadTask?.resume()
}

func restorePendingDownloads() {
    // Grab all the pending tasks associated with the downloadSession
    downloadSession.getAllTasks { tasksArray in
        // For each task, restore the state in the app
        for task in tasksArray {
            guard let downloadTask = task as? AVAssetDownloadTask else { break }
            // Restore asset, progress indicators, state, etc...
            let asset = downloadTask.urlAsset
            downloadTask.resume()
        }
    }
}

func playOfflineAsset() -> AVURLAsset? {
    guard let assetPath = UserDefaults.standard.value(forKey: "assetPath") as? String else {
        // Present Error: No offline version of this asset available
        return nil
    }
    let baseURL = URL(fileURLWithPath: NSHomeDirectory())
    let assetURL = baseURL.appendingPathComponent(assetPath)
    let asset = AVURLAsset(url: assetURL)
    if let cache = asset.assetCache, cache.isPlayableOffline {
        return asset
        // Set up player item and player and begin playback
    } else {
        return  nil
        // Present Error: No playable version of this asset exists offline
    }
}

func getPath() -> String {
    return UserDefaults.standard.value(forKey: "assetPath") as? String ?? ""
}

func deleteOfflineAsset() {
    do {
        let userDefaults = UserDefaults.standard
        if let assetPath = userDefaults.value(forKey: "assetPath") as? String {
            let baseURL = URL(fileURLWithPath: NSHomeDirectory())
            let assetURL = baseURL.appendingPathComponent(assetPath)
            try FileManager.default.removeItem(at: assetURL)
            userDefaults.removeObject(forKey: "assetPath")
        }
    } catch {
        print("An error occured deleting offline asset: \(error)")
    }
}
}

extension DownloadManager: AVAssetDownloadDelegate {
    func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange) {
        var percentComplete = 0.0
        // Iterate through the loaded time ranges
        for value in loadedTimeRanges {
        // Unwrap the CMTimeRange from the NSValue
        let loadedTimeRange = value.timeRangeValue
        // Calculate the percentage of the total expected asset duration
        percentComplete += loadedTimeRange.duration.seconds / timeRangeExpectedToLoad.duration.seconds
    }
        percentComplete *= 100

    debugPrint("Progress \( assetDownloadTask) \(percentComplete)")

    let params = ["percent": percentComplete]
    NotificationCenter.default.post(name: NSNotification.Name(rawValue: "completion"), object: nil, userInfo: params)
    // Update UI state: post notification, update KVO state, invoke callback, etc.
}

func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
    // Do not move the asset from the download location
    UserDefaults.standard.set(location.relativePath, forKey: "assetPath")
}

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
    debugPrint("Download finished: \(location)")
}

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    debugPrint("Task completed: \(task), error: \(String(describing: error))")

    guard error == nil else { return }
    guard let task = task as? AVAssetDownloadTask else { return }

    print("DOWNLOAD: FINISHED")
}
}

My problem comes when I try to call my setupAssetDownload function. Everytime time I try to resume a downloadTask I get an error message in the urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) delegate function.

The log of the message is:

Task completed: <__NSCFBackgroundAVAssetDownloadTask: 0x7ff57fc024a0>{ taskIdentifier: 1 }, error: Optional(Error Domain=AVFoundationErrorDomain Code=-11800 \"The operation could not be completed\" UserInfo={NSLocalizedFailureReason=An unknown error occurred (-12780), NSLocalizedDescription=The operation could not be completed})

To give you all the relevant information the URL I past to my setupAssetDownload function is of type URL(string: "https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8")!

I been looking for a cause and solution for this error but I don't seem to be able to find one for the time being. I would be very grateful for any tips or any clues on how resolve this issue or any indication of errors in my singleton implementation that could explain this behaviour.

Thank you in advance.

Martin

EDIT:

It seems that this bug occurs on a simulator. I launch my app on a real device and the download started without any problem. Hope this helps. Still don't understand why I cannot try this behaviour on a simulator.