視頻播放器選擇怎樣的丟幀策略~~
丟幀的出現(xiàn)
說起視頻播放器大家都很熟悉了,覆蓋各種平臺,使用簡單操作方面,但是視頻播放器里面的原理卻非常的復(fù)雜,牽扯到很多方面的知識點。
今天我們來探討一下當視頻解碼和渲染的總時間大于了視頻指定的時間時,就會出現(xiàn)聲音比畫面快的情況,單個畫面延后的時間在人眼不能察覺的范圍內(nèi)還是能接受的,但是如此累計起來就會造成這個延遲的加大,導(dǎo)致后面聲話完全不同步,這是不能接受的。
那么為了解決這種問題,視頻“丟幀”就出現(xiàn)了。
視頻播放原理
我們看到的視頻其實就是一幅一幅的圖片組成的,就和電影一樣的原理,在很短的時間內(nèi)連續(xù)把這些圖片展示出來,這樣就達到了視頻連續(xù)的效果,比如每秒中展示25幅圖片。
而在這25幅圖片中某幾幅(不能太多)圖片沒有展示出來,我們也是很難察覺的,這就是我們“丟幀”的基礎(chǔ)了。
如果圖片丟失多了,明眼人一眼就看出來了,那么就不用再討論“丟幀”了,而是不會看你的這個視頻了。
視頻編碼過程(H264)
現(xiàn)在視頻編碼比較流行的就是H264編碼,它的壓縮(編碼)模式有很多種,適合于不同的場景,比如網(wǎng)絡(luò)直播、本地文件、UDP傳輸?shù)榷紩蓸硬煌膲嚎s(編碼)模式。
h264編碼器會把一幅幅的圖片壓縮(編碼)成體積很小的一個一個的單元(NALU),并且這些一個一個的單元之間并不是完全獨立的。
比如:有10幅圖片,經(jīng)過編碼后,第一幅圖片會單獨生成一個單元,而第二個圖片編碼后生成的單元只會包含和第一幅圖片不同的信息(有可能第二幅圖片和第一幅圖片只有一個文字不一樣,那么第二個單元編碼后的數(shù)據(jù)就僅僅包含了這個文字的信息,這樣的結(jié)果就是體積非常?。?,然后后面編碼后的第三個、第四個單元一直到第十個單元都只包含和前一個單元或幾個單元不同的信息(當然實際編碼時很復(fù)雜的),這樣的結(jié)果就是一個原本只有1G大小的一組圖片編碼后可能只有十多兆大小,大大減小了存儲空間和傳輸數(shù)據(jù)量的大小。
H264中 I幀、P幀、B幀的含義
前面提到的第一幅圖片是被單獨編碼成一個單元(NALU)的,在H264中我們定義關(guān)鍵幀(用字母I表示,I幀,包含一幅圖片的所有信息,可獨立解碼成一幅完整的圖片)。
后面的第二個單元一直到第十個單元中的每一個單元我們定義為P幀(差別幀,因為它不包含完整的畫面,只包含和前面幀的差別的信息,不能獨立解碼成一幅完整的圖片,需要和前面解碼后的圖片一起才能解碼出完整的圖片)。
當然H264中還有B幀(雙向幀,需要前后的數(shù)據(jù)才能解碼成單獨的圖片),這就是我們經(jīng)常聽說的視頻的I幀、P幀、B幀。
視頻解碼過程(H264)
通過前面的講解,相信大家對視頻編碼后圖片的變化過程有了大概的了解了(了解過程就行,具體技術(shù)細節(jié)就不用追究了),那么我們的重點就來了,播放器播放視頻的過程就和圖片編碼成視頻單元(NALU)的過程相反,而是把我們編碼后的I幀、P幀、B幀中的信息解碼后,依照編碼順序還原出原來的圖片,并按照一定的時間顯示(比如每秒顯示25幅圖片,那么每幅圖片之間的間隔就是40ms,也就是每隔40ms顯示一幅圖片)。
請注意這里的一定的時間(這里的40ms)里面播放器需要做許多的事情:
讀取視頻文件或網(wǎng)絡(luò)數(shù)據(jù)
識別讀取的數(shù)據(jù)中的視頻相關(guān)的數(shù)據(jù)
解析出里面的每一個單元(NALU),即每一幀(I、P、B)
然后把這些幀解碼出完整的圖片(I幀可以解碼成完整圖片,P、B幀則不可以,需要參考其他幀的數(shù)據(jù))
最后按照一定的時間間隔把解碼出來的圖片顯示出來
大多數(shù)情況下,播放器所在設(shè)備的軟硬件環(huán)境的解碼能力都是可以讓播放器在這個一定時間(比如40ms)內(nèi)完成圖片的顯示的,這種情況下就是最好不過的了。
而也有設(shè)備軟硬件環(huán)境的解碼能力不能在這個一定時間(比如40ms)內(nèi)完成圖片的顯示,但是呢又相差不大(比如相差幾毫秒),但是隨著解碼的次數(shù)增加,這個時間就會累計,后面就有可能相差幾秒、幾十秒、幾分鐘等,這樣就需要“丟幀”操作了。
開始丟幀
丟幀丟幀,怎么丟,丟掉哪些幀我們怎么決定呢,這就要從視頻圖像是怎么解碼得到的原理下手了,不然隨便丟幀的話,最容易出現(xiàn)的情況就是花屏,導(dǎo)致視頻基本不能看。下面我就舉個例子來說明怎樣丟幀:
比如我們的視頻規(guī)定的是隔40ms(每秒25幀,且沒秒的第一幀是I幀)顯示一幅圖片,而我們的設(shè)備解碼能力有限,最快的解碼出一幅圖片的時間也需要42ms,這樣本來該在40ms出顯示第一幅圖片,但是由于解碼時間花了42ms,那么這一幅圖片就在42ms時才顯示出來。
比規(guī)定的時間(40ms)延遲了(42-40)2ms,當我們連續(xù)解碼24幅圖片時,這個延遲就到了20 * 2ms = 40ms,假設(shè)這個40ms的延遲已經(jīng)很大了,再加大延遲就會造成我們明顯感覺到視頻的聲音和畫面不同步了。
所以我們就需要把后面的(25-24)1幀沒解碼的給丟了不顯示(因為此時解碼24幀的時間已經(jīng)消耗了24*42=1008ms了,也就是說下一個40ms該顯示第二秒的第一幀了,如果再顯示第一秒的最后一幀,這樣就會發(fā)生明顯不同步的現(xiàn)象了),而是接著第二秒的數(shù)據(jù)開始解碼顯示,這樣我們就成功的丟掉了一幀數(shù)據(jù),來盡量保證我們的聲音和畫面同步了。
丟幀優(yōu)化
前面提到的都是理想情況(每秒25幀,并且每一秒的第一幀都是I幀,能獨立解碼出圖像,不依賴其他幀)下的丟幀,而不理想的情況(2個I幀直接的間隔不是定長的,比如第一個I幀和第二個I幀直接間隔24個其他幀,而第三個I幀和第二個I幀之間相差35個其他幀)則是經(jīng)常遇到的。
這種情況下我們就不能寫死解碼播放24幀然后丟掉第25幀,因為可能出現(xiàn)丟掉25幀后的下一幀仍然不是I幀,這樣解碼就會解不出完整的圖片,顯示出來的畫面就會有花屏,影響體驗。
那么比較好的辦法就是,我們定義一個內(nèi)存緩沖區(qū)域,盡量在這個區(qū)域里面包含2個及以上的I幀(注意是解碼前),比如:播放器從第一個關(guān)鍵幀開始解碼播放,由于解碼能力有限,當理論時間應(yīng)該馬上解碼顯示第二個關(guān)鍵幀時,而此時播放器還在解碼這個關(guān)鍵幀之前的第5幀,也就是說播放器還得再解碼5幀才能到這個關(guān)鍵幀,那么我們就可以把這5幀給丟掉了,不解碼了,直接從這個關(guān)鍵幀開始解碼,這樣就能保證在每個關(guān)鍵幀解碼播放時都和理論播放的時間幾乎一致,讓人察覺不到不同步現(xiàn)象,而還不會造成花屏的現(xiàn)象。
這種丟幀個人覺得才是比較不錯的方案。
FFMpeg解碼偽代碼
bool?dropPacket?=?false;
while(true)
{
?AVPacket?*pkt?=?getVideoPacket();
?if(audioClock?>=?lastKeyFrameClock?+?offsetTime)//當前音頻時間已經(jīng)超過了下一幀關(guān)鍵幀之前了,就需要丟幀了
?{
??dropPacket?=?true;
?}
?if(pkt->flags?==?KEY_FRAME)//關(guān)鍵幀不丟
?{
??dropPacket?=?false;
?}
?if(dropPacket)
?{
??av_packet_free(pkt);
??av_free(&pkt);
??pkt?=?NULL;
??continue;
?}
?//正常解碼
?...
}
最后來一張出自靈魂畫手的丟幀圖:

來源:https://blog.csdn.net/ywl5320/article/details/86514391

技術(shù)交流,歡迎加我微信:ezglumes ,拉你入技術(shù)交流群。
私信領(lǐng)取相關(guān)資料
推薦閱讀:
開通專輯 | 細數(shù)那些年寫過的技術(shù)文章專輯
Android NDK 免費視頻在線學(xué)習?。。?/span>
覺得不錯,點個在看唄~

