是什么导致 AVAssetCache 报告无法离线播放完全下载的资产?
What causes AVAssetCache to report not playable offline for a fully downloaded asset?
我正在开发一个 iOS 应用程序,它通过 HLS 播放 FairPlay 加密的音频,并支持下载和流式传输。在飞行模式下我无法播放下载的内容。如果我在下载完成时从本地 URL 创建一个 AVURLAsset
,asset.assetCache.isPlayableOffline
returns 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
.
这里有没有我没有解决的问题?播放列表文件应该以某种方式不同吗?是否有一些 属性 的任务或资产是我遗漏的?
我认为您在检查 asset.assetCache.isPlayableOffline
之前没有什么要检查的。
- 您的 KSM 配置是否支持 fairplay 离线游戏?
- 访问Apple's FairPlay Streaming Website
- 下载fairplay示例SDK (FairPlay Streaming Server SDK (4.2.0))
- 打开 HLSCatalogWithFPS - AVAssetResourceLoader 或 HLSCatalogWithFPS - AVContentKeySession
- 使您的 KSM 适应示例项目以检查 FPS 离线播放是否正常
- 检查您的密钥请求过程
- 由于您没有提供任何与密钥请求过程相关的代码,我不知道您是否正确请求并收到了 ckc 数据
- 完成下载并不意味着您已获得 ckc 或持久密钥。调试以检查您是否从 KSM 获得正确的 ckc 数据。 (如果您的 KSM 未将内容配置为可离线播放,则在使用永久密钥选项请求 ckc 时可能会出错)
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.")
}
}
事实证明,这是因为 URL 我正在从中下载音频(例如 https://mywebsite.com/path/to/master.m3u8
正在重定向到 CDN url (https://my.cdn/other/path/to/master.m3u8
) . AVAssetDownloadTask
簿记出了点问题,以至于当我尝试离线播放生成的下载文件时,它认为它需要来自网络的更多文件。我已将其归档为雷达 43285278。我通过手动解决了这个问题向同一个 URL 发出 HEAD
请求,然后给 AVAssetDownloadTask
生成的重定向 URL.
我正在开发一个 iOS 应用程序,它通过 HLS 播放 FairPlay 加密的音频,并支持下载和流式传输。在飞行模式下我无法播放下载的内容。如果我在下载完成时从本地 URL 创建一个 AVURLAsset
,asset.assetCache.isPlayableOffline
returns 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
.
这里有没有我没有解决的问题?播放列表文件应该以某种方式不同吗?是否有一些 属性 的任务或资产是我遗漏的?
我认为您在检查 asset.assetCache.isPlayableOffline
之前没有什么要检查的。
- 您的 KSM 配置是否支持 fairplay 离线游戏?
- 访问Apple's FairPlay Streaming Website
- 下载fairplay示例SDK (FairPlay Streaming Server SDK (4.2.0))
- 打开 HLSCatalogWithFPS - AVAssetResourceLoader 或 HLSCatalogWithFPS - AVContentKeySession
- 使您的 KSM 适应示例项目以检查 FPS 离线播放是否正常
- 检查您的密钥请求过程
- 由于您没有提供任何与密钥请求过程相关的代码,我不知道您是否正确请求并收到了 ckc 数据
- 完成下载并不意味着您已获得 ckc 或持久密钥。调试以检查您是否从 KSM 获得正确的 ckc 数据。 (如果您的 KSM 未将内容配置为可离线播放,则在使用永久密钥选项请求 ckc 时可能会出错)
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.")
}
}
事实证明,这是因为 URL 我正在从中下载音频(例如 https://mywebsite.com/path/to/master.m3u8
正在重定向到 CDN url (https://my.cdn/other/path/to/master.m3u8
) . AVAssetDownloadTask
簿记出了点问题,以至于当我尝试离线播放生成的下载文件时,它认为它需要来自网络的更多文件。我已将其归档为雷达 43285278。我通过手动解决了这个问题向同一个 URL 发出 HEAD
请求,然后给 AVAssetDownloadTask
生成的重定向 URL.