Swift AVPlayer 播放網(wǎng)絡(luò)視頻之基礎(chǔ)篇

本篇效果圖
效果

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 肾胯、ReadyToPlayFailed 只有在 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è)事件

ZZPlayerViewawakeFromNib最底下加上以下代碼:

// 按下的時(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末纹笼,一起剝皮案震驚了整個(gè)濱河市纹份,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌廷痘,老刑警劉巖蔓涧,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件削咆,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡蠢笋,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門鳞陨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)昨寞,“玉大人,你說我怎么就攤上這事厦滤≡遥” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵掏导,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng)肺魁,這世上最難降的妖魔是什么渺蒿? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮值纱,結(jié)果婚禮上鳞贷,老公的妹妹穿的比我還像新娘。我一直安慰自己虐唠,他們只是感情好搀愧,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著疆偿,像睡著了一般咱筛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上杆故,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天迅箩,我揣著相機(jī)與錄音,去河邊找鬼反番。 笑死沙热,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的罢缸。 我是一名探鬼主播篙贸,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼枫疆!你這毒婦竟也來(lái)了爵川?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤息楔,失蹤者是張志新(化名)和其女友劉穎寝贡,沒想到半個(gè)月后扒披,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡圃泡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年碟案,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颇蜡。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡价说,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出风秤,到底是詐尸還是另有隱情鳖目,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布缤弦,位于F島的核電站领迈,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏碍沐。R本人自食惡果不足惜狸捅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望累提。 院中可真熱鬧薪贫,春花似錦、人聲如沸刻恭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)鳍贾。三九已至鞍匾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間骑科,已是汗流浹背橡淑。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留咆爽,地道東北人梁棠。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像斗埂,于是被迫代替她去往敵國(guó)和親符糊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容