我最近收到一個這樣的需求:我有一個,嗯,東西,上面存放了一些 MP4 檔案,我要在 iOS app 裡頭播放這些檔案 — 聽起來普通不過,隨便哪個 iOS 工程師,都可以馬上告訴你,就用 AVPlayer 實作就是了。
但,因為一些不能夠明說的原因,這台設備上面的 web server,沒辦法處理在 header 中傳遞的 byte range(就是 `Range: bytes=0–100` 這種形式的 HTTP header),你想要抓取某個檔案的某一段資料,必須對某支 cgi,用 GET 參數的形式傳入。
AVPlayer 在播放 MP4 檔案的行為大概是,首先發第一個連線,傳入 byte range,抓取前兩個 bytes,然後從回應的 Content-Range 資訊中知道整部影片總共有多少 bytes,之後再發送多個連線,抓取不同的範圍,再把整個檔案組合起來,如果你的 server 不支援 byte range,基本上 AVPlayer 就不播了。你應該不會為了連線方式,而想要自己寫一個 player 出來,這樣你還得自己處理 decoding 、音聲同步之類的一大堆問題。
比較簡單的作法,則是在手機這端開一個 local 的 HTTP server 當做 proxy,這個 proxy 可以接收帶 byte range 的 HTTP 連線,然後去設備上抓取資料後,回應給 AVPlayer。這種作法在處理 DRM 加密的影片還頂常見的,像是,你想要在 iOS app 中播放 Widevine 加密的 MPEG DASH 影片,也是透過一個 local 的 HTTP server,轉換成 AVPlayer 支援的Apple HLS 格式。
至於 iOS 上有哪些 Web server library,首選大概就是 GCDWebServer 了。
GCDWebServer 支援很多種不同的回應格式,如果要回應的是一般的 HTML 或是比較小的資料,可以用 GCDWebServerDataResponse:如果要回傳某個放在手機端的檔案,則可以用 GCDWebServerFileResponse;至於要建立某種 socket 連線,就可以用串流形式的 GCDWebServerStreamedResponse。
以我的狀況,雖然是要回應算是影片檔案形式的資料,但是檔案並不在手機上,所以 GCDWebServerFileResponse 顯然不適合;而一部這樣的影片大概有幾十 MB 的大小,如果用 GCDWebServerDataResponse ,就會把幾十 MB 的資料抓下來,整個塞入記憶體中,然後回應,這也不實際。看來只有 GCDWebServerStreamedResponse 是比較好的選擇,但…GCDWebServerStreamedResponse 的文件實在很不清楚。
如果要 GCDWebServer 處理 mp4 結尾的 URL,大概像這樣
這段 code 的意思是,我們先回應一個 response 物件,讓 GCDWebServer 寫入 response header,然後在另一個 block 裡頭,繼續讀取資料,寫入 HTTP response 裡頭。問題就出在:這段讀資料的 block 該怎麼寫。也就是這段:
let response = GCDWebServerStreamedResponse(contentType: “video/mp4”, asyncStreamBlock: { block in
/// Fetch data
})
我一開始以為,整個 asyncStreamBlock 只會被呼叫一次,然橫,這邊傳入的 block 我可以重複呼叫,也就寫成了:
結果讀出來的資料亂七八糟的,而且開了一大堆奇怪的 thread。搞了老半天,原來 asyncStreamBlock 的用法跟我想得完全不一樣,只要在這裡頭,block 被呼叫一次,asyncStreamBlock 也就會被呼叫一次。所以這邊的邏輯得寫成:
說起來就是這樣。不過,為了搞懂到底發生什麼事,也大概把整個 GCDWebServer 的 code 看過一遍。根本問題出在,GCDWebServer 在文件中根本就沒說這幾個 block 到底會被怎樣呼叫啊。