AVPlayer Stops Playing AES encrypted offline HLS V

2019-05-31 04:48发布

问题:

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 custom encryption key for it. After downloading AES encrypted HLS video I am using below given code to supply key for decryption of video.

- (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.dataRequest respondWithData:data];
        [loadingRequest finishLoading];
    } else {
        // Data loading fail
    }
}
return NO; }       

I am intercepting a request for a key and passing key stored in UserDefaults for decryption.

This AES encrypted HLS video with custom key plays well when my device's wifi or data connection is off.

If I start playing this video when my device's wifi or data connection is enabled or if I enable my device's wifi or data connection while playing video; video stops playing immediately without any error and never plays again.

I have checked accessLog and errorLog of playerItem but haven't found anything helpful.

To provide a custom URL key after downloading of HLS content I am updating a content of .m3u8 file by replacing

URI="..."

string with

URI="ckey://..."

Is this a correct way to provide key for AES encrypted video?

and what could be the reason of this behaviour and how to solve this issue?

Thanks in advance.

回答1:

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.