iosswiftswiftuivideoavplayer

Navigating away from video causes AVPlayer NSInternalInconsistencyException crash


I have a video player view that plays a video based on a URL. When I navigate away from the video I get a crash and the error below is printed. In my deinit I tried invalidating the observer but that didn't work. I'm missing a step in my deinit function that's causing this error.

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Cannot remove an observer <NSKeyValueObservance 0x2809113b0> for the key path "currentItem.videoComposition from <AVQueuePlayer 0x28079dfc0> 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

This is throw on this code:

import Foundation
import AVKit
import SwiftUI
import UIKit
import Combine

public class LegacyAVPlayerViewController: AVPlayerViewController {
   var onPlayerStatusChange: ((AVPlayer.TimeControlStatus) -> Void)?

   var overlayViewController: UIViewController! {
       willSet { assert(overlayViewController == nil, "contentViewController should be set only once") }
       didSet { attach() }
   }

   var overlayView: UIView { overlayViewController.view }

   private func attach() {
       guard
           let overlayViewController = overlayViewController,
           overlayViewController.parent == nil
       else {
           return
       }

       contentOverlayView?.addSubview(overlayView)
       overlayView.backgroundColor = .clear
       overlayView.sizeToFit()
       overlayView.translatesAutoresizingMaskIntoConstraints = false
       NSLayoutConstraint.activate(contentConstraints)
   }

   private lazy var contentConstraints: [NSLayoutConstraint] = {
       guard let overlay = contentOverlayView else { return [] }
       return [
           overlayView.topAnchor.constraint(equalTo: overlay.topAnchor),
           overlayView.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
           overlayView.bottomAnchor.constraint(equalTo: overlay.bottomAnchor),
           overlayView.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
       ]
   }()

   private var rateObserver: NSKeyValueObservation?

   public override var player: AVPlayer? {
       willSet { rateObserver?.invalidate() }
       didSet { rateObserver = player?.observe(\AVPlayer.rate, options: [.new], changeHandler: rateHandler(_:change:)) }
   }

   deinit { rateObserver?.invalidate() }

   private func rateHandler(_ player: AVPlayer, change: NSKeyValueObservedChange<Float>) {
       guard let item = player.currentItem,
             item.currentTime().seconds > 0.5,
             player.status == .readyToPlay
       else { return }

       onPlayerStatusChange?(player.timeControlStatus)
   }
}

public struct LegacyVideoPlayer<Overlay: View>: UIViewControllerRepresentable {
   var overlay: () -> Overlay
   let url: URL

   var onTimeControlStatusChange: ((AVPlayer.TimeControlStatus) -> Void)?

   @State var isPlaying = true
   @State var isLooping = true
   @State var showsPlaybackControls = false

   public func makeCoordinator() -> CustomPlayerCoordinator<Overlay> {
       CustomPlayerCoordinator(customPlayer: self)
   }

   public func makeUIViewController(context: UIViewControllerRepresentableContext<LegacyVideoPlayer>) -> LegacyAVPlayerViewController {
       let controller = LegacyAVPlayerViewController()

       controller.delegate = context.coordinator
       makeAVPlayer(in: controller, context: context)
       playIfNeeded(controller.player)

       return controller
   }

   public func updateUIViewController(_ uiViewController: LegacyAVPlayerViewController, context: UIViewControllerRepresentableContext<LegacyVideoPlayer>) {
       makeAVPlayer(in: uiViewController, context: context)
       playIfNeeded(uiViewController.player)
       updateOverlay(in: uiViewController, context: context)
   }

   private func updateOverlay(in controller: LegacyAVPlayerViewController, context: UIViewControllerRepresentableContext<LegacyVideoPlayer>) {
       guard let hostController = controller.overlayViewController as? UIHostingController<Overlay> else {
           let host = UIHostingController(rootView: overlay())
           
           controller.overlayViewController = host
           return
       }

       hostController.rootView = overlay()
   }

   private func makeAVPlayer(in controller: LegacyAVPlayerViewController, context: UIViewControllerRepresentableContext<LegacyVideoPlayer>) {
       if isLooping {
           let item = AVPlayerItem(url: url)
           let player = AVQueuePlayer(playerItem: item)
           let loopingPlayer = AVPlayerLooper(player: player, templateItem: item)
           controller.videoGravity = AVLayerVideoGravity.resizeAspectFill
           context.coordinator.loopingPlayer = loopingPlayer
           controller.player = player
       } else {
           controller.player = AVPlayer(url: url)
       }

       controller.showsPlaybackControls = showsPlaybackControls

       controller.onPlayerStatusChange = onTimeControlStatusChange
   }

   private func playIfNeeded(_ player: AVPlayer?) {
       if isPlaying { player?.play() }
       else { player?.pause() }
   }
}

public class CustomPlayerCoordinator<Overlay: View>: NSObject, AVPlayerViewControllerDelegate, AVPictureInPictureControllerDelegate {

   let customPlayer: LegacyVideoPlayer<Overlay>

   var loopingPlayer: AVPlayerLooper?

   public init(customPlayer: LegacyVideoPlayer<Overlay>) {
       self.customPlayer = customPlayer
       super.init()
   }

   public func playerViewController(_ playerViewController: AVPlayerViewController,
                                    restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
       completionHandler(true)
   }
}
public extension LegacyVideoPlayer {
   func play(_ isPlaying: Bool = true, isLooping: Bool = false) -> LegacyVideoPlayer {
       LegacyVideoPlayer(overlay: overlay,
                         url: url,
                         onTimeControlStatusChange: onTimeControlStatusChange,
                         isPlaying: isPlaying,
                         isLooping: isLooping,
                         showsPlaybackControls: showsPlaybackControls)
   }

   func onTimeControlStatusChange(_ onTimeControlStatusChange: @escaping (AVPlayer.TimeControlStatus) -> Void) -> LegacyVideoPlayer {
       LegacyVideoPlayer(overlay: overlay,
                         url: url,
                         onTimeControlStatusChange: onTimeControlStatusChange,
                         isPlaying: isPlaying,
                         isLooping: isLooping,
                         showsPlaybackControls: showsPlaybackControls)
   }

   func showingPlaybackControls(_ showsPlaybackControls: Bool = true) -> LegacyVideoPlayer {
       LegacyVideoPlayer(overlay: overlay,
                         url: url,
                         onTimeControlStatusChange: onTimeControlStatusChange,
                         isPlaying: isPlaying,
                         isLooping: isLooping,
                         showsPlaybackControls: showsPlaybackControls)
   }
}
extension LegacyVideoPlayer {
   public init(url: URL) where Overlay == EmptyView {
       self.init(url: url, overlay: { EmptyView() })
   }

   public init(url: URL, @ViewBuilder overlay: @escaping () -> Overlay) {
       self.url = url
       self.overlay = overlay
   }
}

Solution

  • I ran into a similar problem:

    Fatal Exception: NSInternalInconsistencyException Cannot remove an observer <NSKeyValueObservance 0x30333c780> for the key path "currentItem.videoComposition" from <AVQueuePlayer 0x303aa3c80>, 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.

    These are my findings:

    I did a deeper analysis of the crash call stack.

    Fatal Exception: NSInternalInconsistencyException
    0  CoreFoundation                 0x83f20 __exceptionPreprocess
    1  libobjc.A.dylib                0x16018 objc_exception_throw
    2  Foundation                     0x13c6ac -[NSKeyValueNestedProperty object:didRemoveObservance:recurse:]
    3  Foundation                     0x13cf60 -[NSObject(NSKeyValueObserverRegistration) _removeObserver:forProperty:]
    4  Foundation                     0x13ce00 -[NSObject(NSKeyValueObserverRegistration) removeObserver:forKeyPath:]
    5  Foundation                     0x13c574 -[NSKeyValueNestedProperty object:didRemoveObservance:recurse:]
    6  Foundation                     0x13cf60 -[NSObject(NSKeyValueObserverRegistration) _removeObserver:forProperty:]
    7  Foundation                     0x13ce00 -[NSObject(NSKeyValueObserverRegistration) removeObserver:forKeyPath:]
    8  Foundation                     0x13cd18 -[NSObject(NSKeyValueObserverRegistration) removeObserver:forKeyPath:context:]
    9  AVKit                          0xa04e8 -[AVProxyKVOObserver stopObserving]
    10 AVKit                          0x37e30 -[AVObservationController _stopAllObservation]
    11 AVKit                          0x37d64 -[AVObservationController stopAllObservation]
    12 AVKit                          0x9f63c -[AVVideoFrameVisualAnalyzer _updateObserversIfNeeded]
    13 AVKit                          0x9f7cc -[AVVideoFrameVisualAnalyzer _updateActualEnabledStateIfNeeded]
    14 AVKit                          0x4fa4 __105-[AVObservationController startObserving:keyPaths:includeInitialValue:includeChanges:observationHandler:]_block_invoke
    15 AVKit                          0x4e80 -[AVProxyKVOObserver _handleValueChangeForKeyPath:ofObject:oldValue:newValue:context:]
    16 AVKit                          0xa4a0 -[AVProxyKVOObserver observeValueForKeyPath:ofObject:change:context:]
    17 Foundation                     0x1a684 NSKeyValueNotifyObserver
    18 Foundation                     0x1a378 NSKeyValueDidChange
    19 Foundation                     0x147c58 -[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
    20 Foundation                     0x1478ac -[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
    21 Foundation                     0x146e40 _NSSetObjectValueAndNotify
    22 AVKit                          0x1e824 -[AVPlayerController _prepareAssetForInspectionIfNeeded]
    23 AVKit                          0x4fa4 __105-[AVObservationController startObserving:keyPaths:includeInitialValue:includeChanges:observationHandler:]_block_invoke
    24 AVKit                          0x4e80 -[AVProxyKVOObserver _handleValueChangeForKeyPath:ofObject:oldValue:newValue:context:]
    25 AVKit                          0xa4a0 -[AVProxyKVOObserver observeValueForKeyPath:ofObject:change:context:]
    26 Foundation                     0x1a684 NSKeyValueNotifyObserver
    27 Foundation                     0x1a378 NSKeyValueDidChange
    28 Foundation                     0x19fa4 NSKeyValueDidChangeWithPerThreadPendingNotifications
    29 Foundation                     0x1361a0 -[NSKeyValueObservance observeValueForKeyPath:ofObject:change:context:]
    30 Foundation                     0x1a684 NSKeyValueNotifyObserver
    31 Foundation                     0x1a378 NSKeyValueDidChange
    32 Foundation                     0x19fa4 NSKeyValueDidChangeWithPerThreadPendingNotifications
    33 AVFCore                        0x34b7c __109-[AVPlayer _runOnIvarAccessQueueOperationThatMayChangeCurrentItemWithPreflightBlock:modificationBlock:error:]_block_invoke_2
    34 AVFCore                        0x47578 -[AVSerializedMostlySynchronousReentrantBlockScheduler scheduleBlock:]
    35 AVFCore                        0x4720c -[AVPlayer _runOnIvarAccessQueueOperationThatMayChangeCurrentItemWithPreflightBlock:modificationBlock:error:]
    36 AVFCore                        0x7a534 -[AVPlayer _removeItem:]
    37 AVFCore                        0x7951c -[AVPlayer _advanceCurrentItemAccordingToFigPlaybackItem:]
    38 AVFCore                        0x38c8 __avplayer_fpNotificationCallback_block_invoke
    39 libdispatch.dylib              0x213c _dispatch_call_block_and_release
    40 libdispatch.dylib              0x3dd4 _dispatch_client_callout
    41 libdispatch.dylib              0x125a4 _dispatch_main_queue_drain
    42 libdispatch.dylib              0x121b8 _dispatch_main_queue_callback_4CF
    43 CoreFoundation                 0x56710 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
    44 CoreFoundation                 0x53914 __CFRunLoopRun
    45 CoreFoundation                 0x52cd8 CFRunLoopRunSpecific
    46 GraphicsServices               0x11a8 GSEventRunModal
    47 UIKitCore                      0x40a90c -[UIApplication _run]
    48 UIKitCore                      0x4be9d0 UIApplicationMain
    

    I see that at frame 14 of the stack the AVVideoFrameVisualAnalyzer is doing stuff related to the observers and listeners.

    My guess is that this is related to the root cause of the crash, that it is most likely a bug in the AVPlayer.

    The AVVideoFrameVisualAnalyzer is related to a new feature (introduced in iOS 16) where the player view tries to find objects, text, and people when you pause media playback. If it finds an object, the user is able to interact with it using a long press to present a context menu.

    Fortunately, this new feature (enabled by default) can be disabled using the allowsVideoFrameAnalysis property.

    In my use case, I completely avoid the crash by simply setting allowsVideoFrameAnalysis to false

    if #available(iOS 16.0, *) {
       playerViewController.allowsVideoFrameAnalysis = false
    } 
    

    Of course, this is only a good solution if you are not interested in this brand new feature. I am not :-)