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

AVPlayer搭配CachingPlayerItem与Cache播放远程音频需点击两次问题咨询

问题根因

你遇到的首次播放失败、二次才能播的问题,核心是以下三个原因:

  1. 你强制关闭了automaticallyWaitsToMinimizeStalling,远程未缓存资源首次加载时没有足够的初始缓冲,播放器会直接终止播放逻辑
  2. 你没有监听AVPlayerItem的状态,刚把item塞进播放器就立刻调用play(),此时item还没完成资源元数据加载、没有进入可播放状态
  3. 你写死了缓存读取时的MIMEType和文件扩展名,如果Firebase返回的资源类型和你写的不匹配,也会导致首次加载解析失败

修复方案

步骤1:移除不必要的配置

删掉self.automaticallyWaitsToMinimizeStalling = false这行代码,这个配置只适合本地已缓存的资源播放场景,远程流媒体场景开启会直接导致播放失败。

步骤2:添加播放器状态监听

监听AVPlayerItem的status属性,等状态变为readyToPlay时再触发播放,不要直接调用play()。

步骤3:修正缓存读取的资源类型匹配逻辑

建议缓存时同时存储资源的MIMEType和扩展名,不要写死,避免和实际资源格式不兼容。


修正后的代码

import AVKit
import Cache
import Combine

class AudioPlayer: AVPlayer, ObservableObject {
    private let diskConfig = DiskConfig(name: "DiskCache")
    private let memoryConfig = MemoryConfig(expiry: .never, countLimit: 10, totalCostLimit: 10)
    // 存储状态观察器,避免被释放
    private var statusObserver: AnyCancellable?

    lazy var storage: Cache.Storage<String, Data>? = {
        return try? Cache.Storage(diskConfig: diskConfig, memoryConfig: memoryConfig, transformer: TransformerFactory.forData())
    }()

    /// Plays audio either from the network if it's not cached or from the cache.
    func startPlayback(with url: URL) {
        // 先清空之前的观察器
        statusObserver?.cancel()
        
        let playerItem: CachingPlayerItem
        do {
            let result = try storage!.entry(forKey: url.absoluteString)
            // 缓存命中,用本地数据初始化
            playerItem = CachingPlayerItem(data: result.object, mimeType: "audio/mp4", fileExtension: "m4a")
        } catch {
            // 未命中缓存,用远程url初始化
            playerItem = CachingPlayerItem(url: url)
        }

        playerItem.delegate = self
        replaceCurrentItem(with: playerItem)
        
        // 监听item状态,准备好再播放
        statusObserver = playerItem.publisher(for: \.status)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] status in
                guard let self = self else { return }
                if status == .readyToPlay {
                    self.play()
                    // 播放触发后取消观察,避免重复调用
                    self.statusObserver?.cancel()
                } else if status == .failed {
                    // 可在这里添加播放失败的降级逻辑
                    print("播放失败,错误:\(playerItem.error?.localizedDescription ?? "未知错误")")
                }
            }
    }
}

extension AudioPlayer: CachingPlayerItemDelegate {
    func playerItem(_ playerItem: CachingPlayerItem, didFinishDownloadingData data: Data) {
        // 下载完成后存入缓存
        storage?.async.setObject(data, forKey: playerItem.url.absoluteString, completion: { _ in })
        print("缓存完成")
    }
    
    // 可选实现,处理下载错误
    func playerItem(_ playerItem: CachingPlayerItem, didDownloadBytesSoFar bytesDownloaded: Int, outOf bytesExpected: Int) {
        // 可在这里更新下载进度UI
    }
    
    func playerItem(_ playerItem: CachingPlayerItem, didFinishDownloadingWithError error: Error) {
        print("资源下载失败:\(error.localizedDescription)")
    }
}

内容的提问来源于stack exchange,提问作者gadha007

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 00:15:08