在日常生活中,視訊類應用占據了我們越來越多的時間,各大公司也紛紛殺入這個戰場,不管是抖音、快手等短視訊類型,虎牙、鬥魚等直播類型,騰訊視訊、愛奇藝、優酷等長視訊類型,還是Vue、美拍等視訊編輯美顔類型,總有一款适合你。 未來随着5G普及以及網絡資費的下降,音視訊的前景是非常廣闊的。但是另一方面,無論是音視訊的編解碼和播放器、視訊編輯和美顔的各種算法,還是視訊與人工智能的結合(AI剪片、視訊修複、超清化等),它們都涉及了方方面面的底層知識,學習曲線比較陡峭,門檻相對比較高,是以也造成了目前各大公司音視訊相關人才的緊缺。
如果你對音視訊開發感興趣,我也非常建議你去往這個方向嘗試,我個人是非常看好音視訊開發這個領域的。 當然音視訊開發的經驗是靠着一次又一次的“填坑”成長起來的,下面我們一起來看看關于音視訊的認識和思考。 不管作為開發者還是使用者,現在我們每天都會接觸到各種各樣的短視訊、直播類的App,與之相關的音視訊方向的開發也變得越來越重要。但是對于大多數Android開發者來說,從事Android音視訊相關的開發可能目前還算是個小衆領
域,雖然可能目前深入這個領域的開發者還不是太多,但這個方向涉及的知識點可一點都不少。 音視訊的基礎知識 1. 音視訊相關的概念 說到音視訊,先從我們熟悉也陌生的視訊格式說起。 對于我們來說,最常見的視訊格式就是MP4格式,這是一個通用的容器格式。所謂容器格式,就意味内部要有對應的資料流用來承載内容。而且既然是一個視訊,那必然有音軌和視軌,而音軌、視軌本身也有對應的格式。常見的音軌、視軌格式包括:
- 視軌:H.265(HEVC)、H.264。其中,目前大部分Android手機都支援H.264格式的直接硬體編碼和解碼;對于H.265來說,Android 5.0以上的機器就支援直接硬體解碼了,但是對于硬體編碼,目前隻有一部分高端晶片可以支援,例如高通的8xx系列、華為的98x系列。對于視軌編碼來說,分辨率越大性能消耗也就越大,編碼所需的時間就越長。
- 音軌:AAC。這是一種曆史悠久音頻編碼格式,Android手機基本可以直接硬體編解碼,幾乎很少遇到相容性問題。可以說作為視訊的音軌格式,AAC已經非常成熟了。
對于編碼本身,上面提到的這些格式都是有損編碼,是以壓縮編碼本身還需要一個衡量壓縮之後,資料量多少的名額,這個标準就是碼率。同一個壓縮格式下,碼率越高品質也就越好。 小結一下,要拍攝一個MP4視訊,我們需要将視軌 + 音軌分别編碼,然後作為MP4的資料流,再合成出一個MP4檔案。 2. 音視訊編碼的流程 接下來,我們再來看看一個視訊是怎麼拍攝出來的。首先,既然是拍攝,少不了跟攝像頭、麥克風打交道。從流程來說,以H.264/AAC編碼為例,錄制視訊的總體流程是:
我們分别從攝像頭/錄音裝置采集資料,将資料送入編碼器,分别編碼出視軌/音軌之後,再送入合成器(MediaRemuxer或者類似mp4v2、FFmpeg之類的處理庫),最終輸出MP4檔案。接下來,我主要以視軌為例,來介紹下編碼的流程。 首先,直接使用系統的MediaRecorder錄制整個視訊,這是最簡單的方法,直接就能輸出MP4檔案。但是這個接口可定制化很差,比如我們想錄制一個正方形的視訊,除非攝像頭本身支援寬高一緻的分辨率,否則隻能後期處理或者各種Hack。另外,在實際App中,除非對視訊要求不是特别高,一般也不會直接使用MediaRecorder。 視軌的處理是錄制視訊中相對比較複雜的部分,輸入源頭是Camera的資料,最終輸出是編碼的H.264/H.265資料。下面我來介紹兩種處理模型。
第一種方法是利用Camera擷取攝像頭輸出的原始資料接口(例如onPreviewFrame),經過預處理,例如縮放、裁剪之後,送入編碼器,輸出H.264/H.265。 攝像頭輸出的原始資料格式為NV21,這是YUV顔色格式的一種。差別于RGB顔色,YUV資料格式占用空間更少,在視訊編碼領域使用十分廣泛。 一般來說,因為攝像頭直接輸出的NV21格式大小跟最終視訊不一定比對,而且編碼器往往也要求輸入另外一種YUV格式(一般來說是YUV420P),是以在擷取到NV21顔色格式之後,還需要進行各種縮放、裁剪之類的操作,一般會使用FFmpeg、libyuv這樣的庫處理YUV資料。 最後會将資料送入到編碼器。在視訊編碼器的選擇上,我們可以直接選擇系統的MediaCodec,利用手機本身的硬體編碼能力。但如果對最終輸出的視訊大小要求比較嚴格的話,使用的碼率會偏低,這種情況下大部分手機的硬體編碼器輸出的畫質可能會比較差。另外一種常見的選擇是利用x264來進行編碼,畫質表現相對較好,但是比起硬體編碼器,速度會慢很多,是以在實際使用時最好根據場景進行選擇。 除了直接處理攝像頭原始資料以外,還有一種常見的處理模型,利用Surface作為編碼器的輸入源。
免費分享《Andoird音視訊開發必備手冊+音視訊學習視訊+學習文檔資料包+大廠面試真題+2022最新學習路線圖》
對于Android攝像頭的預覽,需要設定一張Surface/SurfaceTexture來作為攝像頭預覽資料的輸出,而MediaCodec在API 18+之後,可以通過createInputSurface來建立一張Surface作為編碼器的輸入。這裡所說的另外一種方式就是,将攝像頭預覽Surface的内容,輸出到MediaCodec的InputSurface上。 而在編碼器的選擇上,雖然InputSurface是通過MediaCodec來建立的,乍看之下似乎隻能通過MediaCodec來進行編碼,無法使用x264來編碼,但利用PreviewSurface,我們可以建立一個OpenGL的上下文,這樣所有繪制的内容,都可以通過glReadPixel來擷取,然後再講讀取資料轉換成YUV再輸入到x264即可(另外,如果是在GLES 3.0的環境,我們還可以利用PBO來加速glReadPixles的速度)。
由于這裡我們建立了一個OpenGL的上下文,對于目前的視訊類App來說,還有各種各樣的濾鏡和美顔效果,實際上都可以基于OpenGL來實作。 而至于這種方式錄制視訊具體實作代碼,你可以參考下grafika中示例。 視訊處理 1. 視訊編輯 在當下視訊類App中,你可以見到各種視訊裁剪、視訊編輯的功能,例如:
- 裁剪視訊的一部分。
- 多個視訊進行拼接。
對于視訊裁剪、拼接來說,Android直接提供了MediaExtractor的接口,結合seek以及對應讀取幀資料readSampleData的接口,我們可以直接擷取對應時間戳的幀的内容,這樣讀取出來的是已經編碼好的資料,是以無需重新編碼,直接可以輸入合成器再次合成為MP4。
我們隻需要seek到需要裁剪原視訊的時間戳,然後一直讀取sampleData,送入MediaMuxer即可,這是視訊裁剪最簡單的實作方式。 但在實踐時會發現,seekTo并不會對所有時間戳都生效。比如說,一個4min左右的視訊,我們想要seek到大概2min左右的位置,然後從這個位置讀取資料,但實際調用seekTo到2min這個位置之後,再從MediaExtractor讀取資料,你會發現實際擷取的資料上可能是從2min這裡前面一點或者後面一點位置的内容。這是因為MediaExtractor這個接口隻能seek到視訊關鍵幀的位置,而我們想要的位置并不一定有關鍵幀。這個問題還是要回到視訊編碼,在視訊編碼時兩個關鍵幀之間是有一定間隔距離的。
如上圖所示,關鍵幀被成為I幀,可以被認為是一幀沒有被壓縮的畫面,解碼的時候無需要依賴其他視訊幀。但是在兩個關鍵幀之間,還存在這B幀、P幀這樣的壓縮幀,需要依賴其他幀才能完整解碼出一個畫面。至于兩個關鍵幀之間的間隔,被稱為一個GOP ,在GOP内的幀,MediaExtractor是無法直接seek到的,因為這個類不負責解碼,隻能seek到前後的關鍵幀。但如果GOP過大,就會導緻視訊編輯非常不精準了(實際上部分手機的ROM有改動,實作的MediaExtractor也能精确seek)。 既然如此,那要實作精确裁剪也就隻能去依賴解碼器了。解碼器本身能夠解出所有幀的内容,在引入解幀之後,整個裁剪的流程就變成了下面的樣子。
我們需要先seek到需要位置的前一I幀上,然後送入解碼器,解碼器解除一幀之後,判斷目前幀的PTS是否在需要的時間戳範圍内,如果是的話,再将資料送入編碼器,重新編碼再次得到H.264視軌資料,然後合成MP4檔案。 上面是基礎的視訊裁剪流程,對于視訊拼接,也是類似得到多段H.264資料之後,才一同送入合成器。 另外,在實際視訊編輯中,我們還會添加不少視訊特效和濾鏡。前面在視訊拍攝的場景下,我們利用Surface作為MediaCodec的輸入源,并且利用Surface建立了OpenGL的上下文。而MediaCodec作為解碼器的時候,也可以在configure的時候,指定一張Surface作為其解碼的輸出。大部分視訊特效都是可以通過OpenGL來實作的,是以要實作視訊特效,一般的流程是下面這樣的。
我們将解碼之後的渲染交給OpenGL,然後輸出到編碼器的InputSurface上,來實作整套編碼流程。 2. 視訊播放 任何視訊類App都會涉及視訊播放,從錄制、剪輯再到播放,構成完整的視訊體驗。對于要播放一個MP4檔案,最簡單的方式莫過于直接使用系統的MediaPlayer,隻需要簡單幾行代碼,就能直接播放視訊。對于本地視訊播放來說,這是最簡單的實作方式,但實際上我們可能會有更複雜的需求:
- 需要播放的視訊可能本身并不在本地,很多可能都是網絡視訊,有邊下邊播的需求。
- 播放的視訊可能是作為視訊編輯的一部分,在剪輯時需要實時預覽視訊特效。
對于第二種場景,我們可以簡單配置播放視訊的View為一個GLSurfaceView,有了OpenGL的環境,我們就可以在這上實作各種特效、濾鏡的效果了。而對于視訊編輯常見的快進、倒放之類的播放配置,MediaPlayer也有直接的接口可以設定。 更為常見的是第一種場景,例如一個視訊流界面,大部分視訊都是線上視訊,雖然MediaPlayer也能實作線上視訊播放,但實際使用下來,會有兩個問題:
- 通過設定MediaPlayer視訊URL方式下載下傳下來的視訊,被放到了一個私有的位置,App不容易直接通路,這樣會導緻我們沒法做視訊預加載,而且之前已經播放完、緩沖完的視訊,也不能重複利用原有緩沖内容。
- 同視訊剪輯直接使用MediaExtractor傳回的資料問題一樣,MediaPlayer同樣無法精确seek,隻能seek到有關鍵幀的地方。
對于第一個問題,我們可以通過視訊URL代理下載下傳的方式來解決,通過本地使用Local HTTP Server的方式代理下載下傳到一個指定的地方。現在開源社群已經有很成熟的項目實作,例如AndroidVideoCache。 而對于第二個問題來說,沒法精确seek的問題在有些App上是緻命的,産品可能無法接受這樣的體驗。那同視訊編輯一樣,我們隻能直接基于MediaCodec來自行實作播放器,這部分内容就比較複雜了。當然你也可以直接使用Google開源的ExoPlayer,簡單又快捷,而且也能支援設定線上視訊URL。 看似所有問題都有了解決方案,是不是就萬事大吉了呢? 常見的網絡邊下邊播視訊的格式都是MP4,但有些視訊直接上傳到伺服器上的時候,我們會發現無論是使用MediaPlayer還是ExoPlayer,似乎都隻能等待到整個視訊都下載下傳完才能開始播放,沒有達到邊下邊播的體驗。這個問題的原因實際上是因為MP4的格式導緻的,具體來看,是跟MP4格式中的moov有關。
MP4格式中有一個叫作moov的地方存儲這目前MP4檔案的元資訊,包括目前MP4檔案的音軌視軌格式、視訊長度、播放速率、視軌關鍵幀位置偏移量等重要資訊,MP4檔案線上播放的時候,需要moov中的資訊才能解碼音軌視軌。 而上述問題發生的原因在于,當moov在MP4檔案尾部的時候,播放器沒有足夠的資訊來進行解碼,是以視訊變得需要直接下載下傳完之後才能解碼播放。是以,要實作MP4檔案的邊下邊播,則需要将moov放到檔案頭部。目前來說,業界已經有非常成熟的工具,FFmpeg跟mp4v2都可以将一個MP4檔案的moov提前放到檔案頭部。例如使用FFmpeg,則是如下指令:
ffmpeg -i input.mp4 -movflags faststart -acodec copy -vcodec copy output.mp4
使用-movflags faststart,我們就可以把視訊檔案中的moov提前了。 另外,如果想要檢測一個MP4的moov是否在前面,可以使用類似AtomicParsley的工具來檢測。 在視訊播放的實踐中,除了MP4格式來作為邊下邊播的格式以外,還有更多的場景需要使用其他格式,例如m3u8、FLV之類,業界在用戶端中常見的實作包括ijkplayer、ExoPlayer,有興趣的同學可以參考下它們的實作。 音視訊開發的學習之路 音視訊相關開發涉及面很廣,今天我也隻是簡單介紹一下其中基本的架構,如果想繼續深入這個領域發展,從我個人學習的經曆來看,想要成為一名合格的開發者,除了基礎的Android開發知識以外,還要深入學習,我認為還需要掌握下面的技術棧。 語言
- C/C++:音視訊開發經常需要跟底層代碼打交道,掌握C/C++是必須的技能。這方面資料很多,相信我們都能找到。
- ARM NEON彙編:這是一項進階技能,在視訊編解碼、各種幀處理低下時很多都是利用NEON彙編加速,例如FFmpeg/libyuv底層都大量利用了NEON彙編來加速處理過程。雖說它不是必備技能,但有興趣也可以多多了解,具體資料可以參考ARM社群的教程。
架構
- FFmpeg:可以說是業界最出名的音視訊處理架構了,幾乎囊括音視訊開發的所有流程,可以說是必備技能。
- libyuv:Google開源的YUV幀處理庫,因為攝像頭輸出、編解碼輸入輸出也是基于YUV格式,是以也經常需要這個庫來操作資料(FFmpeg也有提供了這個庫裡面所有的功能,在libswscale都可以找到類似的實作。不過這個庫性能更好,也是基于NEON彙編加速)。
- libx264/libx265:目前業界最為廣泛使用的H.264/H.265軟編解碼庫。移動平台上雖然可以使用寫死,但很多時候出于相容性或畫質的考慮,因為不少低端的Android機器,在低碼率的場景下還是軟編碼的畫質會更好,最終可能還是得考慮使用軟編解碼。
- OpenGL ES:當今,大部分視訊特效、美顔算法的處理,最終渲染都是基于GLES來實作的,是以想要深入音視訊的開發,GLES是必備的知識。另外,除了GLES以外,Vulkan也是近幾年開始發展起來的一個更高性能的圖形API,但目前來看,使用還不是特别廣泛。
- ExoPlayer/ijkplayer:一個完整的視訊類App肯定會涉及視訊播放的體驗,這兩個庫可以說是當下業界最為常用的視訊播放器了,支援衆多格式、協定,如果你想要深入學習視訊播放處理,它們幾乎也算是必備技能。
從實際需求出發,基于上述技術棧,我們可以從下面兩條路徑來深入學習。 1. 視訊相關特效開發 直播、小視訊相關App目前越來越多,幾乎每個App相關的特效,往往都是利用OpenGL本身來實作。對于一些簡單的特效,可以使用類似Color Look Up Table的技術,通過修改素材配合Shader來查找顔色替換就能實作。如果要繼續學習更加複雜的濾鏡,推薦你可以去shadertoy學習參考,上面有非常多Shader的例子。 而美顔、美型相關的效果,特别是美型,需要利用人臉識别擷取到關鍵點,對人臉紋理進行三角劃分,然後再通過Shader中放大、偏移對應關鍵點紋理坐标來實作。如果想要深入視訊特效類的開發,我推薦可以多學習OpenGL相關的知識,這裡會涉及很多優化點。 2. 視訊編碼壓縮算法 H.264/H.265都是非常成熟的視訊編碼标準,如何利用這些視訊編碼标準,在保證視訊品質的前提下,将視訊大小最小化,進而節省帶寬,這就需要對視訊編碼标準本身要有非常深刻的了解。這可能是一個門檻相對較高的方向,有興趣的同學可以閱讀相關編碼标準的文檔。