唱吧 iOS 音視訊快取處理框架

Single發表於2017-11-30

專案介紹

唱吧 iOS 團隊為了解決音視訊線上播放的快取問題,開發了 KTVHTTPCache 這個框架。設計之初是為了解決音視訊線上播放的快取問題,但其本質是對 HTTP 請求進行快取,對傳輸內容並沒有限制,因此應用場景不限於音視訊線上播放,也可以用於檔案下載、圖片載入、普通網路請求等場景。

技術背景

對於有重度音視訊線上播放需求的應用,快取無疑是必不可少的功能。目前常用的方案有 Local HTTP Server 和 AVAssetResourceLoader 兩種。二者實現及原理雖有不同,但本質都是要 Hook 到播放器資源載入的請求,從而接管資源載入邏輯。根據快取狀態,自行決定是否需要通過網路載入資源。從應用場景的角度看,二者有一個比較大的差異是前者可以搭配任意前端播放器,而後者只能配合 AVPlayer 使用。

個人認為,由於 AVAssetResourceLoader 是黑盒且會干預 AVPlayer 本身的播放邏輯,導致坑多且難排查。並且不同的版本之間會有行為差異(例如近期發現在最新的 iOS 11 系統中,原本工作正常的程式碼,因為一個細小的行為變化,引發了一個 Bug),去適配它的邏輯會有不小的工作量。相反 Local HTTP Server 是完全 Open Source,我們能夠全面接管資源載入邏輯,可以儘可能的規避快取策略的引入帶來的風險。

功能特點

  • 支援相同 URL 併發操作且執行緒安全。
  • 全路徑 Log,支援控制檯列印和輸出到檔案,可準確定位問題。
  • 細粒度的快取管理,可精確檢視指定 URL 的完整快取資訊。
  • 模組相互獨立,提供使用不同 Level 的介面。
  • 下載層高度可配置。
  • 低耦合,整合簡單。

結構設計 & 工作流程

KTVHTTPCache 由 HTTP Server 和 Data Storage 兩大模組組成。前者負責與 Client 互動,後者負責資源載入及快取處理。為方便擴充,Data Storage 為獨立模組,也可直接與 Client 互動(例如可與 AVAssetResourceLoader 配合使用)。

結構及工作流程圖如下:

KTVHTTPCache Flow Chart
KTVHTTPCache Flow Chart

下面簡述一下工作流程:
  1. Client 發出的請求被 HTTP Srever 接收到,HTTP Server 通過分析 HTTP Request 建立用於訪問 Data Storage 的 Data Request 物件。
  2. HTTP Server 使用 Data Request 建立 Data Reader,並以此作為從 Data Storage 獲取資料的通道。
  3. Data Reader 分析 Data Request 中的 Range 建立對應的網路資料來源 Data Network Source 和檔案資料來源 Data File Source,並通過 Data Sourcer 進行管理。
  4. Data Sourcer 開始載入資料。
  5. Data Reader 從 Data Sourcer 讀取資料並通過 HTTP Server 回傳給 Client。

快取策略

以網路使用最小化為原則,設計了分片載入資料的功能。有 Network Source 和 File Source 兩種用於載入資料的 Source,分別用於下載網路資料和讀取本地資料。通過分析 Data Request 的 Range 和本地快取狀態來對應建立。

例如一次請求的 Range 為 0-999,本地快取中已有 200-499 和 700-799 兩段資料。那麼會對應生成 5 個 Source,分別是:

  1. Data Network Source: 0-199
  2. Data File Source: 200-499
  3. Data Network Source: 500-699
  4. Data File Source: 700-799
  5. Data Network Source: 800-999

它們由 Data Sourcer 進行管理,對外僅暴露一個 Read Data 的介面,根據當前的 Read Offset 自行選擇向外界提供資料的 Source。

使用示例

// 使用簡單,基本可以忽略整合成本

// 啟動(全域性啟動一次即可)
NSError * error;
[KTVHTTPCache proxyStart:&error];

// 使用
NSString * URLString = [KTVHTTPCache proxyURLStringWithOriginalURLString:@"原始 URL"];
AVPlayer * player = [AVPlayer playerWithURL:[NSURL URLWithString:URLString]];複製程式碼

唱吧的實踐過程

方案演進

在音視訊快取上,我們一共採用過如下 4 個方案:

  1. AVPlayer 純線上播放。
  2. AVPlayer + AVAssetResourceLoader + 下載模組。
  3. AVPlayer + 一個開源的快取專案(同樣基於 AVAssetResourceLoader + 下載模組)。
  4. AVPlayer + KTVHTTPCache。
  • 方案 1 簡單直接,缺點也不必多說。
  • 方案 2 的下載模組設計的比較簡單,只能順序下載,不支援分片。導致只能 Seek 到已下載完的地方,在使用者體驗上會有較大的缺陷。
  • 方案 3 在功能上已經可以滿足需求,但在使用中問題較多,我們在原始碼基礎上做了很多修改來填坑。但穩定性依然不是很理想,上線不長時間就將該功能下掉了。
  • 方案 4 是唱吧現在的線上方案,目前在我們的使用場景中還沒有發現問題。除穩定性的提升外,比較大的改進是增加了全路徑的 Log 模組。若使用者或測試同學遇到問題,只需簡單描述並回傳 Log,就可以快速定位到原因,大大提高了除錯效率。

踩過的坑

1. Content-Type 和 Path Extension

AVPlayer 在播放時會優先根據 Response Header 中的 Content-Type 判斷當前資源是否可以播放。當 Content-Type 無法給出有效資訊時再去判斷 URL 中的 Path Extension。

對應關係如下:

URL Content-Type 是否可播
changba.com/video.mp4 video/mp4 YES
changba.com/video.mp4 application/octet-stream YES
changba.com/video video/mp4 YES
changba.com/video application/octet-stream NO

因此要想讓 AVPlayer 正常播放,Content-Type 和 Path Extension 中至少能提供一個有效資訊,否則將直接報 Error。

  • 發現這一問題是因為在做 Original URL -> Proxy URL 對映時,將 Original URL 中的 Path Extension 資訊在 URL Encode 時丟失了,再碰上某些情況 CDN 返回的 Content-Type 是 application/octet-stream 而不是 video/mp4 之類的確切型別時,AVPlayer 會直接報 Error。

2. 鎖屏後 Server Socket 失效

在本地 Server 中有一個 Socket 用於接收 AVPlayer 發出的請求。如果在 AVPlayer 為非播放狀態時鎖屏,一段時間後再喚起 App,FD 雖然還在,但 Listen 的埠會被回收,導致 FD 接收不到事件,AVPlayer 發出的請求也就無法被本地 Server 接收到。我們的解決辦法是在做 URL 對映時 Ping 一下本地 Server,如果 Ping 不通,會重啟本地 Server。

最後

專案已經開源,GitHub 地址: github.com/ChangbaDevs…

對重度影音類應用而言,音視訊快取屬於比較重要的一環,對穩定性也有比較高的要求,我們在這上走過一些彎路、踩過一些坑。希望 KTVHTTPCache 的開源能給大家帶來一些幫助。也非常歡迎大家在專案中使用,如果遇到問題可以在 GitHub 提 Issue 給我。

相關文章