在iOS上使用AES-128加密播放离线HLS视频

10
我想通过AVFoundation在iOS中集成离线HLS。 我有一个使用简单AES-128加密的HLS,但它不想在离线模式下播放。 我尝试集成AVAssetResourceLoaderDelegate,但不知道如何集成https://developer.apple.com/streaming/fps/示例中的applicationCertificatecontentKeyFromKeyServerModuleWithSPCData。我感觉自己做错了什么。 这是一个样例AES-128加密,甚至不是DRM
没有网络的情况下,AVPlayer仍在尝试通过GET请求获取加密密钥。 如果有人成功地将加密密钥保存到本地并与AVURLAsset一起提供给AVPlayer,那就太好了。
有人成功地集成了吗?

你最终解决了这个问题吗? - bnussey
是的,我现在正在为我的问题写答案。 - Cyklet
2个回答

22

我已经向苹果支持写过信,他们的回复对我来说并不新鲜。在我与他们交谈之前,他们提供给我的信息都是我在wwdc视频和文档中得到的。(https://developer.apple.com/streaming/fps/

此外,我将描述如何使用AES-128加密以离线模式播放HLS。 GitHub上的示例 描述了以下过程。请注意AVDownloadTask在模拟器上无法正常工作,因此您应该有一台设备进行此实现。 首先,您需要一个流URL。

步骤1: 在创建AVURLAsset之前,我们应该取得流URL,并将其方案更改为无效(例如:https->fakehttps,我通过URLComponents完成),并将AVAssetResourceLoaderDelegate分配给新创建的URL资产。所有这些更改都会强制AVAssetDownloadTask调用:

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

(因为AVFoundation看到一个无效的URL并且不知道该怎么处理,所以它在调用。)

步骤2:当代理被调用时,我们应该检查URL是否与之前的相同。我们需要将方案更改回有效的方案,并使用它创建一个简单的URLSession。我们将获得第一个应该类似于以下内容的.m3u8文件:

#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

步骤 3: 从这个数据中解析出所有需要的信息,并将所有 https 方案更改为无效的 fakehttps

现在,您应该设置 AVAssetResourceLoadingRequest,从 shouldWaitForLoadingOfRequestedResource 委托中进行:

loadingRequest.contentInformationRequest?.contentType = response.mimeType
loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true
loadingRequest.contentInformationRequest?.contentLength = response.expectedContentLength
loadingRequest.dataRequest?.respond(with: modifiedData)
loadingRequest.finishLoading()
downloadTask?.resume()

其中: response -> 来自URLSession的响应, modifiedData -> 带有更改URL的数据

恢复您的下载任务,并在shouldWaitForLoadingOfRequestedResource委托中返回true

步骤4: 如果一切正常,AVAssetDownloadDelegate将会触发:

- (void)URLSession:(NSURLSession *)session assetDownloadTask:(AVAssetDownloadTask *)assetDownloadTask didResolveMediaSelection:(AVMediaSelection *)resolvedMediaSelection NS_AVAILABLE_IOS(9_0) {
}

第五步: 当AVFoundation选择最佳媒体流URL时,我们已经将所有的https更改为fakehttps。此时,shouldWaitForLoadingOfRequestedResource将再次触发,并从第一个.m3u8文件中选择一个URL。

第六步: 当委托再次被调用时,我们应该检查URL是否是我们需要的那个。然后,再次将伪装方案更改为有效方案,并使用该URL创建一个简单的URLSession。我们将获得第二个.m3u8文件:

#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
第七步:解析第二个 .m3u8 文件并从中获取所有所需的信息,同时注意

#EXT-X-KEY:METHOD=AES-128,URI="https://avid.avid.net/avid/key”

我们有一个用于加密密钥的URL。

步骤8:在将一些信息发送回AVAssetDownloadDelegate之前,我们需要从服务器下载密钥并将其本地保存在设备上。之后,您应该将第二个.m3u8文件中的URI=https://avid.avid.net/avid/key更改为无效的URI=fakehttps://avid.avid.net/avid/key,或者可能是您保存本地密钥的本地文件路径。现在,您应该从shouldWaitForLoadingOfRequestedResource委托中设置AVAssetResourceLoadingRequest,例如:

loadingRequest.contentInformationRequest?.contentType = response.mimeType
loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true
loadingRequest.contentInformationRequest?.contentLength = response.expectedContentLength
loadingRequest.dataRequest?.respond(with: modifiedData)
loadingRequest.finishLoading()
downloadTask?.resume()

其中:response -> 来自URLSession的响应,modifiedData -> 含有更改后URL的数据

恢复您的下载任务,并在shouldWaitForLoadingOfRequestedResource代理中返回true(与第3步相同)。

步骤9:当下载任务尝试使用修改的 URI= 创建请求时,又是一个无效的请求,此时 shouldWaitForLoadingOfRequestedResource 会再次触发。在这种情况下,您需要检测并创建新的数据与您的持久键(本地保存的键)。要注意, contentType 应为 AVStreamingKeyDeliveryPersistentContentKeyType ,否则AVFoundation将无法理解其中包含的密钥。

loadingRequest.contentInformationRequest?.contentType = AVStreamingKeyDeliveryPersistentContentKeyType
loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true
loadingRequest.contentInformationRequest?.contentLength = keyData.count
loadingRequest.dataRequest?.respond(with: keyData)
loadingRequest.finishLoading()
downloadTask?.resume()

步骤10: AVFoundation会自动下载块。 下载完成后,将调用此代理:

func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
}

当你想从设备播放流时,应该将location保存在某个地方,并从此location URL创建AVURLAsset。

所有这些信息都由AVFoundation本地保存,因此下次尝试在离线AVURLAsset中播放本地内容时,委托将被调用,因为URI = fakehttps://avid.avid.net/avid/key,那是一个无效的链接,在这里你将再次执行第9步,视频将以离线模式播放。

如果有更好的实现方法,欢迎告诉我。

Github上的示例


谢谢伙计,我在使用AES-128位HLS时遇到了另一个问题。 https://stackoverflow.com/questions/46097856/avplayer-stops-playing-aes-encrypted-offline-hls-video-in-online-mode/46300060#46300060 你的答案给了我一些提示,让我知道该怎么做了。 - Martin
@Sikander,你找到解决方案了吗? - Amrit Tiwari
@AmritTiwari 是的。最终不得不实现AVAssetResourceLoaderDelegate。 - Sikander
@Sikander 你好,能帮我解决如何在离线状态下播放下载的AES-128位加密视频吗? - Amrit Tiwari
@Sikander 用于初始化 AVURLAsset,以及当前状态的进度。 - Amrit Tiwari
显示剩余8条评论

0

@Cyklet的回答帮了我很多!谢谢。

不过,为了使hls流媒体正常工作,我还需要执行额外的步骤。


在使用shouldWaitForLoadingOfRequestedResource时,苹果文档指出:

contentType

在完成AVAssetResourceLoadingRequest实例的加载之前,如果其contentInformationRequest属性不为nil,则将此属性的值设置为表示所请求资源包含的数据类型的UTI。

在尝试实现HLS流媒体时,可以使用两个UTIs(据我所知...)。

  • AVStreamingKeyDeliveryPersistentContentKeyType

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

    • "com.apple.streamingkeydelivery.contentkey"

检查allowedContentTypes使用哪种UTI。请参见下面的可能实现:

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

....
   var contentType = AVStreamingKeyDeliveryPersistentContentKeyType
            
   if let allowedContentType = contentInformationRequest.allowedContentTypes?.first{
       if allowedContentType == AVStreamingKeyDeliveryContentKeyType{
           contentType = AVStreamingKeyDeliveryContentKeyType
       }
    }
....
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接