iOS 播放視頻可以用MPMoviePlayerController
纽窟,MPMoviePlayerController
是系統(tǒng)高度封裝的VC,使用起來(lái)相對(duì)簡(jiǎn)單兼吓,但是靈活性缺失臂港,
一般播放視頻會(huì)選擇使用AVPlayer
, 它可以高度自定義视搏。雖說 AVPlayer
也有一些庫(kù)审孽,如果項(xiàng)目比較緊可以用第三方,但是如果有時(shí)間還是要自己學(xué)習(xí)的浑娜。本篇以AVPlayer
播放網(wǎng)絡(luò)視頻為例佑力,介紹 AVPlayer
的基本用法。后面慢慢重構(gòu)筋遭,盡量寫出一個(gè)功能強(qiáng)大的播放器打颤。
準(zhǔn)備工作
找到Info.plist 右鍵 Open As -> Source Code , 在 </dict>
上面加上:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
不加的不能處理http請(qǐng)求暴拄。都懂的。
加載出視頻
首先來(lái)介紹用到的幾個(gè)對(duì)象:
-
AVPlayerItem
一個(gè)媒體資源管理對(duì)象编饺,管理者視頻的一些基本信息和狀態(tài)乖篷,如 播放進(jìn)度、緩存進(jìn)度等 反肋。 一個(gè)AVPlayerItem對(duì)應(yīng)著一個(gè)視頻資源那伐。 -
AVPlayer
視頻操作對(duì)象,但是無(wú)法顯示視頻石蔗,需要把自己添加到一個(gè)AVPlayerLayer
上 -
AVPlayerLayer
用來(lái)顯示視頻的
我們先來(lái)自定義一個(gè) ZZPlayerView
繼承自 UIView
罕邀, 這個(gè) View 是用來(lái)顯示視頻和處理一些基本操作的。
1养距、添加一個(gè)變量 var playerLayer:AVPlayerLayer?
2诉探、在layoutSubviews
方法中指定layer的大小
override func layoutSubviews() {
super.layoutSubviews()
playerLayer?.frame = self.bounds
}
然后在Main.stroyboard
中拖一個(gè)UIView
到VC上 , 把 Class
設(shè)置成ZZPlayerView
.
然后在VC中聲明幾個(gè)對(duì)象 :
@IBOutlet weak var playerView:ZZPlayerView!
var playerItem:AVPlayerItem!
var avplayer:AVPlayer!
var playerLayer:AVPlayerLayer!
上面提到的三個(gè)對(duì)象和自定義的View 棍厌。
然后ViewDidLoad
中 添加如下代碼 :
// 檢測(cè)連接是否存在 不存在報(bào)錯(cuò)
guard let url = NSURL(string: "http://bos.nj.bpc.baidu.com/tieba-smallvideo/11772_3c435014fb2dd9a5fd56a57cc369f6a0.mp4") else { fatalError("連接錯(cuò)誤") }
playerItem = AVPlayerItem(URL: url) // 創(chuàng)建視頻資源
// 監(jiān)聽緩沖進(jìn)度改變
playerItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: NSKeyValueObservingOptions.New, context: nil)
// 監(jiān)聽狀態(tài)改變
playerItem.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.New, context: nil)
// 將視頻資源賦值給視頻播放對(duì)象
self.avplayer = AVPlayer(playerItem: playerItem)
// 初始化視頻顯示layer
playerLayer = AVPlayerLayer(player: avplayer)
// 設(shè)置顯示模式
playerLayer.videoGravity = AVLayerVideoGravityResizeAspect
playerLayer.contentsScale = UIScreen.mainScreen().scale
// 賦值給自定義的View
self.playerView.playerLayer = self.playerLayer
// 位置放在最底下
self.playerView.layer.insertSublayer(playerLayer, atIndex: 0)
上面加了監(jiān)聽記得在頁(yè)面銷毀的時(shí)候remove
掉 :
deinit{
playerItem.removeObserver(self, forKeyPath: "loadedTimeRanges")
playerItem.removeObserver(self, forKeyPath: "status")
}
然后處理監(jiān)聽事件:
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
guard let playerItem = object as? AVPlayerItem else { return }
if keyPath == "loadedTimeRanges"{
// 緩沖進(jìn)度 暫時(shí)不處理
}else if keyPath == "status"{
// 監(jiān)聽狀態(tài)改變
if playerItem.status == AVPlayerItemStatus.ReadyToPlay{
// 只有在這個(gè)狀態(tài)下才能播放
self.avplayer.play()
}else{
print("加載異常")
}
}
}
一共有三種狀態(tài)
Unknown
肾胯、ReadyToPlay
、Failed
只有在ReadyToPlay
狀態(tài)下視頻才能播放耘纱。
好了 運(yùn)行視頻敬肚,如果一切正常的話(一般沒問題)。視頻可以正常播放出來(lái)的束析。網(wǎng)絡(luò)加載的艳馒,網(wǎng)速慢的 等等。
時(shí)間顯示
沒見過哪個(gè)視頻播放器是光禿禿的员寇,啥都沒有弄慰, 沒有進(jìn)度條 、沒有時(shí)間顯示 蝶锋。我們先給播放器加上時(shí)間顯示 陆爽。
我們的布局最好使用AutoLayout
, 因?yàn)橐曨l一般是支持橫豎屏的,AutoLayout
會(huì)省去你很多麻煩扳缕, 代碼寫Autolayout
比較麻煩慌闭,所以這里選用了SnapKit
, 一個(gè)AutoLayout
的庫(kù), 使AutoLayout
在代碼中的使用語(yǔ)法異常簡(jiǎn)潔.為了簡(jiǎn)單躯舔,我直接將源碼拖到工程目錄下使用了驴剔。下載地址 : SnapKit
因?yàn)槲覀冞@里使用了storyboard
創(chuàng)建的ZZPlayerView
, 所以要手動(dòng)添加View的話,需要在 awakeFromNib
方法中處理
在ZZPlayerView
中聲明變量 var timeLabel:UILabel!
.
然后在 awakeFromNib
中布局它的位置庸毫。
timeLabel = UILabel()
timeLabel.textColor = UIColor.whiteColor()
timeLabel.font = UIFont.systemFontOfSize(12)
self.addSubview(timeLabel)
timeLabel.snp_makeConstraints { (make) in
make.right.equalTo(self)
make.bottom.equalTo(self).inset(5)
}
然后回到ViewController
中,寫一個(gè)將秒轉(zhuǎn)成時(shí)間字符串的方法衫樊,因?yàn)槲覀儗⒌玫矫搿?/p>
func formatPlayTime(secounds:NSTimeInterval)->String{
if secounds.isNaN{
return "00:00"
}
let Min = Int(secounds / 60)
let Sec = Int(secounds % 60)
return String(format: "%02d:%02d", Min, Sec)
}
因?yàn)樵趧傞_始的時(shí)候我們可能得到的不是一個(gè)數(shù)字飒赃,所以加上了判斷 利花。
因?yàn)槲覀円獙?shí)時(shí)計(jì)算時(shí)間,這里加上一個(gè)計(jì)時(shí)器载佳。一般會(huì)選擇NSTimer
, 但是這里我們選擇CADisplayLink
.
聲明一個(gè)變量 var link:CADisplayLink!
在viewDidLoad
最底下加兩句話 炒事。
self.link = CADisplayLink(target: self, selector: #selector(update))
self.link.addToRunLoop( NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
CADisplayLink
的執(zhí)行次數(shù)相當(dāng)于屏幕的幀數(shù),iPhone
不卡頓的時(shí)候是每秒60次蔫慧。把它加入主loop中挠乳,默認(rèn)Mode 。 關(guān)于NSRunLoop
又是一個(gè)大話題姑躲,感興趣的睡扬,以后一起討論。這里先這么寫著黍析。差不多每秒執(zhí)行60次卖怜。
然后我們來(lái)處理這個(gè)update
方法
func update(){
// 當(dāng)前播放到的時(shí)間
let currentTime = CMTimeGetSeconds(self.avplayer.currentTime())
// 總時(shí)間
let totalTime = NSTimeInterval(playerItem.duration.value) / NSTimeInterval(playerItem.duration.timescale)
// timescale 這里表示壓縮比例
let timeStr = "\(formatPlayTime(currentTime))/\(formatPlayTime(totalTime))" // 拼接字符串
playerView.timeLabel.text = timeStr // 賦值
// TODO: 播放進(jìn)度
}
現(xiàn)在執(zhí)行。是不是可以實(shí)時(shí)顯示時(shí)間了阐枣。
處理進(jìn)度條
進(jìn)度條我們使用UISlider
+ UIProgressView
的方式马靠。
首先在ZZPlayerView
添加一個(gè)UISlider
。聲明變量var slider:UISlider!
蔼两。
awakeFromNib
中加入以下代碼:
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
// 從最大值滑向最小值時(shí)桿的顏色
slider.maximumTrackTintColor = UIColor.clearColor()
// 從最小值滑向最大值時(shí)桿的顏色
slider.minimumTrackTintColor = UIColor.whiteColor()
// 在滑塊圓按鈕添加圖片
slider.setThumbImage(UIImage(named:"slider_thumb"), forState: UIControlState.Normal)
我們指定了slider的位置甩鳄,并指定了最大值和最小值。當(dāng)前值额划,還有更換了滑塊的圖片妙啃,系統(tǒng)的太丑了。
然后在update
方法后面加上下面一句話锁孟,就可以看到播放進(jìn)度了 彬祖。
self.playerView.slider.value = Float(currentTime/totalTime)
運(yùn)行下, 不錯(cuò)確實(shí)有效果品抽。 而且滑塊還不錯(cuò) 储笑。( 如果你替換了個(gè)不錯(cuò)的圖片的話 ,你也可以下載我的項(xiàng)目圆恤,使用我的圖片突倍。我這邊圖片是用Sketch
隨便畫的 )
這時(shí)候手動(dòng)滑動(dòng)滑塊并沒有用,會(huì)立刻回去盆昙。我們還需要處理滑塊事件羽历,改變視頻播放進(jìn)度。只需要處理兩個(gè)事件
在ZZPlayerView
的awakeFromNib
最底下加上以下代碼:
// 按下的時(shí)候
slider.addTarget(self, action: #selector(sliderTouchDown( _:)), forControlEvents: UIControlEvents.TouchDown)
// 彈起的時(shí)候
slider.addTarget(self, action: #selector(sliderTouchUpOut( _:)), forControlEvents: UIControlEvents.TouchUpOutside)
slider.addTarget(self, action: #selector(sliderTouchUpOut( _:)), forControlEvents: UIControlEvents.TouchUpInside)
slider.addTarget(self, action: #selector(sliderTouchUpOut( _:)), forControlEvents: UIControlEvents.TouchCancel)
為了保險(xiǎn)淡喜,我們?cè)趶椘鸬臅r(shí)候監(jiān)聽了三個(gè)方法秕磷。
因?yàn)槲覀儾幌M覀冊(cè)诨瑒?dòng)的時(shí)候還一直改變播放進(jìn)度,所以在ZZPlayerView
加上變量var sliding = false
表示是否正在滑動(dòng)
然后處理按下和彈起事件:
func sliderTouchDown(slider:UISlider){
self.sliding = true
}
func sliderTouchUpOut(slider:UISlider){
// TODO: -代理處理
}
在update
方法中修改處理播放進(jìn)度的邏輯
// 滑動(dòng)不在滑動(dòng)的時(shí)候
if !self.playerView.sliding{
// 播放進(jìn)度
self.playerView.slider.value = Float(currentTime/totalTime)
}
滑動(dòng)結(jié)束的時(shí)候需要改變視頻進(jìn)度炼团,所以這里寫一個(gè)代理澎嚣。
protocol ZZPlayerViewDelegate:NSObjectProtocol {
func zzplayer(playerView:ZZPlayerView,sliderTouchUpOut slider:UISlider)
}
ZZPlayerView
中聲明 weak var delegate:ZZPlayerViewDelegate?
然后彈起事件中加入:
delegate?.zzplayer(self, sliderTouchUpOut: slider)
然后在VC中實(shí)現(xiàn)代理:(別忘記在viewDidLoad
中加上self.playerView.delegate = self
)
extension ViewController:ZZPlayerViewDelegate{
// 滑動(dòng)滑塊 指定播放位置
func zzplayer(playerView: ZZPlayerView, sliderTouchUpOut slider: UISlider) {
//當(dāng)視頻狀態(tài)為AVPlayerStatusReadyToPlay時(shí)才處理
if self.avplayer.status == AVPlayerStatus.ReadyToPlay{
let duration = slider.value * Float(CMTimeGetSeconds(self.avplayer.currentItem!.duration))
let seekTime = CMTimeMake(Int64(duration), 1)
// 指定視頻位置
self.avplayer.seekToTime(seekTime, completionHandler: { (b) in
// 別忘記改狀態(tài)
playerView.sliding = false
})
}
}
}
這時(shí)候運(yùn)行已經(jīng)可以根據(jù)滑塊改變進(jìn)度了疏尿,但是我們一直忘記處理緩存進(jìn)度了。
在ZZPlayerView
中加一個(gè)變量 var progressView:UIProgressView!
易桃。
還是在awakeFromNib
中進(jìn)行布局褥琐。
progressView = UIProgressView()
progressView.backgroundColor = UIColor.lightGrayColor()
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.redColor()
progressView.progress = 0
為了比較清晰的看到進(jìn)度,我們這里設(shè)置了紅色晤郑。進(jìn)度需要顯示在slider下面和slider位置一樣敌呈。
我VC中寫一個(gè)方法來(lái)計(jì)算當(dāng)前的緩沖進(jìn)度
func avalableDurationWithplayerItem()->NSTimeInterval{
guard let loadedTimeRanges = avplayer?.currentItem?.loadedTimeRanges,first = loadedTimeRanges.first else {fatalError()}
let timeRange = first.CMTimeRangeValue
let startSeconds = CMTimeGetSeconds(timeRange.start)
let durationSecound = CMTimeGetSeconds(timeRange.duration)
let result = startSeconds + durationSecound
return result
}
然后在我們前面寫的TODO也就是KVO監(jiān)聽緩沖進(jìn)度的地方換成下面代碼 :
if keyPath == "loadedTimeRanges"{
// 通過監(jiān)聽AVPlayerItem的"loadedTimeRanges",可以實(shí)時(shí)知道當(dāng)前視頻的進(jìn)度緩沖
let loadedTime = avalableDurationWithplayerItem()
let totalTime = CMTimeGetSeconds(playerItem.duration)
let percent = loadedTime/totalTime // 計(jì)算出比例
// 改變進(jìn)度條
self.playerView.progressView.progress = Float(percent)
}
至此造寝,我們已經(jīng)很好的處理了進(jìn)度相關(guān)的工作 磕洪。 但是我們視頻缺少播放和暫停鍵 。
播放-暫停
廢話不多匹舞,繼續(xù)干活褐鸥。
ZZPlayerView
中添加var playBtn:UIButton!
變量。
awakeFromNib
中進(jìn)行布局赐稽。
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: "player_pause"), forState: UIControlState.Normal)
// 點(diǎn)擊事件
playBtn.addTarget(self, action: #selector(playAndPause( _:)) , forControlEvents: UIControlEvents.TouchUpInside)
這里又會(huì)設(shè)置一個(gè)狀態(tài)表示叫榕,是否在播放,用來(lái)切換按鈕的圖片姊舵。播放和暫停是不同圖片(同樣可以下載我的項(xiàng)目使用晰绎,最后下載項(xiàng)目一期看)
添加變量var playing = true
表示是否正在播放 , 因?yàn)椴シ藕蜁和J窃赩C中處理括丁,這里依舊是代理荞下。
協(xié)議中添加方法:
func zzplayer(playerView:ZZPlayerView,playAndPause playBtn:UIButton)
處理點(diǎn)擊事件
func playAndPause(btn:UIButton){
let tmp = !playing
playing = tmp // 改變狀態(tài)
// 根據(jù)狀態(tài)設(shè)定圖片
if playing {
playBtn.setImage(UIImage(named: "player_pause"), forState: UIControlState.Normal)
}else{
playBtn.setImage(UIImage(named: "player_play"), forState: UIControlState.Normal)
}
// 代理方法
delegate?.zzplayer(self, playAndPause: btn)
}
在VC中加上:
func zzplayer(playerView: ZZPlayerView, playAndPause playBtn: UIButton) {
if !playerView.playing{
self.avplayer.pause()
}else{
if self.avplayer.status == AVPlayerStatus.ReadyToPlay{
self.avplayer.play()
}
}
}
然后在update
的最前面加上
//暫停的時(shí)候
if !self.playerView.playing{
return
}
因?yàn)樵跁和5臅r(shí)候不需要計(jì)算。
至此史飞,我們基礎(chǔ)版本的播放器實(shí)現(xiàn)完了尖昏,效果如圖。這個(gè)播放器還缺少很多功能构资,比如:右劃快進(jìn)抽诉、左劃快退 ,播放速度 1x 2x 0.5x等 左邊上滑調(diào)整亮度 右邊上滑調(diào)整音量 吐绵、 清晰度切換迹淌、 緩存下載等 。 而且還需要進(jìn)一步封裝己单。有時(shí)間會(huì)把這些補(bǔ)上 唉窃。
github鏈接:https://github.com/smalldu/ZZPlayer
blog同步地址:Swift AVPlayer 播放網(wǎng)絡(luò)視頻之基礎(chǔ)篇
參考鏈接:
AVPlayer 本地、網(wǎng)絡(luò)視頻播放相關(guān)
BMPlayer