世界上早就有一些優(yōu)秀的 app 視頻播放器挺份,如優(yōu)酷、愛奇藝等欲间,能續(xù)播和下載視頻楚里。想用用不了,于是琢磨著自己實(shí)現(xiàn)一款類似功能的播放器猎贴。僅僅做一款不帶緩存功能播放器班缎,使用 AVPlayerViewController 即可滿足,如若還要自定義界面嘱能,使用 AVFoundation 也是分分鐘的事吝梅,想做緩存功能虱疏,由于系統(tǒng)庫沒有直接支持惹骂,則需要翻倍工作。
方案探索
-
FFmpeg
功能強(qiáng)大做瞪,能滿足復(fù)雜需求对粪,也意味著復(fù)雜,需要掌握視頻編解碼知識以及和 C 語言打交道装蓬。
-
HttpServer
在應(yīng)用內(nèi)搭建一個 HttpServer著拭,Server 再請求視頻資源。說白了是一個 agent牍帚,雖說比 FFmpeg 簡單些儡遮,可是工作量也是不小。
-
ResourceLoader
AVURLAsset 有個 AVAssetResourceLoaderDelegate暗赶,是資源加載的 delgate鄙币,輕量級,適用蹂随。關(guān)于該方案十嘿,可以參考這篇博文,這博文附帶的 demo 有坑(少了一行代碼)。
既然選好了方案岳锁,最好是了解一下相關(guān)背景知識绩衷,包括 AVFoundation、視頻斷點(diǎn)下載的最佳實(shí)現(xiàn)方式以及加載視頻激率。
AVFoundation
AVFoundation 是 iOS 自帶的庫咳燕,從圖可以看出,支持播放音樂乒躺、視頻和動畫效果招盲。往細(xì)里看,關(guān)注 AVAsset聪蘸、AVPlayerItem宪肖、AVPlayer表制、AVPlayerLayer
- AVAsset
一般來說,更加常用的是其子類 AVURLAsset控乾,也可以自定義 AV****Asset么介。該類管理音視頻軌道、格式類型蜕衡,加載視頻等等壤短。 - AVPlayerItem
正如其名,代表著控制播放慨仿,包括播放暫停久脯、快進(jìn)快退等×海總得來說帘撰,管理視頻播放的狀態(tài)。 - AVPlayer
player 負(fù)責(zé)解碼視頻万皿,可以設(shè)置播放速率摧找,可以控制播放暫停,快進(jìn)快退等牢硅,建議把這個職責(zé)交給 AVPlayerItem蹬耘。 - AVPlayerLayer
layer 負(fù)責(zé)渲染視頻,如果不設(shè)置减余,只播放語音综苔。
//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
視頻斷點(diǎn)下載
斷點(diǎn)下載方案有有幾套,需要了解各套方案位岔,從而得出最佳方式如筛。
- NSURLSessionDownloadTask
通過 NSURLSession 生成 Task,執(zhí)行下載任務(wù)赃承。中途可以取消下載妙黍,只需要保存上下文,即可恢復(fù)下載任務(wù)瞧剖。 - Stream
使用文件流來完成下載任務(wù)拭嫁,在配置的時候跳過部分字節(jié),也算是簡單的一種方案抓于。 - Http 頭的 Range 請求頭
在構(gòu)造 Request 時設(shè)置Range即可從某字節(jié)開始下載資源做粤。比起前面兩種方法,不用生產(chǎn) Task捉撮,不用打開關(guān)閉流怕品,更加方便簡單,也不需要存著下載進(jìn)度巾遭,繼續(xù)下載只需要讀取文件肉康,取得長度闯估,設(shè)置 Range 即可,因此使用 Range 是最佳斷點(diǎn)下在發(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刚夺,表明服務(wù)器支持Range 請求,支持的單位是字節(jié)末捣;如果Accept-Ranges: none侠姑,表明服務(wù)器不支持,要用其他方案箩做。值得開心的是莽红,大部分 web 服Range頭域可以請求實(shí)體的一個或者多個子范圍。
設(shè)置 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()
接下來重點(diǎn)關(guān)注 AVAssetResourceLoaderDelegate 的實(shí)現(xiàn)观谦。
AVAssetResourceLoaderDelegate
該 delegate 是連接視頻播放和視頻斷點(diǎn)下載的橋梁。
optional func resourceLoader(
_ resourceLoader: AVAssetResourceLoader,
shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest
) -> Bool
當(dāng)視頻播放器要加載視頻桨菜,通過這個方法發(fā)起一個請求豁状,只要給請求提供返回,就實(shí)現(xiàn)了視頻播放倒得。該接口會被調(diào)用多次泻红,請求不同片段的視頻數(shù)據(jù),應(yīng)當(dāng)保存這些請求霞掺,在請求的數(shù)據(jù)全部響應(yīng)完畢才銷毀該請求谊路。
optional func resourceLoader(
_ resourceLoader:AVAssetResourceLoader,
didCancelLoadingRequest loadingRequest: AVAssetResourceLoadingRequest
)
當(dāng)視頻播放器要取消請求時,相應(yīng)的菩彬,也應(yīng)該停止下載這部分?jǐn)?shù)據(jù)缠劝。通常在拖拽視頻進(jìn)度時調(diào)這方法潮梯。
optional func resourceLoader(
_ resourceLoader: AVAssetResourceLoader,
shouldWaitForRenewalOfRequestedResource renewalRequest: AVAssetResourceRenewalRequest
) -> Bool
當(dāng)視頻播放器播放新的視頻時,需要把之前發(fā)起的請求全部請求惨恭,并發(fā)起新的視頻請求酷麦。
整套方案
- 搭好視頻播放器 UI
提醒的一點(diǎn)是監(jiān)聽播放器的狀態(tài),如視頻可以開始播放等等喉恋。 - 斷點(diǎn)下載
下載的數(shù)據(jù)保存在文件系統(tǒng)沃饶,用 URL 的 MD5 后值為文件名,下次再下載時檢查是否已經(jīng)下載過轻黑,并讀取進(jìn)度糊肤,在向服務(wù)器發(fā)起下載請求。 - ResourceLoader
使用自定義的 scheme 播放視頻氓鄙,保存所有的請求馆揉,并在下載數(shù)據(jù)后響應(yīng)請求,保證每個請求都有合適的響應(yīng)抖拦。
寫在最后
當(dāng)初決定寫一個帶緩存功能的播放器時升酣,覺得是困難的,在那之前态罪,沒有學(xué)習(xí)過視頻相關(guān)的知識噩茄。既然決定要做了,開始翻閱大量資料复颈,感觸最深的是绩聘,逐個擊破。將任務(wù)分解成多個小任務(wù)耗啦,每天只專注于一個凿菩,很快就完成任務(wù)。