AVPlayer在缓冲音频流的开始时"冻结"应用程序

rai*_*xer 46 dropbox audio-streaming ios avplayer avqueueplayer

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

insertItem:afterItem:用来添加我的AVPlayerItem.我测试并确保这是造成延迟的方法.也许它可能是在将它添加到队列AVPlayerItem时被激活的东西AVQueuePlayer.

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

[[self restClient] loadStreamableURLForFile:metadata.path];
Run Code Online (Sandbox Code Playgroud)

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

[self.player insertItem:[AVPlayerItem playerItemWithURL:url] afterItem:nil];
Run Code Online (Sandbox Code Playgroud)

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

谢谢.

Gig*_*mmo 71

不要使用playerItemWithURL它的同步.当您收到带有网址的回复时,请尝试以下操作:

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

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

  • 请注意,在主线程中不调用completionHandler块(或者至少不保证),这一点非常重要,那么你应该在其中使用dispatch_async(dispatch_get_main_queue(),^ {}). (22认同)
  • 如果只使用AVPlayer,您可以遵循相同的方法,而不是"insertItem"使用"replaceCurrentItemWithPlayerItem".(并从AVPlayerItem获取资产使用"item.asset".) (3认同)
  • 我怎样才能在AVPlayer中实现这一点.实际上,我没有使用AVQUEUEPLAYER.你能告诉我任何想法:) @Gigisommo (2认同)
  • 你是男人中的神. (2认同)
  • 所有可能的键是什么? (2认同)

Ann*_*awn 11

凹凸,因为这是一个高度评价的问题,在线类似的问题要么已经过时答案,要么不是很好.整个想法很简单,AVKit并且AVFoundation,这意味着不再依赖于第三方库.唯一的问题是它需要一些修修补补并将各个部分放在一起.

AVFoundationPlayer()与初始化url显然不是线程安全的,或者更确切地说,它并不意味着是.这意味着,无论你如何在后台线程中初始化它,玩家属性都将被加载到主队列中,导致UI冻结,特别是在UITableViews和UICollectionViews.为了解决这个问题,Apple提供AVAsset了一个URL,它可以帮助加载媒体属性,如曲目,回放,持续时间等,并且可以异步执行,最好的部分是这个加载过程是可取消的(与其他Dispatch队列后台线程不同)结束任务可能不是那么直接).这意味着,当您在表视图或集合视图上快速滚动时,无需担心后台中存在延迟的僵尸线程,最终会在内存中堆积大量未使用的对象.此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
   }
})
Run Code Online (Sandbox Code Playgroud)

注意:

根据你的应用程序想要实现的目标,你可能仍然需要进行一些修改来调整它以便在一个UITableView或更好的滚动UICollectionView.您可能还需要在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
   }
})
Run Code Online (Sandbox Code Playgroud)

编辑:根据文档,AVPlayerLooper要求duration资产的属性完全加载,以便能够循环播放视频.此外,如果您想要无限循环timeRange: timeRange,那么AVPlayerLooper初始化中的开始和结束时间范围实际上是可选的.我已经意识到,因为我发布了这个答案,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()
                                }
                            }
                        })
Run Code Online (Sandbox Code Playgroud)


Dav*_*eek 5

Gigisommo对Swift 3的回答包括评论的反馈:

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)

        }

}
Run Code Online (Sandbox Code Playgroud)