ZYPlayer 基于 AVFoundation AVPlayer 的视频播放器 swift 3.0

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对播放器进行监听,这些都是必须的,要不你怎么知道啥时候卡了,啥时候播放器准备好了呢。四个key 各自有何总用可以看后面代码即可知道各自的作用。
/** 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)
    }
  • KVO处理 注意后面这个方法需要处理,本人之前就是写了监听,做下面的处理,结果程序无限挂,懵逼了好久
// 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 这个东西,视频在播放的时候,如果用户退出界面,务必要提供一个手动销毁播放的方法,不然妥妥的内存泄漏。

  • 这里插句嘴,中间注明下,本文写于16年11月底,原创 TRS 的ronaldozhang发布于简书。

  • 另外做为一个视频播放器,还有些细节要做哦,监听下面4个通知必不可少的。应用进入后台,你的视频虽然看不见了,声音一直放也不行吧? 另外视频都播放完了,播放器要么直接销毁,要么提供一个重播功能等等,这些就看你的需求了,但是也是需要处理的吧

// 监听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
            }
        })

最后

  • 附上demo 地址吧,觉得还行呢,麻烦顺手star 一发, 里面有详细的用法,简单的api 相信能够解决你的问题。几遍不用,也能给你提供一种思路!
    https://github.com/r9ronaldozhang/ZYPlayer
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容