Swift中AVKit无法播放带CloudFront签名Cookie的M3U8流问题
- 通过
AVURLAssetHTTPHeaderFieldsKey注入了有效未过期的CloudFront签名Cookie,但播放仍失败 - 手动用URLSession获取master.m3u8响应正常,解析变体播放列表后播放依旧黑屏
- Xcode控制台无报错,确认AVPlayer仅初始请求携带Cookie,分片(.ts)请求缺失Cookie
- 已尝试实现自定义
AVAssetResourceLoaderDelegate拦截请求注入Cookie,仍无法播放,KVO显示status==0
原因分析
- AVURLAsset头部仅作用于初始请求:
AVURLAssetHTTPHeaderFieldsKey设置的HTTP头部只会应用于初始的m3u8请求,后续的变体列表、分片请求不会自动继承这些头部,导致CloudFront验证失败。 - 自定义ResourceLoader未正确覆盖所有请求:之前的实现可能未正确拦截所有子请求(变体m3u8、ts分片),或是注入Cookie的逻辑存在疏漏。
- 手动解析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) } }
关键修改点说明
- 移除手动解析master列表逻辑:删除冗余的
fetchAndPlayPlaylist和parsePlaylist方法,让AVPlayer自动处理列表解析,避免破坏内部逻辑链。 - 全局拦截请求注入Cookie:通过
AVAssetResourceLoaderDelegate拦截所有HLS相关请求(初始m3u8、变体列表、ts分片),为每个请求注入Cookie头部,确保CloudFront验证通过。 - 添加播放状态监听:通过KVO监听
AVPlayer的currentItem?.status,可以直接捕获播放失败的具体错误信息,便于排查问题。 - 优化播放器资源管理:在
stop方法中清空当前播放项,避免资源残留导致的异常。
内容的提问来源于stack exchange,提问作者Aakash Bondwal
相关产品推荐
相关产品推荐

