iOS開發 - AVPlayer實現流音頻邊播邊存

邊播邊下有三套左右實現思路,本文使用AVPlayer + AVURLAsset實現。git

概述

1. AVPlayer簡介

  • AVPlayer存在於AVFoundation中,能夠播放視頻和音頻,能夠理解爲一個隨身聽
  • AVPlayer的關聯類:
    • AVAsset:一個抽象類,不能直接使用,表明一個要播放的資源。能夠理解爲一個磁帶子類AVURLAsset是根據URL生成的包含媒體信息的資源對象。咱們就是要經過這個類的代理實現音頻的邊播邊下的
    • AVPlayerItem:能夠理解爲一個裝在磁帶盒子裏的磁帶

2. AVPlayer播放原理

  • 給播放器設置好想要它播放的URL
  • 播放器向URL所在的服務器發送請求,請求兩個東西
    • 所需音頻片斷的起始offset
    • 所需的音頻長度
  • 服務器根據請求的內容,返回數據
  • 播放器拿到數據拼裝成文件
  • 播放器從拼裝好的文件中,找出如今須要播放的片斷,進行播放

3. 邊播邊下的原理

實現邊下邊播,其實就是手動實現AVPlayer的上列播放過程。github

  • 當播放器須要預先緩存一些數據的時候,不讓播放器直接向服務器發起請求,而是向咱們本身寫的某個類(暫且稱之爲播放器的祕書)發起緩存請求
  • 祕書根據播放器的緩存請求的請求內容,向服務器發起請求。
  • 服務器返回祕書所需的數據
  • 祕書把服務器返回的數據寫進本地的緩存文件
  • 當須要播放某段聲音的時候,向祕書發出播放請求索要這段音頻文件
  • 祕書從本地的緩存文件中找到播放器播放請求所需片斷,返回給播放器
  • 播放器拿到數據開心滴播放
  • 當整首歌都緩存完成之後,祕書須要把緩存文件拷貝一份,改個名字,這個文件就是咱們所須要的本地持久化文件
  • 下次播放器再播放歌曲的時候,先判斷下本地有木有這個名字的文件,有則播放本地文件,木有則向祕書要數據

技術實現

OK,邊播邊下的原理知道了,咱們能夠正式寫代碼了~建議先從文末連接處把Demo下載下來,對着Demo我們慢慢道來~swift

1. 類

共須要三個類:數組

  • MusicPlayerManagerCEO。單例,負責整個工程全部的播放、暫停、下一曲、結束、判斷應該播放本地文件仍是從服務器拉數據之類的事情
  • RequestLoader:就是上文所說的祕書,負責給播放器提供播放所需的音頻片斷,以及找人向服務器索要數據
  • RequestTask祕書的小弟。負責和服務器鏈接、向服務器請求數據、把請求回來的數據寫到本地緩存文件、把寫完的緩存文件移到持久化目錄去。全部髒活累活都是他作。

2. 方法

先從小弟提及緩存

2.1. RequestTask

2.1.0. 概說

如上文所說,小弟是負責作髒活累活的。 負責和服務器鏈接、向服務器請求數據、把請求回來的數據寫到本地緩存文件、把寫完的緩存文件移到持久化目錄去服務器

2.1.1. 初始化音頻文件持久化文件夾 & 緩存文件

private func _initialTmpFile() { do { try NSFileManager.defaultManager().createDirectoryAtPath(StreamAudioConfig.audioDicPath, withIntermediateDirectories: true, attributes: nil) } catch { print("creat dic false -- error:\(error)") } if NSFileManager.defaultManager().fileExistsAtPath(StreamAudioConfig.tempPath) { try! NSFileManager.defaultManager().removeItemAtPath(StreamAudioConfig.tempPath) } NSFileManager.defaultManager().createFileAtPath(StreamAudioConfig.tempPath, contents: nil, attributes: nil) }

2.1.2. 與服務器創建鏈接請求數據

/** 鏈接服務器,請求數據(或拼range請求部分數據)(此方法中會將協議頭修改成http) - parameter offset: 請求位置 */ public func set(URL url: NSURL, offset: Int) { func initialTmpFile() { try! NSFileManager.defaultManager().removeItemAtPath(StreamAudioConfig.tempPath) NSFileManager.defaultManager().createFileAtPath(StreamAudioConfig.tempPath, contents: nil, attributes: nil) } _updateFilePath(url) self.url = url self.offset = offset // 若是創建第二次請求,則需初始化緩衝文件 if taskArr.count >= 1 { initialTmpFile() } // 初始化已下載文件長度 downLoadingOffset = 0 // 把stream://xxx的頭換成http://的頭 let actualURLComponents = NSURLComponents(URL: url, resolvingAgainstBaseURL: false) actualURLComponents?.scheme = "http" guard let URL = actualURLComponents?.URL else {return} let request = NSMutableURLRequest(URL: URL, cachePolicy: NSURLRequestCachePolicy.ReloadIgnoringCacheData, timeoutInterval: 20.0) // 若非從頭下載,且視頻長度已知且大於零,則下載offset到videoLength的範圍(拼request參數) if offset > 0 && videoLength > 0 { request.addValue("bytes=\(offset)-\(videoLength - 1)", forHTTPHeaderField: "Range") } connection?.cancel() connection = NSURLConnection(request: request, delegate: self, startImmediately: false) connection?.setDelegateQueue(NSOperationQueue.mainQueue()) connection?.start() }

2.1.3. 響應服務器的Response頭

public func connection(connection: NSURLConnection, didReceiveResponse response: NSURLResponse) { isFinishLoad = false guard response is NSHTTPURLResponse else {return} // 解析頭部數據 let httpResponse = response as! NSHTTPURLResponse let dic = httpResponse.allHeaderFields let content = dic["Content-Range"] as? String let array = content?.componentsSeparatedByString("/") let length = array?.last // 拿到真實長度 var videoLength = 0 if Int(length ?? "0") == 0 { videoLength = Int(httpResponse.expectedContentLength) } else { videoLength = Int(length!)! } self.videoLength = videoLength //TODO: 此處須要修改成真實數據格式 - 從字典中取 self.mimeType = "video/mp4" // 回調 recieveVideoInfoHandler?(task: self, videoLength: videoLength, mimeType: mimeType!) // 鏈接加入到任務數組中 taskArr.append(connection) // 初始化文件傳輸句柄 fileHandle = NSFileHandle.init(forWritingAtPath: StreamAudioConfig.tempPath) }

2.1.4. 處理服務器返回的數據 - 寫入緩存文件中

public func connection(connection: NSURLConnection, didReceiveData data: NSData) { // 尋址到文件末尾 self.fileHandle?.seekToEndOfFile() self.fileHandle?.writeData(data) self.downLoadingOffset += data.length self.receiveVideoDataHandler?(task: self) // print("線程 - \(NSThread.currentThread())") // 注意,這裏用子線程有問題 let queue = dispatch_queue_create("com.azen.taskConnect", DISPATCH_QUEUE_SERIAL) dispatch_async(queue) { // // 尋址到文件末尾 // self.fileHandle?.seekToEndOfFile() // self.fileHandle?.writeData(data) // self.downLoadingOffset += data.length // self.receiveVideoDataHandler?(task: self) // let thread = NSThread.currentThread() // print("線程 - \(thread)") }

2.1.5. 服務器文件返回完畢,把緩存文件放入持久化文件夾

public func connectionDidFinishLoading(connection: NSURLConnection) { func tmpPersistence() { isFinishLoad = true let fileName = url?.lastPathComponent // let movePath = audioDicPath.stringByAppendingPathComponent(fileName ?? "undefine.mp4") let movePath = StreamAudioConfig.audioDicPath + "/\(fileName ?? "undefine.mp4")" _ = try? NSFileManager.defaultManager().removeItemAtPath(movePath) var isSuccessful = true do { try NSFileManager.defaultManager().copyItemAtPath(StreamAudioConfig.tempPath, toPath: movePath) } catch { isSuccessful = false print("tmp文件持久化失敗") } if isSuccessful { print("持久化文件成功!路徑 - \(movePath)") } } if taskArr.count < 2 { tmpPersistence() } receiveVideoFinishHanlder?(task: self) }

其餘

其餘方法包括斷線重連以及公開一個cancel方法cancel掉和服務器的鏈接session

2.2. RequestTask

2.2.0. 概說

祕書要乾的最主要的事情就是響應播放器老大的號令,全部方法都是圍繞着播放器老大來的。祕書須要遵循AVAssetResourceLoaderDelegate協議才能被錄用。app

2.2.1. 代理方法,播放器須要緩存數據的時候,會調這個方法

這個方法實際上是播放器在說:小祕呀,我想要這段音頻文件。你能如今給我仍是等等給我啊?
必定要返回:true,告訴播放器,我等等給你。
而後,立馬找本地緩存文件裏有木有這段數據,有把數據拿給播放器,若是木有,則派祕書的小弟向服務器要。
具體實現代碼有點多,這裏就不所有貼出來了。能夠去看看文末的Demo記得賞顆星喲~async

/** 播放器問:是否應該等這requestResource加載完再說? 這裏會出現不少個loadingRequest請求, 須要爲每一次請求做出處理 - parameter resourceLoader: 資源管理器 - parameter loadingRequest: 每一小塊數據的請求 - returns: <#return value description#> */ public func resourceLoader(resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool { // 添加請求到隊列 pendingRequset.append(loadingRequest) // 處理請求 _dealWithLoadingRequest(loadingRequest) print("----\(loadingRequest)") return true }

2.2.2. 代理方法,播放器關閉了下載請求

/** 播放器關閉了下載請求 播放器關閉一箇舊請求,都會發起一到多個新請求,除非已經播放完畢了 - parameter resourceLoader: 資源管理器 - parameter loadingRequest: 待關請求 */ public func resourceLoader(resourceLoader: AVAssetResourceLoader, didCancelLoadingRequest loadingRequest: AVAssetResourceLoadingRequest) { guard let index = pendingRequset.indexOf(loadingRequest) else {return} pendingRequset.removeAtIndex(index) }

2.3. MusicPlayerManager

2.3.0. 概說

負責調度全部播放器的,負責App中的一切涉及音頻播放的事件
唔。。犯個小懶。。代碼直接貼上來咯~要趕不上樓下的538路公交啦~~謝謝你們體諒哦~ide

public class MusicPlayerManager: NSObject { // public var status public var currentURL: NSURL? { get { guard let currentIndex = currentIndex, musicURLList = musicURLList where currentIndex < musicURLList.count else {return nil} return musicURLList[currentIndex] } } /**播放狀態,用於須要獲取播放器狀態的地方KVO*/ public var status: ManagerStatus = .Non /**播放進度*/ public var progress: CGFloat { get { if playDuration > 0 { let progress = playTime / playDuration return progress } else { return 0 } } } /**已播放時長*/ public var playTime: CGFloat = 0 /**總時長*/ public var playDuration: CGFloat = CGFloat.max /**緩衝時長*/ public var tmpTime: CGFloat = 0 public var playEndConsul: (()->())? /**強引用控制器,防止被銷燬*/ public var currentController: UIViewController? // private status private var currentIndex: Int? private var currentItem: AVPlayerItem? { get { if let currentURL = currentURL { let item = getPlayerItem(withURL: currentURL) return item } else { return nil } } } private var musicURLList: [NSURL]? // basic element public var player: AVPlayer? private var playerStatusObserver: NSObject? private var resourceLoader: RequestLoader = RequestLoader() private var currentAsset: AVURLAsset? private var progressCallBack: ((tmpProgress: Float?, playProgress: Float?)->())? public class var sharedInstance: MusicPlayerManager { struct Singleton { static let instance = MusicPlayerManager() } // 後臺播放 let session = AVAudioSession.sharedInstance() do { try session.setActive(true) } catch { print(error) } do { try session.setCategory(AVAudioSessionCategoryPlayback) } catch { print(error) } return Singleton.instance } public enum ManagerStatus { case Non, LoadSongInfo, ReadyToPlay, Play, Pause, Stop } } // MARK: - basic public funcs extension MusicPlayerManager { /** 開始播放 */ public func play(musicURL: NSURL?) { guard let musicURL = musicURL else {return} if let index = getIndexOfMusic(music: musicURL) { // 歌曲在隊列中,則按順序播放 currentIndex = index } else { putMusicToArray(music: musicURL) currentIndex = 0 } playMusicWithCurrentIndex() } public func play(musicURL: NSURL?, callBack: ((tmpProgress: Float?, playProgress: Float?)->())?) { play(musicURL) progressCallBack = callBack } public func next() { currentIndex = getNextIndex() playMusicWithCurrentIndex() } public func previous() { currentIndex = getPreviousIndex() playMusicWithCurrentIndex() } /** 繼續 */ public func goOn() { player?.rate = 1 } /** 暫停 - 可繼續 */ public func pause() { player?.rate = 0 } /** 中止 - 沒法繼續 */ public func stop() { endPlay() } } // MARK: - private funcs extension MusicPlayerManager { private func putMusicToArray(music URL: NSURL) { if musicURLList == nil { musicURLList = [URL] } else { musicURLList!.insert(URL, atIndex: 0) } } private func getIndexOfMusic(music URL: NSURL) -> Int? { let index = musicURLList?.indexOf(URL) return index } private func getNextIndex() -> Int? { if let musicURLList = musicURLList where musicURLList.count > 0 { if let currentIndex = currentIndex where currentIndex + 1 < musicURLList.count { return currentIndex + 1 } else { return 0 } } else { return nil } } private func getPreviousIndex() -> Int? { if let currentIndex = currentIndex { if currentIndex - 1 >= 0 { return currentIndex - 1 } else { return musicURLList?.count ?? 1 - 1 } } else { return nil } } /** 從頭播放音樂列表 */ private func replayMusicList() { guard let musicURLList = musicURLList where musicURLList.count > 0 else {return} currentIndex = 0 playMusicWithCurrentIndex() } /** 播放當前音樂 */ private func playMusicWithCurrentIndex() { guard let currentURL = currentURL else {return} // 結束上一首 endPlay() player = AVPlayer(playerItem: getPlayerItem(withURL: currentURL)) observePlayingItem() } /** 本地不存在,返回nil,不然返回本地URL */ private func getLocationFilePath(url: NSURL) -> NSURL? { let fileName = url.lastPathComponent let path = StreamAudioConfig.audioDicPath + "/\(fileName ?? "tmp.mp4")" if NSFileManager.defaultManager().fileExistsAtPath(path) { let url = NSURL.init(fileURLWithPath: path) return url } else { return nil } } private func getPlayerItem(withURL musicURL: NSURL) -> AVPlayerItem { if let locationFile = getLocationFilePath(musicURL) { let item = AVPlayerItem(URL: locationFile) return item } else { let playURL = resourceLoader.getURL(url: musicURL)! // 轉換協議頭 let asset = AVURLAsset(URL: playURL) currentAsset = asset asset.resourceLoader.setDelegate(resourceLoader, queue: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) let item = AVPlayerItem(asset: asset) return item } } private func setupPlayer(withURL musicURL: NSURL) { let songItem = getPlayerItem(withURL: musicURL) player = AVPlayer(playerItem: songItem) } private func playerPlay() { player?.play() } private func endPlay() { status = ManagerStatus.Stop player?.rate = 0 removeObserForPlayingItem() player?.replaceCurrentItemWithPlayerItem(nil) resourceLoader.cancel() currentAsset?.resourceLoader.setDelegate(nil, queue: nil) progressCallBack = nil resourceLoader = RequestLoader() playDuration = 0 playTime = 0 playEndConsul?() player = nil } } extension MusicPlayerManager { public override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) { guard object is AVPlayerItem else {return} let item = object as! AVPlayerItem if keyPath == "status" { if item.status == AVPlayerItemStatus.ReadyToPlay { status = .ReadyToPlay print("ReadyToPlay") let duration = item.duration playerPlay() print(duration) } else if item.status == AVPlayerItemStatus.Failed { status = .Stop print("Failed") stop() } } else if keyPath == "loadedTimeRanges" { let array = item.loadedTimeRanges guard let timeRange = array.first?.CMTimeRangeValue else {return} // 緩衝時間範圍 let totalBuffer = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration) // 當前緩衝長度 tmpTime = CGFloat(tmpTime) print("共緩衝 - \(totalBuffer)") let tmpProgress = tmpTime / playDuration progressCallBack?(tmpProgress: Float(tmpProgress), playProgress: nil) } } private func observePlayingItem() { guard let currentItem = self.player?.currentItem else {return} // KVO監聽正在播放的對象狀態變化 currentItem.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.New, context: nil) // 監聽player播放狀況 playerStatusObserver = player?.addPeriodicTimeObserverForInterval(CMTimeMake(1, 1), queue: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), usingBlock: { [weak self] (time) in guard let `self` = self else {return} // 獲取當前播放時間 self.status = .Play let currentTime = CMTimeGetSeconds(time) let totalTime = CMTimeGetSeconds(currentItem.duration) self.playDuration = CGFloat(totalTime) self.playTime = CGFloat(currentTime) print("current time ---- \(currentTime) ---- tutalTime ---- \(totalTime)") self.progressCallBack?(tmpProgress: nil, playProgress: Float(self.progress)) if totalTime - currentTime < 0.1 { self.endPlay() } }) as? NSObject // 監聽緩存狀況 currentItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: NSKeyValueObservingOptions.New, context: nil) } private func removeObserForPlayingItem() { guard let currentItem = self.player?.currentItem else {return} currentItem.removeObserver(self, forKeyPath: "status") if playerStatusObserver != nil { player?.removeTimeObserver(playerStatusObserver!) playerStatusObserver = nil } currentItem.removeObserver(self, forKeyPath: "loadedTimeRanges") } } public struct StreamAudioConfig { static let audioDicPath: String = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true).last! + "/streamAudio" // 緩衝文件夾 static let tempPath: String = audioDicPath + "/temp.mp4" // 緩衝文件路徑 - 非持久化文件路徑 - 當前邏輯下,有且只有一個緩衝文件 }

iOS音頻邊播邊下Demo,戳這裏~

 

本人學習收藏

文/Azen(簡書做者) 原文連接:http://www.jianshu.com/p/4f586d63a532 著做權歸做者全部,轉載請聯繫做者得到受權,並標註「簡書做者」。
相關文章
相關標籤/搜索