引言
在上一部分中,我们介绍了如何使用 AVPlayer 来实现视频的基础播放功能,并讲解了 AVAsset、AVPlayerItem 和 AVPlayer 的基本配置。通过这些内容,我们能够实现视频的加载与播放。然而,在实际的应用场景中,视频播放的流畅性不仅仅依赖于如何播放视频,还涉及到如何优化播放过程中各种状态和进度的管理。
AVPlayer 提供了多种监听机制,包括播放状态、缓冲状态、播放进度等,通过合理利用这些监听,我们可以实现更精准的播放控制,如优化加载、减少卡顿、提升用户体验等。
本篇博客将带你深入探讨 AVPlayer 的状态监听、缓冲状态监听、播放进度监听等优化技术,并以此为基础,我们将开始构建一个 PHPlayerController 播放控制类,来更好地管理播放过程中的各项操作和优化策略。
PHPlayerController 播放控制类
PHPlayerController 将是一个我们用来封装所有播放器功能的播放器管理类,它的主要功能会包括:
- 创建 AVAsset、AVPlayerItem 以及初始化 AVPlayer。
- 监听播放状态、缓冲状态、播放进度、播放完成等。
- 对外暴露播放、暂停、快进、倍速等方法。
它的基本代码如下:
import UIKit
import AVFoundationclass PHPlayerController:NSObject {/// 媒体资源private var asset: AVAsset?/// 视频资源AVPlayerItemprivate 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: - KVOoverride 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: changeprivate func _observerAVPlayerItemStatus(_ change: [NSKeyValueChangeKey : Any]?) {let status = change?[.newKey] as? AVPlayerItem.Statusswitch 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: - KVOoverride 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) inlet 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: - KVOoverride 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: changeprivate func _observerAVPlayerTimeControlStatus(_ change: [NSKeyValueChangeKey : Any]?) {let status = change?[.newKey] as? AVPlayer.TimeControlStatusswitch 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,包括播放、暂停、进度条以及拖拽快进功能。