iOS swfit 利用AVPlayer自定义播放器播放网络视频

今天我们来聊聊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

你可能感兴趣的:(iOS swfit 利用AVPlayer自定义播放器播放网络视频)