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. Im 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.'” 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
}
}




