Search code examples
iosasynchronousavfoundationavasset

Asset property of AVAssetTrack is nil sometimes


I'm trying to get the asset property from an AVAssetTrack object, but it's nil sometimes. It seems like the problem occurs only after I use Dispatch.main.async.

According to the documentation, it's necessary to use loadValuesAsynchronously(forKeys:, completion:) to avoid blocking the main thread, and return to the main thread after loading is done.

let asset = AVURLAsset(url: videoInAppBundleURL)
let track = asset.tracks(withMediaType: .video).first!
assert(track.asset != nil) // passes
track.loadValuesAsynchronously(forKeys: [#keyPath(AVAssetTrack.asset)]) {
    assert(track.asset != nil) // passes
    DispatchQueue.main.async {
        assert(track.asset != nil) // FAILS
        // [...]
    }
}

What I found out is:

  • It makes no difference whether I'm running on a device or the simulator.
  • It seems not to be a problem with the video / videoURL. The video is part of the main bundle, I tried both .mp4 and .mov files and I made sure the video works by displaying it via an AVPlayerViewController.

Here is a working demo project.

I'm also wondering: why is AVAssetTrack's asset property optional? (all!! the other properties are non optional)

Note: this question has been edited after reading Matt's helpful comments and further investigation.


Solution

  • I reproduced the issue, with some tweaking of your github example, like this:

        let asset = AVURLAsset(url: videoInAppBundleURL)
        let tracksKey = #keyPath(AVAsset.tracks)
        asset.loadValuesAsynchronously(forKeys: [tracksKey]) {
            let track = asset.tracks(withMediaType: .video).first!
            DispatchQueue.main.async {
                assert(track.asset != nil) // fails
            }
        }
    

    Okay, but now watch closely as I perform an amazing trick:

        let asset = AVURLAsset(url: videoInAppBundleURL)
        let tracksKey = #keyPath(AVAsset.tracks)
        asset.loadValuesAsynchronously(forKeys: [tracksKey]) {
            let track = asset.tracks(withMediaType: .video).first!
            DispatchQueue.main.async {
                print(asset) // <-- amazing trick
                assert(track.asset != nil) // passes!
            }
        }
    

    Whoa! All I did was add a print statement — and now suddenly the very same assertion passes. This in fact is parallel to your original statement (which you later edited out) that "Sometimes the problems are gone, when stepping through the code with the debugger.”

    So, now, my suspicions being thoroughly aroused, I did something unbelievably clever (even if I do say so myself). I removed the print(asset), but I switched the scheme’s configuration from Debug to Release. Presto, the assertion still passes.

    So what you’ve found is a quirk of the compiler — dare I call it a bug?

    But wait, there’s more. You asked, quite reasonably, why asset is Optional. It’s because it’s weak:

    weak open var asset: AVAsset? { get }
    

    So there’s your answer. The track has only a weak reference to its asset. If we pass the track down into an asynchronous queue, and we do not bring the asset itself along with us, then the weak reference lets go and the asset is lost — in a Debug build.

    Hope this helps. You are probably waiting for me to make some grand conclusory statement about whether this constitutes a bug, but I’m not going to, sorry. I’ve provided two workarounds (use a Release build, or deliberately carry the asset reference down into the async queue) and that’s as far as I can go.