I want to integrate offline HLS in iOS through AVFoundation
.
I have an encrypted HLS with simple AES-128 and it doesn't want to play in offline mode, I was trying to integrate AVAssetResourceLoaderDelegate
, but don't know how to integrate applicationCertificate
and contentKeyFromKeyServerModuleWithSPCData
that are in https://developer.apple.com/streaming/fps/ examples. I have a feeling that I am doing something wrong. It is a sample AES-128 encryption
, not even DRM
.
Without the internet, AVPlayer
is still trying to get encryption key
through GET
request.
It would be great if someone succeeded to save the encrypted key locally and somehow gave it to AVPlayer
together with AVURLAsset
.
Did someone manage to integrate this?
In fact, there are two encryption schemes which are supported by HLS: - AES-128 encryption: This means media segments are completely encrypted using the Advanced Encryption Standard with a 128-bit key. It also allows for the usage of initialisation vectors to optimise the protection.
In practice, AES-128 is the most commonly used method for HLS encryption. This method is also often the easiest to achieve using standard streaming servers and tools. How safe is this AES-128 protection?
Using AES-128 encryption can be done by encrypting your media files and signalling this using the EXT-X-KEY-tag within the manifest file. This tag signals the URL to the decryption key.
The usage of AES encryption recently became part of the common encryption standard for MPEG-DASH as well. In general, it might be safe to say this level of AES encryption will not be broken soon. The AES encryption itself can be declared safe.
I have written to apple support and their responses weren't new for me. Information that they provided to me I got from wwdc videos and documentation before I started a conversation with them. (https://developer.apple.com/streaming/fps/)
Further, I will describe how I achieve to play HLS in offline mode with AES-128 encryption. The Example On Github describes the below process. Take care AVDownloadTask doesn’t work on the simulator so you should have a device for this implementation. At the beginning, you need a stream URL.
Step 1: Before creating AVURLAsset we should take stream URL and change scheme to an invalid one (example: https -> fakehttps, I did it through URLComponents) and assign AVAssetResourceLoaderDelegate to the new created url asset. All this changes force AVAssetDownloadTask to call:
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
}
(it is calling because AVFoundation see an invalid URL and doesn’t know what to do with it)
Step 2: When delegate is called we should check that url is that one that we had before. We need to change back scheme to valid one and create a simple URLSession with it. We will get first .m3u8 file that should be like:
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1697588,RESOLUTION=1280x720,FRAME-RATE=23.980,CODECS="mp4a"
https://avid.avid.net/avid/information_about_stream1
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1132382,RESOLUTION=848x480,FRAME-RATE=23.980,CODECS="mp4a"
https://avid.avid.net/avid/information_about_stream2
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=690409,RESOLUTION=640x360,FRAME-RATE=23.980,CODECS="mp4a"
https://avid.avid.net/avid/information_about_stream3
Step 3: Parse all needed information from this data and change all https schemes to invalid one fakehttps Now you should setup AVAssetResourceLoadingRequest from shouldWaitForLoadingOfRequestedResource delegate like:
loadingRequest.contentInformationRequest?.contentType = response.mimeType
loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true
loadingRequest.contentInformationRequest?.contentLength = response.expectedContentLength
loadingRequest.dataRequest?.respond(with: modifiedData)
loadingRequest.finishLoading()
downloadTask?.resume()
where: response -> response from URLSession, modifiedData -> data with changed URL’s
Resume your download task and return true in shouldWaitForLoadingOfRequestedResource delegate
Step 4: If everything will be ok AVAssetDownloadDelegate will fire with:
- (void)URLSession:(NSURLSession *)session assetDownloadTask:(AVAssetDownloadTask *)assetDownloadTask didResolveMediaSelection:(AVMediaSelection *)resolvedMediaSelection NS_AVAILABLE_IOS(9_0) {
}
Step 5: We have changed all https to fakehttps when AVFoundation will select best media stream URL, shouldWaitForLoadingOfRequestedResource will trigger again with one of the URL from first .m3u8
Step 6: When delegate is called again we should check that url is that one that we needed. Change again fake scheme to a valid one and create a simple URLSession with this url. We will get second .m3u8 file:
#EXTM3U
#EXT-X-TARGETDURATION:12
#EXT-X-ALLOW-CACHE:YES
#EXT-X-KEY:METHOD=AES-128,URI="https://avid.avid.net/avid/key”
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:1
#EXTINF:6.006,
https://avid.avid.net/avid/information_about_stream1
#EXTINF:4.713,
https://avid.avid.net/avid/information_about_stream2
#EXTINF:10.093,
https://avid.avid.net/avid/information_about_stream3
#EXT-X-ENDLIST
Step 7: Parse second .m3u8 file and take all information that you need from it, also take a look on
#EXT-X-KEY:METHOD=AES-128,URI="https://avid.avid.net/avid/key”
We have URL for encryption key
Step 8:
Before sending some information back to AVAssetDownloadDelegate we need to download the key from the server and save it locally on the device. After this you should change URI=https://avid.avid.net/avid/key
from second .m3u8 to an invalid URI=fakehttps://avid.avid.net/avid/key
, or maybe a local file path where you have saved your local key.
Now you should setup AVAssetResourceLoadingRequest from shouldWaitForLoadingOfRequestedResource delegate smth. like:
loadingRequest.contentInformationRequest?.contentType = response.mimeType
loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true
loadingRequest.contentInformationRequest?.contentLength = response.expectedContentLength
loadingRequest.dataRequest?.respond(with: modifiedData)
loadingRequest.finishLoading()
downloadTask?.resume()
where: response -> response from URLSession, modifiedData -> data with changed URL’s
Resume your download task and return true in shouldWaitForLoadingOfRequestedResource delegate (Same as on Step 3)
Step 9:
Of course, when download task will try to create request with modified URI=
that again is not a valid one shouldWaitForLoadingOfRequestedResource will trigger again. In this case, you should detect this and create new data with your persistent key(the key that you saved locally. Take care here contentType
should be AVStreamingKeyDeliveryPersistentContentKeyType
without it AVFoundation doesn’t understand that this contains key).
loadingRequest.contentInformationRequest?.contentType = AVStreamingKeyDeliveryPersistentContentKeyType
loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true
loadingRequest.contentInformationRequest?.contentLength = keyData.count
loadingRequest.dataRequest?.respond(with: keyData)
loadingRequest.finishLoading()
downloadTask?.resume()
Step 10: Chunks will be downloaded automatically by AVFoudnation. When download is finished this delegate will be called:
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
}
You should save location
somewhere, when you want to play stream from device you should create AVURLAsset from this location
URL
All this information is saved locally by AVFoundation so next time when you will try to play local content in offline AVURLAsset delegate will be called because of URI=fakehttps://avid.avid.net/avid/key
, that is an invalid link, here you will do Step 9 again and video will play in offline mode.
This works for me if anyone knows better implementation I will be glad to know.
Example On Github
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With