AVPlayer 停止在在线模式下播放 AES 加密的离线 HLS 视频

2023-12-03

我编写了一个代码来下载 HLS 视频并在离线模式下播放。 此代码适用于编码视频。现在我有一个 AES 加密的视频,我们为其提供了自定义加密密钥。下载 AES 加密的 HLS 视频后,我使用下面给出的代码来提供用于解密视频的密钥。

- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {

NSString *scheme = loadingRequest.request.URL.scheme;

if ([scheme isEqualToString:@"ckey"]) {

    NSString *request = loadingRequest.request.URL.host;
    NSData *data = [[NSUserDefaults standardUserDefaults] objectForKey:request];

    if (data) {
        [loadingRequest.dataRequest respondWithData:data];
        [loadingRequest finishLoading];
    } else {
        // Data loading fail
    }
}
return NO; }       

我正在拦截对密钥的请求并传递存储在 UserDefaults 中的密钥进行解密。

当我的设备的 WiFi 或数据连接关闭时,此带有自定义密钥的 AES 加密 HLS 视频可以正常播放。

如果我在设备的 Wi-Fi 或数据连接启用时开始播放此视频,或者如果 我在播放视频时启用设备的 WiFi 或数据连接;视频立即停止播放,没有任何错误,并且不再播放。

我已经检查了playerItem的accessLog和errorLog,但没有发现任何有用的东西。

为了在下载 HLS 内容后提供自定义 URL 密钥,我通过替换来更新 .m3u8 文件的内容

URI=“...”

字符串与

URI=“ckey://...”

这是为 AES 加密视频提供密钥的正确方法吗?

这种行为的原因是什么以及如何解决这个问题?

提前致谢。


最后我设法解决了这个问题。下载的HLS视频的大致包结构如下:

HLS.movpkg 
 |_ 0-12345
    |_ 123.m3u8
    |_ StreamInfoBoot.xml
    |_ StreamInfoRoot.xml
    |_ <>.frag
 |_ boot.xml
  1. boot.xml 包含 HLS 的网络 URL(基于 https:)
  2. StreamBootInfo.xml 包含 HLS URL(基于 https:)和本地下载的 .frag 文件之间的映射。

在离线模式下,HLS 视频播放完美。但是,当启用网络连接时,它引用的是 https: URL 而不是本地 .frag 文件。

我用自定义方案(fakehttps:)替换了这些文件中的 https: 方案,以限制 AVPlayer 上网获取资源。

这个东西解决了我的问题,但我不知道其背后的确切原因以及AVPlayer如何播放HLS。

我提到this并得到了一些想法,所以尝试了一些东西。

我正在进一步更新此答案,以解释如何在离线模式下播放加密视频。

  1. 获取视频解密所需的密钥。

  2. 将该密钥保存在某个地方。

您可以将该密钥另存为NSData or Data对象在UserDefault我使用视频文件名作为密钥来将关键数据保存在 UserDefaults 中。

  1. Use FileManager用于迭代内部所有文件的 API.movpkg.

  2. 获取每一个的内容.m3u8文件并替换URI="some key url"与 URI="ckey://keyusedToSaveKeyDataInUserDefaults"

您可以参考下面给出的代码来了解此过程。

  if let url = asset.asset?.url, let data = data {
            
            let keyFileName = "\(asset.contentCode!).key"
            UserDefaults.standard.set(data, forKey: keyFileName)
            
            do {
                
                // ***** Create key file *****
                let keyFilePath = "ckey://\(keyFileName)"
                
                let subDirectories = try fileManager.contentsOfDirectory(at: url,
                                                                                 includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants)
                
                for url in subDirectories {
                    
                    var isDirectory: ObjCBool = false
                    
                    if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
                        
                        if isDirectory.boolValue {
                            
                            let path = url.path as NSString
                            
                            let folderName = path.lastPathComponent
                            let playlistFilePath = path.appendingPathComponent("\(folderName).m3u8")
                            
                            if fileManager.fileExists(atPath: playlistFilePath) {
                                
                                var fileContent = try String.init(contentsOf: URL.init(fileURLWithPath: playlistFilePath))
                                
                                let stringArray = self.matches(for: "URI=\"(.+?)\"", in: fileContent)
                                
                                for pattern in stringArray {
                                    fileContent = fileContent.replacingOccurrences(of: pattern, with: "URI=\"\(keyFilePath)\"")
                                }
                                
                                try fileContent.write(toFile: playlistFilePath, atomically: true, encoding: .utf8)
                            }
                            
                            let streamInfoXML = path.appendingPathComponent("StreamInfoBoot.xml")

                            if fileManager.fileExists(atPath: streamInfoXML) {

                                var fileContent = try String.init(contentsOf: URL.init(fileURLWithPath: streamInfoXML))
                                fileContent = fileContent.replacingOccurrences(of: "https:", with: "fakehttps:")
                                try fileContent.write(toFile: streamInfoXML, atomically: true, encoding: .utf8)
                            }
                        } else {

                            if url.lastPathComponent == "boot.xml" {

                                let bootXML = url.path

                                if fileManager.fileExists(atPath: bootXML) {

                                    var fileContent = try String.init(contentsOf: URL.init(fileURLWithPath: bootXML))
                                    fileContent = fileContent.replacingOccurrences(of: "https:", with: "fakehttps:")
                                    try fileContent.write(toFile: bootXML, atomically: true, encoding: .utf8)
                                }
                            }
                        }
                    }
                }
                
                userInfo[Asset.Keys.state] = Asset.State.downloaded.rawValue
                
                // Update download status to db
                let user = RoboUser.sharedObject()
                let sqlDBManager = RoboSQLiteDatabaseManager.init(databaseManagerForCourseCode: user?.lastSelectedCourse)
                sqlDBManager?.updateContentDownloadStatus(downloaded, forContentCode: asset.contentCode!)
               
                self.notifyServerAboutContentDownload(asset: asset)
                
                NotificationCenter.default.post(name: AssetDownloadStateChangedNotification, object: nil, userInfo: userInfo)
            } catch  {
            }
        }

func matches(for regex: String, in text: String) -> [String] {
    
    do {
        let regex = try NSRegularExpression(pattern: regex)
        let nsString = text as NSString
        let results = regex.matches(in: text, range: NSRange(location: 0, length: nsString.length))
        return results.map { nsString.substring(with: $0.range)}
    } catch let error {
        print("invalid regex: \(error.localizedDescription)")
        return []
    }
}

这将更新您的下载包结构,以便在离线模式下播放加密视频。

现在要做的最后一件事是实现 AVAssetResourceLoader 类的给定方法,如下所示

- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
    
    NSString *scheme = loadingRequest.request.URL.scheme;
    
    if ([scheme isEqualToString:@"ckey"]) {
        
        NSString *request = loadingRequest.request.URL.host;
        NSData *data = [[NSUserDefaults standardUserDefaults] objectForKey:request];
        
        if (data) {
            loadingRequest.contentInformationRequest.contentType = AVStreamingKeyDeliveryPersistentContentKeyType;
            loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES;
            loadingRequest.contentInformationRequest.contentLength = data.length;
            [loadingRequest.dataRequest respondWithData:data];
            [loadingRequest finishLoading];
        } else {
            // Data loading fail
        }
    }
    
    return YES;
}

此方法将在播放时提供视频密钥以对其进行解密。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

AVPlayer 停止在在线模式下播放 AES 加密的离线 HLS 视频 的相关文章

随机推荐