Playing Offline HLS with AES-128 encryption iOS
Asked Answered
B

2

10

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?

Bunch answered 14/8, 2017 at 8:53 Comment(2)
Did you end up resolving this?Equivalency
yeap I am writing right now answer for my questionBunch
B
22

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

Bunch answered 30/8, 2017 at 9:44 Comment(13)
Thanks buddy I was facing another issue with AES-128 bit HLS #46098356 Your answer gave me some hint about what to do.Aton
Would be very helpful if you could share complete class or project code. Thanks!Kerstin
@Kerstin did you find the solution?Madonnamadora
@AmritTiwari yes. Had to implement AVAssetResourceLoaderDelegate finally.Kerstin
@Kerstin can you please provide me a solution? I am stuck, if you provide me with a solution then it will be a great help to me.Madonnamadora
@Kerstin Hello, can you help me to play downloaded AES-128 bit encrypted video on offline?Madonnamadora
@Kerstin here is the sample code github.com/tiwariammit/HLSVideoDownloaderMadonnamadora
@Kerstin used for initialization of AVURLAsset, progress of the current state.Madonnamadora
I followed the above steps but chunks are not getting downloaded. First chunk starts to download and then it fails also didFinishDownloadingTo method is getting called after the failure of first chunk.Jesusa
@Cyklet: I am looking for same approach, Can you please provide sample code for this. It will be helpful for every one.Mattock
I have followed all the steps. Final it is calling below delegate method. So is this procedure still is in working state. Please let me know. func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {Mattock
I know that it can be too late but I just added in my answer a link github.com/Cyklet/VidLoader with an example of how this process has been implemented.Bunch
@Bunch after days trying to resolve, you finally saved my life! Thank you!Egor
S
0

@Cyklet Answer helped me a ton! Thanks.

There is however an additional step I had to do to get hls streaming to work.


When using shouldWaitForLoadingOfRequestedResource apples documentation states:

contentType

Before finishing loading an AVAssetResourceLoadingRequest instance, if its contentInformationRequest property is not nil, set the value of this property to a UTI indicating the type of data contained by the requested resource.

When trying to implement HLS Streaming there are two UTIs that may be used (as far as I know...).

  • AVStreamingKeyDeliveryPersistentContentKeyType

    • "com.apple.streamingkeydelivery.persistentcontentkey"
  • AVStreamingKeyDeliveryContentKeyType

    • "com.apple.streamingkeydelivery.contentkey"

Check allowedContentTypes what UTI to use. See possible implementation below:

func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {

....
   var contentType = AVStreamingKeyDeliveryPersistentContentKeyType
            
   if let allowedContentType = contentInformationRequest.allowedContentTypes?.first{
       if allowedContentType == AVStreamingKeyDeliveryContentKeyType{
           contentType = AVStreamingKeyDeliveryContentKeyType
       }
    }
....
}
Squirmy answered 21/8, 2022 at 16:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.