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
}
}
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 :-)