ZYPlayer 基于 AVFoundation AVPlaye
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