最近工作是开发通用的视频播放器,给公司不同部门使用。主要产出是:
- CachedPlayer
- 封装
AVPlayer
,提供更友好API - 视频边播边缓存
- 预加载
- 封装
- CachedPlayerView
- 封面图
- 加载状态 loadingView
- FullScreenVideoBoxView
- 视频播放UI集成
- 轻松嵌入到
UITableViewCell
,UICollectionViewCell
- 自动处理进入和退出全屏
- 手势拖拽退出全屏
系列文章目录:
- 《iOS 视频播放器开发》
- 《iOS 视频缓存与预加载》
- 《iOS 全屏播放动画与手势》
CachedPlayer
AVPlayer
的功能十分强大,但是API并不友好。我们需要通过KVO监听AVPlayer
与AVPlayerItem
的多个属性才能获得其确切状态以及播放进度。CachedPlayer
提供更加简单直接的API,将AVPlayer
的复杂性封装于内部。
状态
enum Status {
case unknown // 初始状态
case buffering // 加载中
case playing // 播放中
case paused // 暂停
case end // 播放到末尾
case error // 播放出错
}
private(set) var status = Status.unknown // 初始默认为unknown
AVPlayer
并没有一个确切的status
来让我们获取当前播放器状态,在使用过程中往往需要通过多个属性联合判断当前状态。CachedPlayer
的首要任务就是对状态进行封装。CachedPlayer
通过对AVPlayer
和AVPlayerItem
的多个属性进行监听,然后调用updateStatus()
统一改变当前状态。
private func updateStatus() {
DispatchQueue.main.async { // 在主线程改变状态,因为外部通常会监听status改变进行UI操作
guard let currentItem = self.currentItem else {
return
}
if self.player.error != nil || currentItem.error != nil {
self.status = .error
return
}
if #available(iOS 10, *) {
switch self.player.timeControlStatus {
case .playing:
self.status = .playing
case .paused:
self.status = .paused
case .waitingToPlayAtSpecifiedRate:
self.status = .buffering
}
} else {
if self.player.rate != 0 { // 期望速率不为0
if currentItem.isPlaybackLikelyToKeepUp {
self.status = .playing
} else {
self.status = .buffering
}
} else {
self.status = .paused
}
}
}
}
iOS 10 之后推出了
timeControlStatus
让我们可以得到当前播放器处于播放、缓冲还是暂停状态。并且当player.automaticallyWaitsToMinimizeStalling = false
时,AVPlayer
加载数据即立即播放,不会有waitingToPlayAtSpecifiedRate
状态,只会在playing
和paused
之间切换。
属性监听
KVO
AVPlayer
:
- rate: 期望播放速率
- status: 播放器状态【播放是否失败】
- timeControlStatus: 当前播放状态【暂停、缓冲、播放】
AVPlayerItem
:
- status: 播放状态
- playbackLikelyToKeepUp: 是否在播放
- isPlaybackBufferEmpty: 缓冲区是否为空
- isPlaybackBufferFull: 缓冲区是否已满
TimeObserver
AVPlayer
可以添加定时的监听器获取其当前播放时间。
timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.1, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), queue: DispatchQueue.main, using: { [unowned self] (time) in
self.updateStatus()
guard let total = self.currentItem?.duration.seconds else {
return
}
if total.isNaN || total.isZero {
return
}
self.duration = total
self.playedDuration = time.seconds
})
timeObserver
需要在deinit
时从播放器移除。
Notification
目前仅对AVPlayerItemDidPlayToEndTime
进行监听,当播放到结尾,则将status
设置为end
。
API
对外提供基本播放方法:
func replace(with url: URL) {
currentItem = AVPlayerItem(url: url)
player.replaceCurrentItem(with: currentItem)
addItemObservers()
}
func stop() {
removeItemObservers()
currentItem = nil
player.replaceCurrentItem(with: nil)
status = .unknown
}
func play() {
player.play()
}
func pause() {
player.pause()
}
func seek(to time: TimeInterval) {
player.seek(to: CMTime(seconds: time, preferredTimescale: CMTimeScale(NSEC_PER_SEC)))
}
回调
var statusDidChangeHandler: ((Status) -> Void)?
var playedDurationDidChangeHandler: ((TimeInterval, TimeInterval) -> Void)?
private(set) var playedDuration: TimeInterval = 0 {
didSet {
playedDurationDidChangeHandler?(playedDuration, duration)
}
}
private(set) var status = Status.unknown {
didSet {
guard status != oldValue else {
return
}
statusDidChangeHandler?(status)
}
}
外部通过这两个闭包来分别监听播放状态的改变以及播放进度的改变。
CachedPlayerView
CachedPlayerView
是提供UIKit
的API,将CachedPlayer
集成在里面。在开发中,我们直接创建CachedPlayerView
实例添加到View上。
class CachedPlayerView: UIView {
private(set) var player = CachedPlayer()
override class var layerClass: AnyClass {
get {
return AVPlayerLayer.self
}
}
override init(frame: CGRect) {
super.init(frame: frame)
player.bind(to: layer as! AVPlayerLayer)
}
}
在CachedPlayer
里面添加
func bind(to playerLayer: AVPlayerLayer) {
playerLayer.player = player
}
那么使用起来就简单了,在ViewController
里面只用简单几行代码,就能播放视频了
let playerView = CachedPlayerView()
playerView.player.statusDidChangeHandler = { status in
print(status)
}
playerView.player.playedDurationDidChangeHandler = { (played, total) in
print("\(played)/\(total)")
}
playerView.frame = view.bounds
playerView.player.replace(with: url)
playerView.player.play()
更多
到此播放器的基本封装已经完成,提供了简单的对外接口,已经统一了状态监听。
下一篇文章会讲如何通过AVAssetResourceLoaderDelegate
实现边播边下载功能。之后会对CachedPlayer
进行扩充,封装缓存逻辑调用。
源码地址