世界上早就有一些優(yōu)秀的 app 視頻播放器,如優(yōu)酷、愛奇藝等,能續(xù)播和下載視頻。想用用不了,于是琢磨著自己實現(xiàn)一款類似功能的播放器。僅僅做一款不帶緩存功能播放器,使用 AVPlayerViewController 即可滿足,如若還要自定義界面,使用 AVFoundation 也是分分鐘的事,想做緩存功能,由于系統(tǒng)庫沒有直接支持,則需要翻倍工作。
方案探索
-
FFmpeg
功能強大,能滿足復雜需求,也意味著復雜,需要掌握視頻編解碼知識以及和 C 語言打交道。
-
HttpServer
在應用內(nèi)搭建一個 HttpServer,Server 再請求視頻資源。說白了是一個 agent,雖說比 FFmpeg 簡單些,可是工作量也是不小。
-
ResourceLoader
AVURLAsset 有個 AVAssetResourceLoaderDelegate,是資源加載的 delgate,輕量級,適用。關于該方案,可以參考這篇博文,這博文附帶的 demo 有坑(少了一行代碼)。
既然選好了方案,最好是了解一下相關背景知識,包括 AVFoundation、視頻斷點下載的最佳實現(xiàn)方式以及加載視頻。
AVFoundation

AVFoundation 是 iOS 自帶的庫,從圖可以看出,支持播放音樂、視頻和動畫效果。往細里看,關注 AVAsset、AVPlayerItem、AVPlayer、AVPlayerLayer
- AVAsset
一般來說,更加常用的是其子類 AVURLAsset,也可以自定義 AV****Asset。該類管理音視頻軌道、格式類型,加載視頻等等。 - AVPlayerItem
正如其名,代表著控制播放,包括播放暫停、快進快退等。總得來說,管理視頻播放的狀態(tài)。 - AVPlayer
player 負責解碼視頻,可以設置播放速率,可以控制播放暫停,快進快退等,建議把這個職責交給 AVPlayerItem。 - AVPlayerLayer
layer 負責渲染視頻,如果不設置,只播放語音。
//Demo
let videoURL = NSURL(string: "your video url here")!
let videoAsset = AVURLAsset(URL: videoURL)
let playerItem = AVPlayerItem(asset: videoAsset)
let player = AVPlayer(playerItem: playerItem)
let layer = AVPlayerLayer(player: player)
// add layer to your view
視頻斷點下載
斷點下載方案有有幾套,需要了解各套方案,從而得出最佳方式。
- NSURLSessionDownloadTask
通過 NSURLSession 生成 Task,執(zhí)行下載任務。中途可以取消下載,只需要保存上下文,即可恢復下載任務。 - Stream
使用文件流來完成下載任務,在配置的時候跳過部分字節(jié),也算是簡單的一種方案。 - Http 頭的 Range 請求頭
在構(gòu)造 Request 時設置Range即可從某字節(jié)開始下載資源。比起前面兩種方法,不用生產(chǎn) Task,不用打開關閉流,更加方便簡單,也不需要存著下載進度,繼續(xù)下載只需要讀取文件,取得長度,設置 Range 即可,因此使用 Range 是最佳斷點下在發(fā)方式。
Range 請求頭
發(fā)起一個 Http 請求后,會收到返回,一般來說有以下內(nèi)容。
HTTP/1.0 200 OK
Content-Type: image/png
Content-Length: 36907
Connection: keep-alive
Server: nginx
Accept-Ranges: bytes
看到Accept-Ranges: bytes,表明服務器支持Range 請求,支持的單位是字節(jié);如果Accept-Ranges: none,表明服務器不支持,要用其他方案。值得開心的是,大部分 web 服Range頭域可以請求實體的一個或者多個子范圍。
設置 Range 的值也是非常簡單,key 是 Range,value 是 bytes=XXX,其中 bytes=0-499表示頭500個字節(jié),bytes=-500表示最后500字節(jié),bytes=2000-表示第2000之后的所有字節(jié),同時也可以指定多個范圍,如 bytes=500-1000,1200-1800。
ResourceLoader
使用 AVFoundation 播放音視頻,給AVURLAsset的屬性resourceLoader 指定 delegate 后,在資源的 URL 不能被系統(tǒng)識別時可以自定義視頻加載,如 Lemur://www.lemur.work/player.mov。
let offset: UInt64 = xxx
let url = NSURL(string: urlString)!
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil)
let request = NSMutableURLRequest(URL: url)
request.setValue("bytes=(offset)-", forHTTPHeaderField: "Range")
let task = session.dataTaskWithRequest(request)
task.resume()
接下來重點關注 AVAssetResourceLoaderDelegate 的實現(xiàn)。
AVAssetResourceLoaderDelegate
該 delegate 是連接視頻播放和視頻斷點下載的橋梁。
optional func resourceLoader(
_ resourceLoader: AVAssetResourceLoader,
shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest
) -> Bool
當視頻播放器要加載視頻,通過這個方法發(fā)起一個請求,只要給請求提供返回,就實現(xiàn)了視頻播放。該接口會被調(diào)用多次,請求不同片段的視頻數(shù)據(jù),應當保存這些請求,在請求的數(shù)據(jù)全部響應完畢才銷毀該請求。
optional func resourceLoader(
_ resourceLoader:AVAssetResourceLoader,
didCancelLoadingRequest loadingRequest: AVAssetResourceLoadingRequest
)
當視頻播放器要取消請求時,相應的,也應該停止下載這部分數(shù)據(jù)。通常在拖拽視頻進度時調(diào)這方法。
optional func resourceLoader(
_ resourceLoader: AVAssetResourceLoader,
shouldWaitForRenewalOfRequestedResource renewalRequest: AVAssetResourceRenewalRequest
) -> Bool
當視頻播放器播放新的視頻時,需要把之前發(fā)起的請求全部請求,并發(fā)起新的視頻請求。
整套方案
- 搭好視頻播放器 UI
提醒的一點是監(jiān)聽播放器的狀態(tài),如視頻可以開始播放等等。 - 斷點下載
下載的數(shù)據(jù)保存在文件系統(tǒng),用 URL 的 MD5 后值為文件名,下次再下載時檢查是否已經(jīng)下載過,并讀取進度,在向服務器發(fā)起下載請求。 - ResourceLoader
使用自定義的 scheme 播放視頻,保存所有的請求,并在下載數(shù)據(jù)后響應請求,保證每個請求都有合適的響應。
寫在最后
當初決定寫一個帶緩存功能的播放器時,覺得是困難的,在那之前,沒有學習過視頻相關的知識。既然決定要做了,開始翻閱大量資料,感觸最深的是,逐個擊破。將任務分解成多個小任務,每天只專注于一個,很快就完成任務。