隨著短視訊興起,各大APP中短視訊隨處可見,feeds流、詳情頁等等。怎樣讓使用者有一個好的視訊觀看體驗顯得越來越重要了。大部分feeds裡面滑動觀看視訊的時候,有明顯的等待感,體驗不是很好。針對這個問題我們展開了一波優化,目標是:視訊播放秒開,視訊播放體驗良好。無圖無真相,上個對比圖,左邊是優化之前的,右邊是優化之後的:
02 問題分析
視訊格式的選擇
在正式分析問題之前有必要說明下:我們現在首頁的視訊,都是320p H.264編碼的mp4視訊。
H.264 & H.265
H.264也稱作MPEG-4AVC(Advanced Video Codec,高階視訊編碼),是一種視訊壓縮標準,同時也是一種被廣泛使用的高精度視訊的錄製、壓縮和釋出格式。H.264因其是藍光光碟的一種編解碼標準而著名,所有藍光播放器都必須能解碼H.264。H.264相較於以前的編碼標準有著一些新特性,如多參考幀的運動補償、變塊尺寸運動補償、幀內預測編碼等,通過利用這些新特性,H.264比其他編碼標準有著更高的視訊質量和更低的位元速率。
H.265/HEVC的編碼架構大致上和H.264/AVC的架構相似,也主要包含:幀內預測(intra prediction)、幀間預測(inter prediction)、轉換 (transform)、量化 (quantization)、去區塊濾波器(deblocking filter)、熵編碼(entropy coding)等模組。但在HEVC編碼架構中,整體被分為了三個基本單位,分別是:編碼單位(coding unit,CU)、預測單位(predict unit,PU) 和轉換單位(transform unit,TU )。
總的來說H.265壓縮效率更高,傳輸位元速率更低,視訊畫質更優。看起來使用H.265似乎是很明智的選擇,但我們這裡選擇的是H.264。原因是:H.264支援的機型範圍更為廣泛。
*PS:閒魚H.265視訊在寶貝詳情頁會在近期上線,敬請關注體驗!
TS & FLV & MP4
TS是日本高清攝像機拍攝下進行的封裝格式,全稱為MPEG2-TS。TS即"Transport Stream"的縮寫。MPEG2-TS格式的特點就是要求從視訊流的任一片段開始都是可以獨立解碼的。下述命令可以把mp4轉換成ts格式,從結果來看ts檔案(4.3MB)比mp4檔案(3.9MB)大10%左右。
ffmpeg -i input.mp4 -c copy output.ts
FLV是FLASH VIDEO的簡稱,FLV流媒體格式是隨著Flash MX的推出發展而來的視訊格式。由於它形成的檔案極小、載入速度極快,使得網路觀看視訊檔案成為可能,它的出現有效地解決了視訊檔案匯入Flash後,使匯出的SWF檔案體積龐大,不能在網路上很好的使用等問題。FLV只支援一個音訊流、一個視訊流,不能在一個檔案裡包含多路音訊流。音訊取樣率不支援48k,視訊編碼不支援H.265。相同編碼格式下,檔案大小和mp4幾乎沒有區別。
ffmpeg -i input.mp4 -c copy output.flv
MP4是為大家所熟知的一種視訊封裝格式,MP4或稱MPEG-4第14部分是一種標準的數字多媒體容器格式。MPEG-4第14部分的擴充名為.mp4,以儲存數字音訊及數字視訊為主,但也可以儲存字幕和靜止影象。因其可容納支援位元流的視訊流,MP4可以在網路傳輸時使用流式傳輸。其相容性很好,幾乎所有的移動裝置都支援,而且還能在瀏覽器、桌面系統進行播放。綜合上面幾個封裝格式的特點,我們的最終選擇是MP4。
播放流程
一個視訊在客戶端的播放流程是怎麼樣的?播放首開慢耗時在什麼地方?耗時點是否能夠快速低成本的解決?瞭解視訊的播放流程有助於找到問題的突破口。視訊從載入到播放可以分為三個階段:
讀取(IO):“獲取” 內容 -> 從 “本地” or “伺服器” 上獲取
解析(Parser):“理解” 內容 -> 參考 “格式&協議” 來 “理解” 內容
渲染(Render):“展示” 內容 -> 通過揚聲器/螢幕來 “展示” 內容
可以看出內容獲取從“伺服器”改為“本地”,這樣會節省很大一部分時間,而且成本很低,是一個很好的切入點。事實也是如此,我們的優化正是圍繞此點展開。
*PS: 我們使用的網路庫,播放器都是集團內部的,本身做了很多優化。本文不涉及網路協議,播放器方面的優化討論。
鑑於上面的分析,我們要做的工作是:把mp4檔案提前快取一部分,到feeds滑動要播放的時候,播放本地的mp4檔案。由於使用者可能繼續觀看視訊,所以本地的資料播放完後,需要從網路下載資料進行播放。這裡需要解決兩個問題:
應該提前下載多少資料
快取資料播放完成後該怎麼切換到網路資料
MOOV BOX的位置
對於第一個問題,我們不得不分析一下mp4的檔案結構,看看我們應該下載多少資料量合適。MP4是由很多Box 組成的,Box裡面可以巢狀Box:
這裡不詳細介紹MP4的格式資訊。但是可以看出moov box對播放很關鍵,它提供的資訊如:寬高、時長、位元速率、編碼格式、幀列表、關鍵幀列表等等。播放器沒有獲取到moov box是沒辦法進行播放的。所以下載的資料應該要包含moov box再加上幾十幀的資料。
做了一個簡單的計算:閒魚短視訊一般最長是30s,feeds裡面的解析度是320p,位元速率是1141kb/s,ftyp+moov這個視訊的資料量在31kb左右(開啟檔案可以看出mdat是從31754byte的位置開始的),所以,頭部資訊+10幀的資料大約是:(31kb + 1141kb/3)/8 = 51KB。
Proxy
第二個問題:快取資料播放完成後該怎麼切換到網路資料呢?在本地資料播放完成之後,設定一個網路地址給播放器,告訴播放器下載的offset是多少,然後繼續從網路下載資料播放。這樣看起來可行,但是需要播放器提供支援:本地資料播放完成的回撥;設定網路url並支援offset。另外,服務端需要支援range引數,而且切換到網路播放的時候需要新建立網路連線,很可能會造成卡頓。
最終,我們選擇了proxy的方式,把proxy作為中間人,負責預載入資料、給播放器提供資料,切換邏輯在proxy裡面來完成。未加入proxy之前流程是這樣的:
加入了proxy之後流程是這樣的:
這樣做的好處很明顯,我們可以在proxy裡面做很多事情:例如本地檔案快取資料和網路資料的切換工作。甚至是和CDN使用其它的協議進行通訊。我們這裡假定預載入工作已經完成,看看播放器是怎麼和proxy進行互動的。播放的時候會用Proxy提供的一個localhost的url進行播放,這樣代理伺服器會收到網路請求,把本地預載入的資料返回給播放器。播放器完全感知不到proxy模組、預載入模組的存在。播放器、預載入模組都是Proxy的client,呼叫邏輯都是一樣。圖示說明如下:
下面逐步解釋一下,資料的載入過程:
Client發起http請求獲取資料,箭頭1所示
檔案快取如果存在所請求的資料則直接返回資料,箭頭2所示
若本地檔案快取資料不夠,則發起網路請求,向CDN請求資料,箭頭3所示
獲取網路資料,寫入檔案快取,箭頭4所示
返回請求的資料給Client,箭頭2所示
預載入模組
確定了技術方案後,預載入模組還是有很多工作要做的。在列表網路資料解析完成後會觸發視訊預載入,首先會根據url生成md5值,然後去檢視這個md5值對應的任務是否存在,如果存在則不會重複提交。生成任務後會提交到執行緒池,在後臺執行緒進行處理。網路從Wifi切換到3G的時候,會把任務取消,防止消耗使用者的資料流量。
預載入任務線上程池執行的時候,其流程是這樣的:首先會獲取一個本地代理的url。然後發起http請求。Proxy會收到http請求進行處理,開始做真正的資料預載入工作。預載入模組讀取到指定的資料量後終止。到此,預載入的任務就已完成。流程圖如下所示:
在使用者快速滑動的時候,怎麼能保證視訊還能繼續秒開呢?預載入模組對於每一個任務都會維護一個狀態機,在Fling的時候會把劃過的任務暫停下,把最新要顯示的任務優先順序提高,讓其優先執行。
Proxy模組
Proxy內部有個local的httpServer負責攔截播放器和預載入模組的http請求。client在請求時會帶入CDN的url,在本地快取資料沒有的時候會去CDN獲取新鮮資料。因為有多個地方向Proxy請求資料,所以用執行緒池來處理多個client的連線很有必要,這樣多個client可以並行,不會因為前面有client在請求而阻塞。
檔案快取使用LruDiskCache,在超過指定檔案大小後,老的快取檔案會刪除,這是一個在使用檔案快取時很容易忽視的問題。由於我們的場景視訊是連續播放的,不存在seek的情況,所以檔案快取相對比較簡單,不用考慮檔案分段的情況。Proxy內部對於同一個url會對映到一個client,如果預載入和播放同時進行,資料只會有一份,不會去重複下載資料。再來一個Proxy內部構造示意圖:
遇到的問題
在測試中發現,有的視訊還是會播放很慢,仔細檢視本地的確快取了期望的資料大小,但是播放的時候還是有較長的等待時間,這種視訊有個特點:moov box在尾部。對於moov在尾部的視訊,是整個檔案都下載完成後才進行播放的,原因是moov box裡面存了很多關鍵資訊,前面分析mp4格式的時候有提到。對於這個問題有兩個解法:
解法一:
服務端在進行轉碼的時候保證moov的頭部在前面,發現moov位置不正確的視訊服務端進行訂正。
*PS:檢視moov在檔案中的位置可以用hex文字編輯器開啟,按字元搜尋moov所在的位置即可,MAC上面還可以使用MediaParser , 另外還可以用ffmpeg命令生成moov在頭部或者尾部的mp4檔案。
例如:
從1.mp4 copy一個檔案,使其moov頭在尾部
ffmpeg -i 1.mp4 -c copy -f mp4 output.mp4
從1.mp4 copy一個檔案,使其moov頭在頭部:
ffmpeg -i 1.mp4 -c copy -f mp4 -movflags faststart output2.mp4
解法二
不用修改moov box的位置,而是在播放端進行處理,播放端需要檢測流資訊,如果moov前面沒有,就去請求檔案的尾部資訊。具體就是:發起 HTTP MP4 請求,讀取響應 body 的開頭,如果發現 moov 在開頭,就接著往下讀mdat。如果發現開頭沒有,先讀到 mdat,馬上 RESET 這個連線,然後通過 Range 頭讀取檔案末尾資料,因為前面一個 HTTP 請求已經獲取到了 Content-Length ,知道了 MP4 檔案的整個大小,通過 Range 頭讀取部分檔案尾部資料也是可以的。示意圖如下:
這個方案的缺點是:對於moov box在尾部的視訊會多兩次http connection。
本文介紹了常見的視訊編碼格式,視訊封裝格式,介紹了moov頭資訊對於視訊播放的影響。隨著對於播放流程的分析,我們找到了問題的切入點。簡單說就是圍繞著資料預載入展開,把網路請求資料的工作提前完成,播放的時候直接從快取讀取,而且後續的視訊回看都是從快取讀取,不僅解決了視訊初始化播放慢的問題,還解決了播放快取問題,可以說是一箭雙鵰。Proxy是這個方案的核心思想,本地localhost的url是一個關鍵紐帶,視訊預載入模組和播放器模組解耦徹底,換了播放器照樣可以使用。到此為止,視訊feeds秒開優化就已完成。上線後的資料來看視訊開啟速度在800ms左右。
回過頭來,或許我們還可以更進一步,可以對預載入收到的資料進行驗證,確保快取了準確的資訊,而不是固定的數值。還可以進行更加深度的優化,讓使用者觀看視訊的體驗更加順滑。
* [AndroidVideoCache](https://github.com/danikula/AndroidVideoCache)
* [視訊的封裝格式和編碼格式](https://www.jianshu.com/p/8034fa1ed682)
* [播放器技術分享(1):架構設計](http://blog.51cto.com/ticktick/2324928?source=dra)
* [MP4檔案格式的解析,以及MP4檔案的分割演算法](https://cloud.tencent.com/developer/article/1120604)
* [從天貓某活動視訊3次請求說起](https://juejin.im/post/5c0e0f75e51d45410c5e1aea)
* [視音訊編解碼學習工程:FLV封裝格式分析器]https://blog.csdn.net/leixiaohua1020/article/details/17934487
* https://www.adobe.com/content/dam/acom/en/devnet/flv/video_file_format_spec_v10_1.pdf
* https://baike.baidu.com/item/flv
* https://standards.iso.org/ittf/PubliclyAvailableStandards/index.html