視頻播放器丟幀策略

我的視頻課程(基礎):《(NDK)FFmpeg打造Android萬能音頻播放器》

我的視頻課程(進階):《(NDK)FFmpeg打造Android視頻播放器》

我的視頻課程(編碼直播推流):《Android視頻編碼和直播推流》

 

1、丟幀的出現

說起視頻播放器大家都很熟悉了,覆蓋各種平臺,使用簡單操作方面,但是視頻播放器裏面的原理卻非常的複雜,牽扯到很多方面的知識點。今天我們來探討一下當視頻解碼和渲染的總時間大於了視頻指定的時間時,就會出現聲音比畫面快的情況,單個畫面延後的時間在人眼不能察覺的範圍內還是能接受的,但是如此累計起來就會造成這個延遲的加大,導致後面聲話完全不同步,這是不能接受的,那麼爲了解決這種問題,視頻“丟幀”就出現了。

2、視頻播放原理

我們看到的視頻其實就是一幅一幅的圖片組成的,就和電影一樣的原理,在很短的時間內連續把這些圖片展示出來,這樣就達到了視頻連續的效果,比如每秒中展示25幅圖片。而在這25幅圖片中某幾幅(不能太多)圖片沒有展示出來,我們也是很難察覺的,這就是我們“丟幀”的基礎了。如果圖片丟失多了,明眼人一眼就看出來了,那麼就不用再討論“丟幀”了,而是不會看你的這個視頻了。

3、視頻編碼過程(H264)

現在視頻編碼比較流行的就是H264編碼,它的壓縮(編碼)模式有很多種,適合於不同的場景,比如網絡直播、本地文件、UDP傳輸等都會採樣不同的壓縮(編碼)模式,h264編碼器會把一幅幅的圖片壓縮(編碼)成體積很小的一個一個的單元(NALU),並且這些一個一個的單元之間並不是完全獨立的,比如:有10幅圖片,經過編碼後,第一幅圖片會單獨生成一個單元,而第二個圖片編碼後生成的單元只會包含和第一幅圖片不同的信息(有可能第二幅圖片和第一幅圖片只有一個文字不一樣,那麼第二個單元編碼後的數據就僅僅包含了這個文字的信息,這樣的結果就是體積非常小),然後後面編碼後的第三個、第四個單元一直到第十個單元都只包含和前一個單元或幾個單元不同的信息(當然實際編碼時很複雜的),這樣的結果就是一個原本只有1G大小的一組圖片編碼後可能只有十多兆大小,大大減小了存儲空間和傳輸數據量的大小。

4、H264中 I幀、P幀、B幀的含義

前面提到的第一幅圖片是被單獨編碼成一個單元(NALU)的,在H264中我們定義關鍵幀(用字母I表示,I幀,包含一幅圖片的所有信息,可獨立解碼成一幅完整的圖片),後面的第二個單元一直到第十個單元中的每一個單元我們定義爲P幀(差別幀,因爲它不包含完整的畫面,只包含和前面幀的差別的信息,不能獨立解碼成一幅完整的圖片,需要和前面解碼後的圖片一起才能解碼出完整的圖片),當然H264中還有B幀(雙向幀,需要前後的數據才能解碼成單獨的圖片),這就是我們經常聽說的視頻的I幀、P幀、B幀。

5、視頻解碼過程(H264)

通過前面的講解,相信大家對視頻編碼後圖片的變化過程有了大概的瞭解了(瞭解過程就行,具體技術細節就不用追究了),那麼我們的重點就來了,播放器播放視頻的過程就和圖片編碼成視頻單元(NALU)的過程相反,而是把我們編碼後的I幀、P幀、B幀中的信息解碼後,依照編碼順序還原出原來的圖片,並按照一定的時間顯示(比如每秒顯示25幅圖片,那麼每幅圖片之間的間隔就是40ms,也就是每隔40ms顯示一幅圖片)。請注意這裏的一定的時間(這裏的40ms)裏面播放器需要做許多的事情:

1、讀取視頻文件或網絡數據

2、識別讀取的數據中的視頻相關的數據

3、解析出裏面的每一個單元(NALU),即每一幀(I、P、B)

4、然後把這些幀解碼出完整的圖片(I幀可以解碼成完整圖片,P、B幀則不可以,需要參考其他幀的數據)

5、最後按照一定的時間間隔把解碼出來的圖片顯示出來

大多數情況下,播放器所在設備的軟硬件環境的解碼能力都是可以讓播放器在這個一定時間(比如40ms)內完成圖片的顯示的,這種情況下就是最好不過的了。而也有設備軟硬件環境的解碼能力不能在這個一定時間(比如40ms)內完成圖片的顯示,但是呢又相差不大(比如相差幾毫秒),但是隨着解碼的次數增加,這個時間就會累計,後面就有可能相差幾秒、幾十秒、幾分鐘等,這樣就需要“丟幀”操作了。

6、開始丟幀

丟幀丟幀,怎麼丟,丟掉哪些幀我們怎麼決定呢,這就要從視頻圖像是怎麼解碼得到的原理下手了,不然隨便丟幀的話,最容易出現的情況就是花屏,導致視頻基本不能看。下面我就舉個例子來說明怎樣丟幀:

比如我們的視頻規定的是隔40ms(每秒25幀,且沒秒的第一幀是I幀)顯示一幅圖片,而我們的設備解碼能力有限,最快的解碼出一幅圖片的時間也需要42ms,這樣本來該在40ms出顯示第一幅圖片,但是由於解碼時間花了42ms,那麼這一幅圖片就在42ms時才顯示出來,比規定的時間(40ms)延遲了(42-40)2ms,當我們連續解碼24幅圖片時,這個延遲就到了20 * 2ms = 40ms,假設這個40ms的延遲已經很大了,再加大延遲就會造成我們明顯感覺到視頻的聲音和畫面不同步了,所以我們就需要把後面的(25-24)1幀沒解碼的給丟了不顯示(因爲此時解碼24幀的時間已經消耗了24*42=1008ms了,也就是說下一個40ms該顯示第二秒的第一幀了,如果再顯示第一秒的最後一幀,這樣就會發生明顯不同步的現象了),而是接着第二秒的數據開始解碼顯示,這樣我們就成功的丟掉了一幀數據,來儘量保證我們的聲音和畫面同步了。

7、丟幀優化

前面提到的都是理想情況(每秒25幀,並且每一秒的第一幀都是I幀,能獨立解碼出圖像,不依賴其他幀)下的丟幀,而不理想的情況(2個I幀直接的間隔不是定長的,比如第一個I幀和第二個I幀直接間隔24個其他幀,而第三個I幀和第二個I幀之間相差35個其他幀)則是經常遇到的,這種情況下我們就不能寫死解碼播放24幀然後丟掉第25幀,因爲可能出現丟掉25幀後的下一幀仍然不是I幀,這樣解碼就會解不出完整的圖片,顯示出來的畫面就會有花屏,影響體驗。那麼比較好的辦法就是,我們定義一個內存緩衝區域,儘量在這個區域裏面包含2個及以上的I幀(注意是解碼前),比如:播放器從第一個關鍵幀開始解碼播放,由於解碼能力有限,當理論時間應該馬上解碼顯示第二個關鍵幀時,而此時播放器還在解碼這個關鍵幀之前的第5幀,也就是說播放器還得再解碼5幀才能到這個關鍵幀,那麼我們就可以把這5幀給丟掉了,不解碼了,直接從這個關鍵幀開始解碼,這樣就能保證在每個關鍵幀解碼播放時都和理論播放的時間幾乎一致,讓人察覺不到不同步現象,而還不會造成花屏的現象。這種丟幀個人覺得纔是比較不錯的方案。

8、FFMpeg解碼僞代碼

bool dropPacket = false;
while(true)
{
	AVPacket *pkt = getVideoPacket();
	if(audioClock >= lastKeyFrameClock + offsetTime)//當前音頻時間已經超過了下一幀關鍵幀之前了,就需要丟幀了
	{
		dropPacket = true;
	}
	if(pkt->flags == KEY_FRAME)//關鍵幀不丟
	{
		dropPacket = false;
	}
	if(dropPacket)
	{
		av_packet_free(pkt);
		av_free(&pkt);
		pkt = NULL;
		continue;
	}
	//正常解碼
	...
}

最後來一張出自靈魂畫手的丟幀圖:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章