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

iOS Swift:AVPlayer播放DRM保护MPD视频时streamingContentKeyRequestData获取失败

问题解决:AVPlayer集成FairPlay DRM调用streamingContentKeyRequestData失败

核心问题分析

你遇到的streamingContentKeyRequestData调用失败,主要源于Content ID提取错误、DRM请求处理逻辑不严谨、平台DRM支持混淆以及代码中的基础错误,下面是针对性的修复方案:


关键修复点

1. 修正基础错误:MPD URL拼写

你代码中MPD URL写成了htps://(缺少字母t),这会导致AVURLAsset无法正常加载,先修正为https://:

guard let url = URL(string: "https://z2cltd.akamaized.net/mini01/vods/49/s1e1-andhera-ujala-ghani-chaon-copy-3395/64366c9ccc181-4606-mp4-cbcs.mpd") else {
    return
}

2. 正确提取Content ID

你当前硬编码MPD的host作为Content ID是错误的,Content ID需要从DRM请求的初始化数据中提取,或者从MPD的ContentProtection字段获取。如果后端约定用视频唯一标识作为Content ID,可以从MPD URL中解析:

// 从MPD URL解析Content ID(根据后端约定调整)
guard let mpdUrl = asset?.url,
      let lastPathComponent = mpdUrl.pathComponents.last,
      let contentId = lastPathComponent.components(separatedBy: "-mp4-cbcs.mpd").first,
      let contentIdData = contentId.data(using: .utf8) else {
    print("无法提取Content ID")
    loadingRequest.finishLoading(with: NSError(domain: "DRMError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无法提取Content ID"]))
    return false
}

3. 先验证DRM请求类型

在shouldWaitForLoadingOfRequestedResource中,先判断当前请求是否是DRM密钥请求,避免处理无关请求:

func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
    // 仅处理DRM密钥请求
    guard loadingRequest.contentInformationRequest?.contentType == AVStreamingKeyDeliveryContentKeyType else {
        // 非DRM请求,返回false让系统默认处理
        return false
    }
    
    // 后续DRM处理逻辑...
}

4. 移除不必要的Persistent Key选项

如果你不需要离线播放的持久化密钥,先移除AVAssetResourceLoadingRequestStreamingContentKeyRequestRequiresPersistentKey选项,简化调试流程:

// 移除Persistent Key选项,先测试基础播放流程
_spcData = try loadingRequest.streamingContentKeyRequestData(forApp: certificateData, contentIdentifier: contentIdData, options: nil)

5. 处理Widevine DRM的平台限制

iOS的AVPlayer不支持Widevine DRM,Widevine是Google针对Android平台的DRM方案。iOS上只能播放MPD中未加密的内容,或者要求后端提供适配FairPlay DRM的视频流。


修改后的完整代码示例

class ViewController: UIViewController {
    
    var player: AVPlayer?
    var asset: AVURLAsset?
    // 从后端返回的配置中获取正确地址
    let fairPlayCertificateUrl = URL(string: "https://mw.example.net/static/FairPlay.der")!
    let keyServerUrl = URL(string: "https://mw.example.net/ksm")!

    override func viewDidLoad() {
        super.viewDidLoad()
       
        guard let mpdUrl = URL(string: "https://z2cltd.akamaized.net/mini01/vods/49/s1e1-andhera-ujala-ghani-chaon-copy-3395/64366c9ccc181-4606-mp4-cbcs.mpd") else {
            print("MPD URL无效")
            return
        }

        asset = AVURLAsset(url: mpdUrl)
        asset?.resourceLoader.setDelegate(self, queue: DispatchQueue.global(qos: .default))

        let playerItem = AVPlayerItem(asset: asset!)
        // 监听playerItem状态
        playerItem.observe(\.status, changeHandler: onStatusObserverChanged)
        
        player = AVPlayer(playerItem: playerItem)

        let playerLayer = AVPlayerLayer(player: player)
        playerLayer.frame = view.bounds
        playerLayer.videoGravity = .resizeAspectFill
        view.layer.addSublayer(playerLayer)

        player?.play()
    }

    private func onStatusObserverChanged(playerItem: AVPlayerItem, change: NSKeyValueObservedChange<AVPlayerItem.Status>) {
        switch playerItem.status {
        case .failed:
            if let error = playerItem.error {
                print("播放失败:\(error.localizedDescription)")
            }
        case .readyToPlay:
            print("视频准备就绪")
        default:
            break
        }
    }
}

extension ViewController: AVAssetResourceLoaderDelegate {
    func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
        // 仅处理DRM密钥请求
        guard loadingRequest.contentInformationRequest?.contentType == AVStreamingKeyDeliveryContentKeyType else {
            return false
        }
        
        // 从MPD URL提取Content ID(根据后端约定调整)
        guard let mpdUrl = asset?.url,
              let lastPathComponent = mpdUrl.pathComponents.last,
              let contentId = lastPathComponent.components(separatedBy: "-mp4-cbcs.mpd").first,
              let contentIdData = contentId.data(using: .utf8) else {
            print("无法提取Content ID")
            loadingRequest.finishLoading(with: NSError(domain: "DRMError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无法提取Content ID"]))
            return false
        }
        
        // 请求FairPlay证书
        let certificateTask = URLSession.shared.dataTask(with: fairPlayCertificateUrl) { [weak self] data, _, error in
            guard let self = self else { return }
            guard let certificateData = data, error == nil else {
                print("证书请求失败:\(error?.localizedDescription ?? "未知错误")")
                loadingRequest.finishLoading(with: error ?? NSError(domain: "DRMError", code: -2, userInfo: [NSLocalizedDescriptionKey: "证书请求失败"]))
                return
            }
            
            do {
                // 生成SPC数据
                let spcData = try loadingRequest.streamingContentKeyRequestData(forApp: certificateData, contentIdentifier: contentIdData, options: nil)
                
                // 请求CKC密钥
                let spcBase64 = spcData.base64EncodedString()
                let bodyData = "spc=\(spcBase64)&assetId=\(contentId)".data(using: .utf8)
                var keyRequest = URLRequest(url: self.keyServerUrl)
                keyRequest.httpMethod = "POST"
                keyRequest.httpBody = bodyData
                
                URLSession.shared.dataTask(with: keyRequest) { data, _, error in
                    guard let ckcData = data, error == nil else {
                        print("CKC请求失败:\(error?.localizedDescription ?? "未知错误")")
                        loadingRequest.finishLoading(with: error ?? NSError(domain: "DRMError", code: -3, userInfo: [NSLocalizedDescriptionKey: "密钥请求失败"]))
                        return
                    }
                    
                    // 处理CKC响应,返回给AVPlayer
                    guard let dataRequest = loadingRequest.dataRequest else {
                        loadingRequest.finishLoading(with: NSError(domain: "DRMError", code: -4, userInfo: [NSLocalizedDescriptionKey: "数据请求无效"]))
                        return
                    }
                    
                    dataRequest.respond(with: ckcData)
                    loadingRequest.finishLoading()
                }.resume()
                
            } catch let spcError {
                print("生成SPC失败:\(spcError.localizedDescription)")
                loadingRequest.finishLoading(with: spcError)
            }
        }
        certificateTask.resume()
        
        return true
    }
}

额外调试建议

  • 打印streamingContentKeyRequestData抛出的具体错误信息,排查证书格式、Content ID匹配等问题;
  • 抓包验证证书请求和CKC请求的响应是否符合后端要求;
  • 确认后端FairPlay服务接受的参数格式(如SPC编码方式、Content ID传递规则)是否与你的请求一致。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 18:47:36