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

AVPlayer播放受保护HLS流时令牌过期该如何更新?

AVPlayer 短有效期令牌HLS流401中断问题修复方案

原有实现失效核心原因

  • 未正确触发AVAssetResourceLoader回调:默认http/https scheme的HLS资源,AVPlayer会走系统默认加载逻辑,不会调用你实现的代理方法,修改header自然不生效。必须将初始AVURLAsset的scheme替换为自定义scheme,系统才会把所有资源请求交给自定义代理处理。
  • 未完整填充AVAssetResourceLoadingRequest的必填字段:你只返回了请求数据,没有填充内容信息相关字段,AVPlayer会忽略自定义请求的结果,继续走默认请求流程,必然返回401。
  • 重复创建URLSession、每次请求都强制刷新令牌属于不必要的性能开销,会提升请求失败概率。

修复步骤

第一步:初始化AVURLAsset时替换为自定义scheme

// 原始播放地址
let originalURL = URL(string: "https://your-protected-hls-url.m3u8")!
// 替换为自定义scheme
var components = URLComponents(url: originalURL, resolvingAgainstBaseURL: false)
components?.scheme = "custom-https"
let customURL = components?.url!
// 初始化asset并设置代理
let asset = AVURLAsset(url: customURL!)
asset.resourceLoader.setDelegate(self, queue: DispatchQueue(label: "com.yourproject.resourceLoaderQueue"))

第二步:修正resourceLoader代理实现

// 缓存当前有效的令牌,避免每次请求都刷新
private var validTokenCookie: String?
private let sharedSession = URLSession(configuration: .default)

func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
    // 把自定义scheme换回真实的https
    guard let customURL = loadingRequest.request.url,
          var components = URLComponents(url: customURL, resolvingAgainstBaseURL: false) else {
        loadingRequest.finishLoading(with: NSError(domain: "ResourceLoaderError", code: -1, userInfo: nil))
        return false
    }
    components.scheme = "https"
    guard let realURL = components.url else {
        loadingRequest.finishLoading(with: NSError(domain: "ResourceLoaderError", code: -2, userInfo: nil))
        return false
    }
    
    var request = URLRequest(url: realURL)
    request.allHTTPHeaderFields = loadingRequest.request.allHTTPHeaderFields
    
    // 按需刷新令牌,这里可以加令牌有效期判断逻辑,避免重复刷新
    let needRefreshToken = (validTokenCookie == nil || isTokenExpired())
    let requestBlock = { [weak self] in
        guard let self = self else { return }
        request.setValue(self.validTokenCookie, forHTTPHeaderField: "Cookie")
        
        self.sharedSession.dataTask(with: request) { data, response, error in
            if let error = error {
                loadingRequest.finishLoading(with: error)
                return
            }
            guard let httpResponse = response as? HTTPURLResponse,
                  let data = data else {
                loadingRequest.finishLoading(with: NSError(domain: "ResourceLoaderError", code: -3, userInfo: nil))
                return
            }
            // 处理401情况,刷新令牌后重试
            if httpResponse.statusCode == 401 {
                self.validTokenCookie = nil
                service.networkService.refreshToken { result in
                    // 刷新成功后更新令牌,重新执行请求
                    if let newCookie = result.cookie {
                        self.validTokenCookie = newCookie
                        requestBlock()
                    } else {
                        loadingRequest.finishLoading(with: NSError(domain: "ResourceLoaderError", code: -4, userInfo: nil))
                    }
                }
                return
            }
            // 填充必填的content信息
            if let infoRequest = loadingRequest.contentInformationRequest {
                infoRequest.contentType = httpResponse.mimeType
                infoRequest.expectedContentLength = httpResponse.expectedContentLength
                infoRequest.isByteRangeAccessSupported = httpResponse.allHeaderFields["Accept-Ranges"] as? String == "bytes"
            }
            // 返回数据
            loadingRequest.dataRequest?.respond(with: data)
            loadingRequest.finishLoading()
        }.resume()
    }
    
    if needRefreshToken {
        service.networkService.refreshToken { [weak self] result in
            guard let self = self, let newCookie = result.cookie else {
                loadingRequest.finishLoading(with: NSError(domain: "ResourceLoaderError", code: -4, userInfo: nil))
                return
            }
            self.validTokenCookie = newCookie
            requestBlock()
        }
    } else {
        requestBlock()
    }
    
    return true
}

注意事项

  • 所有HLS相关请求包括播放列表刷新、分片请求、加密密钥请求都会自动走这个代理,不需要手动修改asset内部的任何参数,所有请求都会自动携带最新的有效令牌,不会再出现401中断问题。
  • 如果服务端返回302重定向,你需要自行处理重定向逻辑,拿到最终响应数据后再返回给loadingRequest。
  • 可以根据业务需要调整令牌刷新的判断逻辑,比如提前30秒刷新令牌,避免请求到一半才发现令牌过期导致卡顿。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 12:36:04