基于AVPlayer封装一个自己的视频播放器

文章的目录如下:

  • 1.前言
  • 2.AVPlayer基本介绍
  • 3.功能拆解封装
  • 3.1 显示视频
  • 3.2 播放,暂停
  • 3.3 时间显示
  • 3.4 时间条拖动
  • 3.5 缓冲
  • 3.6 全屏功能

前言

最近业余用Swift3.0练手写项目用到了网络播放功能。本文按照功能分解,一步一步封装视频播放器。自己在梳理知识的同时,也希望能给大家带来一些帮助。文章最后有Demo地址

播放器.gif

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的大小

基于AVPlayer封装一个自己的视频播放器_第1张图片
OperationView.png

对视频进行播放,暂停操作比较简单,只要对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

视频播放方面主要的功能就是这些,其余的功能可以根据自己具体的需求自己进行封装。码字码了这么多,也挺辛苦,请点个❤️ 鼓励下,谢谢

你可能感兴趣的:(基于AVPlayer封装一个自己的视频播放器)