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

如何使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 20:55:38