AVPlayer中playerItemDidReachEnd不触发及addPeriodicTimeObserver不稳定问题排查
AVPlayer播放结束回调未触发及周期性观察者不稳定的修复方案
核心问题分析与修复
1. 播放结束通知(AVPlayerItemDidPlayToEndTime)未触发的原因
- 重复注册通知且未清理:每次
AVPlayerItem变为readyToPlay时都添加通知观察者,导致同一观察者被多次注册,可能引发回调异常;更换视频时未移除旧的观察者,新的通知可能无法正确关联。 actionAtItemEnd设置时机:当前设置在replaceCurrentItem之后,但未确保每次更换item时都重新设置(虽然当前代码每次setup都会设置,但如果后续更换item需要注意)。- KVO未使用自定义Context:使用
nil作为KVO context,若有其他KVO观察逻辑会导致冲突,影响状态判断。
2. 周期性时间观察者(addPeriodicTimeObserver)不稳定的原因
- 未持有观察者引用:如果没有强引用保存观察者返回的token,系统会自动释放该观察者,导致回调失效。
- 进度判断的精度问题:用
progress >=1判断播放结束,由于浮点数精度误差,可能无法触发;应该用CMTime的精确比较。
修复后的完整代码
// 自定义KVO Context,避免与其他KVO逻辑冲突 private var playerItemStatusContext = 0 class YourViewController: UIViewController { private var avPlayerPlayback = AVPlayer() private var playbackLayer: AVPlayerLayer? private var timeObserverToken: Any? // 持有周期性观察者的引用 func setupVideo(with asset: AVAsset) { // 移除旧的通知观察者 NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: avPlayerPlayback.currentItem) // 移除旧的周期性观察者 if let token = timeObserverToken { avPlayerPlayback.removeTimeObserver(token) timeObserverToken = nil } // 移除旧的AVPlayerLayer playbackLayer?.removeFromSuperlayer() let videoItem = AVPlayerItem(asset: asset) // 使用自定义Context添加KVO videoItem.addObserver(self, forKeyPath: "status", options: [.new, .initial], context: &playerItemStatusContext) avPlayerPlayback.replaceCurrentItem(with: videoItem) // 确保actionAtItemEnd设置正确 avPlayerPlayback.actionAtItemEnd = .none // 创建并配置AVPlayerLayer playbackLayer = AVPlayerLayer(player: avPlayerPlayback) guard let playbackLayer = playbackLayer else { return } playbackLayer.frame = capturedVideoPlaybackView.bounds playbackLayer.videoGravity = .resizeAspectFill capturedVideoPlaybackView.layer.addSublayer(playbackLayer) print("Player status: \(avPlayerPlayback.status.rawValue)") print("Player item duration: \(CMTimeGetSeconds(avPlayerPlayback.currentItem?.duration ?? CMTime.zero))") } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { // 只处理自定义Context的KVO guard context == &playerItemStatusContext else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) return } if keyPath == "status", let item = object as? AVPlayerItem { switch item.status { case .readyToPlay: print("Item is ready to play") DispatchQueue.main.async { [weak self] in guard let self = self else { return } // 注册播放结束通知 NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemDidReachEnd(notification:)), name: .AVPlayerItemDidPlayToEndTime, object: item) self.toggleMedia(shouldHideVideo: false) self.playVideo() print("Player status: \(self.avPlayerPlayback.status.rawValue)") print("Player item duration: \(CMTimeGetSeconds(self.avPlayerPlayback.currentItem?.duration ?? CMTime.zero))") print("playing video") } case .failed: print("Player item failed: \(String(describing: item.error?.localizedDescription))") // 移除KVO观察者 item.removeObserver(self, forKeyPath: "status", context: &playerItemStatusContext) case .unknown: break @unknown default: break } } } func playVideo() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } // 添加周期性时间观察者(可选,用于兜底) self.timeObserverToken = self.avPlayerPlayback.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 2), queue: .main) { [weak self] time in guard let self = self, let currentItem = self.avPlayerPlayback.currentItem else { return } // 用CMTime精确判断是否播放结束 if CMTimeCompare(time, currentItem.duration) >= 0 { self.handlePlaybackItemCompletion() // 移除观察者,避免重复触发 if let token = self.timeObserverToken { self.avPlayerPlayback.removeTimeObserver(token) self.timeObserverToken = nil } } } self.avPlayerPlayback.play() } } @objc func playerItemDidReachEnd(notification: Notification) { print("Playback finished") handlePlaybackItemCompletion() // 移除当前item的通知观察者 NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: notification.object) // 移除KVO观察者 if let item = notification.object as? AVPlayerItem { item.removeObserver(self, forKeyPath: "status", context: &playerItemStatusContext) } } private func handlePlaybackItemCompletion() { // 这里写播放结束后的逻辑 avPlayerPlayback.pause() // 可选:重置播放位置到开头 avPlayerPlayback.seek(to: .zero) } // 页面销毁时清理所有观察者 deinit { NotificationCenter.default.removeObserver(self) if let token = timeObserverToken { avPlayerPlayback.removeTimeObserver(token) } avPlayerPlayback.currentItem?.removeObserver(self, forKeyPath: "status", context: &playerItemStatusContext) } }
关键修复点说明
- 自定义KVO Context:避免与其他KVO逻辑冲突,确保只处理当前playerItem的状态变化。
- 观察者的清理与复用:每次设置新视频时,移除旧的通知观察者、周期性观察者和AVPlayerLayer,防止重复叠加导致的异常。
- 强引用周期性观察者:用
timeObserverToken保存观察者引用,防止被系统释放。 - 精确的播放结束判断:使用
CMTimeCompare替代浮点数比较,避免精度误差导致的回调失效。 - deinit中清理资源:页面销毁时移除所有观察者,防止内存泄漏和野指针问题。
内容的提问来源于stack exchange,提问作者Chris
相关产品推荐
相关产品推荐

