作者:江敏熙 貝聊前端開發工程師
本文同時釋出於個人部落格
前言
我司的官網首頁——貝聊官網,首屏有一個自動播放的背景視訊,一直被詬病視訊載入慢、播放卡。剛開始以為是檔案太大,或者是網速太慢,但當我去優化它的時候,發現並沒有預想的簡單。本文記錄了優化過程和經驗總結,希望能對讀者有所幫助。
現狀
官網的首頁由6屏組成,首屏主要內容是一個自動迴圈播放的背景視訊。頁面無快取時:
- 視訊畫面需要好幾秒才能出現,期間只能看到頁面的背景色,並且出現第一幀以後,畫面會卡著不動,持續很久,有的時候甚至超過10秒,這種情況在下文統一簡稱第一幀卡
- 畫面第一幀卡完了以後播放不流暢,體驗起來像幻燈片
初探
要優化視訊播放卡頓的問題,我首先從視訊的檔案大小入手。 下載MediaInfo檢視視訊檔案:
- 格式:mp4
- 解析度:1080P
- 位元速率:4K
- 大小:7.3M
- 時長:15秒
4K的位元速率在對於線上視訊是非常高的,我使用視訊壓縮工具格式工廠對其進行調整,把位元速率壓到畫質可接受的2400,此時檔案大小4.4M。眼見檔案大小已經瘦身為原來的60%,想必會有明顯的優化效果。
詭異的第一幀
打包發上測試環境,效果卻大跌我的眼鏡:視訊卡頓感比以前減輕了一點,但還是能明顯的感受到不流暢,而視訊的第一幀卡的問題更是幾乎沒有改善。看來通過壓縮位元速率降低檔案大小的做法貌似是杯水車薪。
深入探索
意識到簡單的減少資原始檔大小的方法行不通之後,便上網搜查解決方案,但發現相關文章少之又少,並沒有找出第一幀卡的原因。找不到解決方案,就只好自己摸索摸索了,慢慢的腦裡有個猜想:如果把視訊分成多份,瀏覽器只要載入了第一份就可以播放,這樣會不會減輕視訊的第一幀卡的問題呢?
視訊分塊載入
我把原有的視訊切成了兩段,並通過監聽video標籤的ended事件,在第一段播完後修改src切換到第二段,第二段播完後又切換回第一段,並迴圈這個過程。
這樣雖然給第一幀卡的問題帶來了一定的改善,但是副作用是:切換畫面並不是無縫的,每次切換都會卡一秒左右。
反思
視訊分塊的做法我最終選擇了放棄。原因一方面視訊時長本來才15秒,分塊的意義並不大;另一方面我認為這種方案即使做出來能無縫切換,也不會是最好的方案,因為並沒有解決根本問題(為什麼視訊第一幀卡)。
moov位置導致第一幀卡?打破傳說
對於為什麼第一幀載入慢,我開始懷疑和mp4格式有關,我搜尋了一下,不少文章提及到moov的問題:
mp4雖然支援流傳輸播放,但視訊的“索引”儲存在了moov物件,只有moov下載完視訊才會開始播放。大多mp4檔案會把這個moov放在檔案頭部,但如果放在了尾部則需要下載完整個檔案才能開始播放。參考blog.csdn.net/jinshelj/ar…。
我檢視了壓縮後的mp4檔案,moov的確是在尾部。於是我使用qt-faststart(基於ffmpeg的moov前置工具),對moov物件做了前置處理。但經過我的測試,發現前置了moov並沒有優化第一幀卡的問題,播放表現和在尾部的時候一樣。並且當檔案moov在尾部的時候,視訊在檔案下載完之前就開始播了,並無檔案下載完才能播一說。
於是我再查閱資料,終於找到了原因:如果伺服器本身是支援seek的,那麼mp4視訊也是能正常邊下邊播的,參考segmentfault.com/a/119000001…
既然不是moov導致了第一幀卡,那究竟是什麼原因呢?
更適合網路流傳輸的格式——flv
至此第一幀卡的問題還沒有解決,於是我打算換一種視訊格式試試,那麼是否存在一個比mp4更適合線上播放的視訊格式呢?
我搜尋很多相關資料,flv是一種非常簡潔,天生具備流式特徵,非常適合網路流傳輸的格式。如果說mp4視訊的“索引”是整個一起儲存的,那麼flv的“索引”則是分段儲存的。打個簡單的例子:看一個視訊的開頭,mp4需要下載整個視訊的“索引”才能開始播放,flv只需要下載開頭部分的“索引”。
瀏覽器並沒有原生支援flv解碼,一般是通過flash來完成。但是,來自嗶哩嗶哩的開源外掛——flv.js,能讓video標籤支援flv的播放。為了嘗試flv能否改善第一幀卡的問題,我引入了flv.js,並把原來的視訊轉為flv格式。flv.js壓縮後只佔100KB+,使用起來也非常方便,程式碼例項如下:
const flvjs = require('../fiv.js');
if (flvjs.isSupported()) {
var videoElement = $bgVideo[0];
var flvPlayer = flvjs.createPlayer({
type: 'flv',
url: src // 視訊的地址
});
flvPlayer.attachMediaElement(videoElement);
flvPlayer.load();
flvPlayer.play();
}
複製程式碼
測試了一下,結果讓我驚喜。第一幀卡的問題解決了,但是播放依然是不流暢,體驗像幻燈片。雖然問題沒有完全解決,而且flv.js不相容IE10以下的瀏覽器,但至少獲得了實質性的進展。接著只要集中精力解決視訊播放不流暢的問題就可以了。
揪出搶佔網速的凶手
播放不流暢的問題相對簡單,原因要麼是下載太慢,要麼就是檔案太大。而檔案已經被壓縮,就只需要研究為什麼下載慢了。
這個時候我開始把眼光投在頁面的靜態資源上,看看能否對一些佔用大的資源做優化。靜態資源在釋出生產時已被工具壓縮過,已經沒有什麼再壓縮的空間。我的思路是儘量讓首屏看見的資源立刻載入,而第一屏外的資源延遲載入。開啟瀏覽器的開發者工具,發現大部分的資原始檔都很小,只有一個檔案特別大,高達900+KB,開啟發現是一個動圖,並且不在第一屏內。這個時候就要考慮怎麼樣才能把圖片放在視訊下載完之後才載入。基本思路是視訊下載完之後,再把圖片標籤動態新增到頁面中。
我查閱了video標籤的原生事件。 在眾多事件中,suspend是比較適用於當前場景的,表現為當視訊下載完成後觸發。 但suspend的相容性並不好,在IE 9等低版本瀏覽器下不能觸發,而progress事件卻沒有這個問題。progress事件在視訊下載時觸發,假設一個視訊下載耗時10秒,那10秒內每秒都會觸發progress。
通過監聽progress事件和設定定時器來判斷視訊是否載入完,達到動圖延遲載入的目的。具體程式碼如下:
/**
監聽事件progress,觸發後設定定時器。每一次progress事件觸發便會清空上一次的定時器。
假如在一秒內progress都沒有觸發,則視為下載完成,觸發callback同時刪除繫結。
**/
// $bgVideo[0]是視訊的dom節點
afterDownload($bgVideo[0], function() {
$bgImg.removeClass('hiden');
});
function afterDownload(video, callback) {
// 計時器
var callbackTimer = null;
// progress事件回撥函式。監聽progress,直到一秒間不觸發progress才執行callback
var progressCallBack = function() {
clearTimeout(callbackTimer);
// 設定1秒的定時器,觸發後刪除繫結,刪除定時器
callbackTimer = setTimeout(function() {
callback();
video.removeEventListener('progress', progressCallBack);
}, 1000);
};
// 繫結事件
video.addEventListener('progress', progressCallBack);
}
複製程式碼
在除錯的過程中,上面的程式碼還有點小問題。如果視訊已經被快取,progress事件有時候不會觸發,suspend事件也有同樣的情況。我猜測是視訊下載太快,addEventListener還沒有執行就已經下載完了。我嘗試把事件的繫結寫在html上:
<video onprogress="progressCallBack" .../>
複製程式碼
採用這種做法以後,在本地除錯時不斷按F5重新整理也不會出現問題,但是釋出到伺服器上卻偶爾會出現問題,事件progress又沒有被觸發。最後我選擇一種簡單粗暴的方法,在頁面初始化時在設定一個3秒的定時器:
function afterDownload(video, callback) {
var callbackTimer = null;
var progressCallBack = function() { ... };
// 防止chrome在快取的情況下,不觸發progress
callbackTimer = setTimeout(function() {
callback();
clearTimeout(callbackTimer);
video.removeEventListener('progress', progressCallBack);
}, 3000);
video.addEventListener('progress', progressCallBack);
}
複製程式碼
在進入頁面後,3秒內不觸發progress事件,就認為視訊已被快取,直接執行callback並刪除繫結和定時器。問題就此解決了。
雖然此做法能解決問題,但是我覺得實現做法不太完美。如果您有更好的方法,請在下面留言☺
好用又免費的視訊壓縮工具——小丸工具箱
經過上述的優化,首頁的視訊播放效果已經好了很多,但是還是有偶爾卡頓的情況。之前雖然已經使用格式工廠壓縮了一遍視訊,但考慮到市面上還有很多其他的視訊壓縮工具,於是再去百度裡多找了一下,發現一款口碑不錯的工具,叫“小丸工具箱”。
小丸工具箱相對之前的格式工廠,可以直接去除音訊流(需求裡視訊不需要聲音),這樣視訊體積更小了;操作更傻瓜化了,使用者只需要修改選項裡的CRF和解析度,基本上已經能完成多數情況的壓縮需求。關於CRF,引用小丸作者的話介紹一下:CRF(Const Quality, 固定質量),這種位元速率控制方式是非常優秀的,以至於可以無需2pass壓制,即即使1pass也能實現非常好的位元速率分配利用。像質量模式的壓制方式在其他編碼器也有(如xvid或者壓制rmvb的ERP),但據我所知都只是“固定量化(Const Quantization)”x264的CRF在量化的基礎上,根據人的視覺心理學更為合理地分配位元速率,其目標是讓人在看視訊的時候,視訊的質量儘可能地統一,但位元速率達到儘可能的有效利用。 CRF模式還有個優勢,很多人在壓片的時候不清楚應該給視訊壓到多少位元速率才比較好。CRF就是按需要來分配位元速率的,故其實就省下了到底要多少位元速率的苦惱。
這裡附上小丸工具箱的入門操作教程。
最後使用小丸工具箱嘗試不同了的CRF值,壓制之後再肉眼對比,在畫質和檔案體積間找出一個平衡,把視訊在1080P解析度下壓到了3.4M。 而我再嘗試降低解析度,發現當解析度降為720P時,畫質相差得並不明顯。最終選擇了解析度720p,CRF23的壓縮引數,此時視訊壓制到了2M,相較一開始的7.3M簡直是暴瘦。
總結
至此,視訊能夠快速呈現,流暢播放。同時也發現,無論是使用mp4格式,還是flv格式,第一幀卡的問題都已經不存在了。對於此情況,我用Chrome的限速功能測試過,只有在網速不夠用的情況下(要麼網速太慢,要麼視訊檔案太大),mp4格式視訊才會出現第一幀卡的問題。由於我們已經把視訊的大小壓到了足夠小,並且對大圖做了延遲載入的處理,此時flv和mp4的差距已經微不足道了。最終我把flv.js撤了下來,統一使用mp4檔案播放。
尾聲
本文記錄了我對於首屏的整個優化過程和當中得到的一些經驗,過程磕磕碰碰,希望能幫助讀者少走些彎路。同時我認為優化這個事是永無止境的,特別是我對於視訊壓縮方面的知識較為薄弱,如果文中有什麼不對的地方,或者好的建議,請讀者們不吝賜教。
補充
經過熱心網友的反饋,我發現我對moov的理解存在一些偏差,並且首屏視訊在Chrome播放時,會有兩個問題:
- 一個mp4檔案會發起三次請求
- 有的時候動圖會在視訊載入完之前開始載入(監聽progress事件失敗)
而在IE9和火狐瀏覽器,上述兩個問題並不能重現,由於我在開發時使用主要使用了火狐,所以疏漏上述兩個問題。我搜尋了相關資料,並且用了三個結構順序不同的mp4檔案做了對比測試,現在給大家分享一下結果:
附上moov後置,moov前置無meta,moov前置有meta的三張mp4檔案結構圖:
首先,檔案moov前置和後置會影響視訊的Fast Streaming(快速播放),如果伺服器不支援seek,moov後置的檔案需要下載完才能播放,而如果伺服器支援seek,那麼也是可以在邊下邊播的,但瀏覽器會傳送三個請求。 這三個請求的過程,簡單來說:第一個請求是從檔案的頭部開始下載並查詢moov裡播放所需的後設資料,當獲取不到就會傳送第二個請求從檔案的尾部開始下載並查詢,查詢到了以後再發起第三個請求去請求檔案的內容,這個時候視訊開始播放。對於線上播放,傳送三個請求會比傳送一個請求至少多耗幾百毫秒。第二,moov前置了也並不一定就能Fast Streaming,moov.udta.meta裡存放的是視訊的後設資料,如果沒有moov.udta.meta,那麼也需要三次請求。而我在測試的過程中發現,沒有moov.udta.meta但moov前置的檔案在chrome需要請求三次,而在Firefox只需要一次,這種情況我沒有找到相關資料,暫時認為是不同瀏覽器的採取的策略不同。
由於此前使用小丸工具箱壓制出來的是moov後置無meta的檔案,使用qt-faststart也僅僅是前置了moov但沒有生成moov.udta.meta,所以在Chrome上出現了三次請求並影響了progress事件的觸發,所以導致了上面兩個問題。 此後改為了使用ffmpeg優化(ffmpeg -i input.mp4 -movflags faststart -acodec copy -vcodec copy output.mp4),在moov下生成了moov.udta.meta,上述兩個問題消失。
參考:
Optimizing MP4 Video for Fast Streaming
Understanding the MPEG-4 movie atom | Adobe Developer Connection