Object-C & Swift 自定義音樂播放器詳解(基於 Avplayer)

記得剛寫音樂播放器那會兒,和大多數人同樣。都會想本身寫的也許不夠完善,還可能會出各類問題,並且如今有不少開源的比較完善的播放器,找個好用的就行了呀,以前也確實是這樣作的。可是隨着本身的App 在音頻播放上的業務以及要求變動,你會發現第三方的徹底不夠用,那麼就會想着去修改第三方的東西,這時候你會發現,第三方的東西雖好是不少大牛寫的(基於底層)比較好用,但改起來卻苦不堪言。因此最終仍是迴歸本源,本身定義音頻播放。最近在整理之前的東西,順便在此分享一下,但願能夠給剛寫播放器的兄弟一些幫助.ios

自定義播放用選擇的是ios 新的播放Api AVPlayer

優勢:AVPlayer屬於AVFoundation框架既能夠播放本地音頻也能夠網絡音頻,更接近底層也會更加靈活,定製性比較高git

用AVPlayer 播放音視頻你會發現,它在設計上各個部分相對獨立,這樣更有利拆分使用,更加靈活。(好比 用AVPlayer 播放視頻你會發現 和生活中看視頻套路差很少 它也須要 播放器 顯示屏 磁盤)github

1 AVPlayer:播放器
2 AVPlayerLayer: 顯示屏(若是要播放視頻則要加上畫布,音頻則不用)
3 AVPlayerItem:一個媒體資源管理對象,管理者視頻的一些基本信息和狀態,一個AVPlayerItem對應着一個視頻資源**
相關對象的意義:

AVAsset:AVAsset類專門用於獲取多媒體的相關信息,包括獲取多媒體的畫面、聲音等信息,屬於一個抽象類,不能直接使用。 AVURLAsset:AVAsset的子類,能夠根據一個URL路徑建立一個包含媒體信息的AVURLAsset對象 CMTime:是一個結構體,裏面存儲着當前的播放進度,總的播放時長swift

你會發現 AVPlayer 雖然是一個總體的音頻播放器可是,它內部把各個功能分紅了單獨的對象,定義時就相對獨立,又能夠組合完成功能。這樣作耦合性會下降,這也能給咱們啓發,咱們在寫一些比較大的功能的時候,要把它們細化成小的功能(查錯方便,其餘地方也能夠用)。接下來的自定義播放器也會用到這種思想。數組

接下來,就詳細梳理一下自定義的AVPlayer 以swift 代碼爲例(爲了緊跟時代步伐 - _ -)緩存

oc 和 swift 版本 帶緩衝進度的自定義進度條 友好的圖片高斯模糊處理 所有在這裏了。 WPYPlayerbash

不知道爲何圖片模糊處理後錄出來成這樣了,項目比這個好看多了。 微信

Untitled2.gif

Swift 自定義音樂播放器 主要分爲

1 自定義Avplayer 的基本內容
2 一些附加功能
3 須要注意的問題

一 自定義Avplayer 的基本內容

1 單例初始化網絡

static let playManager = WPY_AVPlayer()
    var player : AVPlayer = {
        let _player = AVPlayer()
        _player.volume = 2.0 //默認最大音量
        
        return _player
    }()
複製代碼

播放器初始化session

func  initPlayer() {  
        //APP進入後臺通知
        NotificationCenter.default.addObserver(self, selector: #selector(configLockScreenPlay) , name:UIApplication.didEnterBackgroundNotification, object: nil)
        
        let session = AVAudioSession.sharedInstance()
        try? session.setActive(true)
        //後臺播放
        Util_OC.setAVAudioSessionCategory(.playback)
    }
複製代碼

播放前須要配置一些監聽事件 例如: 1 監聽播放狀態 (對於音頻的不一樣狀態,給與不懂操做) 2 緩衝加載狀況(便於有加載播放進度條需求) 3 播放進度(就不用本身用定時器來表示播放時間,那樣也不許確,直接用系統的就好) 4 播放結束通知(便於音頻播放結束作相關操做) 5 監聽打斷處理(播放期間被 電話 短信 微信 等打斷後的處理)

// 播放前增長配置 監測
    func currentItemAddObserver(){
        
        //監聽是否靠近耳朵
        NotificationCenter.default.addObserver(self, selector: #selector(sensorStateChange), name:UIDevice.proximityStateDidChangeNotification, object: nil)
        
        //播放期間被 電話 短信 微信 等打斷後的處理
        NotificationCenter.default.addObserver(self, selector: #selector(handleInterreption(sender:)), name:AVAudioSession.interruptionNotification, object:AVAudioSession.sharedInstance())
        
        // 監控播放結束通知
        NotificationCenter.default.addObserver(self, selector: #selector(playMusicFinished), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player.currentItem)
        //監聽狀態屬性 ,注意AVPlayer也有一個status屬性 經過監控它的status也能夠得到播放狀態
        
        self.player.currentItem?.addObserver(self, forKeyPath: "status", options:[.new,.old], context: nil)
        
        //監控緩衝加載狀況屬性
        self.player.currentItem?.addObserver(self, forKeyPath:"loadedTimeRanges", options: [.new,.old], context: nil)
        
        self.timeObserVer = self.player.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: DispatchQueue.main) { [weak self] (time) in
            
            guard let `self` = self else { return }
            
            let currentTime = CMTimeGetSeconds(time)
            self.progress = Float(currentTime)
            if self.isSeekingToTime {
                return
            }
        
            let total = self.durantion
            if total > 0 {
                self.delegate?.updateProgressWith(progress:Float(currentTime)  / Float(total))
            }
            
            
        }
    }
複製代碼

相應的當該 playItem 播放結束時 移除相關監測,觀察

// 播放後   刪除配置 監測
    
    func currentItemRemoveObserver(){
        self.player.currentItem?.removeObserver(self, forKeyPath:"status")
        self.player.currentItem?.removeObserver(self, forKeyPath:"loadedTimeRanges")
        
        NotificationCenter.default.removeObserver(self, name:UIDevice.proximityStateDidChangeNotification, object: nil)
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
        NotificationCenter.default.removeObserver(self, name:AVAudioSession.interruptionNotification, object: nil)
        
        if(self.timeObserVer != nil){
            self.player.removeTimeObserver(self.timeObserVer!)
        }
        
    }
複製代碼

一些監測的相關處理

1 app進入後臺的 進行後臺播放
注意:記得在工程中打開發後臺播放功能 不然不會後臺播放

1545300365959.jpg

//鎖屏 或 退入後臺 保持音頻繼續播放
    
    @objc func configLockScreenPlay() {
        //設置並激活音頻會話類別
        let session = AVAudioSession.sharedInstance()
        
        Util_OC.setAVAudioSessionCategory(.playback)
        try? session.setActive(true)
        //容許應用接收遠程控制
        
        //設置後臺任務ID
        var  newTaskID = UIBackgroundTaskIdentifier.invalid
        newTaskID = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
        if (newTaskID != UIBackgroundTaskIdentifier.invalid) && (self.bgTaskId != UIBackgroundTaskIdentifier.invalid)  {
            UIApplication.shared.endBackgroundTask(self.bgTaskId)
        }
        
        self.bgTaskId = newTaskID
        
    }
複製代碼

監測和耳朵的距離 來判斷是聽筒 仍是 外音 播放

@objc func sensorStateChange() {
        
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
            
            if UIDevice.current.proximityState == true {
                
                //靠近耳朵
             /*  AVAudioSession *session = [AVAudioSession sharedInstance];
    
    [session setCategory:category withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker error:nil]; */
    
    //swift 4.2 後ios 10 如下不兼容 因此用了oc 的方式寫的**
    
    Util_OC.setAVAudioSessionCategory(.playAndRecord)
            }else {
                //遠離耳朵
                Util_OC.setAVAudioSessionCategory(.playback)
            }
        }
    }
複製代碼

處理播放音頻是被來電 或者 其餘 打斷音頻的處理

@objc func handleInterreption(sender:NSNotification) {
        
        let info = sender.userInfo
        guard let type : AVAudioSession.InterruptionType =  info?[AVAudioSessionInterruptionTypeKey] as? AVAudioSession.InterruptionType else { return }
        
        if type == AVAudioSession.InterruptionType.began {
            
            self.pause()
        }else {
            guard  let options = info![AVAudioSessionInterruptionOptionKey] as? AVAudioSession.InterruptionOptions else {return}
            
            if(options == AVAudioSession.InterruptionOptions.shouldResume){
                self.pause()
            }
        }
    }
複製代碼

單個音頻播放結束後的邏輯處理

@objc func playMusicFinished(){
        
        UIDevice.current.isProximityMonitoringEnabled = true
        self.seekToZeroBeforePlay = true
        self.isPlay = false
        self.updateCurrentPlayState(state: AVPlayerPlayState.AVPlayerPlayStateEnd)
        
        //在這裏進邏輯處理
        
        if (self.playType == WPY_AVPlayerType.PlayTypeSpecial) {
            
            self.next()
        }
    }
複製代碼

播放單個音頻的方法 如播放 音效, 試聽, 問題回答, 即無關聯性只有url的

func playMusic(url : String,type:WPY_AVPlayerType){
        
        self.playType = type // 記錄播放類型 以便作出不一樣處理
        self.setPlaySpeed(playSpeed: 1.0) //播放前初始化倍速 1.0
        
        self.currentItemRemoveObserver() //移除上一首的通知 觀察
        
        let playUrl = self.loadAudioWithPlaypath(playpath: url)
        let playerItem = AVPlayerItem(url: playUrl)
        
        self.playerItem = playerItem
        self.currentUrl = url
        self.isImmediately = true
        
        self.player.replaceCurrentItem(with: playerItem)
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
        self .currentItemAddObserver()
    }
複製代碼

播放多個連續音頻的方法 例如 音樂播放器,或者多個連續的音頻

/// 用於播放多個音頻的列表  播放方法
    ///
    /// - Parameters:
    ///   - index: 播放列表中的第幾個音頻
    ///   - isImmediately: 是否當即播放
    
    func playTheLine(index :Int,isImmediately :Bool){
        
        self.currentItemRemoveObserver()
        self.playType = .PlayTypeLine // 記錄播放類型 以便作出不一樣處理
        
        let record = self.musicArray[index]
        
        guard let url = record.playpath else { return }
        let playUrl = self.loadAudioWithPlaypath(playpath:url )
        
        let playerItem = AVPlayerItem(url: playUrl)
        
        self.playerItem = playerItem
        self.currentUrl = url
        self.isImmediately = isImmediately
        self.currentScenicPoint = record
        self.currentIndex = index
        if !isImmediately {
            self.pause()
        }
        self.player.replaceCurrentItem(with: playerItem)
        
        self.currentItemAddObserver()
    }
複製代碼

實現觀察者方法 根據 playitem 的播放狀態作相應操做 以及 及時更新緩衝進度

/// 觀察者   播放狀態  和  緩衝進度
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        
        let item = object as! AVPlayerItem
        if keyPath == "status" {
            switch item.status {
            case AVPlayerItem.Status.readyToPlay:
                
                if isImmediately {
                    self.play()
                }else{
                    self.setNowPlayingInfo()
                }
            case AVPlayerItem.Status.failed,AVPlayerItem.Status.unknown:
                self.updateCurrentPlayState(state: AVPlayerPlayState.AVPlayerPlayStateNotPlay)
            }
        }else if keyPath == "loadedTimeRanges" {
            
            let array = item.loadedTimeRanges
            
            let timeRange = array.first?.timeRangeValue
            
            guard let start = timeRange?.start , let end = timeRange?.end else {
                return
            }
            
            let startSeconds = CMTimeGetSeconds(start)
            let durationSeconds = CMTimeGetSeconds(end)
            
            let totalBuffer = startSeconds + durationSeconds
            
            let total = self.durantion
            if totalBuffer != 0  && total != 0{
                
                delegate?.updateBufferProgress(progress: Float(totalBuffer) / Float(total))
                print("\(Float(totalBuffer) / Float(total))")
            }
        }
    }
}
複製代碼

這樣 一個url 或者 數組 + 播放序列 就能夠實現基本的播放音頻了 接下能夠寫一下 播放器的四個基本操做

暫停

func pause(){....... player.pause() ..........}
複製代碼

播放

func play(){ .......   self.player.play() ......}
複製代碼

上一首

func next(){ ...... changeTheMusicByIndex .......}
複製代碼

下一首

func previous(){ .......... changeTheMusicByIndex .........}
複製代碼

由於通常音頻的切換會有不少相應的操做須要 好比界面的圖片,文字的替換等等 因此咱們統一下載了一個方法裏

func changeTheMusicByIndex(index : Int){
        self.playTheLine(index: index, isImmediately: true)
        
        delegate?.changeMusicToIndex(index: index)
        //
    }
複製代碼

那麼做爲一個成熟的自定義播放器咱們應該給使用的地方提供哪些回調操做呢?

1 音頻混緩衝進度

2 音頻播放進度

3 音頻切換的相應操做

這三個回調咱們採用代理方式 由於這三個操做通常設計到了 播放界面的單獨操做通常爲 一對一的

protocol WPY_AVPlayerDelegate : class {
    
    func updateProgressWith(progress : Float)
    func changeMusicToIndex(index : Int)
    func updateBufferProgress(progress : Float)
}
複製代碼

4 音頻播放狀態 相對於音頻播放狀態而言,就不必定是一對一了, 例如: 有可能tableView 上的每一個cell中都有試聽 操做

並且個能有不一樣類型的各類播放形式,而後最基本的播放狀態都是要的。因此 咱們對播放狀態的回調採用全局通知的形式

裏面最好帶參數
1 播放類型
2 播放連接 這樣能夠在一個界面有多個播放時來準確改變補個view 的狀態 3 播放相應的狀態類型 (統一管理播放狀態)

如: 暫停, 播放, 結束, 緩衝準備, 播放出錯 case AVPlayerPlayStatePreparing // 準備播放 case AVPlayerPlayStateBeigin // 開始播放 case AVPlayerPlayStatePlaying // 正在播放 case AVPlayerPlayStatePause // 播放暫停 case AVPlayerPlayStateEnd // 播放結束 case AVPlayerPlayStateBufferEmpty // 沒有緩存的數據供播放了 case AVPlayerPlayStateBufferToKeepUp //有緩存的數據能夠供播放 case AVPlayerPlayStateseekToZeroBeforePlay case AVPlayerPlayStateNotPlay // 不能播放 case AVPlayerPlayStateNotKnow // 未知狀況

/// 實時更新播放狀態  全局通知(便於多個地方都用到音頻播放,改變播放狀態)
    ///
    /// - Parameter state: 播放狀態
    
    func updateCurrentPlayState(state : AVPlayerPlayState){
        
        if self.currentUrl != nil {
            
            NotificationCenter.default.post(name: NSNotification.Name(rawValue: WPY_PlayerState), object: nil, userInfo: [WPY_PlayerState : state,CurrentPlayUrl : self.currentUrl!,PlayType : self.playType])
            
        }else {
            
            NotificationCenter.default.post(name: NSNotification.Name(rawValue: WPY_PlayerState), object: nil, userInfo: [WPY_PlayerState : state,CurrentPlayUrl : "",PlayType : self.playType])
        }
    }
複製代碼

至此,一個基本的自定義播放器就宣佈完成了

二 附加功能

1 根據靠近耳朵距離 自由切換外音 和 聽筒 模式

監聽

//監聽是否靠近耳朵
        NotificationCenter.default.addObserver(self, selector: #selector(sensorStateChange), name:UIDevice.proximityStateDidChangeNotification, object: nil)
複製代碼

相應操做

//監測是否靠近耳朵  轉換聲音播放模式
    
    @objc func sensorStateChange() {
        
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
            
            if UIDevice.current.proximityState == true {
                
                //靠近耳朵
                Util_OC.setAVAudioSessionCategory(.playAndRecord)
            }else {
                //遠離耳朵
                Util_OC.setAVAudioSessionCategory(.playback)
            }
        }
    }
複製代碼

由於 swift 4.2 對於ios 10.0 如下 不兼容,因此用了調oc的方法解決

有更好處理方式的歡迎交流

+ (void)setAVAudioSessionCategory:(AVAudioSessionCategory) category {
    
    AVAudioSession *session = [AVAudioSession sharedInstance];
    
    [session setCategory:category withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker error:nil];
}
複製代碼

注意:其實在音頻中止播放後 就不該該有這種操做 (聽筒轉換 屏幕息屏)

因此 咱們應該在 本身的暫停 函數中關掉紅外感應

UIDevice.current.isProximityMonitoringEnabled = false

在播放函數中子打開

UIDevice.current.isProximityMonitoringEnabled = true

2 鎖屏 顯示播放信息

鎖屏顯示播放信息 包括到了狀態 因此 咱們先進行狀態相關操做的時候 ,都應該調用信息設置操做

如 : 暫停 播放 改變進度等

/// 設置鎖屏時 播放中心的播放信息、
    
    func setNowPlayingInfo(){
        
        if (self.playType == .PlayTypeLine || self.playType == .PlayTypeSpecial) && self.currentScenicPoint != nil {
            
           // 1  名字
            var info = Dictionary<String,Any>()
            info[MPMediaItemPropertyTitle] = self.currentScenicPoint?.name ?? ""   
            
            // 2 圖片
            
            if let image = UIImage(named: "AppIcon"){
                info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image:image)//顯示的圖片
            }
            
//            if  let url = self.currentScenicPoint?.pictureArray?.first ,let image = UIImage(named: "AppIcon"){
//                imageView.kf.setImage(with: URL(string:url), placeholder: image, options: nil, progressBlock: nil) { (img, _, _, _) in
//
//                    if
//                    info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image:img)//顯示的圖片
//                }
//            }else{
//
//            }
            
            //3  總時長
            info[MPMediaItemPropertyPlaybackDuration] = self.durantion 
            
            if let duration = self.player.currentItem?.currentTime() {
               info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = CMTimeGetSeconds(duration)
            }
            
            //4 播放速率
            info[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
            
            //最後 設置
            MPNowPlayingInfoCenter.default().nowPlayingInfo = info
        }
    }
複製代碼
3 遠程事件操做

記得進入後臺後開啓接收遠程事件

UIApplication.shared.beginReceivingRemoteControlEvents()

在某些不須要遠程事件是要關掉

UIApplication.shared.endReceivingRemoteControlEvents()

//    //後臺操做   在delegate 或者 某個VC 中初始化
    
    //    override func remoteControlReceived(with event: UIEvent?) {
    //        guard let event = event else {
    //            print("no event\n")
    //            return
    //        }
    //
    //        if event.type == UIEventType.remoteControl {
    //            switch event.subtype {
    //            case .remoteControlTogglePlayPause:
    //                print("暫停/播放")
    //
    //            case .remoteControlPreviousTrack:
    //                print("上一首")
    //                self.previous()
    //            case .remoteControlNextTrack:
    //                print("下一首")
    //                self.next()
    //            case .remoteControlPlay:
    //                print("播放")
    //               self.play()
    //            case .remoteControlPause:
    //                print("暫停")
    //                self.pause()
    //            default:
    //                break
    //            }
    //        }
    //    }
    //
複製代碼
4 改變播放速度

設置 playSpeed 屬性用於記錄 改變的播放速率 由於有多是暫停狀態下改的播放速率,不能及時生效。因此要記錄一下

也真由於如此,因此播放時要及時更新下播放速率

self.enableAudioTracks(enable: true, playerItem: self.playerItem!)

注意: 暫停是調用此方法會直接播放,因此要放在播放時再調用

//設置播放速率
    func setPlaySpeed(playSpeed:Float) {
        
        if self.isPlay{
            self.enableAudioTracks(enable: true, playerItem: self.playerItem!)
            self.player.rate = playSpeed;
        }
        self.playSpeed = playSpeed
    }
複製代碼

/// 改變播放速率 必實現的方法

///
    /// - Parameters:
    ///   - enable:
    ///   - playerItem: 當前播放
    func enableAudioTracks(enable:Bool,playerItem : AVPlayerItem){
        
        for track : AVPlayerItemTrack in playerItem.tracks {
            
            if track.assetTrack?.mediaType == AVMediaType.audio {
                
                track.isEnabled = enable
            }
        }
    }
複製代碼
5 判斷網絡狀態 詢問是否播放

這個通常的網絡庫中都有網絡狀態的判斷,那麼應該在哪裏進行此操做呢

最合理的地方應該是 播放 方法裏面。由於在此能夠最大限度的控制流量,即便播到一半暫停 網絡變化後也能夠及時終止

三 須要注意的問題

1 進度條問題

進度條有兩個改變

1 隨着音頻播放,逐漸改變。 2 手動調整位置,調整播放進度

可是這個連個問題會存在交叉問題,即在手動調整進度是若是音頻播放不停,並且進度回調也一直在走,那麼你會發現進度條在拉的時候是在跳動。

解決方案: 因此 在手動拉進度時,應該停掉音頻播放的進度回調,在手動進度結束時,根據進度播放器把音頻跳到指定位置播放,同時恢復音頻進度回調

2 時間進度問題
- (NSString *)timeFormatted:(int)totalSeconds {
    int seconds = totalSeconds % 60;
    int minutes = (totalSeconds / 60);
    return [NSString stringWithFormat:@"%02d:%02d", minutes, seconds];
}
複製代碼

時間通常爲兩個 一個是當前時間 另外一個是剩餘時間或者總時間

這裏就須要將avplayer 的時間 CTime 轉換爲字符串

object-C 的相對來講比較好處理

//視頻的總長度 NSTimeInterval total = CMTimeGetSeconds(self.player.currentItem.duration); 直接取值轉化字符串就好

問題是 swift

由於swift對類型要求比較嚴格,因此要進行類型轉換。這時候你會發如今進行時間賦值是會崩潰

緣由:由於 swift 是不會有默認值的。有時音頻數據沒取回,有可能就已經有賦值操做。

因此 咱們在進行賦值操做前 要進行判斷

if self.isNaN || self.isInfinite {
            
            return "00:00"
        }
複製代碼
目前想到的差很少就這些,歡迎指正交流。
相關文章
相關標籤/搜索