I have written a code to download HLS video and play it in offline mode.
This code works fine for encoded video. Now I have a video which is AES encrypted and we are having custom encryption key for it. After downloading AES encrypted HLS video I am using below given code to supply key for decryption of video.
- (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; }
I am intercepting a request for a key and passing key stored in UserDefaults for decryption.
This AES encrypted HLS video with custom key plays well when my device's wifi or data connection is off.
If I start playing this video when my device's wifi or data connection is enabled or if
I enable my device's wifi or data connection while playing video; video stops playing immediately without any error and never plays again.
I have checked accessLog and errorLog of playerItem but haven't found anything helpful.
To provide a custom URL key after downloading of HLS content I am updating a content of .m3u8 file by replacing
URI="..."
string with
URI="ckey://..."
Is this a correct way to provide key for AES encrypted video?
and what could be the reason of this behaviour and how to solve this issue?
Thanks in advance.
Finally I managed to solve this issue. Rough package structure of downloaded HLS video is like given below:
HLS.movpkg
|_ 0-12345
|_ 123.m3u8
|_ StreamInfoBoot.xml
|_ StreamInfoRoot.xml
|_ <>.frag
|_ boot.xml
- boot.xml contains network URL for HLS (which is https: based)
- StreamBootInfo.xml contains mapping between HLS URL (which is https: based) and .frag file downloaded locally.
In offline mode HLS video was playing perfectly. But when network connection was enabled it was referring to https: URL instead of local .frag files.
I replaced https: scheme in these files with custom scheme (fakehttps:) to restrict AVPlayer going online for resources.
This thing solved my issue but I don't know the exact reason behind it and how HLS is played by AVPlayer.
I referred this and got some idea so tried something .
I am updating this answer further to explain how to play encrypted video in offline mode.
Get the key required for video decryption.
Save that key some where.
You can save that key as NSData
or Data
object in UserDefault
I am using video file name as key to save key data in UserDefaults.
Use FileManager
API to iterate over all the files inside .movpkg
.
Get the content of each .m3u8
file and replace URI="some key url"
with URI="ckey://keyusedToSaveKeyDataInUserDefaults"
You can refer code given below for this process.
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 []
}
}
This will update your download package structure for playing encrypted video in offline mode.
Now last thing to do is implement below given method of AVAssetResourceLoader class as follows
- (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;
}
This method will provide key to video while playing to decrypt it.