如何使用AVPlayer实现无间隙连续播放?
如何用AVPlayer实现无间隙的自定义队列播放?
我之前用AVQueuePlayer实现了多文件无间隙连续播放,代码如下:
let player = AVQueuePlayer() let playerItem1 = ... let playerItem2 = ... player.insert(playerItem1, after: nil) player.insert(playerItem2, after: nil) player.play()
但AVQueuePlayer存在一些缺点,所以我想改用AVPlayer自定义队列。虽然AVQueuePlayer是AVPlayer的子类,但我用通知监听播放结束切换item的方式会出现播放间隙,代码如下:
let playerItem1 = ... let playerItem2 = ... let player = AVPlayer(playerItem: playerItem1) NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem) player.play() @objc func playerDidFinishPlaying() { player.replaceCurrentItem(with: nextPlayerItem) player.play() }
现在需要修改代码,让AVPlayer实现类似AVQueuePlayer的无间隙播放,且不想使用AVAudioEngine。
核心问题分析
用AVPlayerItemDidPlayToEndTime通知切换item出现间隙的原因有两点:一是通知触发存在系统级延迟,二是新item可能未完成加载准备,导致播放中断。要实现无间隙,核心是提前预加载下一个播放项,并精准控制切换时机。
具体实现方案
1. 预加载所有播放项
提前初始化所有需要播放的AVPlayerItem,并监听它们的加载状态,确保只有在item处于.readyToPlay状态时才进行切换。
2. 用周期性进度监听替代结束通知
通过addPeriodicTimeObserver实时监控播放进度,在当前item接近结束时(比如剩余0.1秒)就提前切换,避免通知的延迟问题。
完整代码示例
import AVFoundation class CustomQueuePlayer: NSObject { private let player = AVPlayer() private var currentItemIndex = 0 private let playerItems: [AVPlayerItem] init(items: [AVPlayerItem]) { self.playerItems = items super.init() guard !items.isEmpty else { return } // 初始化第一个播放项 player.replaceCurrentItem(with: items[0]) observeItemStatus(for: items[0]) addProgressObserver() } func play() { player.play() } // 监听单个item的加载状态 private func observeItemStatus(for item: AVPlayerItem) { item.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: [.new, .initial], context: nil) } // 添加播放进度监听 private func addProgressObserver() { let interval = CMTimeMakeWithSeconds(0.1, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] currentTime in self?.checkRemainingTime(currentTime: currentTime) } } // 检查剩余播放时间,提前切换 private func checkRemainingTime(currentTime: CMTime) { guard let currentItem = player.currentItem else { return } let duration = currentItem.duration guard CMTimeCompare(duration, CMTime.zero) != 0 else { return } let remainingSeconds = CMTimeGetSeconds(CMTimeSubtract(duration, currentTime)) // 剩余时间小于0.1秒时触发切换 if remainingSeconds < 0.1 { switchToNextItem() } } // 切换到下一个播放项 private func switchToNextItem() { currentItemIndex += 1 guard currentItemIndex < playerItems.count else { // 队列播放结束,可添加收尾逻辑 return } let nextItem = playerItems[currentItemIndex] if nextItem.status == .readyToPlay { player.replaceCurrentItem(with: nextItem) player.play() observeItemStatus(for: nextItem) } else { // 下一个item未准备好,等待状态变更 observeItemStatus(for: nextItem) } } // KVO回调处理item状态变化 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard let keyPath = keyPath, keyPath == #keyPath(AVPlayerItem.status), let item = object as? AVPlayerItem else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) return } switch item.status { case .readyToPlay: // 如果是当前要切换的item,立即启动播放 if item == playerItems[currentItemIndex] { player.replaceCurrentItem(with: item) player.play() observeItemStatus(for: item) } case .failed, .unknown: // 处理加载失败,直接跳过当前item print("播放项加载失败: \(item.error?.localizedDescription ?? "未知错误")") switchToNextItem() default: break } // 移除监听,避免内存泄漏 item.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.status)) } deinit { player.removeTimeObserver(self) player.currentItem?.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.status)) } }
关键细节说明
- 预加载与状态校验:提前初始化所有播放项,通过KVO监听状态,确保只有就绪的item才会被播放,避免加载等待导致的间隙。
- 精准时机控制:用0.1秒间隔的进度监听替代结束通知,在播放接近尾声时提前切换,消除通知延迟带来的间隙。
- 内存安全:在
deinit中移除所有观察者,避免内存泄漏问题。
内容的提问来源于stack exchange,提问作者Hello
相关产品推荐
相关产品推荐

