You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 14:10:18