Search code examples
iosencryptionhttp-live-streaming

Playing Offline HLS with AES-128 encryption iOS


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?


Solution

  • 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