4

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

这与this SO question有点相关

4

2 回答 2

7

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

  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. Fake the scheme of live stream so that the Resource loader delegate gets called (for normal http/https it doesn't gets called and player tries to handle the stream itself)
  3. Replace the Fake Scheme with Http scheme.
  4. Pass the stream to M3U8 Parser to get the m3u8 file in plain text format.
  5. Parse the plain string to find EXT-X-KEY tags in the current string.
  6. Tokenise the EXT-X-KEY line to get to the "URI" method string.
  7. Append JWT token separately made, with the current URI method in the m3u8.
  8. Replace all instances of URI in the current m3u8 string with the new token appended URI string.
  9. Convert this string to NSData format
  10. Feed it to the player again.

Hope this helps!

于 2017-04-17T07:03:07.200 回答
2

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

于 2013-01-02T08:46:10.647 回答