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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容