响应没有“内容长度”标头时的 AVURLAsset

Posted

技术标签:

【中文标题】响应没有“内容长度”标头时的 AVURLAsset【英文标题】:AVURLAsset when Response doesn't have 'Content-Lenght' header 【发布时间】:2017-08-20 19:58:03 【问题描述】:

我的 ios 应用使用 AVPlayer 从我的服务器播放流式音频并将其存储在设备上。 我实现了 AVAssetResourceLoaderDelegate,所以我可以拦截流。我更改了我的方案(从http 到一个假方案,以便调用 AVAssetResourceLoaderDelegate 方法:

func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool

我遵循了这个教程:

http://blog.jaredsinclair.com/post/149892449150/implementing-avassetresourceloaderdelegate-a

在那里,我把原来的方案放回去,并创建一个会话来从服务器中提取音频。当我的服务器为流式音频文件提供Content-Length(音频文件的大小,以字节为单位)标头时,一切正常。

但有时我会在无法提前提供长度的情况下流式传输音频文件(比如直播播客流)。在这种情况下,AVURLAsset 将长度设置为 -1 并失败:

"Error Domain=AVFoundationErrorDomain Code=-11849 \"Operation Stopped\" UserInfo=NSUnderlyingError=0x61800004abc0 Error Domain=NSOSStatusErrorDomain Code=-12873 \"(null)\", NSLocalizedFailureReason=This media may be damaged., NSLocalizedDescription=Operation Stopped"

而且我无法绕过这个错误。我试图走一个hacky方式,提供假的 Content-Length: 999999999,但在这种情况下,一旦下载了整个音频流,我的会话就会失败:

Loaded so far: 10349852 out of 99999999 The request timed out. //Audio file got downloaded, its size is 10349852 //AVPlayer tries to get the next chunk and then fails with request times out

以前有人遇到过这个问题吗?

附:如果我在 AVURLAsset 中保留原始的 http 方案,AVPlayer 知道如何处理这个方案,所以它可以很好地播放音频文件(即使没有 Content-Length),我不知道它是如何做到的,没有失败。此外,在这种情况下,我的 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)
    


【问题讨论】:

【参考方案1】:

您无法处理这种情况,因为对于 iOS,此文件已损坏,因为标头不正确。系统认为您将播放常规音频文件,但它没有关于它的所有信息。您不知道音频持续时间是多少,只有当您有直播时。 iOS 上的直播是使用 HTTP 直播流协议完成的。 您的 iOS 代码是正确的。您必须修改您的后端并为直播音频提供 m3u8 播放列表,然后 iOS 将接受它作为直播流,音频播放器将开始播放曲目。

一些相关信息可以在here找到。作为一个在流媒体音频/视频方面经验丰富的 iOS 开发人员,我可以告诉你,播放直播/VOD 的代码是一样的。

【讨论】:

这很奇怪。看看我的 P.S.:``` P.S.如果我在 AVURLAsset 中保留原始 http 方案,AVPlayer 知道如何处理这个方案,所以它可以很好地播放音频文件(即使没有 Content-Length),我不知道它是如何做到的,没有失败。此外,在这种情况下,我的 AVAssetResourceLoaderDelegate 从未使用过,因此我无法截取音频文件的内容并将其复制到本地存储。 ``` 我仍然发送没有 Content-Length 标头的常规 mp3 文件,它播放得很好。

以上是关于响应没有“内容长度”标头时的 AVURLAsset的主要内容,如果未能解决你的问题,请参考以下文章

.NET HttpClient - 当响应标头的内容长度不正确时接受部分响应

AVURLAsset的缓存行为

当我使用 Spring Cloud 构建我的微服务时,如何将内容长度添加到响应标头

HTTP 响应数据超过内容长度时的代理/网关行为

HTTP 标头 - ntCoent-Length

内容长度被剥离