AVPlayer Stops Playing AES encrypted offline HLS Video in online mode

后端 未结 1 628
太阳男子
太阳男子 2021-01-16 03:58

I have written a code to download HLS video and play it in offline mode. This code works fine for encoded video. Now I have a video which is AES encrypted and we are having

相关标签:
1条回答
  • 2021-01-16 04:45

    Finally I managed to solve this issue. Rough package structure of downloaded HLS video is like given below:

    HLS.movpkg 
     |_ 0-12345
        |_ 123.m3u8
        |_ StreamInfoBoot.xml
        |_ StreamInfoRoot.xml
        |_ <>.frag
     |_ boot.xml
    
    1. boot.xml contains network URL for HLS (which is https: based)
    2. StreamBootInfo.xml contains mapping between HLS URL (which is https: based) and .frag file downloaded locally.

    In offline mode HLS video was playing perfectly. But when network connection was enabled it was referring to https: URL instead of local .frag files.

    I replaced https: scheme in these files with custom scheme (fakehttps:) to restrict AVPlayer going online for resources.

    This thing solved my issue but I don't know the exact reason behind it and how HLS is played by AVPlayer.

    I referred this and got some idea so tried something .

    I am updating this answer further to explain how to play encrypted video in offline mode.

    1. Get the key required for video decryption.

    2. Save that key some where.

    You can save that key as NSData or Data object in UserDefault I am using video file name as key to save key data in UserDefaults.

    1. Use FileManager API to iterate over all the files inside .movpkg.

    2. Get the content of each .m3u8 file and replace URI="some key url" with URI="ckey://keyusedToSaveKeyDataInUserDefaults"

    You can refer code given below for this process.

      if let url = asset.asset?.url, let data = data {
                
                let keyFileName = "\(asset.contentCode!).key"
                UserDefaults.standard.set(data, forKey: keyFileName)
                
                do {
                    
                    // ***** Create key file *****
                    let keyFilePath = "ckey://\(keyFileName)"
                    
                    let subDirectories = try fileManager.contentsOfDirectory(at: url,
                                                                                     includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants)
                    
                    for url in subDirectories {
                        
                        var isDirectory: ObjCBool = false
                        
                        if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
                            
                            if isDirectory.boolValue {
                                
                                let path = url.path as NSString
                                
                                let folderName = path.lastPathComponent
                                let playlistFilePath = path.appendingPathComponent("\(folderName).m3u8")
                                
                                if fileManager.fileExists(atPath: playlistFilePath) {
                                    
                                    var fileContent = try String.init(contentsOf: URL.init(fileURLWithPath: playlistFilePath))
                                    
                                    let stringArray = self.matches(for: "URI=\"(.+?)\"", in: fileContent)
                                    
                                    for pattern in stringArray {
                                        fileContent = fileContent.replacingOccurrences(of: pattern, with: "URI=\"\(keyFilePath)\"")
                                    }
                                    
                                    try fileContent.write(toFile: playlistFilePath, atomically: true, encoding: .utf8)
                                }
                                
                                let streamInfoXML = path.appendingPathComponent("StreamInfoBoot.xml")
    
                                if fileManager.fileExists(atPath: streamInfoXML) {
    
                                    var fileContent = try String.init(contentsOf: URL.init(fileURLWithPath: streamInfoXML))
                                    fileContent = fileContent.replacingOccurrences(of: "https:", with: "fakehttps:")
                                    try fileContent.write(toFile: streamInfoXML, atomically: true, encoding: .utf8)
                                }
                            } else {
    
                                if url.lastPathComponent == "boot.xml" {
    
                                    let bootXML = url.path
    
                                    if fileManager.fileExists(atPath: bootXML) {
    
                                        var fileContent = try String.init(contentsOf: URL.init(fileURLWithPath: bootXML))
                                        fileContent = fileContent.replacingOccurrences(of: "https:", with: "fakehttps:")
                                        try fileContent.write(toFile: bootXML, atomically: true, encoding: .utf8)
                                    }
                                }
                            }
                        }
                    }
                    
                    userInfo[Asset.Keys.state] = Asset.State.downloaded.rawValue
                    
                    // Update download status to db
                    let user = RoboUser.sharedObject()
                    let sqlDBManager = RoboSQLiteDatabaseManager.init(databaseManagerForCourseCode: user?.lastSelectedCourse)
                    sqlDBManager?.updateContentDownloadStatus(downloaded, forContentCode: asset.contentCode!)
                   
                    self.notifyServerAboutContentDownload(asset: asset)
                    
                    NotificationCenter.default.post(name: AssetDownloadStateChangedNotification, object: nil, userInfo: userInfo)
                } catch  {
                }
            }
    
    func matches(for regex: String, in text: String) -> [String] {
        
        do {
            let regex = try NSRegularExpression(pattern: regex)
            let nsString = text as NSString
            let results = regex.matches(in: text, range: NSRange(location: 0, length: nsString.length))
            return results.map { nsString.substring(with: $0.range)}
        } catch let error {
            print("invalid regex: \(error.localizedDescription)")
            return []
        }
    }
    

    This will update your download package structure for playing encrypted video in offline mode.

    Now last thing to do is implement below given method of AVAssetResourceLoader class as follows

    - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
        
        NSString *scheme = loadingRequest.request.URL.scheme;
        
        if ([scheme isEqualToString:@"ckey"]) {
            
            NSString *request = loadingRequest.request.URL.host;
            NSData *data = [[NSUserDefaults standardUserDefaults] objectForKey:request];
            
            if (data) {
                loadingRequest.contentInformationRequest.contentType = AVStreamingKeyDeliveryPersistentContentKeyType;
                loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES;
                loadingRequest.contentInformationRequest.contentLength = data.length;
                [loadingRequest.dataRequest respondWithData:data];
                [loadingRequest finishLoading];
            } else {
                // Data loading fail
            }
        }
        
        return YES;
    }
    

    This method will provide key to video while playing to decrypt it.

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