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

Swift中AVKit无法播放带CloudFront签名Cookie的M3U8流问题

问题:AVKit播放带CloudFront签名Cookie的HLS流失败
  • 通过AVURLAssetHTTPHeaderFieldsKey注入了有效未过期的CloudFront签名Cookie,但播放仍失败
  • 手动用URLSession获取master.m3u8响应正常,解析变体播放列表后播放依旧黑屏
  • Xcode控制台无报错,确认AVPlayer仅初始请求携带Cookie,分片(.ts)请求缺失Cookie
  • 已尝试实现自定义AVAssetResourceLoaderDelegate拦截请求注入Cookie,仍无法播放,KVO显示status==0
原因分析
  1. AVURLAsset头部仅作用于初始请求:AVURLAssetHTTPHeaderFieldsKey设置的HTTP头部只会应用于初始的m3u8请求,后续的变体列表、分片请求不会自动继承这些头部,导致CloudFront验证失败。
  2. 自定义ResourceLoader未正确覆盖所有请求:之前的实现可能未正确拦截所有子请求(变体m3u8、ts分片),或是注入Cookie的逻辑存在疏漏。
  3. 手动解析master列表属于冗余操作:AVPlayer本身会自动解析master列表并选择合适的变体,手动请求解析反而绕开了AVPlayer的内部逻辑,导致头部无法正确传递。
解决方案

核心思路是通过自定义AVAssetResourceLoaderDelegate拦截所有HLS相关请求,为每个请求注入CloudFront签名Cookie,同时移除手动解析master列表的冗余代码。

修改后的完整代码
import AVFoundation
import AVKit
import UIKit
import SwiftUI

public protocol StreamingVideoPlayerCapabable {
    func add(to view: UIView)
    func play(url: URL, cookieString: String)
    func stop()
    func pause()
}

public class StreamingVideoPlayer: NSObject, StreamingVideoPlayerCapabable, AVAssetResourceLoaderDelegate {
    public let playerViewController = AVPlayerViewController()
    private let avPlayer = AVPlayer()
    private var cookieString: String = ""
    private var statusObservation: NSKeyValueObservation?
    
    private lazy var playerView: UIView = {
        let view: UIView = playerViewController.view!
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    public override init() {
        super.init()
        setupPlayerStatusObservation()
    }
    
    private func setupPlayerStatusObservation() {
        statusObservation = avPlayer.observe(\.currentItem?.status, options: [.new]) { [weak self] player, change in
            guard let status = player.currentItem?.status else { return }
            switch status {
            case .readyToPlay:
                print("播放器准备就绪")
            case .failed:
                print("播放失败: \(player.currentItem?.error?.localizedDescription ?? "未知错误")")
            case .unknown:
                print("播放器状态未知")
            @unknown default:
                break
            }
        }
    }
    
    public func add(to view: UIView) {
        view.addSubview(playerView)
        NSLayoutConstraint.activate([
            playerView.topAnchor.constraint(equalTo: view.topAnchor),
            playerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            playerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            playerView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
    }
    
    public func play(url: URL, cookieString: String) {
        self.cookieString = cookieString
        let asset = AVURLAsset(url: url)
        asset.resourceLoader.setDelegate(self, queue: DispatchQueue.global())
        let playerItem = AVPlayerItem(asset: asset)
        avPlayer.replaceCurrentItem(with: playerItem)
        playerViewController.player = avPlayer
        playerViewController.player?.play()
    }
    
    public func stop() {
        playerViewController.player?.pause()
        avPlayer.replaceCurrentItem(with: nil)
    }
    
    public func pause() {
        playerViewController.player?.pause()
    }
    
    // MARK: - AVAssetResourceLoaderDelegate
    public func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
        guard let url = loadingRequest.request.url else {
            loadingRequest.finishLoading(with: NSError(domain: "StreamingError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效请求URL"]))
            return false
        }
        
        var request = URLRequest(url: url)
        // 为每个请求注入Cookie头部
        request.addValue(cookieString, forHTTPHeaderField: "Cookie")
        
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            defer {
                loadingRequest.finishLoading()
            }
            
            if let error = error {
                loadingRequest.finishLoading(with: error)
                return
            }
            
            if let response = response {
                loadingRequest.response = response
            }
            
            if let data = data {
                loadingRequest.dataRequest?.respond(with: data)
            }
        }
        
        task.resume()
        return true
    }
}

struct StreamingVideoPlayerView: UIViewControllerRepresentable {
    let url: URL
    let cookieString: String
    
    func makeUIViewController(context: Context) -> AVPlayerViewController {
        let player = StreamingVideoPlayer()
        player.add(to: player.playerViewController.view)
        player.play(url: url, cookieString: cookieString)
        return player.playerViewController
    }
    
    func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {}
}

struct ContentView: View {
    var body: some View {
        StreamingVideoPlayerView(
            url: URL(string: "https://d3nzo6itypaz07.cloudfront.net/fe365f21-3e2e-4b6d-ad30-e5914e063881/master.m3u8")!,
            cookieString: "CloudFront-Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9kM256bzZpdHlwYXowNy5jbG91ZGZyb250Lm5ldC9mZTM2NWYyMS0zZTJlLTRiNmQtYWQzMC1lNTkxNGUwNjM4ODEvKiIsIkNvbmRpdGlvbiI6eyJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTc0NTEwNDU4NX19fV19;CloudFront-Signature=CmdcqLorIlteG0q~sATiBtTj2vvpJs4qT2-3Tw82B11~ybBpWM0gEM4EzfyDX-T2nYF1of5v-BJkufPCTeXPqc-05BcYmzZHf3d7QIRt1k7~huaN39jo9sejh0Du14BbBem9iF32NSwXFXYQqhMDnNKTM4xydRi1MhiAaIxfl40o2CayUpa40llN903bQGK66CEl5KCihAbxdkRcs133aCi~~xl9oIPuiGyOhmNQay8oSXNOnsASlzw5GTbKh83JRg9Yg6fEfFUNxz~7TbyOUYMm0WDn9LyCLq5jKlfDdxRC4GrDBqMfcq8ctuv7n7xUWRjWGxYSZRv0catOBW7FrA__;CloudFront-Key-Pair-Id=APKAXVKMENFCRKTG5XE2"
        )
        .edgesIgnoringSafeArea(.all)
    }
}
关键修改点说明
  1. 移除手动解析master列表逻辑:删除冗余的fetchAndPlayPlaylist和parsePlaylist方法,让AVPlayer自动处理列表解析,避免破坏内部逻辑链。
  2. 全局拦截请求注入Cookie:通过AVAssetResourceLoaderDelegate拦截所有HLS相关请求(初始m3u8、变体列表、ts分片),为每个请求注入Cookie头部,确保CloudFront验证通过。
  3. 添加播放状态监听:通过KVO监听AVPlayer的currentItem?.status,可以直接捕获播放失败的具体错误信息,便于排查问题。
  4. 优化播放器资源管理:在stop方法中清空当前播放项,避免资源残留导致的异常。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 08:41:02