今天我们来聊聊AVPlayer,这是一个AVFoundation库里面的类,可以用来播放网络视频使用。
开始正题之前,我们首先来了解几个我们将要使用的对象:
AVPlayerItem :媒体资源管理对象,管理视频的一些基本信息和状态,如 播放进度、缓存进度等 。 一个AVPlayerItem对应着一个视频资源。
AVPlayer :视频操作对象,自己本身无法显示视频,需要把自己添加到一个AVPlayerLayer 上来操作。
AVPlayerLayer: 用来显示视频。
了解之后,我们进入正题。
首先我们创建一个CAplayerView继承于UIView,在这个自定义view上完成一些操作。分析一下,需要包含哪些东西,比如说UI界面,AVPlayer配置,通知,KVO,各种手势等。我们一个一个来实现。
首先我们为CAplayerView写一个便利构造器:
convenience init(frame: CGRect,theUrl:URL) {
self.init(frame: frame)
url = theUrl //视频url
setupUI() //UI界面
setupTap() //手势
setupPlayer() //avplayer
link = CADisplayLink(target: self, selector: #selector(update))
link.add(to: RunLoop.main, forMode: .defaultRunLoopMode) //定时器
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
UI界面
定义我们需要的控件
var timeLabel:UILabel! //视频时间
var slider:UISlider! //视频进度条
var sliding = false
var progressView:UIProgressView! //缓冲条
var playBtn:UIButton! //播放暂停按钮
var playing = true
var backBtn:UIButton! //返回按钮
var fullScreenBtn:UIButton! //全屏按钮
var titleLabel:UILabel! //标题
func setupUI () {
timeLabel = UILabel()
timeLabel.textColor = UIColor.white
timeLabel.font = UIFont.systemFont(ofSize: 12)
self.addSubview(timeLabel)
timeLabel.snp.makeConstraints { (make) in
make.right.equalTo(self).inset(25)
make.bottom.equalTo(self).inset(5)
}
fullScreenBtn = UIButton()
self.addSubview(fullScreenBtn)
fullScreenBtn.snp.makeConstraints { (make) in
make.right.equalTo(self).inset(5)
make.bottom.equalTo(self).inset(5)
make.width.height.equalTo(15)
}
// 设置按钮图片
fullScreenBtn.setImage(UIImage(named: "full_screen"), for: .normal)
// 点击事件
fullScreenBtn.addTarget(self, action: #selector(tapChangeScreen), for: .touchUpInside)
slider = UISlider()
self.addSubview(slider)
slider.snp.makeConstraints { (make) in
make.bottom.equalTo(self).inset(5)
make.left.equalTo(self).offset(50)
make.right.equalTo(self).inset(100)
make.height.equalTo(15)
}
slider.minimumValue = 0
slider.maximumValue = 1
slider.value = 0
// 从最大值滑向最小值时杆的颜色
slider.maximumTrackTintColor = UIColor.clear
// 从最小值滑向最大值时杆的颜色
slider.minimumTrackTintColor = UIColor.white
// 在滑块圆按钮添加图片
slider.setThumbImage(UIImage(named: "knob"), for: .normal)
// 按下的时候
slider.addTarget(self, action: #selector(sliderTouchDown(slider:)), for: .touchDown)
// 弹起的时候
slider.addTarget(self, action: #selector(sliderTouchUpOut(slider:)), for: .touchUpOutside)
slider.addTarget(self, action: #selector(sliderTouchUpOut(slider:)), for: .touchUpInside)
slider.addTarget(self, action: #selector(sliderTouchUpOut(slider:)), for: .touchCancel)
progressView = UIProgressView()
progressView.backgroundColor = UIColor.lightGray
self.insertSubview(progressView, belowSubview: slider)
progressView.snp.makeConstraints { (make) in
make.left.right.equalTo(slider)
make.centerY.equalTo(slider)
make.height.equalTo(2)
}
progressView.tintColor = UIColor.red
progressView.progress = 0
playBtn = UIButton()
self.addSubview(playBtn)
playBtn.snp.makeConstraints { (make) in
make.centerY.equalTo(slider)
make.left.equalTo(self).offset(10)
make.width.height.equalTo(30)
}
// 设置按钮图片
playBtn.setImage(UIImage(named: "pause"), for: .normal)
// 点击事件
playBtn.addTarget(self, action: #selector(playAndPause(btn:)), for: .touchUpInside)
NotificationCenter.default.addObserver(self, selector: #selector(deviceOrientationDidChange), name:NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
backBtn = UIButton()
self.addSubview(backBtn)
backBtn.snp.makeConstraints { (make) in
make.top.equalTo(self).offset(10)
make.left.equalTo(self).offset(10)
make.width.height.equalTo(30)
}
// 设置按钮图片
backBtn.setImage(UIImage(named: "Back-white"), for: .normal)
// 点击事件
backBtn.addTarget(self, action: #selector(onClickBackBtnAction), for: .touchUpInside)
backBtn.isHidden = true
titleLabel = UILabel()
titleLabel.text = "这里显示视频的标题"
titleLabel.font = UIFont.systemFont(ofSize: 14)
titleLabel.textColor = UIColor.white
self.addSubview(titleLabel)
titleLabel.snp.makeConstraints { (make) in
make.top.equalTo(self).offset(10)
make.height.equalTo(30)
make.centerX.equalTo(self)
}
}
AVPlayer配置
var playerLayer:AVPlayerLayer?
var playerItem:AVPlayerItem!
var player:AVPlayer!
var url:URL?
func setupPlayer () {
guard (url != nil) else {
fatalError("连接错误")
}
playerItem = AVPlayerItem(url: url!)
//监听缓冲进度改变
playerItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: .new, context: nil)
// 监听状态改变
playerItem.addObserver(self, forKeyPath: "status", options: .new, context: nil)
player = AVPlayer(playerItem: playerItem)
player.volume = 0.5
playerLayer = AVPlayerLayer(player: player)
playerLayer?.videoGravity = .resizeAspectFill
playerLayer?.contentsScale = UIScreen.main.scale
self.layer.insertSublayer(playerLayer!, at: 0)
}
deinit {
playerItem.removeObserver(self, forKeyPath: "loadedTimeRanges")
playerItem.removeObserver(self, forKeyPath: "status")
}
添加手势
因为Pan手势功能不一样,我们可以写个枚举来定义Pan手势的类型
enum Direction {
case leftOrRight,upOrDown,none
}
var direction:Direction! //pan手势类型
var isVolume = false //是否为改变声音手势
var oldConstriants:Array! //旧的布局
var isFullScreen:Bool! //是否全屏
func setupTap () {
let fullOrNotFullScreenTap = UITapGestureRecognizer(target: self, action: #selector(tapChangeScreen))
fullOrNotFullScreenTap.numberOfTapsRequired = 2
self.addGestureRecognizer(fullOrNotFullScreenTap)
let disOrNotdisAppearTap = UITapGestureRecognizer(target: self, action: #selector(disOrNotDisAppear))
disOrNotdisAppearTap.numberOfTapsRequired = 1
self.addGestureRecognizer(disOrNotdisAppearTap)
//这行很关键,意思是只有当没有检测到双击手势 或者 检测双击手势失败,s单击手势才有效
disOrNotdisAppearTap.require(toFail: fullOrNotFullScreenTap)
let pan = UIPanGestureRecognizer(target: self, action: #selector(changeVoiceOrLightOrProgress(pan:)))
self.addGestureRecognizer(pan)
pan.delegate = self as? UIGestureRecognizerDelegate
}
@objc func changeVoiceOrLightOrProgress (pan:UIPanGestureRecognizer) {
let offsetPoint = pan.translation(in: self)
let locationPoint = pan.location(in: self)
let veloctyPoint = pan.velocity(in: self)
switch pan.state {
case .began:
let x = fabs(veloctyPoint.x)
let y = fabs(veloctyPoint.y)
if x > y {
direction = .leftOrRight
}
else if x < y {
direction = .upOrDown
if locationPoint.x <= self.frame.size.width/2 {
isVolume = false
} else {
isVolume = true
}
}
break
case .changed:
if direction == .upOrDown {
if isVolume == false && offsetPoint.y > 0 {
var newBrightness = UIScreen.main.brightness - 0.01
if newBrightness < 0 {
newBrightness = 0
}
UIScreen.main.brightness = newBrightness
}
else if isVolume == false && offsetPoint.y < 0 {
var newBrightness = UIScreen.main.brightness + 0.01
if newBrightness > 1 {
newBrightness = 1
}
UIScreen.main.brightness = newBrightness
}
else if isVolume == true && offsetPoint.y > 0 {
var newVolume = player.volume - 0.01
if newVolume < 0 {
newVolume = 0
}
player.volume = Float(newVolume)
}
else if isVolume == true && offsetPoint.y < 0 {
var newVolume = player.volume + 0.01
if newVolume > 1 {
newVolume = 1
}
player.volume = Float(newVolume)
}
}
else if direction == .leftOrRight {
//可在这里添加左右滑动改变视频进度的代码
}
break
case .ended:
if direction == .upOrDown {
isVolume = false
}
else if direction == .leftOrRight {
}
break
default:
break
}
pan.setTranslation(CGPoint.zero, in: self)
}
@objc func tapChangeScreen () {
if isFullScreen == false {
let rotation : UIInterfaceOrientationMask = [.landscapeLeft, .landscapeRight]
kAppdelegate?.blockRotation = rotation
} else {
kAppdelegate?.blockRotation = .portrait
}
}
@objc func disOrNotDisAppear () {
if timeLabel.isHidden == false {
timeLabel.isHidden = true
slider.isHidden = true
progressView.isHidden = true
playBtn.isHidden = true
backBtn.isHidden = true
fullScreenBtn.isHidden = true
titleLabel.isHidden = true
}
else if timeLabel.isHidden == true && isFullScreen == true {
timeLabel.isHidden = false
slider.isHidden = false
progressView.isHidden = false
playBtn.isHidden = false
backBtn.isHidden = false
fullScreenBtn.isHidden = true
titleLabel.isHidden = false
}
else if timeLabel.isHidden == true && isFullScreen == false {
timeLabel.isHidden = false
slider.isHidden = false
progressView.isHidden = false
playBtn.isHidden = false
backBtn.isHidden = true
fullScreenBtn.isHidden = false
titleLabel.isHidden = false
}
}
KVO 通知 点击事件以及定时器方法
//MARK:----------KVO方法
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "loadedTimeRanges" {
// 通过监听AVPlayerItem的"loadedTimeRanges",可以实时知道当前视频的进度缓冲
let loadedTime = avalableDurationWithplayerItem()
let totalTime = CMTimeGetSeconds(playerItem.duration)
let percent = loadedTime/totalTime // 计算出比例
// 改变进度条
progressView.progress = Float(percent)
}
else if keyPath == "status" {
if playerItem.status == .readyToPlay {
player.play()
} else {
print("加载异常")
}
}
}
func avalableDurationWithplayerItem()->TimeInterval{
guard let loadedTimeRanges = player?.currentItem?.loadedTimeRanges,let first = loadedTimeRanges.first else {fatalError()}
let timeRange = first.timeRangeValue
let startSeconds = CMTimeGetSeconds(timeRange.start)
let durationSecound = CMTimeGetSeconds(timeRange.duration)
let result = startSeconds + durationSecound
return result
}
//MARK:----------通知方法
@objc func deviceOrientationDidChange() {
let interfaceOrientation = UIApplication.shared.statusBarOrientation
switch interfaceOrientation {
case .landscapeLeft,.landscapeRight:
timeLabel.isHidden = true
slider.isHidden = true
progressView.isHidden = true
playBtn.isHidden = true
backBtn.isHidden = true
fullScreenBtn.isHidden = true
titleLabel.isHidden = true
isFullScreen = true
oldConstriants = getCurrentVC().view.constraints
self.updateConstraintsIfNeeded()
//删除UIView animate可以去除横竖屏切换过渡动画
UIView.animate(withDuration: kTransitionTime, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: .transitionCurlUp, animations: {
UIApplication.shared.keyWindow?.addSubview(self)
self.snp.makeConstraints { (make) in
make.edges.equalTo(UIApplication.shared.keyWindow!)
}
self.layoutIfNeeded()
}) { (bool) in
}
break
case .portrait,.portraitUpsideDown:
timeLabel.isHidden = false
slider.isHidden = false
progressView.isHidden = false
playBtn.isHidden = false
titleLabel.isHidden = false
backBtn.isHidden = true
fullScreenBtn.isHidden = false
isFullScreen = false
getCurrentVC().view.addSubview(self)
UIView.animateKeyframes(withDuration: kTransitionTime, delay: 0, options: .calculationModeLinear, animations: {
if (self.oldConstriants != nil) {
self.getCurrentVC().view.addConstraints(self.oldConstriants)
}
}, completion: nil)
break
case .unknown:
print("UIInterfaceOrientationUnknown")
break
default:
break
}
getCurrentVC().view.layoutIfNeeded()
}
func getCurrentVC()->UIViewController {
var result:UIViewController!
var window = UIApplication.shared.keyWindow
if window?.windowLevel != UIWindowLevelNormal {
let windows:Array = UIApplication.shared.windows
for tmpWin:UIWindow in windows {
if tmpWin.windowLevel == UIWindowLevelNormal {
window = tmpWin
break
}
}
}
let frontView = window?.subviews[0]
let nextResponder = frontView?.next
if (nextResponder?.isKind(of: UIViewController.self))! {
result = nextResponder as? UIViewController
} else {
result = window?.rootViewController
}
return result
}
//MARK:----------全屏按钮点击事件
@objc func onClickBackBtnAction(){
//设置竖屏
kAppdelegate?.blockRotation = .portrait
}
//MARK:----------暂停播放按钮点击方法
@objc func playAndPause(btn:UIButton){
let tmp = !playing
playing = tmp // 改变状态
// 根据状态设定图片
if playing {
playBtn.setImage(UIImage(named: "pause"), for: .normal)
player.play()
}else{
playBtn.setImage(UIImage(named: "play"), for: .normal)
player.pause()
}
}
//MARK:----------slider滑动方法
@objc func sliderTouchDown(slider:UISlider){
self.sliding = true
}
@objc func sliderTouchUpOut(slider:UISlider){
if player.status == .readyToPlay {
let duration = slider.value * Float(CMTimeGetSeconds(player.currentItem!.duration))
let seekTime = CMTimeMake(Int64(duration), 1)
player.seek(to: seekTime) { (bool) in
self.sliding = false
}
}
}
//MARK:----------定时器方法
@objc func update () {
if playing == false {
return
}
// 当前播放到的时间
let currentTime = CMTimeGetSeconds(player.currentTime())
// 总时间
let totalTime = TimeInterval(playerItem.duration.value)/TimeInterval(playerItem.duration.timescale)
let timeStr = "\(formatPlayTime(seconds: currentTime))/\(formatPlayTime(seconds: totalTime))"
timeLabel.text = timeStr
if sliding == false {
slider.value = Float(currentTime/totalTime)
}
}
func formatPlayTime(seconds:TimeInterval)->String{
if seconds.isNaN{
return "00:00"
}
let Min:Int = Int(seconds / 60)
let Sec:Int = Int(seconds) % 60
return String(format: "%02d:%02d", Min, Sec)
}
}
上面我们用到了横竖屏切换的kAppdelegate?.blockRotation为在Appdelegate写的一个extension
let kAppdelegate: AppDelegate? = UIApplication.shared.delegate as? AppDelegate
extension AppDelegate{
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return blockRotation
}
}
var blockRotation: UIInterfaceOrientationMask = .portrait{
didSet{
if blockRotation.contains(.portrait){
//强制设置成竖屏
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
}else{
//强制设置成横屏
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
}
}
}
至此一个简单的播放器就完成了,包含播放暂停、快进快退、进度条、缓冲条、视频时间、横竖屏切换、左滑改变亮度、右滑改变声音等等功能。(改变亮度和声音的功能只能在真机测试有效)
调用的时候,直接在viewDidLoad中:
let playerView = CAplayerView(frame: CGRect(x: 0, y: 0, width: kScreenWidth, height: 250), theUrl: URL(string: "https://www.apple.com/105/media/us/iphone-x/2017/01df5b43-28e4-4848-bf20-490c34a926a7/films/feature/iphone-x-feature-tpl-cc-us-20170912_1280x720h.mp4")!)
playerView.backgroundColor = UIColor.black
self.view.addSubview(playerView)
这样即可。
当然还有倍速播放、清晰度切换、缓存下载等等功能需要完善。实际应用的时候还有对象释放销毁等问题。
因此此代码仅供参考,如有问题可以回复跟我交流~
github地址:
https://github.com/WisdomWang/CAPlayer