[貝聊科技]首屏視頻的優化過程(補充moov的研究)

做者:江敏熙 貝聊前端開發工程師
本文同時發佈於我的博客html

前言

我司的官網首頁——貝聊官網,首屏有一個自動播放的背景視頻,一直被詬病視頻加載慢、播放卡。剛開始覺得是文件太大,或者是網速太慢,但當我去優化它的時候,發現並無預想的簡單。本文記錄了優化過程和經驗總結,但願能對讀者有所幫助。 前端

image

現狀

官網的首頁由6屏組成,首屏主要內容是一個自動循環播放的背景視頻。頁面無緩存時:git

  • 視頻畫面須要好幾秒才能出現,期間只能看到頁面的背景色,而且出現第一幀之後,畫面會卡着不動,持續好久,有的時候甚至超過10秒,這種狀況在下文統一簡稱第一幀卡
  • 畫面第一幀卡完了之後播放不流暢,體驗起來像幻燈片

初探

要優化視頻播放卡頓的問題,我首先從視頻的文件大小入手。 下載MediaInfo查看視頻文件:github

  • 格式:mp4
  • 分辨率:1080P
  • 碼率:4K
  • 大小:7.3M
  • 時長:15秒

4K的碼率在對於在線視頻是很是高的,我使用視頻壓縮工具格式工廠對其進行調整,把碼率壓到畫質可接受的2400,此時文件大小4.4M。眼見文件大小已經瘦身爲原來的60%,想必會有明顯的優化效果。chrome

詭異的第一幀

打包發上測試環境,效果卻大跌個人眼鏡:視頻卡頓感比之前減輕了一點,但仍是能明顯的感覺到不流暢,而視頻的第一幀卡的問題更是幾乎沒有改善。看來經過壓縮碼率下降文件大小的作法貌似是杯水車薪。segmentfault

深刻探索

意識到簡單的減小資源文件大小的方法行不通以後,便上網搜查解決方案,但發現相關文章少之又少,並無找出第一幀卡的緣由。找不到解決方案,就只好本身摸索摸索了,慢慢的腦裏有個猜測:若是把視頻分紅多份,瀏覽器只要加載了第一份就能夠播放,這樣會不會減輕視頻的第一幀卡的問題呢?瀏覽器

視頻分塊加載

我把原有的視頻切成了兩段,並經過監聽video標籤的ended事件,在第一段播完後修改src切換到第二段,第二段播完後又切換回第一段,並循環這個過程。緩存

這樣雖然給第一幀卡的問題帶來了必定的改善,可是反作用是:切換畫面並非無縫的,每次切換都會卡一秒左右。bash

反思

視頻分塊的作法我最終選擇了放棄。緣由一方面視頻時長原本才15秒,分塊的意義並不大;另外一方面我認爲這種方案即便作出來能無縫切換,也不會是最好的方案,由於並無解決根本問題(爲何視頻第一幀卡)。服務器

moov位置致使第一幀卡?打破傳說

對於爲何第一幀加載慢,我開始懷疑和mp4格式有關,我搜索了一下,很多文章說起到moov的問題:

image

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並刪除綁定和定時器。問題就此解決了。

雖然此作法能解決問題,可是我以爲實現作法不太完美。若是您有更好的方法,請在下面留言☺

好用又免費的視頻壓縮工具——小丸工具箱

通過上述的優化,首頁的視頻播放效果已經好了不少,可是仍是有偶爾卡頓的狀況。以前雖然已經使用格式工廠壓縮了一遍視頻,但考慮到市面上還有不少其餘的視頻壓縮工具,因而再去百度裏多找了一下,發現一款口碑不錯的工具,叫「小丸工具箱」。

image
小丸工具箱相對以前的格式工廠,能夠直接去除音頻流(需求裏視頻不須要聲音),這樣視頻體積更小了;操做更傻瓜化了,使用者只須要修改選項裏的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文件會發起三次請求
    image
  • 有的時候動圖會在視頻加載完以前開始加載(監聽progress事件失敗)

而在IE9和火狐瀏覽器,上述兩個問題並不能重現,因爲我在開發時使用主要使用了火狐,因此疏漏上述兩個問題。我搜索了相關資料,而且用了三個結構順序不一樣的mp4文件作了對比測試,如今給你們分享一下結果:

附上moov後置,moov前置無meta,moov前置有meta的三張mp4文件結構圖:

image
首先,文件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

MP4文件格式詳解——結構概述

媒體文件格式分析之MP4

利用ffmpeg修改MP4文件頭信息,使其支持流式加載及播放

相關文章
相關標籤/搜索