ZYPlayer 基于 AVFoundation AVPlayer 的視頻播放器 swift 3.0

ZYPlayer 是一款基于AVFoundation 下AVPlayer 封裝的視頻播放器
前言:寫這篇文章并不是為了記錄下AVPlayer的用法赎懦,因?yàn)锳VPlayer制作視頻播放器并不存在太大的難點(diǎn)砍鸠。百度的轉(zhuǎn)帖文章都很多拇派,大體差異也不大丙唧。只要細(xì)心的控制好每一個(gè)細(xì)節(jié)凡蜻,相信很多人都能寫出漂亮的播放器來(lái)。本文后面一方面會(huì)附有一份demo筹淫,是自己根據(jù)項(xiàng)目需求封裝的視頻播放器站辉,算是對(duì)swift3.0的語(yǔ)言交流,另外會(huì)著重講講關(guān)于在視頻進(jìn)行旋轉(zhuǎn)全屏控制的一些思路损姜。因?yàn)榕龅搅烁鞣N的坑饰剥,一路走來(lái),很多都并未給出完美的解決方案摧阅,今天給出一種在轉(zhuǎn)屏的完美解決思路汰蓉。有需求的可以繼續(xù)往下看,后面也會(huì)稍微帶上AVPlayer的用法棒卷,新手沒有也可以看看
一. 你可能碰到的轉(zhuǎn)屏問題

  1. 在項(xiàng)目部署的地方 設(shè)置好你需要支持的屏幕方向
    好處:由于開啟了屏幕橫屏的支持方向顾孽,通過監(jiān)聽通知能夠拿到轉(zhuǎn)屏后的正確的frame
    缺點(diǎn):使用frame來(lái)控制播放器的尺寸祝钢,縮放旋轉(zhuǎn) 相當(dāng)?shù)穆闊R话闳斯烙?jì)要不了幾下就轉(zhuǎn)暈了若厚。另外如果因?yàn)橐粋€(gè)視頻播放器要支持多方向拦英,那么導(dǎo)致整個(gè)項(xiàng)目都要支持多方向,顯然很不可取测秸。
    2.在項(xiàng)目部署的地方 設(shè)置只支持豎屏 手動(dòng)控制屏幕的旋轉(zhuǎn)
    好處:比起上面講的手動(dòng)控制旋轉(zhuǎn)這種方法思路實(shí)現(xiàn)起來(lái)相對(duì)更加清晰疤估,利用transform做90°的旋轉(zhuǎn),控制更加方便
    缺點(diǎn): 項(xiàng)目總不能因?yàn)槟阋謩?dòng)轉(zhuǎn)屏霎冯,把本來(lái)支持的多方向修改掉吧铃拇?

結(jié)論:我們需要做的是無(wú)論項(xiàng)目如何部署方向,都不影響對(duì)視頻播放器的控制沈撞!

本文采用監(jiān)聽屏幕旋轉(zhuǎn)通知慷荔,手動(dòng)對(duì)屏幕進(jìn)行旋轉(zhuǎn)。下面只講如何實(shí)現(xiàn)关串,具體原因就不啰嗦了,比較來(lái)翻文章的都是來(lái)找解決方案的

A . 你的項(xiàng)目搭建的框架 現(xiàn)在主流多是rootViewController為tabBarController 或者簡(jiǎn)單點(diǎn)的是導(dǎo)航控制器作為rootViewController监徘,那么請(qǐng)按照下面的代碼在根控制器下進(jìn)行設(shè)置

    /********* 指定某些具體的控制器不能自動(dòng)旋轉(zhuǎn) **********/
    
    override var shouldAutorotate: Bool {
        guard let nav = self.selectedViewController as? UINavigationController else {
            return true
        }
        // 填寫播放器所在的類(注意命名空間) 加載這個(gè)控制器的時(shí)候晋修,控制器就不會(huì)自動(dòng)進(jìn)行旋轉(zhuǎn)  無(wú)視你項(xiàng)目部署的支持方向
        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]
        }
        // 填寫播放器所在的類(注意命名空間) 當(dāng)加載這個(gè)控制器的時(shí)候,這個(gè)控制器就只支持豎屏 無(wú)視你項(xiàng)目部署的支持方向
        if (nav.topViewController?.isKind(of: NSClassFromString("ZYPlayerDemo.ViewController")!))! {
            return UIInterfaceOrientationMask.portrait
        }
        return [.portrait, .landscapeLeft, .landscapeRight]
    }
    /********* 指定某些具體的控制器不能自動(dòng)旋轉(zhuǎn) **********/

在rootViewController里面指定了上面的代碼凰盔,那么就解決了項(xiàng)目部署方向支持的問題墓卦,簡(jiǎn)單來(lái)講,可以無(wú)視了户敬,后面你的播放器再也不需要關(guān)注項(xiàng)目的支持方向落剪。關(guān)于上面兩個(gè)方法的詳細(xì)作用,可以自行百度尿庐,我就不再啰嗦

B . 在你的播放器中建立一個(gè)屏幕旋轉(zhuǎn)的通知監(jiān)聽(如果手機(jī)設(shè)置了方向鎖定忠怖,是不會(huì)收到通知的)

1. // 監(jiān)聽屏幕旋轉(zhuǎn)的通知
        NotificationCenter.default.addObserver(self, selector: #selector(self.screenDidRotate(note:)), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)

2./** 屏幕旋轉(zhuǎn)通知 */
    @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
        }
    }

這樣,當(dāng)你屏幕旋轉(zhuǎn)的時(shí)候抄瑟,就能拿到當(dāng)前屏幕的放心凡泣,然后就需要做的是處理屏幕的旋轉(zhuǎn)了。
注意:手動(dòng)旋轉(zhuǎn)屏幕中皮假,有一種叫做強(qiáng)制旋轉(zhuǎn)鞋拟,有爭(zhēng)議說(shuō)該方法算是調(diào)用私有API , 也有人覺得應(yīng)該從KVC來(lái)進(jìn)行理解惹资,個(gè)人贊成后者贺纲,并且之前也嘗試過強(qiáng)制轉(zhuǎn)屏,只是由于我的控制需求褪测,并未才去此方法猴誊,下面貼出強(qiáng)制轉(zhuǎn)屏的代碼塊供參考(強(qiáng)制轉(zhuǎn)到左邊橫屏潦刃,KVC,不認(rèn)為會(huì)被拒)

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

下面附上我在屏幕處理中使用的代碼稠肘,由于我的播放器是UIViewController,為了增減需求方便福铅,布局使用了xib,通過約束來(lái)修改尺寸项阴,AVPlayer則是通過代碼進(jìn)行集成滑黔,這樣做就是為了擴(kuò)展性考慮

轉(zhuǎn)屏并未太復(fù)雜就輕松的控制好了各種選擇,沒錯(cuò)环揽!就是transform + UIView animate動(dòng)畫略荡。,當(dāng)橫屏的時(shí)候是將播放器旋轉(zhuǎn)并且添加到window上歉胶,當(dāng)豎屏的時(shí)候又從window上添加到原來(lái)的父控件上

// MARK: - 屏幕旋轉(zhuǎn)處理
extension ZYPlayer {
    fileprivate func rotateToLandscapeLeft() {
        keyWindow.addSubview(self.view)
        // UIView動(dòng)畫進(jìn)行旋轉(zhuǎn)
        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 
    }
}

你沒有看錯(cuò)汛兜,旋轉(zhuǎn)就這么搞定了!

下面還是講講AVPlayer的使用簡(jiǎn)單概述下

  • 初始化播放器
fileprivate func initPlayer(_ url : String) {
        /** 先進(jìn)行一次release */
        releasePlayer()
        // 添加通知監(jiān)聽
        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對(duì)播放器進(jìn)行監(jiān)聽通今,這些都是必須的粥谬,要不你怎么知道啥時(shí)候卡了,啥時(shí)候播放器準(zhǔn)備好了呢辫塌。四個(gè)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處理 注意后面這個(gè)方法需要處理,本人之前就是寫了監(jiān)聽臼氨,做下面的處理掺喻,結(jié)果程序無(wú)限掛,懵逼了好久
// MARK: - KVO 監(jiān)聽處理
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()    // 準(zhǔn)備播放
            } else {                                // 初始化播放器失敗了
                state = .stopped
            }
        } else if keyPath == "loadedTimeRanges" {                                           //監(jiān)聽播放器的下載進(jìn)度
            calculateBufferedProgress(playerItem)
        } else if keyPath == "playbackBufferEmpty" && playerItem.isPlaybackBufferEmpty {    //監(jiān)聽播放器在緩沖數(shù)據(jù)的狀態(tài)
            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) // 視頻總時(shí)間
        totalDuration.text = timeFormate(time: duration)
        startToPlay()
    }
    
    fileprivate func calculateBufferedProgress(_ palyerItem : AVPlayerItem) {
        let bufferedRanges = playerItem?.loadedTimeRanges
        let timeRange = bufferedRanges?.first?.timeRangeValue   // 獲取緩沖區(qū)域
        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
    }
}

這樣就能拿到各種時(shí)長(zhǎng)感耙,是否準(zhǔn)備好播放了,以及播放器的緩沖進(jìn)度等持隧。修改UI就是你該做的事情了即硼!友情提示,如果使用了Timer 這個(gè)東西屡拨,視頻在播放的時(shí)候谦絮,如果用戶退出界面,務(wù)必要提供一個(gè)手動(dòng)銷毀播放的方法洁仗,不然妥妥的內(nèi)存泄漏层皱。

  • 這里插句嘴,中間注明下赠潦,本文寫于16年11月底叫胖,原創(chuàng) TRS 的ronaldozhang發(fā)布于簡(jiǎn)書。

  • 另外做為一個(gè)視頻播放器她奥,還有些細(xì)節(jié)要做哦瓮增,監(jiān)聽下面4個(gè)通知必不可少的怎棱。應(yīng)用進(jìn)入后臺(tái),你的視頻雖然看不見了绷跑,聲音一直放也不行吧拳恋? 另外視頻都播放完了,播放器要么直接銷毀砸捏,要么提供一個(gè)重播功能等等谬运,這些就看你的需求了,但是也是需要處理的吧

// 監(jiān)聽app 進(jìn)入后臺(tái) 返回前臺(tái)的通知
        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)
        // 監(jiān)聽 playerItem 的狀態(tài)通知
        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()
// 這個(gè)是快進(jìn)梆暖,快退的位置匹配的方法。 第一個(gè)參數(shù)是匹配到視頻的多少秒掂骏,這個(gè)根據(jù)你的slider.value來(lái)定的轰驳,第二個(gè)參數(shù)固定寫法,直接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 一發(fā)田绑, 里面有詳細(xì)的用法勤哗,簡(jiǎn)單的api 相信能夠解決你的問題。幾遍不用辛馆,也能給你提供一種思路俺陋!
    https://github.com/r9ronaldozhang/ZYPlayer
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末豁延,一起剝皮案震驚了整個(gè)濱河市昙篙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌诱咏,老刑警劉巖苔可,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異袋狞,居然都是意外死亡焚辅,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門苟鸯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)同蜻,“玉大人,你說(shuō)我怎么就攤上這事早处⊥迓” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵砌梆,是天一觀的道長(zhǎng)默责。 經(jīng)常有香客問我贬循,道長(zhǎng),這世上最難降的妖魔是什么桃序? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任杖虾,我火速辦了婚禮,結(jié)果婚禮上媒熊,老公的妹妹穿的比我還像新娘奇适。我一直安慰自己,他們只是感情好泛释,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布滤愕。 她就那樣靜靜地躺著,像睡著了一般怜校。 火紅的嫁衣襯著肌膚如雪间影。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天茄茁,我揣著相機(jī)與錄音魂贬,去河邊找鬼。 笑死裙顽,一個(gè)胖子當(dāng)著我的面吹牛付燥,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播愈犹,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼键科,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了漩怎?” 一聲冷哼從身側(cè)響起勋颖,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎勋锤,沒想到半個(gè)月后饭玲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡叁执,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年茄厘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谈宛。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡次哈,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吆录,到底是詐尸還是另有隱情窑滞,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站葛假,受9級(jí)特大地震影響障陶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜聊训,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一抱究、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧带斑,春花似錦鼓寺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至挂滓,卻和暖如春苦银,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背赶站。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工幔虏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贝椿。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓想括,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親烙博。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瑟蜈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容