Что заставляет AVAssetCache сообщать о невозможности воспроизведения в автономном режиме для полностью загруженного актива?

Я работаю над приложением для iOS, которое воспроизводит звук, зашифрованный FairPlay, через HLS и поддерживает как загрузку, так и потоковую передачу. И я не могу воспроизводить загруженный контент в режиме полета. Если я создаю AVURLAsset из локального URL-адреса после завершения загрузки, asset.assetCache.isPlayableOffline возвращает NO, и, конечно же, когда я пытаюсь играть в режиме полета, он все еще пытается запросить один из файлов плейлиста .m3u8.

Мой основной плейлист выглядит так:

#EXTM3U
# Created with Bento4 mp4-hls.py version 1.1.0r623

#EXT-X-VERSION:5
#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES,URI="skd://url/to/key?KID=foobar",KEYFORMAT="com.apple.streamingkeydelivery",KEYFORMATVERSIONS="1"


# Media Playlists
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=133781,BANDWIDTH=134685,CODECS="mp4a.40.2" media-1/stream.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=67526,BANDWIDTH=67854,CODECS="mp4a.40.2" media-2/stream.m3u8

Плейлисты стримов выглядят так:

#EXTM3U
#EXT-X-VERSION:5
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:30
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://url/to/key?KID=foobar",KEYFORMAT="com.apple.streamingkeydelivery",KEYFORMATVERSIONS="1"
#EXTINF:30.000181,
#EXT-X-BYTERANGE:470290@0
media.aac
# more segments...
#EXT-X-ENDLIST

Загрузка актива:

AVURLAsset *asset = [AVURLAsset assetWithURL:myM3u8Url];
[asset.resourceLoader setDelegate:[FairPlayKeyManager instance] queue:[FairPlayKeyManager queue]];
asset.resourceLoader.preloadsEligibleContentKeys = YES;
AVAssetDownloadTask *task = [self.session assetDownloadTaskWithURLAsset:asset assetTitle:@"Track" assetArtworkData:imgData options:nil];
[task resume];

В делегате URLSession:assetDownloadTask:didFinishDownloadingToURL::

self.downloadedPath = location.relativePath;

В делегате URLSession:task:didCompleteWithError::

if (!error)
{
  NSString *strUrl = [NSHomeDirectory() stringByAppendingPathComponent:self.downloadedPath];
  NSURL *url = [NSURL fileURLWithPath:strUrl];
  AVURLAsset *localAsset = [AVURLAsset assetWithURL:url];
  if (!localAsset.assetCache.playableOffline)
    NSLog(@"Oh no!"); //not playable offline
}

Загрузка не выдает ошибки, кроме сообщения о том, что кеш ресурсов не воспроизводится в автономном режиме. Но если вы переключитесь в режим полета и попытаетесь воспроизвести загруженный ресурс, он правильно запросит ключ у делегата загрузчика ресурсов (и я использую постоянные ключи, поэтому он отлично работает в автономном режиме), а затем попробуйте сделать запрос на media-1/stream.m3u8.

Есть ли какие-то ошибки, с которыми я здесь не справляюсь? Должен ли файл плейлиста каким-то образом отличаться? Есть ли какое-то свойство задачи или актива, которое мне не хватает?


person Tom Hamming    schedule 26.07.2018    source источник


Ответы (2)


Как оказалось, это произошло из-за того, что URL-адрес, с которого я загружал аудио (например, https://mywebsite.com/path/to/master.m3u8, перенаправлялся на URL-адрес CDN (https://my.cdn/other/path/to/master.m3u8). Что-то шло не так в бухгалтерии AVAssetDownloadTask, поэтому, когда я пытался воспроизвести загруженные файлы в автономном режиме , он подумал, что ему нужно больше файлов из сети. Я зарегистрировал это как радар 43285278. Я решил эту проблему, вручную выполнив HEAD запрос к тому же URL-адресу, а затем передав AVAssetDownloadTask результирующий URL-адрес перенаправления.

person Tom Hamming    schedule 31.01.2019

Я думаю, вам нужно проверить несколько вещей, прежде чем проверять asset.assetCache.isPlayableOffline.

  1. Does your KSM configured to support fairplay offline play?
    • visit Apple's FairPlay Streaming Website
    • загрузить образец SDK Fairplay (SDK FairPlay Streaming Server (4.2.0))
    • откройте HLSCatalogWithFPS – AVAssetResourceLoader или HLSCatalogWithFPS – AVContentKeySession.
    • адаптируйте свой KSM к образцу проекта, чтобы проверить, хорошо ли работает автономная игра FPS.
  2. Check your key requesting process
    • since you didn't provide any of code related to key requesting process, I have no idea if you properly requested & received ckc data
    • завершение загрузки не означает, что вы приобрели ckc или постоянный ключ. Отладьте, чтобы проверить, получаете ли вы правильные данные ckc из KSM. (Возможно, вы получите сообщение об ошибке при запросе ckc с параметром постоянного ключа, если ваш KSM не настроил контент для воспроизведения в автономном режиме)
func handlePersistableContentKeyRequest(keyRequest: AVPersistableContentKeyRequest) {

        /*
         The key ID is the URI from the EXT-X-KEY tag in the playlist (e.g. "skd://key65") and the
         asset ID in this case is "key65".
         */
        guard let contentKeyIdentifierString = keyRequest.identifier as? String,
            let contentKeyIdentifierURL = URL(string: contentKeyIdentifierString),
            let assetIDString = contentKeyIdentifierURL.host,
            let assetIDData = assetIDString.data(using: .utf8)
            else {
                print("Failed to retrieve the assetID from the keyRequest!")
                return
        }

        do {

            let completionHandler = { [weak self] (spcData: Data?, error: Error?) in
                guard let strongSelf = self else { return }
                if let error = error {
                    keyRequest.processContentKeyResponseError(error)

                    strongSelf.pendingPersistableContentKeyIdentifiers.remove(assetIDString)
                    return
                }

                guard let spcData = spcData else { return }

                do {
                    // Send SPC to Key Server and obtain CKC
                    let ckcData = try strongSelf.requestContentKeyFromKeySecurityModule(spcData: spcData, assetID: assetIDString)

                    let persistentKey = try keyRequest.persistableContentKey(fromKeyVendorResponse: ckcData, options: nil)

                    try strongSelf.writePersistableContentKey(contentKey: persistentKey, withContentKeyIdentifier: assetIDString)

                    /*
                     AVContentKeyResponse is used to represent the data returned from the key server when requesting a key for
                     decrypting content.
                     */
                    let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: persistentKey)

                    /*
                     Provide the content key response to make protected content available for processing.
                     */
                    keyRequest.processContentKeyResponse(keyResponse)

                    let assetName = strongSelf.contentKeyToStreamNameMap.removeValue(forKey: assetIDString)!

                    if !strongSelf.contentKeyToStreamNameMap.values.contains(assetName) {
                        NotificationCenter.default.post(name: .DidSaveAllPersistableContentKey,
                                                        object: nil,
                                                        userInfo: ["name": assetName])
                    }

                    strongSelf.pendingPersistableContentKeyIdentifiers.remove(assetIDString)
                } catch {
                    keyRequest.processContentKeyResponseError(error)

                    strongSelf.pendingPersistableContentKeyIdentifiers.remove(assetIDString)
                }
            }

            // Check to see if we can satisfy this key request using a saved persistent key file.
            if persistableContentKeyExistsOnDisk(withContentKeyIdentifier: assetIDString) {

                let urlToPersistableKey = urlForPersistableContentKey(withContentKeyIdentifier: assetIDString)

                guard let contentKey = FileManager.default.contents(atPath: urlToPersistableKey.path) else {
                    // Error Handling.

                    pendingPersistableContentKeyIdentifiers.remove(assetIDString)

                    /*
                     Key requests should never be left dangling.
                     Attempt to create a new persistable key.
                     */
                    let applicationCertificate = try requestApplicationCertificate()
                    keyRequest.makeStreamingContentKeyRequestData(forApp: applicationCertificate,
                                                                  contentIdentifier: assetIDData,
                                                                  options: [AVContentKeyRequestProtocolVersionsKey: [1]],
                                                                  completionHandler: completionHandler)

                    return
                }

                /*
                 Create an AVContentKeyResponse from the persistent key data to use for requesting a key for
                 decrypting content.
                 */
                let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: contentKey)

                // Provide the content key response to make protected content available for processing.
                keyRequest.processContentKeyResponse(keyResponse)

                return
            }

            let applicationCertificate = try requestApplicationCertificate()

            keyRequest.makeStreamingContentKeyRequestData(forApp: applicationCertificate,
                                                          contentIdentifier: assetIDData,
                                                          options: [AVContentKeyRequestProtocolVersionsKey: [1]],
                                                          completionHandler: completionHandler)
        } catch {
            print("Failure responding to an AVPersistableContentKeyRequest when attemping to determine if key is already available for use on disk.")
        }
    }
person Slowpoke    schedule 31.01.2019
comment
С моей загрузкой ключей все было в порядке — я смоделировал ее точно по образцу кода Apple. - person Tom Hamming; 01.02.2019
comment
как вы получаете keyRequest? А как сгенерировать contentIdentifier? - person Amrit Tiwari; 04.04.2019