邊播邊下有三套左右實現思路,本文使用AVPlayer + AVURLAsset實現。 概述 1. AVPlayer簡介 AVPlayer存在於AVFoundation中,可以播放視頻和音頻,可以理解為一個隨身聽 AVPlayer的關聯類: AVAsset:一個抽象類,不能直接使用,代表一個要播放的資 ...
邊播邊下有三套左右實現思路,本文使用AVPlayer + AVURLAsset實現。
概述
1. AVPlayer簡介
- AVPlayer存在於AVFoundation中,可以播放視頻和音頻,可以理解為一個隨身聽
- AVPlayer的關聯類:
- AVAsset:一個抽象類,不能直接使用,代表一個要播放的資源。可以理解為一個磁帶子類AVURLAsset是根據URL生成的包含媒體信息的資源對象。我們就是要通過這個類的代理實現音頻的邊播邊下的
- AVPlayerItem:可以理解為一個裝在磁帶盒子里的磁帶
2. AVPlayer播放原理
- 給播放器設置好想要它播放的URL
- 播放器向URL所在的伺服器發送請求,請求兩個東西
- 所需音頻片段的起始offset
- 所需的音頻長度
- 伺服器根據請求的內容,返回數據
- 播放器拿到數據拼裝成文件
- 播放器從拼裝好的文件中,找出現在需要播放的片段,進行播放
3. 邊播邊下的原理
實現邊下邊播,其實就是手動實現AVPlayer的上列播放過程。
- 當播放器需要預先緩存一些數據的時候,不讓播放器直接向伺服器發起請求,而是向我們自己寫的某個類(暫且稱之為播放器的秘書)發起緩存請求
- 秘書根據播放器的緩存請求的請求內容,向伺服器發起請求。
- 伺服器返回秘書所需的數據
- 秘書把伺服器返回的數據寫進本地的緩存文件中
- 當需要播放某段聲音的時候,向秘書發出播放請求索要這段音頻文件
- 秘書從本地的緩存文件中找到播放器播放請求所需片段,返回給播放器
- 播放器拿到數據開心滴播放
- 當整首歌都緩存完成以後,秘書需要把緩存文件拷貝一份,改個名字,這個文件就是我們所需要的本地持久化文件
- 下次播放器再播放歌曲的時候,先判斷下本地有木有這個名字的文件,有則播放本地文件,木有則向秘書要數據
技術實現
OK,邊播邊下的原理知道了,我們可以正式寫代碼了~建議先從文末鏈接處把Demo下載下來,對著Demo咱們慢慢道來~
1. 類
共需要三個類:
- MusicPlayerManager:CEO。單例,負責整個工程所有的播放、暫停、下一曲、結束、判斷應該播放本地文件還是從伺服器拉數據之類的事情
- 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掉和伺服器的連接
2.2. RequestTask
2.2.0. 概說
秘書要乾的最主要的事情就是響應播放器老大的號令,所有方法都是圍繞著播放器老大來的。秘書需要遵循AVAssetResourceLoaderDelegate協議才能被錄用。
2.2.1. 代理方法,播放器需要緩存數據的時候,會調這個方法
這個方法其實是播放器在說:小秘呀,我想要這段音頻文件。你能現在給我還是等等給我啊?
一定要返回:true,告訴播放器,我等等給你。
然後,立馬找本地緩存文件里有木有這段數據,有把數據拿給播放器,如果木有,則派秘書的小弟向伺服器要。
具體實現代碼有點多,這裡就不全部貼出來了。可以去看看文末的Demo記得賞顆星喲~
/**
播放器問:是否應該等這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路公交啦~~謝謝大家體諒哦~
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) ->