我正在使用
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
帮助的情况下自行预加载音频流吗?如果是这样,我该怎么做?
谢谢。
不要使用
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];
}];
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。
要循环播放视频,可以使用上面相同的方法并介绍
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()
}
}
})
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)
}
}
Task {
let asset = AVAsset(url: url)
let playable = try await asset.load(.isPlayable)
if playable {
player = AVPlayer(playerItem: .init(asset: asset))
player.play()
}
}