Я получаю эту ошибку, когда пытаюсь загрузить и воспроизвести загруженный URL-контент HLS на iOS 16 и более поздних версиях. В более старых версиях iOS все работает нормально.
Я также задал этот вопрос об изменениях FairPlay в iOS 16, но в моем случае это не сработало.
Error Domain=AVFoundationErrorDomain Code=-11835 «Невозможно открыть» UserInfo={NSLocalizedFailureReason=Этот контент не авторизован., NSLocalizedDescription=Cannot Open, NSUnderlyingError=0x2826f03f0 {Error Domain=NSOSStatusErrorDomain Code=-42668 "(null) )"}} и
Error Domain=AVFoundationErrorDomain Code=-11800 «Операция не может быть завершена» UserInfo={NSLocalizedFailureReason=Произошла неизвестная ошибка (-19156), NSLocalizedDescription=Операция не может быть завершена, NUnderlyingError=0x28272d620 {Ошибка Domain=NSOSStatusErrorDomain Code=-19156 "(null)"}} есть ли какие-либо изменения в ios16, которые могут повлиять на логику моего кода?
вот код
импортировать AVFoundation класс ContentKeyDelegate: NSObject, AVContentKeySessionDelegate { // ОТМЕТКА: Типы перечисление ProgramError: Ошибка { Отсутствует сертификат приложения дело №CKCReturnedByKSM } // ОТМЕТКА: Свойства /// Каталог, используемый для сохранения ключей постоянного содержимого. ленивый вар contentKeyDirectory: URL = { охранник пусть documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else { FatalError("Невозможно определить URL библиотеки") } пусть documentURL = URL (fileURLWithPath: documentPath) пусть contentKeyDirectory = documentURL.appendingPathComponent(".keys", isDirectory: true) если !FileManager.default.fileExists(atPath: contentKeyDirectory.path, isDirectory: ноль) { делать { попробуйте FileManager.default.createDirectory(at: contentKeyDirectory, withIntermediateDirectories: ложь, атрибуты: ноль) } ловить { FatalError("Невозможно создать каталог для ключей содержимого по пути: \(contentKeyDirectory.path)") } } вернуть контентKeyDirectory }() /// Набор, содержащий ожидающие в данный момент идентификаторы ключей контента, связанные с запросами постоянных ключей контента, которые не были завершены. var pendingPersistableContentKeyIdentifiers = Set() /// Словарь, сопоставляющий идентификаторы ключей контента с связанными с ними именами потоков. var contentKeyToStreamNameMap = [Int: String]() func requestApplicationCertificate (_ актив: Актив?) бросает -> Данные { охранник пусть актив = актив еще { вернуть данные() } // пусть группа = DispatchGroup() // вар applicationCertificate: Данные? = ноль // // /// ПРИМЕЧАНИЕ: приведенный ниже код полезен, если вы хотите закрыть API DRMToday в фреймворке. //// let data = попробуйте JSONEncoder().encode(asset.stream) //// letstream = попробуйте JSONDecoder().decode(Stream.self, from: data) // // группа.enter() // DRMToday.getCertificate(stream: assets.stream) { (сертификат) в // applicationCertificate = сертификат // группа.оставить() // } // группа.ожидание() // // охрана сертификата приложения != nil else { // выбрасываем ProgramError.missingApplicationCertificate // } // // возвращаем сертификат приложения! сторожить let certificateURL = URL (строка: «
https://lic.drmtoday.com/license-server ... rt/ibakatv»), пусть certificateData = попробовать? Данные (contentsOf: certificateURL) еще { print("

", #function, "Невозможно прочитать данные сертификата.") бросить ProgramError.missingApplicationCertificate } Сертификат возвратаДанные } func getDocumentsDirectory() -> URL { let paths = FileManager.default.urls (для: .documentDirectory, в: .userDomainMask) обратные пути[0] } func requestContentKeyFromKeySecurityModule (_ актив: Актив?, spcData: Данные, assetsID: String, persistable: Bool) выдает -> Данные { охранник пусть актив = актив еще { вернуть данные() } /*guard let token = assetsToken else { вернуть данные() }*/ вар ckcData: Данные? = ноль /// ПРИМЕЧАНИЕ. Код ниже полезен, если вы хотите закрыть DRMToday API в фреймворке. // пусть данные = попробуйте JSONEncoder().encode(asset.stream) // пусть поток = попробуйте JSONDecoder().decode(Stream.self, from: data) пусть группа = DispatchGroup() группа.введите() DRMToday.getLicense(stream: assets.stream, spcData: spcData, offline: persistable) { (ckc) в ckcData = ckc let filename = self.getDocumentsDirectory().appendingPathComponent("").appendingPathComponent(asset.stream.assetId! + (постоянный? "оффлайн": "онлайн") + ".dat") делать { попробуйте ckcData?.write(to: имя файла, параметры: .atomicWrite) } ловить { print("Невозможно записать файл лицензии") } группа.оставить() } группа.ожидание() охранник ckcData != ноль еще { бросить ProgramError.noCKCReturnedByKSM } верните ckcData! } /// Предварительно загружает все ключи содержимого, связанные с активом, для сохранения на диске. /// /// Рекомендуется использовать AVContentKeySession для инициации процесса загрузки ключа. /// и для онлайн-ключей тоже. Время загрузки ключа может составлять значительную часть вашего воспроизведения. /// время запуска, поскольку приложения обычно загружают ключи, когда получают запрос по требованию /// запрос ключа. Вы можете улучшить процесс запуска воспроизведения для своих пользователей, если /// загружаем клавиши еще до того, как пользователь выберет что-нибудь для воспроизведения. AVContentKeySession позволяет /// вы инициируете процесс загрузки ключа, а затем используете полученный запрос ключа для загрузки /// ключи, независимые от сеанса воспроизведения. Это называется предварительной загрузкой ключа. После загрузки /// ключи, которые вы можете запросить на воспроизведение, поэтому во время воспроизведения вам не придется загружать какие-либо клавиши, /// и расшифровка воспроизведения может начаться немедленно. /// /// В этом примере используйте Streams.plist, чтобы указать собственные идентификаторы ключей контента, которые будут использоваться /// для загрузки ключей контента для вашего медиа. Дополнительную информацию смотрите в документе README. /// /// - Актив параметра: `Актив` для предварительной загрузки ключей. func requestPersistableContentKeys (актив forAsset: Актив) { для идентификатора в assets.stream.contentKeyIDList ?? [] { Guard let contentKeyIdentifierURL = URL (строка: идентификатор) еще {продолжить} пусть assetsID = contentKeyIdentifierURL.absoluteString.hash pendingPersistableContentKeyIdentifiers.insert(assetID) contentKeyToStreamNameMap[assetID] = assets.stream.name ContentKeyManager.shared.contentKeySession.processContentKeyRequest(withIdentifier: идентификатор, данные инициализации: ноль, параметры: ноль) } } /// Возвращает, должен ли ключ содержимого сохраняться на диске. /// /// — Идентификатор параметра: идентификатор актива, связанный с запросом ключа контента. /// - Возвращает: `true`, если запрос ключа содержимого должен быть постоянным, `false` в противном случае. func mustRequestPersistableContentKey (идентификатор withIdentifier: Int) -> Bool { вернуть pendingPersistableContentKeyIdentifiers.contains(идентификатор) } // ОТМЕТКА: Методы AVContentKeySessionDelegate /* Следующий обратный вызов делегата вызывается, когда клиент инициирует запрос ключа или AVFoundation. определяет, что контент зашифрован на основе списка воспроизведения, предоставленного клиентом при запросе воспроизведения. */ func contentKeySession (_ session: AVContentKeySession, DidProvide keyRequest: AVContentKeyRequest) { handleStreamingContentKeyRequest (keyRequest: keyRequest) } /* Предоставляет получателю новый запрос ключа контента, представляющий обновление существующего ключа контента. Будет вызван AVContentKeySession в результате вызова -renewExpiringResponseDataForContentKeyRequest:. */ func contentKeySession (_ session: AVContentKeySession, DidProvideRenewingContentKeyRequest keyRequest: AVContentKeyRequest) { handleStreamingContentKeyRequest (keyRequest: keyRequest) } /* Предоставляет получателю запрос ключа контента, который следует повторить, поскольку предыдущий запрос ключа контента не удался. Будет вызываться AVContentKeySession, когда необходимо повторить запрос ключа контента. Причина неудачи в указан предыдущий запрос ключа контента. Получатель может решить, хочет ли он запросить AVContentKeySession для повторите запрос этого ключа в зависимости от причины. Если получатель возвращает YES, AVContentKeySession перезапустит ключевой процесс запроса. Если получатель возвращает NO или если он не реализует этот метод делегата, ключ содержимого запрос завершится неудачей, и AVContentKeySession сообщит получателю об этом через -contentKeySession:contentKeyRequest:didFailWithError:. */ func contentKeySession (_ session: AVContentKeySession, mustRetry keyRequest: AVContentKeyRequest, причина retryReason: AVContentKeyRequest.RetryReason) -> Bool { вар долженRetry = ложь print("повторить \(retryReason)"); переключить retryReason { /* Указывает, что запрос ключа контента следует повторить, поскольку ответ ключа также не был установлен достаточно скоро. из-за того, что первоначальный запрос/ответ занимал слишком много времени, или в это время истекал срок аренды. */ случай AVContentKeyRequest.RetryReason.timedOut: долженRetry = истина /* Указывает, что запрос ключа контента следует повторить, поскольку на сервере был установлен ответ ключа с истекшим сроком аренды. предыдущий запрос ключа контента. */ случай AVContentKeyRequest.RetryReason.receivedResponseWithExpiredLease: долженRetry = истина /* Указывает, что запрос ключа содержимого следует повторить, поскольку на предыдущем этапе был установлен устаревший ответ на ключ. запрос ключа контента. */ случай AVContentKeyRequest.RetryReason.receivedObsoleteContentKey: долженRetry = истина по умолчанию: перерыв } возврат долженПовторить попытку } // Сообщает получателю, что запрос ключа контента не удался. func contentKeySession (_ session: AVContentKeySession, contentKeyRequest keyRequest: AVContentKeyRequest, DidFailWithError err: Error) { // Добавьте сюда свой код для обработки ошибок. print("Ошибка: \(err)") пытаться? keyRequest.respondByRequestingPersistableContentKeyRequestAndReturnError() } // ОТМЕТКА: API func handleStreamingContentKeyRequest (keyRequest: AVContentKeyRequest) { охранник пусть contentKeyIdentifierString = keyRequest.identifier как? Нить, пусть contentKeyIdentifierURL = URL (строка: contentKeyIdentifierString), let assetsIDString = contentKeyIdentifierURL.queryParameters?.count!= ноль? contentKeyIdentifierURL.queryParameters!["keyId"] : contentKeyIdentifierString, пусть assetsIDData = contentKeyIdentifierString.data (с использованием: .utf8) еще { print("Не удалось получить идентификатор актива из keyRequest!") возвращаться } пусть assetsID = contentKeyIdentifierURL.absoluteString.hash вар актив: Актив? = ноль для индекса в 0.. Void = { () -> Пустота в делать { // пусть applicationCertificate = попробуйте self.requestApplicationCertificate(актив) сторожить let certificateURL = URL (строка: «
https://lic.drmtoday.com/license-server ... rt/ibakatv»), пусть applicationCertificate = попробовать? Данные (contentsOf: certificateURL) еще { print("

", #function, "Невозможно прочитать данные сертификата.") бросить ProgramError.missingApplicationCertificate } letcompleteHandler = {[weak self] (spcData: Data?, error: Error?) в Guard let StrongSelf = self else { return } если пусть ошибка = ошибка { keyRequest.processContentKeyResponseError(ошибка) возвращаться } охранник пусть spcData = spcData еще {возвращение} делать { // Отправляем SPC на сервер ключей и получаем CKC let ckcData = попробуйте StrongSelf.requestContentKeyFromKeySecurityModule (актив, spcData: spcData, assetsID: assetsIDString, persistable: false) /* AVContentKeyResponse используется для представления данных, возвращаемых с сервера ключей при запросе ключа для расшифровка контента. */ let keyResponse = AVContentKeyResponse (fairPlayStreamingKeyResponseData: ckcData) /* Предоставьте ответ ключа контента, чтобы сделать защищенный контент доступным для обработки.[AVContentKeyRequestProtocolVersionsKey: [1]] */ keyRequest.processContentKeyResponse(keyResponse) } ловить { keyRequest.processContentKeyResponseError(ошибка) } } keyRequest.makeStreamingContentKeyRequestData(forApp: applicationCertificate, идентификатор контента: активИДдата, параметры: [AVContentKeyRequestProtocolVersionsKey: [1]], завершениеHandler: завершениеHandler) } ловить { keyRequest.processContentKeyResponseError(ошибка) } } #if os(iOS) /* Когда вы получаете AVContentKeyRequest через -contentKeySession:didProvideContentKeyRequest: и вы хотите, чтобы в результате ответа ключа был создан ключ, который может сохраняться в нескольких сеансов воспроизведения, вы должны вызвать -respondByRequestingPersistableContentKeyRequest для этого AVContentKeyRequest, чтобы сигнализировать о том, что вы хотите обработать AVPersistableContentKeyRequest. вместо. Если базовый протокол поддерживает постоянные ключи содержимого, в ответ ваш Делегат получит AVPersistableContentKeyRequest через -contentKeySession:didProvidePersistableContentKeyRequest:. */ если следуетRequestPersistableContentKey(withIdentifier: assetsID) || persistableContentKeyExistsOnDisk (withContentKeyIdentifier: assetsIDString) { // Запросить запрос постоянного ключа. делать { попробуйте keyRequest.respondByRequestingPersistableContentKeyRequestAndReturnError() } ловить { /* Этот случай произойдет, когда клиент получит запрос на загрузку ключа из сеанса AirPlay. Вам следует ответить на запрос ключа, используя онлайн-ключ с вашего сервера ключей. */ обеспечитьОнлайнключ() } возвращаться } #endif обеспечитьОнлайнключ() } }