如何通过单独提供密钥文件来播放M3U8加密播放列表



我有一个 m3u8播放列表文件(我们称其为prime),该文件指向另一个播放列表文件,又,该文件带有键文件URL的ts url。使用MPMoviePlayer我当前可以播放prime m3u8文件。这些段为encrypted,具有AES-128位加密,密钥文件在最终的m3u8文件中。有什么方法可以提供最终的m3u8文件,并告诉该应用使用本地密钥文件来解密视频,因此我不必公开发布密钥文件。

这与这个问题有些相关

我实现了与此相似的东西。我们所做的是:

  1. 使用JWT在运行时加密实时流段的每个段具有关键价值对和时间戳的组合的令牌验证。
  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. 实现资产资源加载程序代表以自定义流的流程。
  2. 伪造实时流的方案,以便调用资源加载程序代表(对于普通的http/https,它不会被调用,并且玩家尝试处理流本身)
  3. 用HTTP方案替换假方案。
  4. 将流传递到M3U8解析器,以纯文本格式获取M3U8文件。
  5. 解析普通字符串以在当前字符串中查找Ext-X-Key标签。
  6. tokenise ext-x-key系列要进入" uri"方法字符串。
  7. 分别用M3U8中的当前URI方法添加JWT令牌。
  8. 用新的令牌附加了URI字符串。
  9. 将此字符串转换为NSDATA格式
  10. 再次将其喂给玩家。

希望这会有所帮助!

是 - 您可以在传递给播放器之前修改最终M3U8文件。例如,更改键行以参考http://localhost/key。然后,您需要运行本地HTTP服务器,例如Cocoahttpserver,以将密钥传递给视频播放器。

最新更新