Bump,因为这是一个评分很高的问题,网上的类似问题要么答案过时,要么不太好。使用AVKit 和AVFoundation,整个想法非常简单,这意味着不再依赖第三方库。唯一的问题是它需要一些修补才能将各个部分组合在一起。
AVFoundation 的 Player() 初始化与 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
}
})
注意:
根据您的应用想要实现的目标,您可能仍需要进行一些修改来调整它以在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
}
})
编辑:根据documentation,AVPlayerLooper 要求资产的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()
}
}
})