【问题标题】:AVPlayer "freezes" the app at the start of buffering an audio streamAVPlayer 在缓冲音频流开始时“冻结”应用程序
【发布时间】:2011-12-03 19:29:49
【问题描述】:

我正在使用 AVQueuePlayer 的子类,当我添加带有流式 URL 的新 AVPlayerItem 时,应用程序会冻结大约一两秒钟。冻结我的意思是它不响应用户界面上的触摸。此外,如果我已经播放了一首歌曲,然后将另一首歌曲添加到队列中,AVQueuePlayer 会自动开始预加载这首歌曲,同时它仍在播放第一首歌曲。这使得应用程序在两秒钟内不会响应 UI 上的触摸,就像添加第一首歌曲但歌曲仍在播放时一样。所以这意味着AVQueuePlayer 正在主线程中做一些导致明显“冻结”的事情。

我正在使用insertItem:afterItem: 添加我的AVPlayerItem。我测试并确保这是导致延迟的方法。也许这可能是AVPlayerItem 在将其添加到队列时被AVQueuePlayer 激活时所做的事情。

必须指出,我正在使用 Dropbox API v1 beta 通过此方法调用来获取流 URL:

[[self restClient] loadStreamableURLForFile:metadata.path];

然后,当我收到流 URL 时,我将其发送到 AVQueuePlayer,如下所示:

[self.player insertItem:[AVPlayerItem playerItemWithURL:url] afterItem:nil];

所以我的问题是:我该如何避免这种情况? 我是否应该在没有AVPlayer 帮助的情况下自行预加载音频流?如果是这样,我该怎么做?

谢谢。

【问题讨论】:

  • 即使在不同的线程上调用也会发生这种情况吗?您是否尝试过分离并在那里调用它?
  • 我试过但没用。后来我阅读了文档,结果发现 AVPlayer 方法应该总是在主线程上调用。
  • 您可以尝试挂在 AVPlayerItem 上,直到其状态为 AVPlayerItemStatusReadyToPlay。例如,每秒检查几次以查看它是否准备好播放,然后将其添加到队列中。也许 insertItem 正在同步等待 AVPlayerItem 准备好。
  • 这并不意味着它不会在歌曲之间冻结。我不想让它永远冻结。
  • 对我来说同样的问题。我必须使用 AVPlayer 来代替。

标签: ios audio-streaming dropbox avplayer avqueueplayer


【解决方案1】:

不要使用playerItemWithURL,它是同步的。 当您收到带有 url 的响应时,请尝试以下操作:

AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:url options:nil];
NSArray *keys = @[@"playable"];

[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^() {
    [self.player insertItem:[AVPlayerItem playerItemWithAsset:asset] afterItem:nil];
}];

【讨论】:

  • 非常重要的是要注意,completionHandler 块在主线程中不会被调用(或者至少不保证会),那么你应该在里面使用一个 dispatch_async(dispatch_get_main_queue(), ^{ }) 。
  • 如何在 AVPlayer 中实现这一点。实际上,我没有使用 AVQUEUEPLAYER。你能给我任何想法来实现吗:) @Gigisommo
  • 如果只使用 AVPlayer,您可以采用相同的方法,但不要使用“insertItem”,而是使用“replaceCurrentItemWithPlayerItem”。 (并且要从 AVPlayerItem 获取资产,请使用“item.asset”。)
  • 你是男人中的神。
  • 所有可能的键是什么?
【解决方案2】:

Gigisommo 对 Swift 3 的回答包括来自 cmets 的反馈:

let asset = AVAsset(url: url)
let keys: [String] = ["playable"]

asset.loadValuesAsynchronously(forKeys: keys) { 

        DispatchQueue.main.async {

            let item = AVPlayerItem(asset: asset)
            self.playerCtrl.player = AVPlayer(playerItem: item)

        }

}

【讨论】:

    【解决方案3】:

    Bump,因为这是一个评分很高的问题,网上的类似问题要么答案过时,要么不太好。使用AVKitAVFoundation,整个想法非常简单,这意味着不再依赖第三方库。唯一的问题是它需要一些修补才能将各个部分组合在一起。

    AVFoundationPlayer() 初始化与 url 显然不是线程安全的,或者更确切地说,它不应该是。这意味着,无论您如何在后台线程中初始化它,播放器属性都将被加载到主队列中,从而导致 UI 冻结,尤其是在 UITableViews 和 UICollectionViews 中。为了解决这个问题,Apple 提供了AVAsset,它接受一个 URL 并协助加载媒体属性,如轨道、播放、持续时间等,并且可以异步加载,最好的部分是这个加载过程是可取消的(不像其他调度队列结束任务的后台线程可能不是那么简单)。这意味着,当您在表格视图或集合视图上快速滚动时,无需担心在后台挥之不去的僵尸线程,最终会在内存中堆积一大堆未使用的对象。这个cancellable 功能很棒,如果它正在进行中,我们可以取消任何挥之不去的AVAsset 异步加载,但仅限于单元出列期间。异步加载过程可以通过 loadValuesAsynchronously 方法调用,并且可以在以后的任何时间(如果仍在进行中)(随意)取消。

    不要忘记使用loadValuesAsynchronously 的结果正确处理异常。在 Swift (3/4) 中,如果异步进程失败(由于网络速度慢等),您将如何异步加载视频并处理情况-

    TL;DR

    播放视频

    let asset = AVAsset(url: URL(string: self.YOUR_URL_STRING))
    let keys: [String] = ["playable"]
    var player: AVPlayer!                
    
    asset.loadValuesAsynchronously(forKeys: keys, completionHandler: {
         var error: NSError? = nil
         let status = asset.statusOfValue(forKey: "playable", error: &error)
         switch status {
            case .loaded:
                 DispatchQueue.main.async {
                   let item = AVPlayerItem(asset: asset)
                   self.player = AVPlayer(playerItem: item)
                   let playerLayer = AVPlayerLayer(player: self.player)
                   playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
                   playerLayer.frame = self.YOUR_VIDEOS_UIVIEW.bounds
                   self.YOUR_VIDEOS_UIVIEW.layer.addSublayer(playerLayer)
                   self.player.isMuted = true
                   self.player.play()
                }
                break
            case .failed:
                 DispatchQueue.main.async {
                     //do something, show alert, put a placeholder image etc.
                }
                break
             case .cancelled:
                DispatchQueue.main.async {
                    //do something, show alert, put a placeholder image etc.
                }
                break
             default:
                break
       }
    })
    

    注意

    根据您的应用想要实现的目标,您可能仍需要进行一些修改来调整它以在UITableViewUICollectionView 中获得更流畅的滚动。您可能还需要在 AVPlayerItem 属性上实现一些 KVO 才能使其正常工作,SO 中有很多帖子详细讨论了 AVPlayerItem KVO。


    循环播放资产(视频循环/GIF)

    要循环播放视频,您可以使用与上述相同的方法并引入AVPlayerLooper。这是一个循环播放视频(或者可能是 GIF 风格的短视频)的示例代码。 注意使用我们的视频循环所需的duration 键。

    let asset = AVAsset(url: URL(string: self.YOUR_URL_STRING))
    let keys: [String] = ["playable","duration"]
    var player: AVPlayer!
    var playerLooper: AVPlayerLooper!
    
    asset.loadValuesAsynchronously(forKeys: keys, completionHandler: {
         var error: NSError? = nil
         let status = asset.statusOfValue(forKey: "duration", error: &error)
         switch status {
            case .loaded:
                 DispatchQueue.main.async {
                     let playerItem = AVPlayerItem(asset: asset)
                     self.player = AVQueuePlayer()
                     let playerLayer = AVPlayerLayer(player: self.player)
    
                     //define Timerange for the loop using asset.duration
                     let duration = playerItem.asset.duration
                     let start = CMTime(seconds: duration.seconds * 0, preferredTimescale: duration.timescale)
                     let end = CMTime(seconds: duration.seconds * 1, preferredTimescale: duration.timescale)
                     let timeRange = CMTimeRange(start: start, end: end)
    
                     self.playerLooper = AVPlayerLooper(player: self.player as! AVQueuePlayer, templateItem: playerItem, timeRange: timeRange)
                     playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
                     playerLayer.frame = self.YOUR_VIDEOS_UIVIEW.bounds
                   self.YOUR_VIDEOS_UIVIEW.layer.addSublayer(playerLayer)
                     self.player.isMuted = true
                     self.player.play()
                }
                break
            case .failed:
                 DispatchQueue.main.async {
                     //do something, show alert, put a placeholder image etc.
                }
                break
             case .cancelled:
                DispatchQueue.main.async {
                    //do something, show alert, put a placeholder image etc.
                }
                break
             default:
                break
       }
    })
    

    编辑:根据documentationAVPlayerLooper 要求资产的duration 属性完全加载,以便能够循环播放视频。此外,如果您想要无限循环,AVPlayerLooper 初始化中的开始和结束时间范围的timeRange: timeRange 确实是可选的。自从我发布此答案后,我也意识到 AVPlayerLooper 在循环视频中的准确率只有 70-80%,尤其是当您的 AVAsset 需要从 URL 流式传输视频时。为了解决这个问题,有一种完全不同(但很简单)的方法来循环播放视频-

    //this will loop the video since this is a Gif
      let interval = CMTime(value: 1, timescale: 2)
      self.timeObserverToken = self.player?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { (progressTime) in
                                if let totalDuration = self.player?.currentItem?.duration{
                                    if progressTime == totalDuration{
                                        self.player?.seek(to: kCMTimeZero)
                                        self.player?.play()
                                    }
                                }
                            })
    

    【讨论】:

    • 先生,你是救命稻草
    • 我将代码翻译成 Objective-c,但它仍然无法正常工作。代码冻结了主线程。
    猜你喜欢
    • 2015-12-13
    • 2011-06-01
    • 2012-06-30
    • 2016-08-08
    • 1970-01-01
    • 2018-05-12
    • 2017-07-08
    • 1970-01-01
    • 2012-12-09
    相关资源
    最近更新 更多