(三)深入了解AVFoundation-播放:AVPlayer 进阶 播放状态 进度监听全解析
引言
在上一部分中,我们介绍了如何使用 AVPlayer 来实现视频的基础播放功能,并讲解了 AVAsset、AVPlayerItem 和 AVPlayer 的基本配置。通过这些内容,我们能够实现视频的加载与播放。然而,在实际的应用场景中,视频播放的流畅性不仅仅依赖于如何播放视频,还涉及到如何优化播放过程中各种状态和进度的管理。
AVPlayer 提供了多种监听机制,包括播放状态、缓冲状态、播放进度等,通过合理利用这些监听,我们可以实现更精准的播放控制,如优化加载、减少卡顿、提升用户体验等。
本篇博客将带你深入探讨 AVPlayer 的状态监听、缓冲状态监听、播放进度监听等优化技术,并以此为基础,我们将开始构建一个 PHPlayerController 播放控制类,来更好地管理播放过程中的各项操作和优化策略。
PHPlayerController 播放控制类
PHPlayerController 将是一个我们用来封装所有播放器功能的播放器管理类,它的主要功能会包括:
- 创建 AVAsset、AVPlayerItem 以及初始化 AVPlayer。
- 监听播放状态、缓冲状态、播放进度、播放完成等。
- 对外暴露播放、暂停、快进、倍速等方法。
它的基本代码如下:
import UIKit
import AVFoundation
class PHPlayerController:NSObject {
/// 媒体资源
private var asset: AVAsset?
/// 视频资源AVPlayerItem
private var playerItem: AVPlayerItem!
/// 播放器
private var player: AVPlayer!
/// 播放视频
/// - Parameter url: 视频地址
func play(url: URL) {
...
let asset = AVURLAsset(url: url)
playerItem = AVPlayerItem(asset: asset)
if player == nil {
player = AVPlayer(playerItem: playerItem)
} else {
player.replaceCurrentItem(with: playerItem)
}
player.play()
self.asset = asset
....
}
PHPlayerController基本代码中包含了播放器的几个主要组成部分,在后面我们会陆陆续续的往里添加新的功能和方法。
五大监听解析
一个播放器,并不是说只有 “播放” 功能就算完成了。在实际的项目开发中,我们需要对播放器进行更加精细的控制,比如:
- 监听加载状态,确保视频能够正常播放。
- 监听缓冲进度,反馈给用户视频的加载进度。
- 追踪播放进度,实现进度条更新,提供良好的用户体验。
- 监听播放完成,以便执行自动重播或播放下一个视频资源。
- 监听播放状态,比如暂停、播放中、缓冲中,以优化UI展示反馈给用户。
通过这些监听,我们可以更好地掌控播放器的行为、让视频播放更加流程,提升用户体验。
这些监听可以分为两部分,前半部分是对 AVPlayerItem 播放资源的监听,而后半部分是对AVPlayer播放器的监听。
监听加载状态(AVPlayerItem.status)
AVPlayerItem 的 status 属性用于检查视频是否已经加载完成,以及视频是否可以播放。
这是我们在进行视频播放时,最先进行的一步监听,当 AVPlayerItem 绑定到 AVPlayer 后就会开始准备资源,当我们执行play() 方法时,一旦资源准备完毕,就会直接开始自动播放。
监听加载状态:
/// 监听 AVPlayerItem状态的keypath
private let kStatusKeyPath = "status"
/// 监听 AVPlayerItem 的标识
private var kPlayerItemStatusContext = 0
/// 监听AVPlayerItem的加载状态
private func _addObserverAVPlayerItemStatus() {
print("PHPlayerController: addObserverAVPlayerItemStatus")
playerItem.addObserver(self, forKeyPath: kStatusKeyPath, options: .new, context: &kPlayerItemStatusContext)
}
/// 移除监听AVPlayerItem的加载状态
private func _removeObserverAVPlayerItemStatus() {
print("PHPlayerController: removeObserverAVPlayerItemStatus")
playerItem.removeObserver(self, forKeyPath: kStatusKeyPath, context: &kPlayerItemStatusContext)
}
重写observeValue(forKeyPath:of:change:context)方法,实现监听。
//MARK: - KVO
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &kPlayerItemStatusContext {
if keyPath == kStatusKeyPath {
// 处理AVPlayerItem状态监听
_observerAVPlayerItemStatus(change)
}
} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
/// 处理 AVPlayerItem监听
/// - Parameter change: change
private func _observerAVPlayerItemStatus(_ change: [NSKeyValueChangeKey : Any]?) {
let status = change?[.newKey] as? AVPlayerItem.Status
switch status {
case .failed:
print("PHPlayerController: AVPlayerItem failed")
case .readyToPlay:
print("PHPlayerController: AVPlayerItem readyToPlay")
case .unknown:
print("PHPlayerController: AVPlayerItem unknown")
default:
break
}
}
监听缓冲进度(AVPlayerItem.loadedTimeRanges)
loadedTimeRanges 属性可以用于获取缓冲数据的时间范围,可以根据数据给用户视觉上缓冲进度反馈,当然如果没有过分追求细节,这个监听其实可以省略,依赖播放状态来显示缓冲样式。
监听缓冲进度:
/// 监听 AVPlayerItem 的缓冲进度的keypath
private let kLoadedTimeRangesKeyPath = "loadedTimeRanges"
/// 监听 AVPlayerItem 的缓冲进度的标识
private var kPlayerItemLoadedTimeRangesContext = 1
/// 监听 AVPlayerItem 的缓冲进度
private func _addObserverAVPlayerItemLoadedTimeRanges() {
print("PHPlayerController: addObserverAVPlayerItemLoadedTime")
playerItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: .new, context: &kPlayerItemLoadedTimeRangesContext)
}
/// 移除监听 AVPlayerItem 的缓冲进度
private func _removeObserverAVPlayerItemLoadedTimeRanges() {
print("PHPlayerController: removeObserverAVPlayerItemLoadedTime")
playerItem.removeObserver(self, forKeyPath: "loadedTimeRanges", context: &kPlayerItemLoadedTimeRangesContext)
}
在observeValue(forKeyPath:of:change:context)方法内实现监听的处理。
//MARK: - KVO
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &kPlayerItemStatusContext {
...
} else if context == &kPlayerItemLoadedTimeRangesContext {
if keyPath == kLoadedTimeRangesKeyPath {
// 处理AVPlayerItem缓冲进度监听
_observerAVPlayerItemLoadedTimeRanges(change)
}
} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
监听播放完成(AVPlayerItemDidPlayToEndTime)
当视频资源播放完成时,我们可以通过监听 AVPlayerItem 的 AVPlayerItemDidPlayToEndTime,然后执行相关的操作,比如自动重播。
/// 监听播放完成
private func _addObserverAVPlayerItemDidPlayToEndTime() {
NotificationCenter.default.addObserver(self, selector: #selector(_observerAVPlayerItemDidPlayToEndTime), name: .AVPlayerItemDidPlayToEndTime, object: nil)
}
/// 处理播放完成
/// - Parameter notification: notification
@objc private func _observerAVPlayerItemDidPlayToEndTime(_ notification: Notification) {
print("PHPlayerController: AVPlayerItemDidPlayToEndTime")
// 播放完成后,重新播放
player.seek(to: .zero)
player.play()
}
监听(读取)播放进度(addPeriodicTimeObserver)
对于播放进度,其实并没有所谓的监听,也没有办法使用KVO的形式来监听播放进度。AVPlayer 为我们提供了一个 addPeriodicTimeObserver(forInterval:queue:using:) 方法来定期读取播放进度。
该方法需要我们传入一个读取的时间间隔、回调的队列、和回调的block。
/// 监听播放进度的观察者
private var timeObserver: Any?
/// 监听播放进度
private func _addPeriodicTimeObserver() {
player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1), queue: .main) { [weak self] (time) in
let currentTime = CMTimeGetSeconds(time)
print("PHPlayerController: currentTime: \(currentTime)")
}
}
/// 移除播放进度监听
private func _removePeriodicTimeObserver() {
if let observer = timeObserver {
player.removeTimeObserver(observer)
timeObserver = nil
}
}
监听播放状态(AVPlayer.timeControlStatus)
timeControlStatus 属性可以用于判断当前播放器是在播放、暂停还是等待缓冲的状态。
监听播放状态:
/// 监听 AVPlayer 的播放状态的keypath
private let kPlayerTimeControlStatus = "timeControlStatus"
/// 监听 AVPlayer 的播放状态的标识
private var kPlayerTimeControlStatusContext = 2
/// 监听播放状态
private func _addObserverAVPlayerTimeControlStatus() {
print("PHPlayerController: addObserverAVPlayerTimeControlStatus")
player.addObserver(self, forKeyPath: kPlayerTimeControlStatus, options: .new, context: &kPlayerTimeControlStatusContext)
}
/// 移除监听播放状态
private func _removeObserverAVPlayerTimeControlStatus() {
print("PHPlayerController: removeObserverAVPlayerTimeControlStatus")
player.removeObserver(self, forKeyPath: kPlayerTimeControlStatus, context: &kPlayerTimeControlStatusContext)
}
处理播放状态的监听:
//MARK: - KVO
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &kPlayerItemStatusContext {
...
} else if context == &kPlayerItemLoadedTimeRangesContext {
...
} else if context == &kPlayerTimeControlStatusContext {
if keyPath == kPlayerTimeControlStatus {
// 处理AVPlayer播放状态
_observerAVPlayerTimeControlStatus(change)
}
} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
/// 处理播放状态
/// - Parameter change: change
private func _observerAVPlayerTimeControlStatus(_ change: [NSKeyValueChangeKey : Any]?) {
let status = change?[.newKey] as? AVPlayer.TimeControlStatus
switch status {
case .paused:
print("PHPlayerController: AVPlayer TimeControlStatus paused")
case .playing:
print("PHPlayerController: AVPlayer TimeControlStatus playing")
case .waitingToPlayAtSpecifiedRate:
print("PHPlayerController: AVPlayer TimeControlStatus waitingToPlayAtSpecifiedRate")
default:
break
}
}
结语
通过对 AVPlayerItem 和 AVPlayer 的5个关键点监听,我们可以全面掌控视频的播放状态,根据这些状态和数据,我们可以进行UI的视觉反馈,反馈给用户,从而提升播放体验。本篇博客还是实现了 PHPlayerController 播放管理器,为后序的播放器博客内容打下基础。
希望通过本篇博客的分享,能够帮助大家进步了解 AVFoundation 中的视频播放细节,在本系列博客的下一篇博客当中,将会分享,使用通过自定义UI控件 结合 PHPlayerController 实现自定义播放控制的UI,包括播放、暂停、进度条以及拖拽快进功能。