最后我设法解决了这个问题。下载的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并得到了一些想法,所以尝试了一些东西。
我正在进一步更新此答案,以解释如何在离线模式下播放加密视频。
-
获取视频解密所需的密钥。
-
将该密钥保存在某个地方。
您可以将该密钥另存为NSData
or Data
对象在UserDefault
我使用视频文件名作为密钥来将关键数据保存在 UserDefaults 中。
-
Use 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;
}
此方法将在播放时提供视频密钥以对其进行解密。