談?wù)剬ndroid音視頻開發(fā)的探究

在日常生活中,視頻類應(yīng)用占據(jù)了我們越來越多的時間,各大公司也紛紛殺入這個戰(zhàn)場,不管是抖音、快手等短視頻類型,虎牙、斗魚等直播類型,騰訊視頻、愛奇藝、優(yōu)酷等長視頻類型,還是Vue、美拍等視頻編輯美顏類型,總有一款適合你。

未來隨著5G普及以及網(wǎng)絡(luò)資費(fèi)的下降,音視頻的前景是非常廣闊的。但是另一方面,無論是音視頻的編解碼和播放器、視頻編輯和美顏的各種算法,還是視頻與人工智能的結(jié)合(AI剪片、視頻修復(fù)、超清化等),它們都涉及了方方面面的底層知識,學(xué)習(xí)曲線比較陡峭,門檻相對比較高,所以也造成了目前各大公司音視頻相關(guān)人才的緊缺。如果你對音視頻開發(fā)感興趣,我也非常建議你去往這個方向嘗試,我個人是非??春靡粢曨l開發(fā)這個領(lǐng)域的。

當(dāng)然音視頻開發(fā)的經(jīng)驗是靠著一次又一次的“填坑”成長起來的,下面我們一起來看看關(guān)于音視頻的認(rèn)識和思考。

不管作為開發(fā)者還是用戶,現(xiàn)在我們每天都會接觸到各種各樣的短視頻、直播類的App,與之相關(guān)的音視頻方向的開發(fā)也變得越來越重要。但是對于大多數(shù)Android開發(fā)者來說,從事Android音視頻相關(guān)的開發(fā)可能目前還算是個小眾領(lǐng)域,雖然可能目前深入這個領(lǐng)域的開發(fā)者還不是太多,但這個方向涉及的知識點(diǎn)可一點(diǎn)都不少。

音視頻的基礎(chǔ)知識

1. 音視頻相關(guān)的概念

說到音視頻,先從我們熟悉也陌生的視頻格式說起。

對于我們來說,最常見的視頻格式就是MP4格式,這是一個通用的容器格式。所謂容器格式,就意味內(nèi)部要有對應(yīng)的數(shù)據(jù)流用來承載內(nèi)容。而且既然是一個視頻,那必然有音軌和視軌,而音軌、視軌本身也有對應(yīng)的格式。常見的音軌、視軌格式包括:

  • 視軌:H.265(HEVC)、H.264。其中,目前大部分Android手機(jī)都支持H.264格式的直接硬件編碼和解碼;對于H.265來說,Android 5.0以上的機(jī)器就支持直接硬件解碼了,但是對于硬件編碼,目前只有一部分高端芯片可以支持,例如高通的8xx系列、華為的98x系列。對于視軌編碼來說,分辨率越大性能消耗也就越大,編碼所需的時間就越長。
  • 音軌:AAC。這是一種歷史悠久音頻編碼格式,Android手機(jī)基本可以直接硬件編解碼,幾乎很少遇到兼容性問題??梢哉f作為視頻的音軌格式,AAC已經(jīng)非常成熟了。

對于編碼本身,上面提到的這些格式都是有損編碼,因此壓縮編碼本身還需要一個衡量壓縮之后,數(shù)據(jù)量多少的指標(biāo),這個標(biāo)準(zhǔn)就是碼率。同一個壓縮格式下,碼率越高質(zhì)量也就越好。更多Android本身支持的編解碼格式,你可以參考官方文檔。

小結(jié)一下,要拍攝一個MP4視頻,我們需要將視軌 + 音軌分別編碼,然后作為MP4的數(shù)據(jù)流,再合成出一個MP4文件。

2. 音視頻編碼的流程

接下來,我們再來看看一個視頻是怎么拍攝出來的。首先,既然是拍攝,少不了跟攝像頭、麥克風(fēng)打交道。從流程來說,以H.264/AAC編碼為例,錄制視頻的總體流程是:

我們分別從攝像頭/錄音設(shè)備采集數(shù)據(jù),將數(shù)據(jù)送入編碼器,分別編碼出視軌/音軌之后,再送入合成器(MediaRemuxer或者類似mp4v2、FFmpeg之類的處理庫),最終輸出MP4文件。接下來,我主要以視軌為例,來介紹下編碼的流程。

首先,直接使用系統(tǒng)的MediaRecorder錄制整個視頻,這是最簡單的方法,直接就能輸出MP4文件。但是這個接口可定制化很差,比如我們想錄制一個正方形的視頻,除非攝像頭本身支持寬高一致的分辨率,否則只能后期處理或者各種Hack。另外,在實際App中,除非對視頻要求不是特別高,一般也不會直接使用MediaRecorder。

視軌的處理是錄制視頻中相對比較復(fù)雜的部分,輸入源頭是Camera的數(shù)據(jù),最終輸出是編碼的H.264/H.265數(shù)據(jù)。下面我來介紹兩種處理模型。

第一種方法是利用Camera獲取攝像頭輸出的原始數(shù)據(jù)接口(例如onPreviewFrame),經(jīng)過預(yù)處理,例如縮放、裁剪之后,送入編碼器,輸出H.264/H.265。

攝像頭輸出的原始數(shù)據(jù)格式為NV21,這是YUV顏色格式的一種。區(qū)別于RGB顏色,YUV數(shù)據(jù)格式占用空間更少,在視頻編碼領(lǐng)域使用十分廣泛。

一般來說,因為攝像頭直接輸出的NV21格式大小跟最終視頻不一定匹配,而且編碼器往往也要求輸入另外一種YUV格式(一般來說是YUV420P),因此在獲取到NV21顏色格式之后,還需要進(jìn)行各種縮放、裁剪之類的操作,一般會使用FFmpeg、libyuv這樣的庫處理YUV數(shù)據(jù)。

最后會將數(shù)據(jù)送入到編碼器。在視頻編碼器的選擇上,我們可以直接選擇系統(tǒng)的MediaCodec,利用手機(jī)本身的硬件編碼能力。但如果對最終輸出的視頻大小要求比較嚴(yán)格的話,使用的碼率會偏低,這種情況下大部分手機(jī)的硬件編碼器輸出的畫質(zhì)可能會比較差。另外一種常見的選擇是利用x264來進(jìn)行編碼,畫質(zhì)表現(xiàn)相對較好,但是比起硬件編碼器,速度會慢很多,因此在實際使用時最好根據(jù)場景進(jìn)行選擇。

除了直接處理攝像頭原始數(shù)據(jù)以外,還有一種常見的處理模型,利用Surface作為編碼器的輸入源。

對于Android攝像頭的預(yù)覽,需要設(shè)置一張Surface/SurfaceTexture來作為攝像頭預(yù)覽數(shù)據(jù)的輸出,而MediaCodec在API 18+之后,可以通過createInputSurface來創(chuàng)建一張Surface作為編碼器的輸入。這里所說的另外一種方式就是,將攝像頭預(yù)覽Surface的內(nèi)容,輸出到MediaCodec的InputSurface上。

而在編碼器的選擇上,雖然InputSurface是通過MediaCodec來創(chuàng)建的,乍看之下似乎只能通過MediaCodec來進(jìn)行編碼,無法使用x264來編碼,但利用PreviewSurface,我們可以創(chuàng)建一個OpenGL的上下文,這樣所有繪制的內(nèi)容,都可以通過glReadPixel來獲取,然后再講讀取數(shù)據(jù)轉(zhuǎn)換成YUV再輸入到x264即可(另外,如果是在GLES 3.0的環(huán)境,我們還可以利用PBO來加速glReadPixles的速度)。

由于這里我們創(chuàng)建了一個OpenGL的上下文,對于目前的視頻類App來說,還有各種各樣的濾鏡和美顏效果,實際上都可以基于OpenGL來實現(xiàn)。

而至于這種方式錄制視頻具體實現(xiàn)代碼,你可以參考下grafika中示例。

視頻處理

1. 視頻編輯

在當(dāng)下視頻類App中,你可以見到各種視頻裁剪、視頻編輯的功能,例如:

  • 裁剪視頻的一部分。
  • 多個視頻進(jìn)行拼接。

對于視頻裁剪、拼接來說,Android直接提供了MediaExtractor的接口,結(jié)合seek以及對應(yīng)讀取幀數(shù)據(jù)readSampleData的接口,我們可以直接獲取對應(yīng)時間戳的幀的內(nèi)容,這樣讀取出來的是已經(jīng)編碼好的數(shù)據(jù),因此無需重新編碼,直接可以輸入合成器再次合成為MP4。

我們只需要seek到需要裁剪原視頻的時間戳,然后一直讀取sampleData,送入MediaMuxer即可,這是視頻裁剪最簡單的實現(xiàn)方式。

但在實踐時會發(fā)現(xiàn),seekTo并不會對所有時間戳都生效。比如說,一個4min左右的視頻,我們想要seek到大概2min左右的位置,然后從這個位置讀取數(shù)據(jù),但實際調(diào)用seekTo到2min這個位置之后,再從MediaExtractor讀取數(shù)據(jù),你會發(fā)現(xiàn)實際獲取的數(shù)據(jù)上可能是從2min這里前面一點(diǎn)或者后面一點(diǎn)位置的內(nèi)容。這是因為MediaExtractor這個接口只能seek到視頻關(guān)鍵幀的位置,而我們想要的位置并不一定有關(guān)鍵幀。這個問題還是要回到視頻編碼,在視頻編碼時兩個關(guān)鍵幀之間是有一定間隔距離的。

如上圖所示,關(guān)鍵幀被成為I幀,可以被認(rèn)為是一幀沒有被壓縮的畫面,解碼的時候無需要依賴其他視頻幀。但是在兩個關(guān)鍵幀之間,還存在這B幀P幀這樣的壓縮幀,需要依賴其他幀才能完整解碼出一個畫面。至于兩個關(guān)鍵幀之間的間隔,被稱為一個GOP ,在GOP內(nèi)的幀,MediaExtractor是無法直接seek到的,因為這個類不負(fù)責(zé)解碼,只能seek到前后的關(guān)鍵幀。但如果GOP過大,就會導(dǎo)致視頻編輯非常不精準(zhǔn)了(實際上部分手機(jī)的ROM有改動,實現(xiàn)的MediaExtractor也能精確seek)。

既然如此,那要實現(xiàn)精確裁剪也就只能去依賴解碼器了。解碼器本身能夠解出所有幀的內(nèi)容,在引入解幀之后,整個裁剪的流程就變成了下面的樣子。

我們需要先seek到需要位置的前一I幀上,然后送入解碼器,解碼器解除一幀之后,判斷當(dāng)前幀的PTS是否在需要的時間戳范圍內(nèi),如果是的話,再將數(shù)據(jù)送入編碼器,重新編碼再次得到H.264視軌數(shù)據(jù),然后合成MP4文件。

上面是基礎(chǔ)的視頻裁剪流程,對于視頻拼接,也是類似得到多段H.264數(shù)據(jù)之后,才一同送入合成器。

另外,在實際視頻編輯中,我們還會添加不少視頻特效和濾鏡。前面在視頻拍攝的場景下,我們利用Surface作為MediaCodec的輸入源,并且利用Surface創(chuàng)建了OpenGL的上下文。而MediaCodec作為解碼器的時候,也可以在configure的時候,指定一張Surface作為其解碼的輸出。大部分視頻特效都是可以通過OpenGL來實現(xiàn)的,因此要實現(xiàn)視頻特效,一般的流程是下面這樣的。

我們將解碼之后的渲染交給OpenGL,然后輸出到編碼器的InputSurface上,來實現(xiàn)整套編碼流程。

2. 視頻播放

任何視頻類App都會涉及視頻播放,從錄制、剪輯再到播放,構(gòu)成完整的視頻體驗。對于要播放一個MP4文件,最簡單的方式莫過于直接使用系統(tǒng)的MediaPlayer,只需要簡單幾行代碼,就能直接播放視頻。對于本地視頻播放來說,這是最簡單的實現(xiàn)方式,但實際上我們可能會有更復(fù)雜的需求:

  • 需要播放的視頻可能本身并不在本地,很多可能都是網(wǎng)絡(luò)視頻,有邊下邊播的需求。
  • 播放的視頻可能是作為視頻編輯的一部分,在剪輯時需要實時預(yù)覽視頻特效。

對于第二種場景,我們可以簡單配置播放視頻的View為一個GLSurfaceView,有了OpenGL的環(huán)境,我們就可以在這上實現(xiàn)各種特效、濾鏡的效果了。而對于視頻編輯常見的快進(jìn)、倒放之類的播放配置,MediaPlayer也有直接的接口可以設(shè)置。

更為常見的是第一種場景,例如一個視頻流界面,大部分視頻都是在線視頻,雖然MediaPlayer也能實現(xiàn)在線視頻播放,但實際使用下來,會有兩個問題:

  • 通過設(shè)置MediaPlayer視頻URL方式下載下來的視頻,被放到了一個私有的位置,App不容易直接訪問,這樣會導(dǎo)致我們沒法做視頻預(yù)加載,而且之前已經(jīng)播放完、緩沖完的視頻,也不能重復(fù)利用原有緩沖內(nèi)容。
  • 同視頻剪輯直接使用MediaExtractor返回的數(shù)據(jù)問題一樣,MediaPlayer同樣無法精確seek,只能seek到有關(guān)鍵幀的地方。

對于第一個問題,我們可以通過視頻URL代理下載的方式來解決,通過本地使用Local HTTP Server的方式代理下載到一個指定的地方。現(xiàn)在開源社區(qū)已經(jīng)有很成熟的項目實現(xiàn),例如AndroidVideoCache

而對于第二個問題來說,沒法精確seek的問題在有些App上是致命的,產(chǎn)品可能無法接受這樣的體驗。那同視頻編輯一樣,我們只能直接基于MediaCodec來自行實現(xiàn)播放器,這部分內(nèi)容就比較復(fù)雜了。當(dāng)然你也可以直接使用Google開源的ExoPlayer,簡單又快捷,而且也能支持設(shè)置在線視頻URL。

看似所有問題都有了解決方案,是不是就萬事大吉了呢?

常見的網(wǎng)絡(luò)邊下邊播視頻的格式都是MP4,但有些視頻直接上傳到服務(wù)器上的時候,我們會發(fā)現(xiàn)無論是使用MediaPlayer還是ExoPlayer,似乎都只能等待到整個視頻都下載完才能開始播放,沒有達(dá)到邊下邊播的體驗。這個問題的原因?qū)嶋H上是因為MP4的格式導(dǎo)致的,具體來看,是跟MP4格式中的moov有關(guān)。

[圖片上傳失敗...(image-f034df-1663837644722)]

MP4格式中有一個叫作moov的地方存儲這當(dāng)前MP4文件的元信息,包括當(dāng)前MP4文件的音軌視軌格式、視頻長度、播放速率、視軌關(guān)鍵幀位置偏移量等重要信息,MP4文件在線播放的時候,需要moov中的信息才能解碼音軌視軌。

而上述問題發(fā)生的原因在于,當(dāng)moov在MP4文件尾部的時候,播放器沒有足夠的信息來進(jìn)行解碼,因此視頻變得需要直接下載完之后才能解碼播放。因此,要實現(xiàn)MP4文件的邊下邊播,則需要將moov放到文件頭部。目前來說,業(yè)界已經(jīng)有非常成熟的工具,FFmpegmp4v2都可以將一個MP4文件的moov提前放到文件頭部。例如使用FFmpeg,則是如下命令:

ffmpeg -i input.mp4 -movflags faststart -acodec copy -vcodec copy output.mp4

使用-movflags faststart,我們就可以把視頻文件中的moov提前了。

另外,如果想要檢測一個MP4的moov是否在前面,可以使用類似AtomicParsley的工具來檢測。

在視頻播放的實踐中,除了MP4格式來作為邊下邊播的格式以外,還有更多的場景需要使用其他格式,例如m3u8、FLV之類,業(yè)界在客戶端中常見的實現(xiàn)包括ijkplayerExoPlayer,有興趣的同學(xué)可以參考下它們的實現(xiàn)。

音視頻開發(fā)的學(xué)習(xí)之路

音視頻相關(guān)開發(fā)涉及面很廣,今天我也只是簡單介紹一下其中基本的架構(gòu),如果想繼續(xù)深入這個領(lǐng)域發(fā)展,從我個人學(xué)習(xí)的經(jīng)歷來看,想要成為一名合格的開發(fā)者,除了基礎(chǔ)的Android開發(fā)知識以外,還要深入學(xué)習(xí),我認(rèn)為還需要掌握下面的技術(shù)棧。

語言

  • C/C++:音視頻開發(fā)經(jīng)常需要跟底層代碼打交道,掌握C/C++是必須的技能。這方面資料很多,相信我們都能找到。
  • ARM NEON匯編:這是一項進(jìn)階技能,在視頻編解碼、各種幀處理低下時很多都是利用NEON匯編加速,例如FFmpeg/libyuv底層都大量利用了NEON匯編來加速處理過程。雖說它不是必備技能,但有興趣也可以多多了解,具體資料可以參考ARM社區(qū)的教程。

框架

  • FFmpeg:可以說是業(yè)界最出名的音視頻處理框架了,幾乎囊括音視頻開發(fā)的所有流程,可以說是必備技能。
  • libyuv:Google開源的YUV幀處理庫,因為攝像頭輸出、編解碼輸入輸出也是基于YUV格式,所以也經(jīng)常需要這個庫來操作數(shù)據(jù)(FFmpeg也有提供了這個庫里面所有的功能,在libswscale都可以找到類似的實現(xiàn)。不過這個庫性能更好,也是基于NEON匯編加速)。
  • libx264/libx265:目前業(yè)界最為廣泛使用的H.264/H.265軟編解碼庫。移動平臺上雖然可以使用硬編碼,但很多時候出于兼容性或畫質(zhì)的考慮,因為不少低端的Android機(jī)器,在低碼率的場景下還是軟編碼的畫質(zhì)會更好,最終可能還是得考慮使用軟編解碼。
  • OpenGL ES:當(dāng)今,大部分視頻特效、美顏算法的處理,最終渲染都是基于GLES來實現(xiàn)的,因此想要深入音視頻的開發(fā),GLES是必備的知識。另外,除了GLES以外,Vulkan也是近幾年開始發(fā)展起來的一個更高性能的圖形API,但目前來看,使用還不是特別廣泛。
  • ExoPlayer/ijkplayer:一個完整的視頻類App肯定會涉及視頻播放的體驗,這兩個庫可以說是當(dāng)下業(yè)界最為常用的視頻播放器了,支持眾多格式、協(xié)議,如果你想要深入學(xué)習(xí)視頻播放處理,它們幾乎也算是必備技能。

從實際需求出發(fā),基于上述技術(shù)棧,我們可以從下面兩條路徑來深入學(xué)習(xí)。

1. 視頻相關(guān)特效開發(fā)

直播、小視頻相關(guān)App目前越來越多,幾乎每個App相關(guān)的特效,往往都是利用OpenGL本身來實現(xiàn)。對于一些簡單的特效,可以使用類似Color Look Up Table的技術(shù),通過修改素材配合Shader來查找顏色替換就能實現(xiàn)。如果要繼續(xù)學(xué)習(xí)更加復(fù)雜的濾鏡,推薦你可以去shadertoy學(xué)習(xí)參考,上面有非常多Shader的例子。

而美顏、美型相關(guān)的效果,特別是美型,需要利用人臉識別獲取到關(guān)鍵點(diǎn),對人臉紋理進(jìn)行三角劃分,然后再通過Shader中放大、偏移對應(yīng)關(guān)鍵點(diǎn)紋理坐標(biāo)來實現(xiàn)。如果想要深入視頻特效類的開發(fā),我推薦可以多學(xué)習(xí)OpenGL相關(guān)的知識,這里會涉及很多優(yōu)化點(diǎn)。

2. 視頻編碼壓縮算法

H.264/H.265都是非常成熟的視頻編碼標(biāo)準(zhǔn),如何利用這些視頻編碼標(biāo)準(zhǔn),在保證視頻質(zhì)量的前提下,將視頻大小最小化,從而節(jié)省帶寬,這就需要對視頻編碼標(biāo)準(zhǔn)本身要有非常深刻的理解。這可能是一個門檻相對較高的方向,我也尚處學(xué)習(xí)階段,有興趣的同學(xué)可以閱讀相關(guān)編碼標(biāo)準(zhǔn)的文檔。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容