Published on

Debugging a spurious AVPlayer KVO Crash

Authors

Introduction

Yesterday we released NanoFlick, and we started to deal with the usual flurry of crash reports. The most difficult one related to a crash after a user had been taking shots and swiping between different shots that occurred on iOS 16 only. Technically, this involved loading, unloading, and reloading different video files multiple times. After the usual wrangling, I reproduce the problem, and am given the following crash log:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Cannot remove an observer <NSKeyValueObservance 0x1163f0a00> for the key path "currentItem.videoComposition" from <AVQueuePlayer 0x132137da0>, most likely because the value for the key "currentItem" has changed without an appropriate KVO notification being sent. Check the KVO-compliance of the AVQueuePlayer class.'

Troubleshooting

The first guess was that currentItem wasn't fully KVO compliant. Apple states that AVPlayer has KVO compliance, but it doesn't explicitly mention currentItem and KVO compliance in other places is often a bit of a moving target and makes me reluctant to trust in its implementation.

However, the crash persisted after removing every player listener I had. Clearly, we were not directly causing the KVO error, so I guessed "sdk bug" and tried to reproduce it in a simple project. This proved not easy, so I went back to staring at the crash stacktrace and tried to figure out any hints on the error origination, and that's when something stood out to me.

Fun challenge for the reader: can you spot the problem?

The offending backtrace

Looking at stack frames 12 and 13, we see AVVideoFrameVisualAnalyzer doing stuff with listeners, presumably on currentItem.videoComposition, directly leading to the crash. iOS 16 added a new feature called Live Video. Because it's on by default, it could cause the crash in any app that uses AVPlayer and was crash-free in iOS 15, which fits the characterization of NanoFlick.

Solution

With a solid lead, the solution should be straightforward: just set allowsVideoFrameAnalysis to false and we're done, right? The problem is that we're using SwiftUI's builtin VideoPlayer and it doesn't expose this property. The supported workaround is to use UIViewControllerRepresentable to wrap the AVPlayerViewController and set the property there. This is a bit of a pain, so I instead opted to swizzle AVPlayerViewController's viewWillAppear method to set the property there, piggybacking off of earlier work to remove the playback controls when the player is embedded in a CaptureView.

I could use swift native swizzling, but I already am using Lumos for other swizzling, so I decided to use that instead.

import Lumos
import AVKit
extension AVPlayerViewController {
    static let fixPlayerBackground: () = {
        Lumos.swizzle(
            type: .instance,
            originalClass: AVPlayerViewController.self,
            originalSelector: #selector(AVPlayerViewController.viewWillAppear(_:)),
            swizzledClass: AVPlayerViewController.self,
            swizzledSelector: #selector(AVPlayerViewController.fixedOnAppear(_:))
        )
    }()

    @objc private func fixedOnAppear(_ animated: Bool) {
        if String(describing: self.parent).contains("CaptureView") {
            self.showsPlaybackControls = false
            if #available(iOS 16.0, *) {
                self.allowsVideoFrameAnalysis = false
            }
        }
        self.fixedOnAppear(animated)
    }
}

Conclusion

It's fortunate that we don't need Live Video for NanoFlick. If we did, we'd probably have to do further investigation and see if we could graciously either handle the deregistration code, the KVO error, or create a mock player that didn't trigger the bug.

Hopefully, one day, Apple will switch to using Swift instead of Objective-C for its framework code and we'll see a reduction in SDK bugs as compiler-enforced KVO safety becomes the norm for SDK code.

Until then, we'll have to keep on swizzling.