iOS HLS Cache 實踐方法探究之旅
使用 AVPlayer 播放 m3u8 串流影音檔時如何做到邊播放邊 Caching 的功能
ℹ️ℹ️ℹ️ Click here to view the English version of this article, translated by OpenAI.
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 中查看、管理已下載打包的音訊檔案。
下方 已下載的影片 部分
詳細實作可參考此範例:
結語
以上的探索路程大概花了快一整週,繞來繞去、快要喪心病狂了;目前還沒有一個可靠的、容易部署的方法。
如果有新的想法再來更新!
參考資料
有任何問題及指教歡迎 與我聯絡 。
本文首次發表於 Medium ➡️ 前往查看
由 ZMediumToMarkdown 與 Medium-to-jekyll-starter 提供自動轉換與同步技術。