swiftiOS音频、视频、直播相关iOS开发

ZYPlayer 基于 AVFoundation AVPlaye

2016-11-23  本文已影响816人  无与伦比Q

ZYPlayer 是一款基于AVFoundation 下AVPlayer 封装的视频播放器
前言:写这篇文章并不是为了记录下AVPlayer的用法,因为AVPlayer制作视频播放器并不存在太大的难点。百度的转帖文章都很多,大体差异也不大。只要细心的控制好每一个细节,相信很多人都能写出漂亮的播放器来。本文后面一方面会附有一份demo,是自己根据项目需求封装的视频播放器,算是对swift3.0的语言交流,另外会着重讲讲关于在视频进行旋转全屏控制的一些思路。因为碰到了各种的坑,一路走来,很多都并未给出完美的解决方案,今天给出一种在转屏的完美解决思路。有需求的可以继续往下看,后面也会稍微带上AVPlayer的用法,新手没有也可以看看
一. 你可能碰到的转屏问题

  1. 在项目部署的地方 设置好你需要支持的屏幕方向
    好处:由于开启了屏幕横屏的支持方向,通过监听通知能够拿到转屏后的正确的frame
    缺点:使用frame来控制播放器的尺寸,缩放旋转 相当的麻烦。一般人估计要不了几下就转晕了。另外如果因为一个视频播放器要支持多方向,那么导致整个项目都要支持多方向,显然很不可取。
    2.在项目部署的地方 设置只支持竖屏 手动控制屏幕的旋转
    好处:比起上面讲的手动控制旋转这种方法思路实现起来相对更加清晰,利用transform做90°的旋转,控制更加方便
    缺点: 项目总不能因为你要手动转屏,把本来支持的多方向修改掉吧?

结论:我们需要做的是无论项目如何部署方向,都不影响对视频播放器的控制!

本文采用监听屏幕旋转通知,手动对屏幕进行旋转。下面只讲如何实现,具体原因就不啰嗦了,比较来翻文章的都是来找解决方案的

A . 你的项目搭建的框架 现在主流多是rootViewController为tabBarController 或者简单点的是导航控制器作为rootViewController,那么请按照下面的代码在根控制器下进行设置

    /********* 指定某些具体的控制器不能自动旋转 **********/
    
    override var shouldAutorotate: Bool {
        guard let nav = self.selectedViewController as? UINavigationController else {
            return true
        }
        // 填写播放器所在的类(注意命名空间) 加载这个控制器的时候,控制器就不会自动进行旋转  无视你项目部署的支持方向
        if (nav.topViewController?.isKind(of: NSClassFromString("ZYPlayerDemo.ViewController")!))! {
            return false
        }
        return true
    }
    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        guard let nav = self.selectedViewController as? UINavigationController else {
            return [.portrait, .landscapeLeft, .landscapeRight]
        }
        // 填写播放器所在的类(注意命名空间) 当加载这个控制器的时候,这个控制器就只支持竖屏 无视你项目部署的支持方向
        if (nav.topViewController?.isKind(of: NSClassFromString("ZYPlayerDemo.ViewController")!))! {
            return UIInterfaceOrientationMask.portrait
        }
        return [.portrait, .landscapeLeft, .landscapeRight]
    }
    /********* 指定某些具体的控制器不能自动旋转 **********/

在rootViewController里面指定了上面的代码,那么就解决了项目部署方向支持的问题,简单来讲,可以无视了,后面你的播放器再也不需要关注项目的支持方向。关于上面两个方法的详细作用,可以自行百度,我就不再啰嗦

B . 在你的播放器中建立一个屏幕旋转的通知监听(如果手机设置了方向锁定,是不会收到通知的)

1. // 监听屏幕旋转的通知
        NotificationCenter.default.addObserver(self, selector: #selector(self.screenDidRotate(note:)), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)

2./** 屏幕旋转通知 */
    @objc fileprivate func screenDidRotate(note : Notification) {
        let orientation = UIDevice.current.orientation
        switch orientation {
        case .portrait:
            rotateToPortrait()
            break
        case .portraitUpsideDown:
            break
        case .landscapeLeft:
            rotateToLandscapeLeft()
        case .landscapeRight:
            rotateToLandscapeRight()
        default:
            break
        }
    }

这样,当你屏幕旋转的时候,就能拿到当前屏幕的放心,然后就需要做的是处理屏幕的旋转了。
注意:手动旋转屏幕中,有一种叫做强制旋转,有争议说该方法算是调用私有API , 也有人觉得应该从KVC来进行理解,个人赞成后者,并且之前也尝试过强制转屏,只是由于我的控制需求,并未才去此方法,下面贴出强制转屏的代码块供参考(强制转到左边横屏,KVC,不认为会被拒)

UIDevice.currentDevice().setValue(UIInterfaceOrientation.LandscapeLeft.rawValue, forKey: "orientation")

下面附上我在屏幕处理中使用的代码,由于我的播放器是UIViewController,为了增减需求方便,布局使用了xib,通过约束来修改尺寸,AVPlayer则是通过代码进行集成,这样做就是为了扩展性考虑

转屏并未太复杂就轻松的控制好了各种选择,没错!就是transform + UIView animate动画。,当横屏的时候是将播放器旋转并且添加到window上,当竖屏的时候又从window上添加到原来的父控件上

// MARK: - 屏幕旋转处理
extension ZYPlayer {
    fileprivate func rotateToLandscapeLeft() {
        keyWindow.addSubview(self.view)
        // UIView动画进行旋转
        UIView.animate(withDuration: 0.4, animations: {
            self.view.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2))
            self.view.frame = self.keyWindow.bounds
            self.playerLayer?.frame = self.view.bounds
        })
        UIApplication.shared.isStatusBarHidden = true
    }
    
    fileprivate func rotateToLandscapeRight() {
        keyWindow.addSubview(self.view)
        UIView.animate(withDuration: 0.4) {
            self.view.transform = CGAffineTransform(rotationAngle: CGFloat(-M_PI_2))
            self.view.frame = self.keyWindow.bounds
            self.playerLayer?.frame = self.view.bounds
        }
        UIApplication.shared.isStatusBarHidden = true
    }
    
    fileprivate func rotateToPortrait() {
        if lastOrientation == .portrait { return }
        orgView?.addSubview(self.view)
        UIView.animate(withDuration: 0.4) {
            self.view.transform = CGAffineTransform(rotationAngle: 0)
            self.view.frame = self.orgFrame!
            self.playerLayer?.frame = self.view.bounds
        }
        UIApplication.shared.isStatusBarHidden = false 
    }
}

你没有看错,旋转就这么搞定了!

下面还是讲讲AVPlayer的使用简单概述下

fileprivate func initPlayer(_ url : String) {
        /** 先进行一次release */
        releasePlayer()
        // 添加通知监听
        addNotificationObserver()
        // 初始化avplayer 本身
        playerItem = AVPlayerItem(url: URL(string: url)!)
        player = AVPlayer(playerItem: playerItem)
        playerLayer = AVPlayerLayer(player: player)
        playerLayer?.frame = playerView.bounds
        playerView.layer.insertSublayer(playerLayer!, at: 0)
        switch fillMode {
        case .resizeAspect:
            playerLayer?.videoGravity = AVLayerVideoGravityResizeAspect
        case .resizeAspectFill:
            playerLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
        case .resize:
            playerLayer?.videoGravity = AVLayerVideoGravityResize
        }
/** KVO */
    fileprivate func addKVOObserver() {
        playerItem?.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)
        playerItem?.addObserver(self, forKeyPath: "loadedTimeRanges", options: NSKeyValueObservingOptions.new, context: nil)
        playerItem?.addObserver(self, forKeyPath: "playbackBufferEmpty", options: NSKeyValueObservingOptions.new, context: nil)
        playerItem?.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: NSKeyValueObservingOptions.new, context: nil)
    }
// MARK: - KVO 监听处理
extension ZYPlayer {
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        let playerItem = object as! AVPlayerItem
        if keyPath == "status" {
            if playerItem.status == AVPlayerItemStatus.readyToPlay {
                monitoringPlayback()    // 准备播放
            } else {                                // 初始化播放器失败了
                state = .stopped
            }
        } else if keyPath == "loadedTimeRanges" {                                           //监听播放器的下载进度
            calculateBufferedProgress(playerItem)
        } else if keyPath == "playbackBufferEmpty" && playerItem.isPlaybackBufferEmpty {    //监听播放器在缓冲数据的状态
            state = .buffering
            indicator.startAnimating()
            indicator.isHidden = false
            pauseToPlay()
        } else if keyPath == "playbackLikelyToKeepUp" {     // 缓存足够了,可以播放
            indicator.stopAnimating()
            indicator.isHidden = true
        }
    }
    
    fileprivate func monitoringPlayback() {
        duration = CGFloat(playerItem!.duration.value) / CGFloat(playerItem!.duration.timescale) // 视频总时间
        totalDuration.text = timeFormate(time: duration)
        startToPlay()
    }
    
    fileprivate func calculateBufferedProgress(_ palyerItem : AVPlayerItem) {
        let bufferedRanges = playerItem?.loadedTimeRanges
        let timeRange = bufferedRanges?.first?.timeRangeValue   // 获取缓冲区域
        let startSeconds = CMTimeGetSeconds(timeRange!.start)
        let durationSeconds = CMTimeGetSeconds(timeRange!.duration)
        let timeInterval = startSeconds + durationSeconds
        let duration = playerItem!.duration
        let totalDuration = CMTimeGetSeconds(duration)
        bufferedProgress = Float(timeInterval)/Float(totalDuration)
        progressView.progress = bufferedProgress
    }
}

这样就能拿到各种时长,是否准备好播放了,以及播放器的缓冲进度等。修改UI就是你该做的事情了!友情提示,如果使用了Timer 这个东西,视频在播放的时候,如果用户退出界面,务必要提供一个手动销毁播放的方法,不然妥妥的内存泄漏。

// 监听app 进入后台 返回前台的通知
        NotificationCenter.default.addObserver(self, selector: #selector(self.appDidEnterBackground), name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.appDidEnterPlayGround), name: NSNotification.Name.UIApplicationDidBecomeActive, object: self)
        // 监听 playerItem 的状态通知
        NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemDidPlayToEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem)
        NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemPlaybackStalled), name: NSNotification.Name.AVPlayerItemPlaybackStalled, object: playerItem)
player?.play()
player?.pause()
// 这个是快进,快退的位置匹配的方法。 第一个参数是匹配到视频的多少秒,这个根据你的slider.value来定的,第二个参数固定写法,直接copy吧!        
player?.seek(to: CMTimeMakeWithSeconds(Float64(second), Int32(NSEC_PER_SEC)) , completionHandler: { [weak self](_) in
            self?.startToPlay()
            if !self!.playerItem!.isPlaybackLikelyToKeepUp {
                self?.state = .buffering
            }
        })

最后

上一篇下一篇

猜你喜欢

热点阅读