当我尝试在 iOS 16 及更高版本上下载并播放下载的 HLS URL 内容时,出现此错误,它在较旧的 iOS 版本上运行良好。
我也提到了这个问题iOS 16 FairPlay Changes但对我的案例不起作用。
Error Domain=AVFoundationErrorDomain Code=-11835 "Cannot Open" UserInfo={NSLocalizedFailureReason=This content is not authorized., NSLocalizedDescription=Cannot Open, NSUnderlyingError=0x2826f03f0 {Error Domain=NSOSStatusErrorDomain Code=-42668 "(null)"}}
和
Error Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed" UserInfo={NSLocalizedFailureReason=An unknown error occurred (-19156), NSLocalizedDescription=The operation could not be completed, NSUnderlyingError=0x28272d620 {Error Domain=NSOSStatusErrorDomain Code=-19156 "(null)"}}
ios16 中是否有任何更改可能会影响我的代码的以下逻辑?
这是代码
import AVFoundation
class ContentKeyDelegate: NSObject, AVContentKeySessionDelegate {
// MARK: Types
enum ProgramError: Error {
case missingApplicationCertificate
case noCKCReturnedByKSM
}
// MARK: Properties
/// The directory that is used to save persistable content keys.
lazy var contentKeyDirectory: URL = {
guard let documentPath =
NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else {
fatalError("Unable to determine library URL")
}
let documentURL = URL(fileURLWithPath: documentPath)
let contentKeyDirectory = documentURL.appendingPathComponent(".keys", isDirectory: true)
if !FileManager.default.fileExists(atPath: contentKeyDirectory.path, isDirectory: nil) {
do {
try FileManager.default.createDirectory(at: contentKeyDirectory,
withIntermediateDirectories: false,
attributes: nil)
} catch {
fatalError("Unable to create directory for content keys at path: \(contentKeyDirectory.path)")
}
}
return contentKeyDirectory
}()
/// A set containing the currently pending content key identifiers associated with persistable content key requests that have not been completed.
var pendingPersistableContentKeyIdentifiers = Set<Int>()
/// A dictionary mapping content key identifiers to their associated stream name.
var contentKeyToStreamNameMap = [Int: String]()
func requestApplicationCertificate(_ asset: Asset?) throws -> Data {
guard let asset = asset else {
return Data()
}
// let group = DispatchGroup()
// var applicationCertificate: Data? = nil
//
// /// NOTE: code below is useful when you want to close DRMToday API in a framework
//// let data = try JSONEncoder().encode(asset.stream)
//// let stream = try JSONDecoder().decode(Stream.self, from: data)
//
// group.enter()
// DRMToday.getCertificate(stream: asset.stream) { (certificate) in
// applicationCertificate = certificate
// group.leave()
// }
// group.wait()
//
// guard applicationCertificate != nil else {
// throw ProgramError.missingApplicationCertificate
// }
//
// return applicationCertificate!
guard
let certificateURL = URL(string: "https://lic.drmtoday.com/license-server-fairplay/cert/ibakatv"),
let certificateData = try? Data(contentsOf: certificateURL) else {
print("🔑", #function, "Unable to read the certificate data.")
throw ProgramError.missingApplicationCertificate
}
return certificateData
}
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
func requestContentKeyFromKeySecurityModule(_ asset: Asset?, spcData: Data, assetID: String, persistable: Bool) throws -> Data {
guard let asset = asset else {
return Data()
}
/*guard let token = assetsToken else {
return Data()
}*/
var ckcData: Data? = nil
/// NOTE: code below is useful when you want to close DRMToday API in a framework
// let data = try JSONEncoder().encode(asset.stream)
// let stream = try JSONDecoder().decode(Stream.self, from: data)
let group = DispatchGroup()
group.enter()
DRMToday.getLicense(stream: asset.stream, spcData: spcData, offline: persistable) { (ckc) in
ckcData = ckc
let filename = self.getDocumentsDirectory().appendingPathComponent("").appendingPathComponent(asset.stream.assetId! + (persistable ? "offline" : "online") + ".dat")
do {
try ckcData?.write(to: filename, options: .atomicWrite)
} catch {
print("Unable to write a license file")
}
group.leave()
}
group.wait()
guard ckcData != nil else {
throw ProgramError.noCKCReturnedByKSM
}
return ckcData!
}
/// Preloads all the content keys associated with an Asset for persisting on disk.
///
/// It is recommended you use AVContentKeySession to initiate the key loading process
/// for online keys too. Key loading time can be a significant portion of your playback
/// startup time because applications normally load keys when they receive an on-demand
/// key request. You can improve the playback startup experience for your users if you
/// load keys even before the user has picked something to play. AVContentKeySession allows
/// you to initiate a key loading process and then use the key request you get to load the
/// keys independent of the playback session. This is called key preloading. After loading
/// the keys you can request playback, so during playback you don't have to load any keys,
/// and the playback decryption can start immediately.
///
/// In this sample use the Streams.plist to specify your own content key identifiers to use
/// for loading content keys for your media. See the README document for more information.
///
/// - Parameter asset: The `Asset` to preload keys for.
func requestPersistableContentKeys(forAsset asset: Asset) {
for identifier in asset.stream.contentKeyIDList ?? [] {
guard let contentKeyIdentifierURL = URL(string: identifier) else { continue }
let assetID = contentKeyIdentifierURL.absoluteString.hash
pendingPersistableContentKeyIdentifiers.insert(assetID)
contentKeyToStreamNameMap[assetID] = asset.stream.name
ContentKeyManager.shared.contentKeySession.processContentKeyRequest(withIdentifier: identifier, initializationData: nil, options: nil)
}
}
/// Returns whether or not a content key should be persistable on disk.
///
/// - Parameter identifier: The asset ID associated with the content key request.
/// - Returns: `true` if the content key request should be persistable, `false` otherwise.
func shouldRequestPersistableContentKey(withIdentifier identifier: Int) -> Bool {
return pendingPersistableContentKeyIdentifiers.contains(identifier)
}
// MARK: AVContentKeySessionDelegate Methods
/*
The following delegate callback gets called when the client initiates a key request or AVFoundation
determines that the content is encrypted based on the playlist the client provided when it requests playback.
*/
func contentKeySession(_ session: AVContentKeySession, didProvide keyRequest: AVContentKeyRequest) {
handleStreamingContentKeyRequest(keyRequest: keyRequest)
}
/*
Provides the receiver with a new content key request representing a renewal of an existing content key.
Will be invoked by an AVContentKeySession as the result of a call to -renewExpiringResponseDataForContentKeyRequest:.
*/
func contentKeySession(_ session: AVContentKeySession, didProvideRenewingContentKeyRequest keyRequest: AVContentKeyRequest) {
handleStreamingContentKeyRequest(keyRequest: keyRequest)
}
/*
Provides the receiver a content key request that should be retried because a previous content key request failed.
Will be invoked by an AVContentKeySession when a content key request should be retried. The reason for failure of
previous content key request is specified. The receiver can decide if it wants to request AVContentKeySession to
retry this key request based on the reason. If the receiver returns YES, AVContentKeySession would restart the
key request process. If the receiver returns NO or if it does not implement this delegate method, the content key
request would fail and AVContentKeySession would let the receiver know through
-contentKeySession:contentKeyRequest:didFailWithError:.
*/
func contentKeySession(_ session: AVContentKeySession, shouldRetry keyRequest: AVContentKeyRequest,
reason retryReason: AVContentKeyRequest.RetryReason) -> Bool {
var shouldRetry = false
print("retry \(retryReason)");
switch retryReason {
/*
Indicates that the content key request should be retried because the key response was not set soon enough either
due the initial request/response was taking too long, or a lease was expiring in the meantime.
*/
case AVContentKeyRequest.RetryReason.timedOut:
shouldRetry = true
/*
Indicates that the content key request should be retried because a key response with expired lease was set on the
previous content key request.
*/
case AVContentKeyRequest.RetryReason.receivedResponseWithExpiredLease:
shouldRetry = true
/*
Indicates that the content key request should be retried because an obsolete key response was set on the previous
content key request.
*/
case AVContentKeyRequest.RetryReason.receivedObsoleteContentKey:
shouldRetry = true
default:
break
}
return shouldRetry
}
// Informs the receiver a content key request has failed.
func contentKeySession(_ session: AVContentKeySession, contentKeyRequest keyRequest: AVContentKeyRequest, didFailWithError err: Error) {
// Add your code here to handle errors.
print("Error: \(err)")
try? keyRequest.respondByRequestingPersistableContentKeyRequestAndReturnError()
}
// MARK: API
func handleStreamingContentKeyRequest(keyRequest: AVContentKeyRequest) {
guard let contentKeyIdentifierString = keyRequest.identifier as? String,
let contentKeyIdentifierURL = URL(string: contentKeyIdentifierString),
let assetIDString = contentKeyIdentifierURL.queryParameters?.count != nil ? contentKeyIdentifierURL.queryParameters!["keyId"] : contentKeyIdentifierString,
let assetIDData = contentKeyIdentifierString.data(using: .utf8)
else {
print("Failed to retrieve the assetID from the keyRequest!")
return
}
let assetID = contentKeyIdentifierURL.absoluteString.hash
var asset: Asset? = nil
for index in 0..<AssetListManager.sharedManager.numberOfAssets() {
let a = AssetListManager.sharedManager.asset(at: index)
if a.stream.contentKeyIDList?.contains(contentKeyIdentifierString) == true {
asset = a
break
}
}
let provideOnlinekey: () -> Void = { () -> Void in
do {
// let applicationCertificate = try self.requestApplicationCertificate(asset)
guard
let certificateURL = URL(string: "https://lic.drmtoday.com/license-server-fairplay/cert/ibakatv"),
let applicationCertificate = try? Data(contentsOf: certificateURL) else {
print("🔑", #function, "Unable to read the certificate data.")
throw ProgramError.missingApplicationCertificate
}
let completionHandler = { [weak self] (spcData: Data?, error: Error?) in
guard let strongSelf = self else { return }
if let error = error {
keyRequest.processContentKeyResponseError(error)
return
}
guard let spcData = spcData else { return }
do {
// Send SPC to Key Server and obtain CKC
let ckcData = try strongSelf.requestContentKeyFromKeySecurityModule(asset, spcData: spcData, assetID: assetIDString, persistable: false)
/*
AVContentKeyResponse is used to represent the data returned from the key server when requesting a key for
decrypting content.
*/
let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: ckcData)
/*
Provide the content key response to make protected content available for processing.[AVContentKeyRequestProtocolVersionsKey: [1]]
*/
keyRequest.processContentKeyResponse(keyResponse)
} catch {
keyRequest.processContentKeyResponseError(error)
}
}
keyRequest.makeStreamingContentKeyRequestData(forApp: applicationCertificate,
contentIdentifier: assetIDData,
options: [AVContentKeyRequestProtocolVersionsKey: [1]],
completionHandler: completionHandler)
} catch {
keyRequest.processContentKeyResponseError(error)
}
}
#if os(iOS)
/*
When you receive an AVContentKeyRequest via -contentKeySession:didProvideContentKeyRequest:
and you want the resulting key response to produce a key that can persist across multiple
playback sessions, you must invoke -respondByRequestingPersistableContentKeyRequest on that
AVContentKeyRequest in order to signal that you want to process an AVPersistableContentKeyRequest
instead. If the underlying protocol supports persistable content keys, in response your
delegate will receive an AVPersistableContentKeyRequest via -contentKeySession:didProvidePersistableContentKeyRequest:.
*/
if shouldRequestPersistableContentKey(withIdentifier: assetID) ||
persistableContentKeyExistsOnDisk(withContentKeyIdentifier: assetIDString) {
// Request a Persistable Key Request.
do {
try keyRequest.respondByRequestingPersistableContentKeyRequestAndReturnError()
} catch {
/*
This case will occur when the client gets a key loading request from an AirPlay Session.
You should answer the key request using an online key from your key server.
*/
provideOnlinekey()
}
return
}
#endif
provideOnlinekey()
}
}
此错误消息意味着内容未经授权或操作过程中发生未知错误。为此,您应该首先检查以下内容
检查ContentKeyDelegate类的实现。
还要检查在handleStreamingContentKeyRequest 方法中是否正确处理内容密钥请求。还有 requestContentKeyFromKeySecurityModule 方法,因为该方法负责从密钥安全模块请求内容密钥
在不同的 iOS 版本上尝试此代码,包括 iOS 16 及更高版本
正如您所知,UAS 打印调试语句或使用断点来识别代码中的任何特定错误或问题
5.最后,如果这里没有任何效果,你应该访问并查阅 AVFoundation 文档或 Apple 开发者论坛。
如果有的话请告诉我