iOS HLS Cache 實踐方法探究之旅
使用 AVPlayer 播放 m3u8 串流影音檔時如何做到邊播放邊 Cache 的功能
photo by Mihis Alex
[2023/03/12] Update
- 下篇「 AVPlayer 實踐本地 Cache 功能大全 」教您實現 AVPlayer Caching
我將之前的實作開源了,有需求的朋友可直接使用。
- 客製化 Cache 策略,可以用 PINCache or 其他…
- 外部只需呼叫 make AVAsset 工廠,帶入 URL,則 AVAsset 就能支援 Caching
- 使用 Combine 實現 Data Flow 策略
- 寫了一些測試
關於
HTTP Live Streaming (簡稱HLS) 是蘋果提出基於HTTP的串流媒體網絡傳輸協議。
以播放音樂來說,非串流情況下我們使用 mp3 作為音樂檔,這個檔案有多大就要花多久時間全部下載下來才能播放;而 HLS 就是把一個檔案分割成多個小檔案,讀到哪播到哪,所以拿到第一個分割區塊就能開始播放,不用整個都下載完!
.m3u8
檔就是紀錄這些分割的 .ts
小檔案的碼率、播放順序、時間 還有整個音訊的資訊,另外也可以做加解密保護、低延遲直播…等等
.m3u8
檔範例(aviciiwakemeup.m3u8):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:9.900411,
aviciiwakemeup–00001.ts
#EXTINF:9.900400,
aviciiwakemeup–00002.ts
#EXTINF:9.900411,
aviciiwakemeup–00003.ts
#EXTINF:9.900411,
.
.
.
#EXTINF:6.269389,
aviciiwakemeup-00028.ts
#EXT-X-ENDLIST
*EXT-X-ALLOW-CACHE 已在 iOS≥ 8/Protocol Ver.7 deprecated ,有沒有這行都沒有用意義了。
目標
對於一個影音串流服務, Cache 非常之重要 ;因為每個音訊檔案小則 MB 大則幾 GB ,如果每次重播都要再從伺服器拉一次檔案,對 Server 的 Loading 來說非常吃力,而且流量都是 \(\) ,如果有個 Cache 層能為服務節省許多金錢,對使用者來說也不用浪費網路、浪費時間重新下載;是一個雙贏的機制 (但要記得設定上限/定時清除,避免把使用者的設備塞爆)。
問題
以往非串流時 mp3/mp4 沒什麼好處理的,就是在播放前先下載到設備上,下載完成才開始播放;反正不管怎樣都要載完才能播,那不如我們自己用 URLSession 下載完檔案後再餵 file:// 下載在本地的檔案路徑給 AVPlayer 做播放即可;或正規方式,使用 AVAssetResourceLoaderDelegate 在 Delegate 方法中對下載的資料進行 Cache 緩存。
遇到串流想法其實也很直白,就是先讀 .m3u8
檔,然後在解析裡面的資訊,對每個 .ts
檔做 Cache 即可;但實作發現事情沒有這麼簡單,處理難度超乎我的想像,所以才會有此篇文章!
播放部分我們一樣直接使用 iOS AVFoundation 的 AVPlayer,在操作上串流/非串流檔案沒有差異。
Example:
1
2
3
let url:URL = URL(string:"https://zhgchg.li/aviciiwakemeup.m3u8")
var player: AVPlayer = AVPlayer(url: url)
player.play()
2021–01–05 更新:
我們退而求其次退回去使用 mp3 檔,這樣就能直接使用 AVAssetResourceLoaderDelegate
進行實作,詳細實作可參考「 AVPlayer 邊播邊 Cache 實戰 」。
實踐方案
針對我們的目標能達成的幾個方案及實踐時遇到的問題。
方案 1. AVAssetResourceLoaderDelegate ❌
第一個想法就是,那我們就照 mp3/mp4 時的做法就好啦!一樣用 AVAssetResourceLoaderDelegate 在 Delegate 方法中緩存 .ts
檔案。
不過很抱歉,此路不通,因為無法在 Delegate 中攔截到 .ts
檔案的下載請求資訊,可以在這則 問答 和 官方文件 上確切此事。
AVAssetResourceLoaderDelegate 實作可參考「 AVPlayer 邊播邊 Cache 實戰 」。
方案 2.1 URLProtocol 攔截請求 ❌
URLProtocol 也是最近才學到的方法,所有基於 URL Loading System
的請求 (URLSession、Call API、下載圖片…) 都可以被我們攔截下來修改 Request、Response 然後再返回,一切就像沒發生一樣,偷偷來;關於 URLProtocol 可以參考 此篇文章 。
應用此方法,我們打算攔截 AVFoundation AVPlayer 在要求 .m3u8
、 .ts
的請求時,攔截下來然後如果本地有 Cache 就直接返回 Cache Data,沒有則再真的再發 Request 出去;這樣也能達到我們的目標。
一樣,很抱歉,此路也不通;因為 AVFoundation AVPlayer 的請求不是在 URL Loading System
上,我們無從攔截。 *有一說是 模擬器上可以但實機上不行
方案 2.2 暴力讓他能進 URLProtocol ❌
根據 方案 2.1 腦洞大開的暴力法,如果我把請求網址換成一個自訂的 Scheme (EX: streetVoiceCache://),因 AVFoundation 無法處理這個請求,所以會丟出來,這樣我們的 URLProtocol 就能攔截到,做我們想做的事。
1
2
3
let url:URL = URL(string:"streetVoiceCache://zhgchg.li/aviciiwakemeup.m3u8?originSchme=https")
var player: AVPlayer = AVPlayer(url: url)
player.play()
URLProtocol 會攔截到 streetVoiceCache://zhgchg.li/aviciiwakemeup.m3u8?originSchme=https
,這時我們只要幫他還原成原來的網址,然後發個 URLSession 去要資料就能在這邊自己做 Cache;m3u8 中的 .ts
檔案請求一樣也會被 URLProtocol 攔截到,一樣我們能在這自己做 Cache。
一切看似都那麼完美,但當我興高采烈的 Build-Run 完 APP 後,蘋果直接搧了我一巴掌:
Error: 12881 “CoreMediaErrorDomain custom url not redirect”
他不吃我給 .ts
檔案 Request 的 Response Data,我只能用 urlProtocol:wasRedirectedTo
這個方法 redirectTo 原始 Https 請求才能正常播放,即使我把 .ts
檔案下載到本地然後 redirectTo 那個 file:// 檔案;他也不接受,查 官方論壇 得到答案就是不能這樣做; .m3u8
只能是來源於 Http/Https (所以即使你把整個 .m3u8
還有所有分割檔 .ts
都放在本地,有無法使用 file:// 給 AVPlayer播放),另外 .ts
也不能使用 URLProtocol 自行給予 Data。
fxxk…
方案 2.2–2 同方案 2.2 但是搭配 方案 1 AVAssetResourceLoaderDelegate 來實現 ❌
實作方式如方案 2.2 ,餵給 AVPlayer 自訂的 Scheme 讓他進 AVAssetResourceLoaderDelegate;然後我們在自己處理。
同 2.2 結果:
Error: 12881 “CoreMediaErrorDomain custom url not redirect”
官方論壇 同樣的回答。
可以拿來做解密處理(可以參考 此篇文章 或 此範例 )但還是無法實現 Cache 功能。
方案 3. Reverse Proxy Server ⍻ (可行,但非完美)
這個方法是在找如何處理 HLS Cache 時,最多人給的答案;就是在 APP 上起一個 HTTP Server 做 Reverse Proxy Server 服務。
原理也很簡單,APP 上 On 一個 HTTP Server 假設是 8080 Port,網址就會是 http://127.0.0.1:8080/
;然後我們可以對連進來的 Request 做處理,給出 Response。
套用到我們的案例就是,把請求網址換成: http://127.0.0.1:8080/aviciiwakemeup.m3u8?origin=http://zhgchg.li/
在 HTTP Server 的 Handler 上對 *.m3u8
攔截處理,這時有 Request 進來就會進到我們的 Handler 中,看我們想幹嘛就幹嘛,想 Response 什麼 Data 都是我們自己控制, .ts
檔同樣會進來;這邊就可以做我們想做的 Cache 機制。
對 AVPlayer 來說就是個 http://.m3u8 的標準串流音訊檔,所以不會有任何問題。
完整實作範例可參考:
因為我也是參考此範例做的,所以 Local HTTP Server 的部分我也是使用 GCDWebServer ,另外還有更新的 Telegraph 可以使用。( CocoaHttpServer 太久沒更新就不推薦用了)
看起來不錯!但有個問題:
我們的服務是音樂串流而非影音播放平台,音樂串流很多時候使用者都是在背景執行音樂切換的;這時候 Local HTTP Server 還會在??
GCDWebServer 的說明是當進入背景時會自動斷線、回前景自動恢復,但可以透過設置參數 GCDWebServerOption_AutomaticallySuspendInBackground:false
不讓他有這個機制。
但是實測如果一段時間沒有發送請求 Server 還是會斷線 (且狀態會是錯的,還是 isRunning) 感覺就是被系統砍了;深掘了 HTTP Server 的做法 後發現底層都是基於 socket,查了 官方對 socket 服務的文件 後,此缺陷是無法解決的,本來在背景下沒有新的連接時就會被系統暫停。
*網路上有找到很繞的方法…就是發個長請求、或不斷發空的請求確保 Server 在背景不會被系統暫停掉。
以上都是針對 APP 在背景的狀況,在前景時 Server 很穩,也不會因為閒置被暫停,沒這問題!
是說畢竟是依賴在其他服務上,開發環境測試沒問題,實際應用也建議要接個 rollback 處理(AVPlayer.AVPlayerItemFailedToPlayToEndTimeErrorKey 通知);否則有個萬一服務掛掉,使用者會卡死。
所以說不完美啊…
方案 4. 使用 HTTP Client 本身的 caching 機制 ❌
我們的 .m3u8/.ts
檔的 Response Headers 都有給予 Cache-Control
、 Age
、 eTag
… 這些 HTTP Client Cache 資訊;我們的網站 Cache 機制在 Chrome 上使用也完全沒問題,另外也在官方新的針對 Protocol Extension for Low-Latency HLS (低延遲HLS) 初步規格文件中提到 Cache 的地方也看到可以設定 cache-control headers 來做緩存。
但實際 AVFoundation AVPlayer 並沒有任何 HTTP Client Caching 效果,此路也不通!單純癡人說夢。
方案 5. 不使用 AVFoundation AVPlayer 播放音訊檔 ✔
自己實現音訊檔解析、緩存、編碼、播放功能。
太硬核了,需要很深的技術能力及大量時間;沒研究。
附上一個網路開源播放器做參考: FreeStreamer ,真要選擇此方案不如站在巨人的肩膀上,直接用第三方套件了。
方案 5–1. 不使用 HLS
同方案 5 , 太硬核了,需要很深的技術能力及大量時間;沒研究。
方案 6. 將 .ts 分割檔轉成 .mp3/.mp4 檔案 ✔
沒研究,但的確可行;不過想起來就覺得複雜,要處理已下載的 .ts
檔案,個別轉成 .mp3 或 .mp4 檔案然後照順序播放、或是壓縮成一個檔案什麼的,想起來就不太好做。
有興趣可參考 此篇文章 。
方案 7. 下載完整檔案後再播放 ⍻
這個方法不能確切叫邊播邊 Cache,實際是載下整個音訊檔案的內容,然後才開始播放;如果是 .m3u8
如同方案 2.2 提到的,不能直接載下來放在本地播放。
要實作的話要用到 iOS ≥ 10 的 API AVAssetDownloadTask.makeAssetDownloadTask
,實際會將 . m3u8
打包成 .movpkg
放在本地,供使用者播放。
這邊比較像是做離線播放而非做 Cache 的功能。
另外使用者也能從「設定」->「一般」->「iPhone 儲存空間」-> APP 中查看、管理已下載打包的音訊檔案。
下方 已下載的影片 部分
詳細實作可參考此範例:
結語
以上的探索路程大概花了快一整週,繞來繞去、快要喪心病狂了;目前還沒有一個可靠的、容易部署的方法。
如果有新的想法再來更新!
參考資料
有任何問題及指教歡迎 與我聯絡 。
===
View the English version of this article here.
本文首次發表於 Medium ➡️ 前往查看