How to play m3u8 encrypted playlists by providing key file separately?

前端 未结 2 1169
半阙折子戏
半阙折子戏 2021-01-01 06:53

I have a m3u8 playlist file(lets call it prime), which points to another playlist file which in turn has the ts URLs with the key file URL. Using MPMovieP

相关标签:
2条回答
  • 2021-01-01 06:59

    Yes -- You can modify the final m3u8 file before passing it to the player. For example, change the KEY lines to refer to http://localhost/key. Then you would want to run a local http server such as cocoahttpserver to deliver the key to the video player.

    0 讨论(0)
  • 2021-01-01 07:06

    I have implemented something similar to this. What we did was:

    1. Encrypt each segment of Live stream segment at runtime with a JWT Token that has a combination of key value pairs and time stamp for validation.
    2. Our server knows how to decrypt this key. and when the decrypted data is valid, the server responds with a .ts file and hence the playback becomes secure.

    Here is the complete working code with steps mentioned:

    //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. Implement Asset Resource Loader Delegate for custom handling of streams.
    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!

    0 讨论(0)
提交回复
热议问题