我有一个视频播放器视图,可以根据 URL 播放视频。当我离开视频时,我遇到崩溃并打印以下错误。在我的 deinit 中,我尝试使观察者无效,但这没有用。我的 deinit 函数中缺少导致此错误的步骤。
由于未捕获的异常“NSInternalInconsistencyException”而终止应用程序,原因:“无法从
中删除键路径“currentItem.videoComposition”的观察者 ,很可能是因为键“currentItem”的值已更改,而没有发送适当的 KVO 通知。检查 AVQueuePlayer 类的 KVO 合规性
这是抛出此代码:
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
}
}
我遇到了类似的问题,这些是我的发现:
我对崩溃调用堆栈进行了更深入的分析。
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
我看到在堆栈的第 14 帧,AVVideoFrameVisualAnalyzer 正在执行与观察者和侦听器相关的操作。
我的猜测是,这与崩溃的根本原因有关,这是一个 AVPlayer 错误。
AVVideoFrameVisualAnalyzer 与一项新功能(在 iOS 16 中引入)相关,其中播放器视图在您暂停媒体播放时尝试查找对象、文本和人物。如果它找到一个对象,用户可以通过长按来显示上下文菜单来与其交互。
幸运的是,可以使用 allowsVideoFrameAnalysis 属性禁用此新功能(默认启用)。
在我的用例中,我只需将 allowedVideoFrameAnalysis 设置为 false 即可完全避免崩溃
if #available(iOS 16.0, *) {
playerViewController.allowsVideoFrameAnalysis = false
}
当然,如果您对这个全新功能不感兴趣,这只是一个很好的解决方案。我不是:-)