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)屏問題
- 在項(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