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

Swift中FairPlay DRM播放求助:AVAssetResourceDelegate未调用

搞定FairPlay DRM的ResourceLoaderDelegate触发与m3u8加载问题

我看了你遇到的问题:用HTTPS URL创建AVURLAsset时,AVAssetResourceLoaderDelegate完全不触发;改成自定义scheme(比如drm)后代理终于调用了,但又拿不到m3u8文件。这其实是AVFoundation的默认行为导致的——系统会直接处理HTTPS协议的请求,不会走自定义ResourceLoader;而自定义scheme的请求,AVFoundation不知道怎么解析,得我们手动转发回HTTPS获取媒体内容。

下面是修复后的完整代码,我给你标了关键修改点:

import Foundation
import AVKit
import os.log

public struct DRMVideoData{
    var drmKey: String?
    var proxyFairPlay: String
    var fileFairPlay: String
    var idVideo: String
}

class VODDRMImplementation: NSObject, AVAssetResourceLoaderDelegate {
    let domain = "DRMDelegate.ContentKeyQueue"
    let contentKeyDelegateQueue = DispatchQueue(label: "DRMDelegate.ContentKeyQueue")
    var drmData: DRMVideoData?
    // 存储原始HTTPS视频地址,后续用来转换自定义scheme请求
    private var originalHttpsUrl: URL?
    
    func startPlayerWithDRM(_ videoDRM: DRMVideoData,_ player: AVPlayer?,_ playerLayer: AVPlayerLayer?, c: @escaping (AVPlayer?, AVPlayerLayer?) -> Void) {
        guard let originalUrl = URL(string: videoDRM.fileFairPlay) else {
            c(nil, nil)
            return
        }
        self.originalHttpsUrl = originalUrl
        
        // 替换scheme为自定义的"drm",确保ResourceLoader能被触发
        var urlComponents = URLComponents(url: originalUrl, resolvingAgainstBaseURL: false)!
        urlComponents.scheme = "drm"
        guard let drmSchemeUrl = urlComponents.url else {
            c(nil, nil)
            return
        }
        
        self.drmData = videoDRM
        let asset = AVURLAsset(url: drmSchemeUrl)
        asset.resourceLoader.setDelegate(self, queue: self.contentKeyDelegateQueue)
        let playerItem = AVPlayerItem(asset: asset)
        let player = AVPlayer(playerItem: playerItem)
        let playerLayer = AVPlayerLayer(player: player)
        player.pause()
        c(player, playerLayer)
    }
    
    func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
        guard let drmSchemeUrl = loadingRequest.request.url, let originalHttpsUrl = self.originalHttpsUrl else {
            os_log("DRM: 请求URL或原始HTTPS地址无效", log: .default, type: .error)
            loadingRequest.finishLoading(with: NSError(domain: domain, code: -1, userInfo: nil))
            return false
        }
        
        // 第一步:处理m3u8/ts媒体文件请求——转换回HTTPS协议获取内容
        if drmSchemeUrl.pathExtension == "m3u8" || drmSchemeUrl.pathExtension == "ts" {
            var originalRequestComponents = URLComponents(url: originalHttpsUrl, resolvingAgainstBaseURL: false)!
            originalRequestComponents.path = drmSchemeUrl.path
            originalRequestComponents.query = drmSchemeUrl.query
            guard let httpsRequestUrl = originalRequestComponents.url else {
                loadingRequest.finishLoading(with: NSError(domain: domain, code: -2, userInfo: nil))
                return false
            }
            
            var httpsRequest = URLRequest(url: httpsRequestUrl)
            httpsRequest.allHTTPHeaderFields = loadingRequest.request.allHTTPHeaderFields
            
            let session = URLSession(configuration: .default)
            let task = session.dataTask(with: httpsRequest) { data, response, error in
                if let error = error {
                    os_log("DRM: 获取媒体内容失败:%@", log: .default, type: .error, error.localizedDescription)
                    loadingRequest.finishLoading(with: error as NSError)
                    return
                }
                
                guard let response = response, let data = data else {
                    os_log("DRM: 媒体请求无响应或数据", log: .default, type: .error)
                    loadingRequest.finishLoading(with: NSError(domain: self.domain, code: -3, userInfo: nil))
                    return
                }
                
                // 把HTTPS获取到的内容返回给AVFoundation
                loadingRequest.contentInformationProvider?.contentType = (response as? HTTPURLResponse)?.mimeType
                loadingRequest.contentInformationProvider?.contentLength = response.expectedContentLength
                loadingRequest.dataRequest?.respond(with: data)
                loadingRequest.finishLoading()
            }
            task.resume()
            return true
        }
        
        // 第二步:处理DRM密钥请求(SPC -> CKC)
        guard let drmData = drmData else {
            os_log("DRM: DRM配置数据缺失", log: .default, type: .error)
            loadingRequest.finishLoading(with: NSError(domain: domain, code: -4, userInfo: nil))
            return false
        }
        
        do {
            guard let certificateData = getCertificateFromServer() else {
                os_log("DRM: 加载证书失败", log: .default, type: .error)
                loadingRequest.finishLoading(with: NSError(domain: domain, code: -5, userInfo: nil))
                return false
            }
            
            let contentId = drmData.idVideo
            guard let contentIdData = contentId.data(using: .utf8),
                  let spcData = try? loadingRequest.streamingContentKeyRequestData(forApp: certificateData, contentIdentifier: contentIdData, options: nil),
                  let dataRequest = loadingRequest.dataRequest else {
                os_log("DRM: 生成SPC数据失败", log: .default, type: .error)
                loadingRequest.finishLoading(with: NSError(domain: domain, code: -6, userInfo: nil))
                return false
            }
            
            guard let ksmServerUrl = URL(string: drmData.proxyFairPlay) else {
                os_log("DRM: KSM服务器地址无效", log: .default, type: .error)
                loadingRequest.finishLoading(with: NSError(domain: domain, code: -7, userInfo: nil))
                return false
            }
            
            // 注意:多数KSM服务器要求用POST传递SPC数据,原代码用GET可能有问题,根据你的KSM配置调整
            var ksmRequest = URLRequest(url: ksmServerUrl)
            ksmRequest.httpMethod = "POST"
            ksmRequest.httpBody = spcData
            
            let session = URLSession(configuration: .default)
            let task = session.dataTask(with: ksmRequest) { data, response, error in
                if let error = error {
                    os_log("DRM: 获取CKC密钥失败:%@", log: .default, type: .error, error.localizedDescription)
                    loadingRequest.finishLoading(with: error as NSError)
                    return
                }
                
                guard let ckcData = data else {
                    os_log("DRM: 未收到CKC数据", log: .default, type: .error)
                    loadingRequest.finishLoading(with: NSError(domain: self.domain, code: -8, userInfo: nil))
                    return
                }
                
                dataRequest.respond(with: ckcData)
                loadingRequest.finishLoading()
            }
            task.resume()
            return true
        } catch {
            os_log("DRM: 处理DRM请求出错:%@", log: .default, type: .error, error.localizedDescription)
            loadingRequest.finishLoading(with: error as NSError)
            return false
        }
    }
    
    func getCertificateFromServer() -> Data? {
        // 修复原代码的路径错误:本地文件路径要用fileURLWithPath,不能用URL(string:)
        guard let filePath = Bundle.main.path(forResource: "privatekey", ofType: "pem"),
              let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
            os_log("DRM: 从Bundle加载证书失败", log: .default, type: .error)
            return nil
        }
        return data
    }
}

关键修改说明

  1. 存储原始HTTPS URL:在初始化时保存视频的原始HTTPS地址,后续把自定义scheme的请求转换回HTTPS,让系统能正确获取m3u8和媒体片段
  2. 区分请求类型:通过文件扩展名判断是媒体请求还是DRM密钥请求,分别处理——媒体请求转发回HTTPS,密钥请求走原有DRM逻辑
  3. 修复证书加载路径:原代码用URL(string: filePath!)加载本地文件是错误的,本地路径必须用URL(fileURLWithPath: filePath)
  4. 调整KSM请求方法:多数KSM服务器要求用POST接收SPC数据,原代码的GET请求可能导致KSM无法解析,记得根据你的KSM配置确认

额外注意事项

  • 测试时打开日志输出,方便定位每一步的问题
  • 确认你的KSM服务器能正确处理SPC数据并返回CKC,可以先用Postman测试KSM接口
  • 如果自定义scheme遇到权限问题,可在Info.plist的LSApplicationQueriesSchemes中添加drm(一般AVAssetResourceLoader处理不需要,但保险起见可以加上)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 03:53:19