今天我們來聊聊AVPlayer,這是一個AVFoundation庫里面的類绅喉,可以用來播放網(wǎng)絡(luò)視頻使用渠鸽。
開始正題之前,我們首先來了解幾個我們將要使用的對象:
AVPlayerItem :媒體資源管理對象柴罐,管理視頻的一些基本信息和狀態(tài)拱绑,如 播放進度、緩存進度等 丽蝎。 一個AVPlayerItem對應(yīng)著一個視頻資源。
AVPlayer :視頻操作對象,自己本身無法顯示視頻屠阻,需要把自己添加到一個AVPlayerLayer 上來操作红省。
AVPlayerLayer: 用來顯示視頻。
了解之后国觉,我們進入正題吧恃。
首先我們創(chuàng)建一個CAplayerView繼承于UIView,在這個自定義view上完成一些操作麻诀。分析一下痕寓,需要包含哪些東西,比如說UI界面蝇闭,AVPlayer配置,通知,KVO他托,各種手勢等祸憋。我們一個一個來實現(xiàn)。
首先我們?yōu)镃AplayerView寫一個便利構(gòu)造器:
convenience init(frame: CGRect,theUrl:URL) {
self.init(frame: frame)
url = theUrl //視頻url
setupUI() //UI界面
setupTap() //手勢
setupPlayer() //avplayer
link = CADisplayLink(target: self, selector: #selector(update))
link.add(to: RunLoop.main, forMode: .defaultRunLoopMode) //定時器
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
UI界面
定義我們需要的控件
var timeLabel:UILabel! //視頻時間
var slider:UISlider! //視頻進度條
var sliding = false
var progressView:UIProgressView! //緩沖條
var playBtn:UIButton! //播放暫停按鈕
var playing = true
var backBtn:UIButton! //返回按鈕
var fullScreenBtn:UIButton! //全屏按鈕
var titleLabel:UILabel! //標(biāo)題
func setupUI () {
timeLabel = UILabel()
timeLabel.textColor = UIColor.white
timeLabel.font = UIFont.systemFont(ofSize: 12)
self.addSubview(timeLabel)
timeLabel.snp.makeConstraints { (make) in
make.right.equalTo(self).inset(25)
make.bottom.equalTo(self).inset(5)
}
fullScreenBtn = UIButton()
self.addSubview(fullScreenBtn)
fullScreenBtn.snp.makeConstraints { (make) in
make.right.equalTo(self).inset(5)
make.bottom.equalTo(self).inset(5)
make.width.height.equalTo(15)
}
// 設(shè)置按鈕圖片
fullScreenBtn.setImage(UIImage(named: "full_screen"), for: .normal)
// 點擊事件
fullScreenBtn.addTarget(self, action: #selector(tapChangeScreen), for: .touchUpInside)
slider = UISlider()
self.addSubview(slider)
slider.snp.makeConstraints { (make) in
make.bottom.equalTo(self).inset(5)
make.left.equalTo(self).offset(50)
make.right.equalTo(self).inset(100)
make.height.equalTo(15)
}
slider.minimumValue = 0
slider.maximumValue = 1
slider.value = 0
// 從最大值滑向最小值時桿的顏色
slider.maximumTrackTintColor = UIColor.clear
// 從最小值滑向最大值時桿的顏色
slider.minimumTrackTintColor = UIColor.white
// 在滑塊圓按鈕添加圖片
slider.setThumbImage(UIImage(named: "knob"), for: .normal)
// 按下的時候
slider.addTarget(self, action: #selector(sliderTouchDown(slider:)), for: .touchDown)
// 彈起的時候
slider.addTarget(self, action: #selector(sliderTouchUpOut(slider:)), for: .touchUpOutside)
slider.addTarget(self, action: #selector(sliderTouchUpOut(slider:)), for: .touchUpInside)
slider.addTarget(self, action: #selector(sliderTouchUpOut(slider:)), for: .touchCancel)
progressView = UIProgressView()
progressView.backgroundColor = UIColor.lightGray
self.insertSubview(progressView, belowSubview: slider)
progressView.snp.makeConstraints { (make) in
make.left.right.equalTo(slider)
make.centerY.equalTo(slider)
make.height.equalTo(2)
}
progressView.tintColor = UIColor.red
progressView.progress = 0
playBtn = UIButton()
self.addSubview(playBtn)
playBtn.snp.makeConstraints { (make) in
make.centerY.equalTo(slider)
make.left.equalTo(self).offset(10)
make.width.height.equalTo(30)
}
// 設(shè)置按鈕圖片
playBtn.setImage(UIImage(named: "pause"), for: .normal)
// 點擊事件
playBtn.addTarget(self, action: #selector(playAndPause(btn:)), for: .touchUpInside)
NotificationCenter.default.addObserver(self, selector: #selector(deviceOrientationDidChange), name:NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
backBtn = UIButton()
self.addSubview(backBtn)
backBtn.snp.makeConstraints { (make) in
make.top.equalTo(self).offset(10)
make.left.equalTo(self).offset(10)
make.width.height.equalTo(30)
}
// 設(shè)置按鈕圖片
backBtn.setImage(UIImage(named: "Back-white"), for: .normal)
// 點擊事件
backBtn.addTarget(self, action: #selector(onClickBackBtnAction), for: .touchUpInside)
backBtn.isHidden = true
titleLabel = UILabel()
titleLabel.text = "這里顯示視頻的標(biāo)題"
titleLabel.font = UIFont.systemFont(ofSize: 14)
titleLabel.textColor = UIColor.white
self.addSubview(titleLabel)
titleLabel.snp.makeConstraints { (make) in
make.top.equalTo(self).offset(10)
make.height.equalTo(30)
make.centerX.equalTo(self)
}
}
AVPlayer配置
var playerLayer:AVPlayerLayer?
var playerItem:AVPlayerItem!
var player:AVPlayer!
var url:URL?
func setupPlayer () {
guard (url != nil) else {
fatalError("連接錯誤")
}
playerItem = AVPlayerItem(url: url!)
//監(jiān)聽緩沖進度改變
playerItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: .new, context: nil)
// 監(jiān)聽狀態(tài)改變
playerItem.addObserver(self, forKeyPath: "status", options: .new, context: nil)
player = AVPlayer(playerItem: playerItem)
player.volume = 0.5
playerLayer = AVPlayerLayer(player: player)
playerLayer?.videoGravity = .resizeAspectFill
playerLayer?.contentsScale = UIScreen.main.scale
self.layer.insertSublayer(playerLayer!, at: 0)
}
deinit {
playerItem.removeObserver(self, forKeyPath: "loadedTimeRanges")
playerItem.removeObserver(self, forKeyPath: "status")
}
添加手勢
因為Pan手勢功能不一樣逻悠,我們可以寫個枚舉來定義Pan手勢的類型
enum Direction {
case leftOrRight,upOrDown,none
}
var direction:Direction! //pan手勢類型
var isVolume = false //是否為改變聲音手勢
var oldConstriants:Array<NSLayoutConstraint>! //舊的布局
var isFullScreen:Bool! //是否全屏
func setupTap () {
let fullOrNotFullScreenTap = UITapGestureRecognizer(target: self, action: #selector(tapChangeScreen))
fullOrNotFullScreenTap.numberOfTapsRequired = 2
self.addGestureRecognizer(fullOrNotFullScreenTap)
let disOrNotdisAppearTap = UITapGestureRecognizer(target: self, action: #selector(disOrNotDisAppear))
disOrNotdisAppearTap.numberOfTapsRequired = 1
self.addGestureRecognizer(disOrNotdisAppearTap)
//這行很關(guān)鍵元践,意思是只有當(dāng)沒有檢測到雙擊手勢 或者 檢測雙擊手勢失敗,s單擊手勢才有效
disOrNotdisAppearTap.require(toFail: fullOrNotFullScreenTap)
let pan = UIPanGestureRecognizer(target: self, action: #selector(changeVoiceOrLightOrProgress(pan:)))
self.addGestureRecognizer(pan)
pan.delegate = self as? UIGestureRecognizerDelegate
}
@objc func changeVoiceOrLightOrProgress (pan:UIPanGestureRecognizer) {
let offsetPoint = pan.translation(in: self)
let locationPoint = pan.location(in: self)
let veloctyPoint = pan.velocity(in: self)
switch pan.state {
case .began:
let x = fabs(veloctyPoint.x)
let y = fabs(veloctyPoint.y)
if x > y {
direction = .leftOrRight
}
else if x < y {
direction = .upOrDown
if locationPoint.x <= self.frame.size.width/2 {
isVolume = false
} else {
isVolume = true
}
}
break
case .changed:
if direction == .upOrDown {
if isVolume == false && offsetPoint.y > 0 {
var newBrightness = UIScreen.main.brightness - 0.01
if newBrightness < 0 {
newBrightness = 0
}
UIScreen.main.brightness = newBrightness
}
else if isVolume == false && offsetPoint.y < 0 {
var newBrightness = UIScreen.main.brightness + 0.01
if newBrightness > 1 {
newBrightness = 1
}
UIScreen.main.brightness = newBrightness
}
else if isVolume == true && offsetPoint.y > 0 {
var newVolume = player.volume - 0.01
if newVolume < 0 {
newVolume = 0
}
player.volume = Float(newVolume)
}
else if isVolume == true && offsetPoint.y < 0 {
var newVolume = player.volume + 0.01
if newVolume > 1 {
newVolume = 1
}
player.volume = Float(newVolume)
}
}
else if direction == .leftOrRight {
//可在這里添加左右滑動改變視頻進度的代碼
}
break
case .ended:
if direction == .upOrDown {
isVolume = false
}
else if direction == .leftOrRight {
}
break
default:
break
}
pan.setTranslation(CGPoint.zero, in: self)
}
@objc func tapChangeScreen () {
if isFullScreen == false {
let rotation : UIInterfaceOrientationMask = [.landscapeLeft, .landscapeRight]
kAppdelegate?.blockRotation = rotation
} else {
kAppdelegate?.blockRotation = .portrait
}
}
@objc func disOrNotDisAppear () {
if timeLabel.isHidden == false {
timeLabel.isHidden = true
slider.isHidden = true
progressView.isHidden = true
playBtn.isHidden = true
backBtn.isHidden = true
fullScreenBtn.isHidden = true
titleLabel.isHidden = true
}
else if timeLabel.isHidden == true && isFullScreen == true {
timeLabel.isHidden = false
slider.isHidden = false
progressView.isHidden = false
playBtn.isHidden = false
backBtn.isHidden = false
fullScreenBtn.isHidden = true
titleLabel.isHidden = false
}
else if timeLabel.isHidden == true && isFullScreen == false {
timeLabel.isHidden = false
slider.isHidden = false
progressView.isHidden = false
playBtn.isHidden = false
backBtn.isHidden = true
fullScreenBtn.isHidden = false
titleLabel.isHidden = false
}
}
KVO 通知 點擊事件以及定時器方法
//MARK:----------KVO方法
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "loadedTimeRanges" {
// 通過監(jiān)聽AVPlayerItem的"loadedTimeRanges"童谒,可以實時知道當(dāng)前視頻的進度緩沖
let loadedTime = avalableDurationWithplayerItem()
let totalTime = CMTimeGetSeconds(playerItem.duration)
let percent = loadedTime/totalTime // 計算出比例
// 改變進度條
progressView.progress = Float(percent)
}
else if keyPath == "status" {
if playerItem.status == .readyToPlay {
player.play()
} else {
print("加載異常")
}
}
}
func avalableDurationWithplayerItem()->TimeInterval{
guard let loadedTimeRanges = player?.currentItem?.loadedTimeRanges,let first = loadedTimeRanges.first else {fatalError()}
let timeRange = first.timeRangeValue
let startSeconds = CMTimeGetSeconds(timeRange.start)
let durationSecound = CMTimeGetSeconds(timeRange.duration)
let result = startSeconds + durationSecound
return result
}
//MARK:----------通知方法
@objc func deviceOrientationDidChange() {
let interfaceOrientation = UIApplication.shared.statusBarOrientation
switch interfaceOrientation {
case .landscapeLeft,.landscapeRight:
timeLabel.isHidden = true
slider.isHidden = true
progressView.isHidden = true
playBtn.isHidden = true
backBtn.isHidden = true
fullScreenBtn.isHidden = true
titleLabel.isHidden = true
isFullScreen = true
oldConstriants = getCurrentVC().view.constraints
self.updateConstraintsIfNeeded()
//刪除UIView animate可以去除橫豎屏切換過渡動畫
UIView.animate(withDuration: kTransitionTime, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: .transitionCurlUp, animations: {
UIApplication.shared.keyWindow?.addSubview(self)
self.snp.makeConstraints { (make) in
make.edges.equalTo(UIApplication.shared.keyWindow!)
}
self.layoutIfNeeded()
}) { (bool) in
}
break
case .portrait,.portraitUpsideDown:
timeLabel.isHidden = false
slider.isHidden = false
progressView.isHidden = false
playBtn.isHidden = false
titleLabel.isHidden = false
backBtn.isHidden = true
fullScreenBtn.isHidden = false
isFullScreen = false
getCurrentVC().view.addSubview(self)
UIView.animateKeyframes(withDuration: kTransitionTime, delay: 0, options: .calculationModeLinear, animations: {
if (self.oldConstriants != nil) {
self.getCurrentVC().view.addConstraints(self.oldConstriants)
}
}, completion: nil)
break
case .unknown:
print("UIInterfaceOrientationUnknown")
break
default:
break
}
getCurrentVC().view.layoutIfNeeded()
}
func getCurrentVC()->UIViewController {
var result:UIViewController!
var window = UIApplication.shared.keyWindow
if window?.windowLevel != UIWindowLevelNormal {
let windows:Array = UIApplication.shared.windows
for tmpWin:UIWindow in windows {
if tmpWin.windowLevel == UIWindowLevelNormal {
window = tmpWin
break
}
}
}
let frontView = window?.subviews[0]
let nextResponder = frontView?.next
if (nextResponder?.isKind(of: UIViewController.self))! {
result = nextResponder as? UIViewController
} else {
result = window?.rootViewController
}
return result
}
//MARK:----------全屏按鈕點擊事件
@objc func onClickBackBtnAction(){
//設(shè)置豎屏
kAppdelegate?.blockRotation = .portrait
}
//MARK:----------暫停播放按鈕點擊方法
@objc func playAndPause(btn:UIButton){
let tmp = !playing
playing = tmp // 改變狀態(tài)
// 根據(jù)狀態(tài)設(shè)定圖片
if playing {
playBtn.setImage(UIImage(named: "pause"), for: .normal)
player.play()
}else{
playBtn.setImage(UIImage(named: "play"), for: .normal)
player.pause()
}
}
//MARK:----------slider滑動方法
@objc func sliderTouchDown(slider:UISlider){
self.sliding = true
}
@objc func sliderTouchUpOut(slider:UISlider){
if player.status == .readyToPlay {
let duration = slider.value * Float(CMTimeGetSeconds(player.currentItem!.duration))
let seekTime = CMTimeMake(Int64(duration), 1)
player.seek(to: seekTime) { (bool) in
self.sliding = false
}
}
}
//MARK:----------定時器方法
@objc func update () {
if playing == false {
return
}
// 當(dāng)前播放到的時間
let currentTime = CMTimeGetSeconds(player.currentTime())
// 總時間
let totalTime = TimeInterval(playerItem.duration.value)/TimeInterval(playerItem.duration.timescale)
let timeStr = "\(formatPlayTime(seconds: currentTime))/\(formatPlayTime(seconds: totalTime))"
timeLabel.text = timeStr
if sliding == false {
slider.value = Float(currentTime/totalTime)
}
}
func formatPlayTime(seconds:TimeInterval)->String{
if seconds.isNaN{
return "00:00"
}
let Min:Int = Int(seconds / 60)
let Sec:Int = Int(seconds) % 60
return String(format: "%02d:%02d", Min, Sec)
}
}
上面我們用到了橫豎屏切換的kAppdelegate?.blockRotation為在Appdelegate寫的一個extension
let kAppdelegate: AppDelegate? = UIApplication.shared.delegate as? AppDelegate
extension AppDelegate{
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return blockRotation
}
}
var blockRotation: UIInterfaceOrientationMask = .portrait{
didSet{
if blockRotation.contains(.portrait){
//強制設(shè)置成豎屏
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
}else{
//強制設(shè)置成橫屏
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
}
}
}
至此一個簡單的播放器就完成了单旁,包含播放暫停、快進快退饥伊、進度條象浑、緩沖條、視頻時間撵渡、橫豎屏切換融柬、左滑改變亮度、右滑改變聲音等等功能趋距。(改變亮度和聲音的功能只能在真機測試有效)
調(diào)用的時候粒氧,直接在viewDidLoad中:
let playerView = CAplayerView(frame: CGRect(x: 0, y: 0, width: kScreenWidth, height: 250), theUrl: URL(string: "https://www.apple.com/105/media/us/iphone-x/2017/01df5b43-28e4-4848-bf20-490c34a926a7/films/feature/iphone-x-feature-tpl-cc-us-20170912_1280x720h.mp4")!)
playerView.backgroundColor = UIColor.black
self.view.addSubview(playerView)
這樣即可。
當(dāng)然還有倍速播放节腐、清晰度切換外盯、緩存下載等等功能需要完善。實際應(yīng)用的時候還有對象釋放銷毀等問題翼雀。
因此此代碼僅供參考饱苟,如有問題可以回復(fù)跟我交流~
github地址: