如何通过分别提供密钥文件来播放m3u8encryption的播放列表?

我有一个m3u8播放列表文件(让我们称之为素数),它指向另一个播放列表文件,该文件又具有带有密钥文件URL的ts URL。 使用MPMoviePlayer我目前可以播放prime m3u8文件。 这些段使用AES-128位encryption进行encryption,密钥文件位于最终的m3u8文件中。 有没有一种方法可以提供最终的m3u8文件,并告诉应用程序使用本地密钥文件来解密video,所以我不必公开发布密钥文件。

这与这个SO问题有些相关

是的 – 你可以在传递给玩家之前修改最终的m3u8文件。 例如,将KEY行改为引用http://localhost/key 。 那么你会想运行一个本地的http服务器,如cocoahttpserver来把密钥传送给video播放器。

我已经实现了类似的东西。 我们所做的是:

  1. 使用具有键值对和时间戳组合的JWT令牌在运行时对每个实时stream片段进行encryption以进行validation。
  2. 我们的服务器知道如何解密这个密钥。 当解密的数据有效时,服务器以.ts文件响应,因此回放变得安全。

这里是完整的工作代码与步骤提到:

 //Step 1,2:- Initialise player, change the scheme from http to fakehttp and set delete of resource loader. These both steps will trigger the resource loader delegate function so that we can manually handle the loading of segments. func setupPlayer(stream: String) { operationQ.cancelAllOperations() let blckOperation = BlockOperation { let currentTStamp = Int(Date().timeIntervalSince1970 + 86400)// let timeStamp = String(currentTStamp) self.token = JWT.encode(["Expiry": timeStamp], algorithm: .hs256("qwerty".data(using: .utf8)!)) self.asset = AVURLAsset(url: URL(string: "fake\(stream)")!, options: nil) let loader = self.asset?.resourceLoader loader?.setDelegate(self, queue: DispatchQueue.main) self.asset!.loadValuesAsynchronously(forKeys: ["playable"], completionHandler: { var error: NSError? = nil let keyStatus = self.asset!.statusOfValue(forKey: "playable", error: &error) if keyStatus == AVKeyValueStatus.failed { print("asset status failed reason \(error)") return } if !self.asset!.isPlayable { //FIXME: Handle if asset is not playable return } self.playerItem = AVPlayerItem(asset: self.asset!) self.player = AVPlayer(playerItem: self.playerItem!) self.playerView.playerLayer.player = self.player self.playerLayer?.backgroundColor = UIColor.black.cgColor self.playerLayer?.videoGravity = AVLayerVideoGravityResizeAspect NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemDidReachEnd(notification:)), name: Notification.Name.AVPlayerItemDidPlayToEndTime, object: self.playerItem!) self.addObserver(self, forKeyPath: "player.currentItem.duration", options: [.new, .initial], context: &playerViewControllerKVOContext) self.addObserver(self, forKeyPath: "player.rate", options: [.new, .old], context: &playerViewControllerKVOContext) self.addObserver(self, forKeyPath: "player.currentItem.status", options: [.new, .initial], context: &playerViewControllerKVOContext) self.addObserver(self, forKeyPath: "player.currentItem.loadedTimeRanges", options: [.new], context: &playerViewControllerKVOContext) self.addObserver(self, forKeyPath: "player.currentItem.playbackLikelyToKeepUp", options: [.new], context: &playerViewControllerKVOContext) self.addObserver(self, forKeyPath: "player.currentItem.playbackBufferEmpty", options: [.new], context: &playerViewControllerKVOContext) }) } operationQ.addOperation(blckOperation) } //Step 2, 3:- implement resource loader delegate functions and replace the fakehttp with http so that we can pass this m3u8 stream to the parser to get the current m3u8 in string format. func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool { var url = loadingRequest.request.url?.absoluteString let contentRequest = loadingRequest.contentInformationRequest let dataRequest = loadingRequest.dataRequest //Check if the it is a content request or data request, we have to check for data request and do the m3u8 file manipulation if (contentRequest != nil) { contentRequest?.isByteRangeAccessSupported = true } if (dataRequest != nil) { //this is data request so processing the url. change the scheme to http url = url?.replacingOccurrences(of: "fakehttp", with: "http") if (url?.contains(".m3u8"))! { // do the parsing on background thread to avoid lags // step 4: self.parsingHandler(url: url!, loadingRequest: loadingRequest, completion: { (success) in return true }) } else if (url?.contains(".ts"))! { let redirect = self.generateRedirectURL(sourceURL: url!) if (redirect != nil) { //Step 9 and 10:- loadingRequest.redirect = redirect! let response = HTTPURLResponse(url: URL(string: url!)!, statusCode: 302, httpVersion: nil, headerFields: nil) loadingRequest.response = response loadingRequest.finishLoading() } return true } return true } return true } func parsingHandler(url: String, loadingRequest: AVAssetResourceLoadingRequest, completion:((Bool)->Void)?) -> Void { DispatchQueue.global(qos: .background).async { var string = "" var originalURIStrings = [String]() var updatedURIStrings = [String]() do { let model = try M3U8PlaylistModel(url: url) if model.masterPlaylist == nil { //Step 5:- string = model.mainMediaPl.originalText let array = string.components(separatedBy: CharacterSet.newlines) if array.count > 0 { for line in array { //Step 6:- if line.contains("EXT-X-KEY:") { //at this point we have the ext-x-key tag line. now tokenize it with , and then let furtherComponents = line.components(separatedBy: ",") for component in furtherComponents { if component.contains("URI") { // Step 7:- //save orignal URI string to replaced later originalURIStrings.append(component) //now we have the URI //get the string in double quotes var finalString = component.replacingOccurrences(of: "URI=\"", with: "").replacingOccurrences(of: "\"", with: "") finalString = "\"" + finalString + "&token=" + self.token! + "\"" finalString = "URI=" + finalString updatedURIStrings.append(finalString) } } } } } if originalURIStrings.count == updatedURIStrings.count { //Step 8:- for uriElement in originalURIStrings { string = string.replacingOccurrences(of: uriElement, with: updatedURIStrings[originalURIStrings.index(of: uriElement)!]) } //print("String After replacing URIs \n") //print(string) } } else { string = model.masterPlaylist.originalText } } catch let error { print("Exception encountered") } loadingRequest.dataRequest?.respond(with: string.data(using: String.Encoding.utf8)!) loadingRequest.finishLoading() if completion != nil { completion!(true) } } } func generateRedirectURL(sourceURL: String)-> URLRequest? { let redirect = URLRequest(url: URL(string: sourceURL)!) return redirect } 
  1. 实施资产资源加载程序委托以自定义处理stream。
  2. 假现场stream的scheme,以便资源加载器委托被调用(对于正常的http / https,它不会被调用,玩家试图处理stream本身)
  3. 用Httpschemereplace假scheme。
  4. 将stream传递给M3U8parsing器,以纯文本格式获取m3u8文件。
  5. parsing纯string以查找当前string中的EXT-X-KEY标记。
  6. 将EXT-X-KEY行标记为“URI”方法string。
  7. 追加单独制作的JWT令牌,使用m3u8中的当前URI方法。
  8. 将当前m3u8string中的URI的所有实例replace为新的附加URI附加URIstring。
  9. 将此string转换为NSData格式
  10. 再次将其馈送给播放器。

希望这可以帮助!