AVPlayer 在在线模式下停止播放 AES 加密的离线 HLS 视频
Posted
技术标签:
【中文标题】AVPlayer 在在线模式下停止播放 AES 加密的离线 HLS 视频【英文标题】:AVPlayer Stops Playing AES encrypted offline HLS Video in online mode 【发布时间】:2017-09-07 13:37:42 【问题描述】:我编写了一个代码来下载 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 中的密钥以进行解密。
这个带有自定义密钥的 AES 加密 HLS 视频在我设备的 wifi 或数据连接关闭时播放良好。
如果我在设备的 wifi 或数据连接启用时开始播放此视频,或者如果 我在播放视频时启用了设备的 wifi 或数据连接;视频立即停止播放,没有任何错误,并且不再播放。
我检查了 playerItem 的 accessLog 和 errorLog 但没有发现任何有用的东西。
为了在下载 HLS 内容后提供自定义 URL 密钥,我通过替换来更新 .m3u8 文件的内容
URI="..."
带有
的字符串URI="ckey://..."
这是为 AES 加密视频提供密钥的正确方法吗?
这种行为的原因是什么以及如何解决这个问题?
提前致谢。
【问题讨论】:
你找到解决办法了吗? @AmritTiwari 下面给出的答案是解决这个问题,你可以试试这个。 我能找到解决方案,你能帮帮我吗? @AmritTiwari 是的,告诉我你的问题是什么。 @AmritTiwari 我已经更新了我的答案,请检查一下。如果有任何疑问,请告诉我。 【参考方案1】:最后我设法解决了这个问题。下载的 HLS 视频的大致包结构如下:
HLS.movpkg
|_ 0-12345
|_ 123.m3u8
|_ StreamInfoBoot.xml
|_ StreamInfoRoot.xml
|_ <>.frag
|_ boot.xml
-
boot.xml 包含 HLS 的网络 URL(基于 https:)
StreamBootInfo.xml 包含 HLS URL(基于 https:)和本地下载的 .frag 文件之间的映射。
在离线模式下,HLS 视频可以完美播放。但是当启用网络连接时,它指的是 https: URL 而不是本地 .frag 文件。
我用自定义方案 (fakehttps:) 替换了这些文件中的 https: 方案,以限制 AVPlayer 在线获取资源。
这件事解决了我的问题,但我不知道它背后的确切原因以及 AVPlayer 是如何播放 HLS 的。
我推荐了this 并得到了一些想法,所以尝试了一些方法。
我正在进一步更新此答案以解释如何在离线模式下播放加密视频。
获取视频解密所需的密钥。
将该密钥保存在某个位置。
您可以将该密钥保存为
UserDefault
中的NSData
或Data
对象我正在使用视频文件名作为密钥来将密钥数据保存在UserDefaults 中。
使用
FileManager
API 遍历.movpkg
中的所有文件。获取每个
.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;
此方法将在播放时提供视频密钥以进行解密。
【讨论】:
感谢您更新您的答案,您能告诉我匹配方法在您的代码中的作用吗? @AmritTiwari 我已经用matches() 方法更新了这个答案。它只是检查正则表达式。 你能告诉我你是怎么做的吗?我对应用加密密钥以及何时设置 AVAssetResourceLoaderDelegate 的 resourceLoader 感到困惑? 你能帮帮我吗? @Martin,谢谢伙计!我希望我可以请你喝咖啡!以上是关于AVPlayer 在在线模式下停止播放 AES 加密的离线 HLS 视频的主要内容,如果未能解决你的问题,请参考以下文章
是否可以强制 AVPlayer 停止在外部播放视频(在 Apple TV 上)