58

我正在使用一个子类,AVQueuePlayer当我添加新AVPlayerItem的流式 URL 时,应用程序会冻结大约一两秒。冻结我的意思是它不响应用户界面上的触摸。此外,如果我已经播放了一首歌曲,然后将另一首歌曲添加到队列中,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吗?如果是这样,我该怎么做?

谢谢。

4

3 回答 3

81

不要使用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];
}];
于 2012-08-03T10:28:02.973 回答
23

凹凸,因为这是一个高度评价的问题,在线类似的问题要么有过时的答案,要么不是很好。AVKit整个想法与and非常直接AVFoundation,这意味着不再依赖第三方库。唯一的问题是它需要一些修补才能将各个部分组合在一起。

AVFoundationPlayer()初始化url显然不是线程安全的,或者说它不应该是。这意味着,无论您如何在后台线程中对其进行初始化,播放器属性都将加载到主队列中,从而导致 UI 冻结,尤其是在UITableViews 和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
   }
})

注意

根据您的应用程序想要实现的目标,您可能仍需要进行一些修改来调整它以在 aUITableViewUICollectionView. 您可能还需要在属性上实现一些 KVOAVPlayerItem才能使其工作,并且 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()
                                }
                            }
                        })
于 2018-01-20T00:40:17.790 回答
7

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)

        }

}
于 2017-05-25T14:32:55.420 回答