【问题标题】:How to play m3u8 encrypted playlists by providing key file separately?如何通过单独提供密钥文件来播放 m3u8 加密播放列表?
【发布时间】:2012-12-08 19:00:48
【问题描述】:

我有一个m3u8 播放列表文件(我们称之为prime),它指向另一个播放列表文件,该文件又包含带有密钥文件URL 的ts URL。使用MPMoviePlayer 我目前可以播放prime m3u8 文件。 这些段是encrypted,使用AES-128 位加密,密钥文件在最终的m3u8 文件中。有没有办法可以提供最终的m3u8 文件并告诉应用程序使用本地密钥文件来解密视频,这样我就不必公开发布密钥文件。

这个和this SO question有点关系

【问题讨论】:

    标签: objective-c ios ios5 http-live-streaming


    【解决方案1】:

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

    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. 实现 Asset Resource Loader Delegate 以自定义处理流。
    2. 伪造直播流的方案,以便调用资源加载器委托(对于正常的 http/https,它不会被调用,播放器会尝试自己处理流)
    3. 将 Fake Scheme 替换为 Http 方案。
    4. 将流传递给 M3U8 Parser 以获取纯文本格式的 m3u8 文件。
    5. 解析纯字符串以在当前字符串中查找 EXT-X-KEY 标签。
    6. 标记 EXT-X-KEY 行以获取“URI”方法字符串。
    7. 附加单独制作的 JWT 令牌,使用 m3u8 中的当前 URI 方法。
    8. 将当前 m3u8 字符串中的所有 URI 实例替换为附加了新令牌的 URI 字符串。
    9. 将此字符串转换为 NSData 格式
    10. 再次将其喂给玩家。

    希望这会有所帮助!

    【讨论】:

    • 在 Avplayer Swift4 iOS 的 m3u8 中解析 Handler 方法不更新加载请求 url(加载请求中的 fakehttp 到 http)
    • 你能帮我解决这个问题吗?
    • 您能否确认您尝试播放的流是否未编码?
    • 您好 Junaid,我也面临同样的问题。一旦调用了解析处理程序方法,就不会调用 shouldWaitForLoadingOfRequestedResource 。 url?.contains(".ts") 这个条件永远不会执行。请帮我。谢谢
    【解决方案2】:

    是的——您可以在将最终的 m3u8 文件传递​​给播放器之前对其进行修改。例如,将 KEY 行更改为引用 http://localhost/key。然后你会想要运行一个本地 http 服务器,比如 cocoahttpserver 来将密钥传递给视频播放器。

    【讨论】:

    • 我的意思是有办法将密钥文件本地存储在应用程序中,而不是通过互联网发布
    • @kitwalker 嗯,是的,这正是我的回答中所描述的。它必须通过 HTTP 传送,但没有必要通过 Internet 传送。
    • 嗨,做类似的事情。在将文件传递给播放器之前,您能指导我如何“更改 KEY 行”吗?
    • @JunaidMukhtar 您可以打开一个关于如何在 iOS 上修改 HTTP 交付资源的文本的新问题。
    猜你喜欢
    • 1970-01-01
    • 2014-09-12
    • 2012-03-15
    • 2018-05-30
    • 2014-12-18
    • 1970-01-01
    • 1970-01-01
    • 2017-11-07
    • 2016-05-01
    相关资源
    最近更新 更多