文章的目录如下:
- 1.前言
- 2.AVPlayer基本介绍
- 3.功能拆解封装
- 3.1 显示视频
- 3.2 播放,暂停
- 3.3 时间显示
- 3.4 时间条拖动
- 3.5 缓冲
- 3.6 全屏功能
前言
最近业余用Swift3.0练手写项目用到了网络播放功能。本文按照功能分解,一步一步封装视频播放器。自己在梳理知识的同时,也希望能给大家带来一些帮助。文章最后有Demo地址
2.AVPlayer基本介绍
AVPlayer已经有很多文章讲的非常棒了,这边就不啰嗦了,一搜一大把。总结下就好了。
__ AVPlayer里面有3样东西很重要: __
1.AVPlayer
- 理解成MVC中的Controller,用来对视频进行一些操作,比如播放,暂停,快进等
2.AVPlayerItem
- 理解成MVC中的Model,用来获取视频的一些信息,比如时长等信息的获取
3.AVPlayerLayer
- 理解成MVC中的View, 用来显示播放的视频
3.功能拆解封装
3.1 显示视频
播放器最重要的就是播放了,首先第一步得到了视频的URL能让视频先展示出来
创建一个自定义的View,我这边取名叫ZHPlayerView,在控制器中使用。
//1.创建播放视图
let playView = ZHPlayerView(frame: CGRect(x: 0, y: 100, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.width*0.6))
//2.添加urlString
playView.urlSrting = "http://mov.bn.netease.com/open-movie/nos/mp4/2015/12/29/SBB3FG5M0_sd.mp4"
//3.添加View
view.addSubview(playView)
ZHPlayerView中:
1.先懒加载3个属性
fileprivate var playerItem: AVPlayerItem?
fileprivate lazy var player: AVPlayer = AVPlayer()
fileprivate lazy var playerLayer: AVPlayerLayer = {
let playerLayer = AVPlayerLayer.init(player: self.player)
//拉伸视频内容达到边框占满
playerLayer.videoGravity = AVLayerVideoGravityResize
return playerLayer
}()
Tip: 关于playerLayer的videoGravity 可以看下这篇博文: http://blog.csdn.net/cool_bear_xx/article/details/52816780
2.添加layer到View的layer上面,并设置frame
self.layer.addSublayer(playerLayer)
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = self.layer.bounds
}
3.在视频的urlString属性的didSet(相当于OC的set方法)中重新给playerItem设置下视频的url
var urlSrting: String?{
didSet{
//swift为了防止urlSrting,url为空,程序崩溃守护下
guard let urlSrting = urlSrting, let url = URL(string: urlSrting) else {
return
}
//设置playerItem,并替代
playerItem = AVPlayerItem(url: url)
player.replaceCurrentItem(with: playerItem)
//监听playItem的status状态属性
playerItem?.addObserver(self, forKeyPath: "status", options: .new, context: nil)
}
}
4.KVO方法处理
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "status" {
let status = change?[NSKeyValueChangeKey.newKey] as? Int
/**
status 3种状态
unknown 0
readyToPlay 1
failed 2
*/
if status == 1 { //准备播放了
//开始播放
player.play()
}else
{
print("视频播放失败")
}
}
}
经过这几步以后,视频就能播放了
3.2 播放,暂停
对视频进行操作,需要创建一个OperationView,在Demo中取名ZHPlayerOperationView。将OperationView的背景颜色设置为clearColor,并且添加在ZHPlayerView上面。大小为ZHPlayerView的大小
对视频进行播放,暂停操作比较简单,只要对ZHPlayerView中的player对象进行操作就行
//1.播放器播放
func Play(){
player.play()
}
//2.播放器暂停
func Pause(){
player.pause()
}
3.3 时间显示
时间分为当前时间和总的视频长度
总的视频时长的获取:
在监听Item的status为readyToPlay的状态时,可以获取视频的总时长
let time = playerItem?.duration
var second: TimeInterval = 0
if let time = time {
//视频的总秒数
second = CMTimeGetSeconds(time)
}
operationview.totalTime.text = String.convertTimeWithSecond(second: second)
当前播放时间的获取:
在获取到urlString,替换原本的Item后,可以对player的播放进度进行监听
//播放观察者
fileprivate var playerTimeObserve: Any?
var urlSrting: String?{
.....
//观察播放进度
playerTimeObserve = observePlayerTimeWithItem(item: playerItem!)
}
}
//观察播放进度
fileprivate func observePlayerTimeWithItem(item: AVPlayerItem) -> Any{
....
//1/30s执行一次 查看播放进度
let observePlayerTime = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 30), queue: DispatchQueue.main, using: {[weak self] (time) in
let currentTimeSecond = TimeInterval(item.currentTime().value)/TimeInterval(item.currentTime().timescale)
//更新operationview的界面
self?.operationview.playProgress.value = Float(currentTimeSecond)
self?.operationview.currentTime.text = String.convertTimeWithSecond(second: currentTimeSecond)
})
return observePlayerTime
}
3.4 时间条拖动
首先在OperationView中创建一个闭包(类似oc的block),在滑动的时候调用
//滑块是否在拖动
var isSlider: Bool = false
//是否在滑动
var sliderChanged: sliderProgressChangedCallBack?
slider控件的监听方法
//1.正在拖动
@IBAction func dragging(_ sender: UISlider) {
isSlider = true
if sliderChanged != nil {
sliderChanged!(TimeInterval(sender.value))
}
}
//2.手指按下
@IBAction func touchDown(_ sender: UISlider) {
//播放器暂停
superView?.Pause()
}
//3.手指抬起
@IBAction func touchUp(_ sender: UISlider) {
//播放器开始播放
superView?.Play()
isSlider = false
}
在ZHPlayerView中
//滑块拖动时
operationview.sliderChanged = {[weak self] (value) in
let time = CMTime(seconds: value, preferredTimescale: CMTimeScale(1*UInt64(NSEC_PER_SEC)))
self?.playerItem?.seek(to: time)
}
3.5 缓冲
var urlSrting: String?{
.....
//监听缓冲进度的属性
playerItem?.addObserver(self, forKeyPath: "loadedTimeRanges", options: .new, context: nil)
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
.....
if keyPath == "loadedTimeRanges" //缓冲属性
{
if operationview.isSlider == false
{
let loadedTime = self.loadedTime //缓冲时间
let totalTime = self.totalTime //总时间
//设置缓冲进度条
operationview.loadedProgress.setProgress(Float(CGFloat(loadedTime/totalTime)), animated: true)
}
}
}
3.6 全屏功能
全屏功能分为2种,一种是在竖屏情况下点击放大按钮,修改transfrom和frame,以达到全屏的效果。还有一种是真正的横屏,通过监听横竖屏变化进行全屏和小屏切换。
第一种:点击了全屏按钮
注: 由于operationView添加在PlayerView上面,所以operationView中通过superview属性获得playerView
//目前屏幕状态 是small 还是fullScreen
var screenType: screenType = .smallScreen
//旋转屏幕
@IBAction func rotateScreen(_ sender: UIButton) {
guard let superView = superView else {
return
}
if screenType == .smallScreen {
let height = UIScreen.main.bounds.width
let width = UIScreen.main.bounds.height
let frame = CGRect(x: (height - width)/2, y: (width - height)/2, width: width, height: height)
//全屏状态
screenType = .fullScreen
UIView.animate(withDuration: 0.3, animations: {
superView.frame = frame
superView.transform = CGAffineTransform(rotationAngle: (CGFloat)(M_PI_2))
})
rotation.isSelected = true
UIApplication.shared.keyWindow?.addSubview(superView)
}else
{
let orientation = UIDevice.current.orientation
if orientation == .portrait //只有当屏幕为竖屏时,缩小时Frame才正常
{
//全屏状态
UIView.animate(withDuration: 0.3, animations: {
superView.transform = CGAffineTransform.identity
superView.frame = superView.originalFrame
})
screenType = .smallScreen
superView.originalSuperView?.addSubview(superView)
rotation.isSelected = false
}
}
}
第二种: 横竖屏切换时的横竖屏
//1.添加通知
NotificationCenter.default.addObserver(self, selector: #selector(orientationChanged(noti:)), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
//2.处理
@objc fileprivate func orientationChanged(noti: NSNotification){
//1.获取屏幕现在的方向
let orientation = UIDevice.current.orientation
switch orientation {
case .portrait:
guard let superView = superView else {
return
}
if screenType == .fullScreen {
screenType = .smallScreen
rotation.isSelected = false
superView.transform = CGAffineTransform.identity
superView.frame = superView.originalFrame
}
case .landscapeLeft:
if screenType == .smallScreen
{
screenType = .fullScreen
rotation.isSelected = true
}
fullScreenWithOrientation(orientation: orientation)
case .landscapeRight:
if screenType == .smallScreen
{
screenType = .fullScreen
rotation.isSelected = true
}
fullScreenWithOrientation(orientation: orientation)
default: break
}
}
private func fullScreenWithOrientation(orientation: UIDeviceOrientation)
{
guard let superView = superView else {
return
}
superView.removeFromSuperview()
superView.transform = CGAffineTransform.identity
let height = UIScreen.main.bounds.height
let width = UIScreen.main.bounds.width
superView.frame = CGRect(x: 0, y: 0, width: width, height: height)
UIApplication.shared.keyWindow?.addSubview(superView)
}
最后附上Demo地址: https://github.com/WzhGoSky/ZHPlayer.git
视频播放方面主要的功能就是这些,其余的功能可以根据自己具体的需求自己进行封装。码字码了这么多,也挺辛苦,请点个❤️ 鼓励下,谢谢