未知长度资产的 AVAssetResourceLoaderDelegate 实现

2024-03-01

我的 iOS 应用程序使用 AVPlayer 播放来自我的服务器的流音频并将其存储在设备上。 我实现了 AVAssetResourceLoaderDelegate,因此我可以拦截该流。我改变了我的计划(从http到一个假方案,以便调用 AVAssetResourceLoaderDelegate 方法:

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

我按照这个教程进行操作:

http://blog.jaredsinclair.com/post/149892449150/implementing-avassetresourceloaderdelegate-a 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

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

附:如果我保留原来的httpAVURLAsset 中的方案,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)
    }

}

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

可以找到一些相关的信息here https://www.quora.com/How-do-I-stream-live-audio-in-an-iOS-application。作为一名在流媒体音频/视频方面拥有丰富经验的 iOS 开发人员,我可以告诉您直播/点播的代码是相同的。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

未知长度资产的 AVAssetResourceLoaderDelegate 实现 的相关文章

随机推荐

  • 从 pandas.read_csv() 中删除重复的列

    默认情况下 pandas read csv https pandas pydata org pandas docs stable generated pandas read csv html破坏 X X 1 X N 形式的重复 CSV 标头
  • LoadedEvent 的路由事件类处理程序不适用于大多数类

    我正在尝试为所有 Loaded 事件注册处理程序 EventManager RegisterClassHandler typeof UIElement FrameworkElement LoadedEvent new RoutedEvent
  • Flyway lambda 的 CDK 自定义资源响应

    我已经从以下位置获取了 Flyway lambda这个仓库 https github com Geekoosh flyway lambda我正在尝试实现一个自定义资源 只要迁移源文件夹发生更改 该资源就会调用 lambda 它工作正常 但偶
  • 处理器处理中断的速度有多快

    我正在研究中断 因此 大多数架构都是中断驱动的 如果一切都是中断驱动的 那么处理器处理所有这些的速度有多快 例如 当按下键盘按键时 它会创建一个中断 要求内核在缓冲区中查找新字符 在这种情况下 处理器的服务速度有多快 而且当发出中断时 处理
  • 如何使用 .env 文件覆盖 fastlane 应用程序文件中的值

    在某些情况下 我们需要覆盖 fastlane 应用程序文件中的值 例如使用不同的苹果帐户来发布应用程序 但没有记录的官方方法 最简单的方法是使用环境变量 Use an Appfile像这样 apple id ENV APPLE ID ema
  • 如果满足条件则打印上一行

    我想 grep 一个单词 然后找到该行中的第二列并检查它是否大于某个值 是的 我想打印上一行 Ex 输入文件 AAAAAAAAAAAAA BB 2 CCCCCCCCCCCCC BB 0 1 Output AAAAAAAAAAAAA 现在 我
  • 如何创建崩溃时重新启动的服务

    我正在使用创建服务CreateService http msdn microsoft com en us library ms682450 28VS 85 29 aspx 如果该服务发生崩溃 它将再次正常运行 并且我希望 Windows 在
  • 如何从 PHP 获取电子邮件及其附件

    我正在为朋友的婚礼编写一个照片库网络应用程序 他们想要一个照片库供客人提交他们当天拍摄的数码照片 在评估了所有选项后 我决定对用户来说最简单的事情就是让他们使用熟悉的界面 他们的电子邮件 然后让他们将图片作为附件发送 我已经创建了一个邮箱
  • 如何将 WCF 客户端代理生成的类中的方法标记为虚拟

    在 VS 2010 中 我创建了一个服务引用 该引用的代码生成 WCF 客户端代理类 Reference cs 我需要此类中的方法具有 Virtual 修饰符 以便它们可以在 Mock 中使用 当然 我可以手动编辑生成的代码 但每次更新引用
  • 通过 ajax 加载的表单上的 :remote => true/data-remote

    在我的 Rails 应用程序中 我有一个使用 jQuery 加载方法通过 Ajax 加载的表单 function load sales form product id sales form load product id 加载的表单有一个
  • Junit测试时如何将日志级别设置为DEBUG?

    我将 SLF4J 与 LOG4J 一起使用 配置通常位于log4j properties 并将日志级别设置为 INFO 但是在测试期间我想将日志设置为 调试 我看不出有什么方法可以实现自动化 也没有类似的东西log4j tests prop
  • PyOpenCL 矩阵乘法

    我有使用 pyopenCL 进行矩阵乘法的代码 我的问题是某些矩阵的结果是错误的 我不明白为什么 经过一番研究后 我认为它与类似的全球规模有关 但我不明白如何设置该值 例如 使用 numpy dtype float32 的矩阵 矩阵1 0
  • 如何使用 Angular 的装饰器模式来增强指令的链接功能?

    我正在开发一个 Angular 库 并寻找一种使用装饰器模式扩展指令的方法 angular module myApp decorator originaldirectiveDirective delegate function delega
  • Jekyll 右对齐文本

    如何在 Jekyll 中右对齐文本 我有一段文本想要右对齐 如果是 HTML 我会这样做 style text align right 我怎样才能用 Jekyll 在 Markdown 中做到这一点 Jekyll 2 0 使用 kramdo
  • DefaultConnection 和membership - localsqlserver 和defaultconnection 之间的连接是什么

    嗯 我真的无法理解这个问题 在 web config 我有
  • 编译器错误消息:CS0029:无法将类型“int”隐式转换为“string”

    我需要转换String表数据库的输入整数值在 C NET 4 中并尝试了受此启发的代码Link https stackoverflow com questions 2344411 how to convert string to integ
  • 获取 ASP.NET MVC 站点在文件系统中的路径

    我有一个 ASP NET MVC 项目 它有一个名为电子邮件的子文件夹 其中包含我的电子邮件模板的 HTM 文件 在网站的某些位置 我有一个控制器需要加载这些模板之一并将其作为电子邮件发送 我想要做的是使用反射来获取当前执行程序集的路径 但
  • Visual Studio Code 无法在 WSL2 上运行

    当我尝试在 WSL2 终端中打开 VS Code 时 如下所示 code 我收到以下错误 mkdir cannot create directory DIRECTORY Permission denied 当使用 sudo 启动时 sudo
  • 使用 Linq to SQL 进行多线程处理

    我正在构建一个应用程序 它需要我使用 DataContext 的内部线程 我的应用程序不断抛出异常InvalidOperationException类似于 There is already an open DataReader associ
  • 未知长度资产的 AVAssetResourceLoaderDelegate 实现

    我的 iOS 应用程序使用 AVPlayer 播放来自我的服务器的流音频并将其存储在设备上 我实现了 AVAssetResourceLoaderDelegate 因此我可以拦截该流 我改变了我的计划 从http到一个假方案 以便调用 AVA