Home iOS HLS Cache 實踐方法探究之旅
Post
Cancel

iOS HLS Cache 實踐方法探究之旅

iOS HLS Cache 實踐方法探究之旅

使用 AVPlayer 播放 m3u8 串流影音檔時如何做到邊播放邊 Cache 的功能

photo by [Mihis Alex](https://www.pexels.com/zh-tw/@mcraftpix?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels){:target="_blank"}

photo by Mihis Alex

關於

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-ControlAgeeTag … 這些 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 ➡️ 前往查看

This post is licensed under CC BY 4.0 by the author.

iOS 逆向工程初體驗

打造舒適的 WFH 智慧居家環境,控制家電盡在指尖

Comments powered by Disqus.