Swift导航远离视频导致崩溃

qoefvg9y  于 7个月前  发布在  Swift
关注(0)|答案(1)|浏览(78)

我有一个基于URL播放视频的视频播放器视图。当我离开视频时,我会崩溃,并打印下面的错误。在我的deinit中,我试图使观察者无效,但没有成功。我在deinit函数中缺少一个步骤,导致了这个错误。
由于未捕获的异常“NSInternalInconsistencyException”而终止应用程序,原因:“无法从中删除<NSKeyValueObservance 0x2809113b0>键路径“currentItem.videoComposition”的观察者,<AVQueuePlayer 0x28079dfc0>很可能是因为键“currentItem”的值已更改,但未发送相应的KVO通知。请检查AVOfficePlayer类的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
   }
}

字符串

ct3nt3jp

ct3nt3jp1#

您的LegacyAVPlayerViewController类正在观察AVPlayerrate property,但错误消息表明currentItem.videoComposition属性存在问题。
可能是AVQueuePlayer示例的修改方式不符合AVQueuePlayerKey-Value Observing (KVO),导致了此崩溃。
例如,参见Sasha Bondar中的“How to use Swift's Key-Value Observing (KVO)“。
尝试并确保对player.currentItem或其属性的任何更改都是以KVO兼容的方式进行的。这对于AVFoundation classes来说可能很棘手,因为它们并不总是严格遵循KVO约定。另请参阅Jianyuan Chen中的“Key-Value Observing in the age of Swift“。
因此,请确保删除LegacyAVPlayerViewControllerdeinit方法中的所有观察者。由于您已经使rateObserver无效,因此请确保在代码的其他地方没有添加其他观察者。(有点像“What is wrong with this AVFoundation KVO pattern for a video player [ref: AVPlayerLayer , AVPlayerItem , AVURLAsset ]?“)
在使你的观察者无效之前,检查玩家是否真的在观察那个关键路径。
确保对player.currentItem或相关属性的任何修改都是以符合KVO的方式完成的。这可能需要在LegacyVideoPlayer类中进行自定义处理。
你的deinit方法应该是:

deinit {
    if player?.observationInfo != nil {
        rateObserver?.invalidate()
    }
}

字符串
由于player?.observationInfo != nil检查没有解决问题,请尝试检查LegacyAVPlayerViewController中的KVO设置和拆除过程:包括确保观察者正确添加和删除,以及在视图控制器生命周期的适当时间添加和删除。
有时,KVO相关的崩溃会发生,因为观察者被添加多次,但只删除一次。请确保观察者只添加一次。这可以通过设置标志或在添加前检查rateObserver是否为nil来管理。

deinit {
    rateObserver?.invalidate()
}

// In your observer setup
if rateObserver == nil {
    rateObserver = player?.observe(\AVPlayer.rate, options: [.new], changeHandler: rateHandler(_:change:))
}


在拆除时,在使观察者无效之前将player设置为nil。这可以确保在视图控制器被释放之前停止所有与玩家相关的观察。仔细检查是否有任何其他代码部分(可能在LegacyAVPlayerViewController之外)可能会向AVPlayer或其属性添加观察者。
确保所有与KVO相关的操作(添加、删除观察者)都在同一个线程上完成,最好是在主线程上,以避免竞态条件。

相关问题