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

时间:2012-12-21 07:32:53

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

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

这与this SO question

有些关联

2 个答案:

答案 0 :(得分:5)

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

  1. 使用JWT在运行时加密实时流段的每个段 具有键值对和时间戳组合的令牌 验证。
  2. 我们的服务器知道如何解密此密钥。当时     解密数据有效,服务器以.ts文件响应     因此播放变得安全。
  3. 以下是完整的工作代码,其中包含以下步骤:

    //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 Parser以获取纯文本格式的m3u8文件。
    5. 解析普通字符串以在当前字符串中查找EXT-X-KEY标记。
    6. 对EXT-X-KEY行进行标记以获取" URI"方法字符串。
    7. 使用m3u8中的当前URI方法单独添加JWT令牌。
    8. 使用新标记附加的URI字符串替换当前m3u8字符串中的所有URI实例。
    9. 将此字符串转换为NSData格式
    10. 再次将其送入播放器。
    11. 希望这有帮助!

答案 1 :(得分:1)

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