ios – Swift navigating away from video causes 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. 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
   }
}

Latest articles

spot_imgspot_img

Related articles

Leave a reply

Please enter your comment!
Please enter your name here

spot_imgspot_img