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

问题描述 投票:0回答:4

我正在使用

AVQueuePlayer
的子类,当我使用流式 URL 添加新的
AVPlayerItem
时,应用程序会冻结大约一两秒。我所说的冻结是指它不会响应用户界面上的触摸。另外,如果我已经播放一首歌曲,然后将另一首歌曲添加到队列中,
AVQueuePlayer
会在仍在播放第一首歌曲时自动开始预加载该歌曲。这使得应用程序在两秒钟内不会响应用户界面上的触摸,就像添加第一首歌曲但歌曲仍在播放时一样。所以这意味着
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
帮助的情况下自行预加载音频流吗?如果是这样,我该怎么做?

谢谢。

ios audio-streaming dropbox avplayer avqueueplayer
4个回答
82
投票

不要使用

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];
}];

24
投票

Bump,因为这是一个评价很高的问题,而网上类似的问题要么有过时的答案,要么不太好。

AVKit
AVFoundation
的整个想法非常简单,这意味着不再依赖第三方库。唯一的问题是需要进行一些修补才能将各个部分组合在一起。

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

不要忘记使用

loadValuesAsynchronously
的结果正确处理异常。在 Swift (3/4) 中,以下是如何异步加载视频并处理异步进程失败(由于网络速度慢等)的情况 -

TL;博士

播放视频

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
   }
})

注意

根据您的应用程序想要实现的目标,您可能仍然需要进行一些修改来调整它,以获得

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
   }
})

编辑:根据文档

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()
                                }
                            }
                        })

8
投票

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)

        }

}

0
投票

iOS 15+,异步等待

Task {
    let asset = AVAsset(url: url)
    let playable = try await asset.load(.isPlayable)
    if playable {
        player = AVPlayer(playerItem: .init(asset: asset))
        player.play()
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.