ZYPlayer 是一款基于AVFoundation 下AVPlayer 封裝的視頻播放器
前言:寫這篇文章并不是為了記錄下AVPlayer的用法,因為AVPlayer制作視頻播放器并不存在太大的難點。百度的轉帖文章都很多,大體差異也不大。只要細心的控制好每一個細節,相信很多人都能寫出漂亮的播放器來。本文后面一方面會附有一份demo,是自己根據項目需求封裝的視頻播放器,算是對swift3.0的語言交流,另外會著重講講關于在視頻進行旋轉全屏控制的一些思路。因為碰到了各種的坑,一路走來,很多都并未給出完美的解決方案,今天給出一種在轉屏的完美解決思路。有需求的可以繼續往下看,后面也會稍微帶上AVPlayer的用法,新手沒有也可以看看
一. 你可能碰到的轉屏問題
- 在項目部署的地方 設置好你需要支持的屏幕方向
好處:由于開啟了屏幕橫屏的支持方向,通過監聽通知能夠拿到轉屏后的正確的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