引言
前面的博客我们已经实现了视频的播放功能,但是作为一个完整的视频播放器仅仅有播放功能是不够的,暂停,快进,播放进度条,显示播放时间,显示视频标题和字幕都是必不可少的功能。
本篇博客我们就对视频的播放,暂停,快进等控制功能做一个详细的解读,为原来的播放器添加这些功能来提升用户的播放体验。
处理时间
AVPlayer和AVPlayerItem都是基于时间的对象,当我们想要调节它的播放时间的时候,需要先了解下AV Foundation框架中时间的呈现方式。
在日常开发中我们通常使用Int或者float或者double来标识时间,我们使用NSTimeInterval表示时间的时候其实也是使用的double,只是对它进行了typedef定义。不过在浮点型数据表示时间实际上会存在一些问题,因为浮点型数据的运算会导致不精确的情况,当这种不精确的时间不断地进行累加就会导致情况越发严重,会导致明显的时间偏移。
所以AV Foundation中使用一种可靠性更高的方式来表示时间CMTime。
CMTime属于Core Media框架,它使用分数的形式来表示时间,具体定义如下:
public struct CMTime {
public init()
public init(value: CMTimeValue, timescale: CMTimeScale, flags: CMTimeFlags, epoch: CMTimeEpoch)
/**< The value of the CMTime. value/timescale = seconds */
public var value: CMTimeValue
/**< The timescale of the CMTime. value/timescale = seconds. */
public var timescale: CMTimeScale
/**< The flags, eg. kCMTimeFlags_Valid, kCMTimeFlags_PositiveInfinity, etc. */
public var flags: CMTimeFlags
/**< Differentiates between equal timestamps that are actually different because
of looping, multi-item sequencing, etc.
Will be used during comparison: greater epochs happen after lesser ones.
Additions/subtraction is only possible within a single epoch,
however, since epoch length may be unknown/variable */
public var epoch: CMTimeEpoch
}
这个结构最关键的两个值是value和timescale,value作为分子,timescale作为分母以分数形式来处理时间。
功能定义
了解了AV Foundation中的时间处理方式之后,接下来我们就开始为播放器定义一些播放,暂停,快进等基本功能。
首先创建一个名为PHPlayerDelegate协议,将播放器需要实现的功能定义到协议中。
protocol PHPlayerDelegate:NSObjectProtocol {
/// 播放
func play()
///暂停
func pause()
/// 停止
func stop()
/// 开始拖拽
func scrubbingDidStart()
/// 拖拽过程
func scrubbedToTime(time:TimeInterval)
/// 停止拖拽
func scrubbedDidEnd(time:TimeInterval)
}
使我们的播放控制器PHPlayerController遵循协议并实现协议方法。
1.播放:直接调用AVPlayer的同名方法。
/// 播放
func play() {
guard let player = player else { return }
player.play()
}
2.暂停:直接调用AVPlayer的同名方法。
/// 暂停
func pause() {
guard let player = player else { return }
player.pause()
}
3.停止:和pause相同我们使用设置rate为0的方式实现。
/// 停止
func stop() {
guard let player = player else { return }
player.rate = 0.0
}
4.开始拖拽:拖拽进度条时将播放器暂停播放。
/// 开始拖拽
func scrubbingDidStart() {
pause()
}
5.拖拽过程:这个方法我们先空实现。
/// 指定播放时间
///
/// - Parameters:
/// - time: 指定播放时间
func scrubbedToTime(time: TimeInterval) {
}
6.停止拖拽:拖拽进度条完成后,首先调用cancelPendingSeeks方法清空上一个快进搜索,避免造成堆积,然后将播放器快进到指定播放位置。
/// 结束拖拽
///
/// - Parameters:
/// - time: 指定播放时间
func scrubbedDidEnd(time: TimeInterval) {
guard let playerItem = playerItem else { return }
guard let player = player else { return }
playerItem.cancelPendingSeeks()
player.seek(to: CMTimeMakeWithSeconds(time, preferredTimescale: Int32(NSEC_PER_SEC)))
}
控制UI组件
播放器的所有控制功能就都已经实现完成了,但是现在还没有对应的UI组件来调用这些功能,接下来我们就来实现一个比较常见的播放器控制UI,包括进度条,播放,暂停按钮等等,整体页面如下图所示,接下来我们就来实现一下吧。
PHControlView页面实现
import UIKit
let offset_x = 30.0
let play_width = 40.0
class PHControlView: UIView,PHControlDelegate {
weak var delegate: PHPlayerDelegate?
/// 返回按钮
let backButton = PHBackButton()
///标题
let titleLabel = UILabel()
/// 当前时间
let currentTimeLabel = UILabel()
/// 总时间
let totalTimeLabel = UILabel()
/// 进度条
let sliderView = UISlider()
/// 播放暂停按钮
let playButton = PHPlayerButton()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
setEvents()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupView() {
self.addSubview(backButton)
self.addSubview(titleLabel)
titleLabel.font = UIFont.systemFont(ofSize: 14.0)
titleLabel.textColor = .white
self.addSubview(currentTimeLabel)
currentTimeLabel.textAlignment = .left
currentTimeLabel.textColor = .white
currentTimeLabel.font = UIFont.systemFont(ofSize: 14.0)
self.addSubview(totalTimeLabel)
totalTimeLabel.textAlignment = .right
totalTimeLabel.textColor = .white
totalTimeLabel.font = UIFont.systemFont(ofSize: 14.0)
self.addSubview(sliderView)
self.addSubview(playButton)
currentTimeLabel.text = "00:00:00"
totalTimeLabel.text = "00:00:00"
titleLabel.text = "视频1"
}
override func layoutSubviews() {
super.layoutSubviews()
backButton.frame = CGRect(x: offset_x, y: 20.0, width: 30.0, height: 30.0)
titleLabel.frame = CGRect(x: CGRectGetMaxX(backButton.frame) + 25.0, y: 20.0, width: 100.0, height: 30.0)
currentTimeLabel.frame = CGRect(x: offset_x, y: self.bounds.size.height - 50.0 - 30.0, width: 120.0, height: 15.0)
totalTimeLabel.frame = CGRect(x: self.bounds.size.width - 120.0 - offset_x, y: CGRectGetMinY(currentTimeLabel.frame), width: 120.0, height: 15.0)
sliderView.frame = CGRect(x: offset_x, y: CGRectGetMaxY(currentTimeLabel.frame) + 12.0, width: self.bounds.size.width - offset_x*2, height: 4.0)
playButton.frame = CGRect(x: offset_x, y: CGRectGetMaxY(sliderView.frame), width: play_width, height: play_width)
}
func timeString(from timeInterval: TimeInterval) -> String {
let hours = Int(timeInterval) / 3600
let minutes = Int(timeInterval) / 60 % 60
let seconds = Int(timeInterval) % 60
return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
}
}
代码的篇幅比较长,但都是一些UI相关的内容,不需要过多的解释。我们把重点放到播放按钮playButton和进度条sliderView上面。
playButton:播放按钮添加点击事件和实现,让delegate去调用对应的播放和暂停方法。
func setEvents() {
playButton.addTarget(self, action: #selector(playeOnclick), for: .touchUpInside)
}
// 播放按钮点击
@objc func playeOnclick(button:UIButton) {
button.isSelected = !button.isSelected
guard let delegate = delegate else { return }
if button.isSelected {
delegate.pause()
} else {
delegate.play()
}
}
sliderView:为进度条添加拖拽事件和实现,让delegate同步播放器的状态
func setEvents() {
playButton.addTarget(self, action: #selector(playeOnclick), for: .touchUpInside)
sliderView.addTarget(self, action: #selector(startSlider), for: .touchDown)
sliderView.addTarget(self, action: #selector(moveSlider), for: .valueChanged)
sliderView.addTarget(self, action: #selector(endSlider), for: .touchUpInside)
}
// 进度条开始拖拽
@objc func startSlider() {
guard let delegate = delegate else { return }
playButton.isSelected = true
delegate.scrubbingDidStart()
}
// 进度条拖拽
@objc func moveSlider() {
}
// 进度条拖拽完成
@objc func endSlider() {
guard let delegate = delegate else { return }
playButton.isSelected = false
delegate.scrubbedDidEnd(time: TimeInterval(sliderView.value))
}
使用
将PHControlView添加到PHPlayerView之上,用来控制播放器的播放,暂停等操作。
import UIKit
import AVFoundation
class PHPlayerView: UIView {
/// 控制图层
let controlView = PHControlView()
/// 重写layerClass方法,
override class var layerClass: AnyClass{
get {
return AVPlayerLayer.self
}
}
/// 重写init方法
///
/// - Parameters:
/// - player: 播放器
init(player:AVPlayer) {
super.init(frame: CGRectZero)
guard let playerLayer = self.layer as? AVPlayerLayer else { return }
playerLayer.player = player
self.addSubview(controlView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
controlView.frame = self.bounds
}
}
在ViewController中使用播放器
class ViewController: UIViewController {
/// 播放控制器
var playerController:PHPlayerController?
override func viewDidLoad() {
super.viewDidLoad()
guard let url = Bundle.main.url(forResource: "hubblecast", withExtension: "m4v") else { return }
playerController = PHPlayerController(url: url)
guard let playerView = playerController?.view else { return }
playerView.backgroundColor = .black
playerView.frame = view.bounds
view.addSubview(playerView)
}
}
结语
一个带有基础功能功能的播放器就已经完成了,目前播放器就拥有了自动播放,暂停,播放,拖拽进度的功能。但是进度显示,播放时间的显示,视频标题等等其它元数据信息显然还没有完成同步。在代码中我们也可以注意到PHControlView遵循了一个PHControlDelegate协议,它就是播放控制器用来同步controlView视图元数据信息的代理,下一篇博客我们将详细的介绍关于播放进度,播放状态,以及其它元数据信息同步的问题。
项目地址:PHPlayer: 视频播放器