Ios AVURLAsset当响应不响应时';我没有';内容长度';标题
我的iOS应用程序使用AVPlayer播放来自我的服务器的流式音频,并将其存储在设备上。 我实现了AVAssetResourceLoaderDelegate,因此可以拦截流。我将方案(从Ios AVURLAsset当响应不响应时';我没有';内容长度';标题,ios,audio,streaming,avplayer,avurlasset,Ios,Audio,Streaming,Avplayer,Avurlasset,我的iOS应用程序使用AVPlayer播放来自我的服务器的流式音频,并将其存储在设备上。 我实现了AVAssetResourceLoaderDelegate,因此可以拦截流。我将方案(从http更改为假方案,以便调用AVAssetResourceLoaderDelegate方法: func resourceLoader(resourceLoader:AVAssetResourceLoader,shouldWaitForLoadingFrequestEndResource loadingReque
http
更改为假方案,以便调用AVAssetResourceLoaderDelegate方法:
func resourceLoader(resourceLoader:AVAssetResourceLoader,shouldWaitForLoadingFrequestEndResource loadingRequest:AVAssetResourceLoadingRequest)->Bool
我遵循了本教程:
在那里,我把原来的方案放回原处,并创建了一个从服务器中提取音频的会话。当我的服务器为流式音频文件提供内容长度
(音频文件的大小以字节为单位)头时,一切都能完美地工作
但有时我会在无法提前提供长度的情况下传输音频文件(比如实时播客流)。在这种情况下,AVURLAsset会将长度设置为-1
,并在以下情况下失败:
“Error Domain=AVFoundationErrorDomain code=-11849\”操作已停止\“UserInfo={NSUnderlyingError=0x6180004abc0{Error Domain=nsossstatuserrordomain code=-12873\”(null)\”,NSLocalizedFailureReason=此媒体可能已损坏,NSLocalizedDescription=操作已停止}
我无法绕过这个错误。我试图用一种不正当的方式,提供虚假信息
内容长度:99999999
,但在这种情况下,一旦下载了整个音频流,我的会话将失败,原因是:
到目前为止已加载:9999999中有10349852个
请求超时。
//已下载音频文件,其大小为10349852
//AVPlayer尝试获取下一个区块,但由于请求超时而失败
以前有人遇到过这个问题吗
另外,如果我在avurlaste中保留原始的http
方案,AVPlayer知道如何处理该方案,因此它可以播放音频文件(即使没有内容长度
),我不知道它是如何在没有失败的情况下运行的。此外,在这种情况下,我的AVAssetResourceLoaderDelegate从未使用过,因此我无法截取音频文件的内容并将其复制到本地存储
以下是实施方案:
import AVFoundation
@objc protocol CachingPlayerItemDelegate {
// called when file is fully downloaded
@objc optional func playerItem(playerItem: CachingPlayerItem, didFinishDownloadingData data: NSData)
// called every time new portion of data is received
@objc optional func playerItemDownloaded(playerItem: CachingPlayerItem, didDownloadBytesSoFar bytesDownloaded: Int, outOf bytesExpected: Int)
// called after prebuffering is finished, so the player item is ready to play. Called only once, after initial pre-buffering
@objc optional func playerItemReadyToPlay(playerItem: CachingPlayerItem)
// called when some media did not arrive in time to continue playback
@objc optional func playerItemDidStopPlayback(playerItem: CachingPlayerItem)
// called when deinit
@objc optional func playerItemWillDeinit(playerItem: CachingPlayerItem)
}
extension URL {
func urlWithCustomScheme(scheme: String) -> URL {
var components = URLComponents(url: self, resolvingAgainstBaseURL: false)
components?.scheme = scheme
return components!.url!
}
}
class CachingPlayerItem: AVPlayerItem {
class ResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate, URLSessionDataDelegate, URLSessionTaskDelegate {
var playingFromCache = false
var mimeType: String? // is used if we play from cache (with NSData)
var session: URLSession?
var songData: NSData?
var response: URLResponse?
var pendingRequests = Set<AVAssetResourceLoadingRequest>()
weak var owner: CachingPlayerItem?
//MARK: AVAssetResourceLoader delegate
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
if playingFromCache { // if we're playing from cache
// nothing to do here
} else if session == nil { // if we're playing from url, we need to download the file
let interceptedURL = loadingRequest.request.url!.urlWithCustomScheme(scheme: owner!.scheme!).deletingLastPathComponent()
startDataRequest(withURL: interceptedURL)
}
pendingRequests.insert(loadingRequest)
processPendingRequests()
return true
}
func startDataRequest(withURL url: URL) {
let request = URLRequest(url: url)
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
configuration.timeoutIntervalForRequest = 60.0
configuration.timeoutIntervalForResource = 120.0
session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
let task = session?.dataTask(with: request)
task?.resume()
}
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) {
pendingRequests.remove(loadingRequest)
}
//MARK: URLSession delegate
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
(songData as! NSMutableData).append(data)
processPendingRequests()
owner?.delegate?.playerItemDownloaded?(playerItem: owner!, didDownloadBytesSoFar: songData!.length, outOf: Int(dataTask.countOfBytesExpectedToReceive))
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
completionHandler(URLSession.ResponseDisposition.allow)
songData = NSMutableData()
self.response = response
processPendingRequests()
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError err: Error?) {
if let error = err {
print(error.localizedDescription)
return
}
processPendingRequests()
owner?.delegate?.playerItem?(playerItem: owner!, didFinishDownloadingData: songData!)
}
//MARK:
func processPendingRequests() {
var requestsCompleted = Set<AVAssetResourceLoadingRequest>()
for loadingRequest in pendingRequests {
fillInContentInforation(contentInformationRequest: loadingRequest.contentInformationRequest)
let didRespondCompletely = respondWithDataForRequest(dataRequest: loadingRequest.dataRequest!)
if didRespondCompletely {
requestsCompleted.insert(loadingRequest)
loadingRequest.finishLoading()
}
}
for i in requestsCompleted {
pendingRequests.remove(i)
}
}
func fillInContentInforation(contentInformationRequest: AVAssetResourceLoadingContentInformationRequest?) {
// if we play from cache we make no URL requests, therefore we have no responses, so we need to fill in contentInformationRequest manually
if playingFromCache {
contentInformationRequest?.contentType = self.mimeType
contentInformationRequest?.contentLength = Int64(songData!.length)
contentInformationRequest?.isByteRangeAccessSupported = true
return
}
// have no response from the server yet
if response == nil {
return
}
let mimeType = response?.mimeType
contentInformationRequest?.contentType = mimeType
if response?.expectedContentLength != -1 {
contentInformationRequest?.contentLength = response!.expectedContentLength
contentInformationRequest?.isByteRangeAccessSupported = true
} else {
contentInformationRequest?.isByteRangeAccessSupported = false
}
}
func respondWithDataForRequest(dataRequest: AVAssetResourceLoadingDataRequest) -> Bool {
let requestedOffset = Int(dataRequest.requestedOffset)
let requestedLength = dataRequest.requestedLength
let startOffset = Int(dataRequest.currentOffset)
// Don't have any data at all for this request
if songData == nil || songData!.length < startOffset {
return false
}
// This is the total data we have from startOffset to whatever has been downloaded so far
let bytesUnread = songData!.length - Int(startOffset)
// Respond fully or whaterver is available if we can't satisfy the request fully yet
let bytesToRespond = min(bytesUnread, requestedLength + Int(requestedOffset))
dataRequest.respond(with: songData!.subdata(with: NSMakeRange(startOffset, bytesToRespond)))
let didRespondFully = songData!.length >= requestedLength + Int(requestedOffset)
return didRespondFully
}
deinit {
session?.invalidateAndCancel()
}
}
private var resourceLoaderDelegate = ResourceLoaderDelegate()
private var scheme: String?
private var url: URL!
weak var delegate: CachingPlayerItemDelegate?
// use this initializer to play remote files
init(url: URL) {
self.url = url
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
scheme = components.scheme
let asset = AVURLAsset(url: url.urlWithCustomScheme(scheme: "fakeScheme").appendingPathComponent("/test.mp3"))
asset.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main)
super.init(asset: asset, automaticallyLoadedAssetKeys: nil)
resourceLoaderDelegate.owner = self
self.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didStopHandler), name:NSNotification.Name.AVPlayerItemPlaybackStalled, object: self)
}
// use this initializer to play local files
init(data: NSData, mimeType: String, fileExtension: String) {
self.url = URL(string: "whatever://whatever/file.\(fileExtension)")
resourceLoaderDelegate.songData = data
resourceLoaderDelegate.playingFromCache = true
resourceLoaderDelegate.mimeType = mimeType
let asset = AVURLAsset(url: url)
asset.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main)
super.init(asset: asset, automaticallyLoadedAssetKeys: nil)
resourceLoaderDelegate.owner = self
self.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didStopHandler), name:NSNotification.Name.AVPlayerItemPlaybackStalled, object: self)
}
func download() {
if resourceLoaderDelegate.session == nil {
resourceLoaderDelegate.startDataRequest(withURL: url)
}
}
override init(asset: AVAsset, automaticallyLoadedAssetKeys: [String]?) {
fatalError("not implemented")
}
// MARK: KVO
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
delegate?.playerItemReadyToPlay?(playerItem: self)
}
// MARK: Notification handlers
func didStopHandler() {
delegate?.playerItemDidStopPlayback?(playerItem: self)
}
// MARK:
deinit {
NotificationCenter.default.removeObserver(self)
removeObserver(self, forKeyPath: "status")
resourceLoaderDelegate.session?.invalidateAndCancel()
delegate?.playerItemWillDeinit?(playerItem: self)
}
}
import-AVFoundation
@objc协议CachingPlayerItemDelegate{
//文件完全下载时调用
@objc可选功能playerItem(playerItem:CachingPlayerItem,didFinishDownloadingData:NSData)
//每次接收到数据的新部分时调用
@objc可选功能playerItem下载(playerItem:CachingPlayerItem,didDownloadBytesSoFar bytesDownloaded:Int,outOf bytesExpected:Int)
//在预缓冲完成后调用,因此播放器项已准备好播放。仅在初始预缓冲后调用一次
@objc可选功能playerItemReadyToPlay(playerItem:CachingPlayerItem)
//当某些媒体未及时到达以继续播放时调用
@objc可选函数playerItemDidStopPlayback(playerItem:CachingPlayerItem)
//当脱硝时被称为
@objc可选函数playerItemWillDeinit(playerItem:CachingPlayerItem)
}
扩展URL{
func urlWithCustomScheme(方案:字符串)->URL{
var components=URLComponents(url:self,resolvingAgainstBaseURL:false)
组件?.scheme=scheme
返回组件!.url!
}
}
CachingPlayerItem类:AVPlayerItem{
类ResourceLoaderDelegate:NSObject、AVAssetResourceLoaderDelegate、URLSessionDelegate、URLSessionDataDelegate、URLSessionTaskDelegate{
var playingFromCache=false
var mimeType:String?//用于从缓存播放(使用NSData)
var会话:URLSession?
数据:NSData?
var响应:URLResponse?
var pendingRequests=Set()
弱var所有者:CachingPlayerItem?
//标记:AVAssetResourceLoader委托
func resourceLoader(resourceLoader:AVAssetResourceLoader,shouldWaitForLoadingFrequestedResource loadingRequest:AVAssetResourceLoadingRequest)->Bool{
如果从缓存播放{//如果我们从缓存播放
//这里没什么可做的
}否则,如果session==nil{//如果我们从url播放,我们需要下载文件
让interceptedURL=loadingRequest.request.url!.urlWithCustomScheme(方案:owner!.scheme!)。删除LastPathComponent()
startDataRequest(带URL:interceptedURL)
}
pendingRequests.insert(加载请求)
processPendingRequests()
返回真值
}
func startDataRequest(带url:url){
let request=URLRequest(url:url)
let configuration=URLSessionConfiguration.default
configuration.requestCachePolicy=.reloadIgnoringLocalAndRemoteCacheData
configuration.timeoutitervalforrequest=60.0
configuration.timeoutitervalforresource=120.0
session=URLSession(配置:配置,委托:self,委托队列:nil)
让任务=会话?.dataTask(带:请求)
任务?.resume()
}
func resourceLoader(resourceLoader:AVAssetResourceLoader,didCancel loadingRequest:AVAssetResourceLoadingRequest){
pendingRequests.remove(加载请求)
}
//标记:URLSession委托
func urlSession(session:urlSession,dataTask:URLSessionDataTask,didReceive data:data){
(songData为!NSMutableData).append(数据)
processPendingRequests()
owner?.delegate?.playerItemDownloaded?(playerItem:owner!,didDownloadBytesSoFar:songData!.length,outOf:Int(dataTask.countOfBytesExpectedToReceive))
}
func urlSession(session:urlSession,dataTask:URLSessionDataTask,didReceive响应:URLResponse,completionHandler:@escaping(urlSession.ResponseDisposition)->Void){
completionHandler(URLSession.ResponseDisposition.allow)
songData=NSMutableData()