這一篇想必是閱讀起來輕松的,因為不會有很多的代碼片段,今天我們來談談如果是Android新手上手App音視頻開發的學習步驟路線應該是什么樣的;最后我們介紹下Android專案中音視頻實際開發會遇到的一些事情以及解決方案,我們今天只談思路涉及具體細節可能在接下來的文章里面會具體體現,好的讓我們開始吧:
首先我認為音視頻作為一個比較垂直的行業,里面的技術背景肯定也是垂直的,這就導致了如果是做通用型App(比如說互聯網里面的電商,O2O等)的工程師一開始上手的時候無從下手,找不到方向,那么我們從一些基礎概念入手今天給大家掰開了,掄圓了說,我們說的音視頻或者說流媒體一般包含以下幾個知識點:
首先需要了解的是音視頻處理的流程:
- 資料分別經歷了 解協議,解封裝,音/視頻解碼,播放 步驟,先上一張圖來解釋:

這是一個比較完整的程序,一般來說我們做播放器的時候處理媒體檔案(例如Mp4)會完整的經歷過這個程序,如果是自定義的流媒體資料可能沒有上面的 解協議,解封裝 步驟
其次是了解音頻PCM資料格式:
- 怎么采樣采樣率,
- 單/雙通道,
- 樣本怎么存盤(8bit/16bit),
- 一幀音頻為多少樣本(通常是按1024個采樣點一幀,每幀采樣間隔為23.22ms)
- 每幀PCM資料大小:(PCM Buffersize=采樣率 * 采樣時間 * 采樣位深/8*通道數(Bytes))
- 每秒的PCM資料大小:(采樣率×采樣位深×聲道數bps)
再次了解視頻YUV資料:
- YUV資料的幾種格式(YUV420P,YUV420SP,NV12,NV21)的排布是怎么樣的 -
- 怎么計算例如YUV420P的大小
- 怎么分解明亮度與色度
既然是音視頻肯定要涉及壓縮編碼,那么首先應該要了解編碼標準:
- 國際標準化組織(ISO)的MPEG-1、MPEG-2與MPEG-4,的規范和標準是哪些
- 其次要了解這個這個主流標準里面MPEG-4的音頻/視頻具體的一種編碼格式,一般來說是AAC(MP3)與H.264
- AAC編碼格式資料:要了解AAC編碼的ADTS frame與ADTS頭是怎么樣子的

-
H.264編碼格式資料:要了解H.264的編碼格式一般主流是兩種AVCC(IOS默認硬編碼),Annex-B(Android默認硬編碼)
-
Annex-B格式里面每個NALU的格式:包含頭與payload是什么樣的

-
AVCC里面extradata里面的資料格式是怎么樣的(包含SPS,PPS在里面)
-
H.264里面的SPS,PPS,I幀,P幀,B幀所表示的意義
說了編碼當然要有解碼:
- Android里面音頻的硬解,軟解(ffpmeg)怎么實作;
- Android里面視頻的硬解(MediaCodec),軟解(ffmpeg)怎么實作;
解碼以后怎么播放,音頻播放:
- Android :(包括不限于:AudioTrack ,OpenSLES);
- 播放中音頻重采樣(播放環境如果與樣本環境不兼容則需要重采樣);
解碼后視頻播放:
- Android:(包括不限于:GLSurfaceView ,OpenGLES);
- Android 平臺EGL的使用
其中OpenGLES 特別是可以作為一個分支來進行加強:
- 物體坐標系:是指繪制物體的坐標系,
- 世界坐標系:是指擺放物體的坐標系,
- 攝像機坐標系:攝像機的在三維空間的位置,攝像機的方向向量,攝像機的up方向向量
- 簡單的繪制一些基本圖形:三角形,正方形,球形
- 紋理坐標:紋理貼圖的方向以及大小 兩種投影:正射投影,透視投影
- 著色器語言GLSL的基本語法以及使用
- 紋理貼圖顯示圖片
- 處理平移、旋轉、縮放等一些3x3 ,4X4的基本矩陣運算
- FBO離屏渲染
什么是封包:
- 然后是資料封包格式:包括MP4,TS的格式大致是什么樣子的,支持哪幾種音視頻的編碼格式;
- DTS(Decoding Time Stamp)和PTS(Presentation Time Stamp)代表的意義;
- TimeBase時間基在做音視頻同步的意義;
音視頻流媒體在網路上怎么傳輸:
- 音視頻在網路傳輸方式:HTTP,HLS,RTMP,HttpFlv
音視頻應用層框架有哪些: - 高級應用框架:ffmpeg的基本使用 - 高級應用框架:OpenCV的基本使用
額外需要掌握哪些技能:
- JNI的使用;
- C/C++ 基礎;
以上是我認為作為音視頻工程師入門應該掌握的知識點,我覺得掌握了這些不敢說成為了一個高手,但應該是成為一個合格的音視頻工程師的 基本功
PS:基本功重要嗎?我認為非常重要,往小了說基本功顯示了一個人的技能扎實,擁有了扎實的基礎才能往更深的方向發展;往大了說基本功顯示了一個人可靠,處事沉穩可以做到了解一個事物的本質能做到萬變不離其中
有了這些基本功那么我們可以接觸一些實際的案例了,如果你想要更進階那么我推薦一本我認為音視頻內容比較全,而且里面有很多實戰例子作為參考的書:

也不用大家花錢去買,可以直接找我拿,以及相關的音視頻資料一同發給你
獲取地址:【Android開發交流】



這本書我認為有幾點比較好的:
-
第一是這本書出于實戰出發(據說是 唱吧App 架構師在做唱吧的時候總結了很多經驗寫的),
-
第二這本書的內容包含了Android,IOS兩個版本的所以有對比參考性,第三這本書從基礎的音視頻到高級的應用場景都介紹了,可謂是內容豐富;
說了這么多好的再說說這本書的一些不好的地方:
-
首先就是我認為這本書不太適合剛剛入門的新手(注意是剛剛入門)如果是這類的工程師一些概念都沒搞清楚的就看這個其實不是很合適;
-
其次就是里面的例子的代碼段過于松散,閱讀起來需要不是很順暢,而且git里面的Demo感覺也跟不上書里面的代碼,里面的Demo目錄結構不是很清晰(一般來說我們見得多的是1章分為一個或多個專案,分別講解對應的內容互相不會干擾,書里面是git commit來區分的感覺體驗性不是很好)
但是瑕不掩瑜如果你是有基礎的話,那么這本書肯定能給你帶了專案中的幫助,
有了前兩步驟作為基礎的話,那么我們接下來就要實戰著手下專案,以及聊一聊專案中實作音視頻以及相關的功能想要用到哪些技術方案:
先介紹下我們是做智能硬體,當然少不了App以及硬體,今天介紹的主要是我們的智能產品在音視頻開發中的解決方案以及技術選型,由于今天是技術選型我們不會涉及具體的實作細節,因為技術選項定下來以后細節實作網上有很多文章,或者接下來我再分開把一些細節給大家寫出來,歡迎大家給我留言,讓我們開始吧:
產品實作的功能: - App 音視頻的資料怎么傳輸 - App 實作音視頻解碼 - App 實作音視軟解的播放 - App 實作截圖拍照 - App 實作錄制視頻 - App 實作音視頻同步
App音視頻的資料怎么傳輸: - App這邊與嵌入式定好傳輸協議,協議資料大致分為協議頭,協議體,協議頭:包括同步碼欄位,幀型別,資料長度,資料方向,時間戳等等拿到資料頭以后 就可以按照長度拿到協議體資料就可以開始解碼了
typedef struct
{
HLE_U8 sync_code[3]; /*幀頭同步碼,固定為0x00,0x00,0x01*/
HLE_U8 type; /*幀型別, */
HLE_U8 enc_std; //編碼標準,0:H.264 ; 1:H.265
HLE_U8 framerate; //幀率(僅I幀有效)
HLE_U16 reserved; //保留位
HLE_U16 pic_width; //圖片寬(僅I幀有效)
HLE_U16 pic_height; //圖片高(僅I幀有效)
HLE_SYS_TIME rtc_time; //當前幀時間戳,精確到秒,非關鍵幀時間戳需根據幀率來計算(僅I幀有效)8位元組
HLE_U32 length; //幀資料長度
HLE_U64 pts_msec; //毫秒級時間戳,一直累加,溢位后自動回繞
} P2P_FRAME_HDR; //32位元組
App實作實時音視頻解碼:
-
Android音視頻的硬解碼:如果是一個長時間做音視頻工程師來說Android的硬解碼絕對是個苦惱的東西,因為以前Android的機型太多太散,而且低中高端機型差異太大,導致硬體和系統不能完全兼容,所以在以前這種情況非常明顯表現為在各個機型視頻的顯示各有不同,有些機型正常顯示有些機型有綠屏,有些有馬賽克現象出現,雖然網路上有對綠屏這種現象有解決方案:例如如果YUV資料源和解碼器不同的話會有這種問題,這種情況還算好解決,但是有些Android機型對于一些YUV排布就不支持,例如我在小米手機以及三星手機同一份代碼效果都不盡相同,考慮到很多的不確定性Android的硬解碼似乎是雞肋,
-
Android的硬解碼真的是雞肋嗎?我覺得未必,這個也要看你的應用場景因為現在的Android中端手機,中高端,高端,甚至旗艦手機的硬解碼的穩定性都已經不錯,如果你是在這種應用場景下那么硬解碼確實是一個首選,因為硬解碼對比軟解有天然的優勢:就是呼叫GPU的專門模塊編碼來解,減少CPU運算,對CPU等硬體要求也相對低點,軟解需要CPU運算,變相加大CPU負擔耗電增加很多,硬體解碼是將原來全部交由CPU來處理的視頻資料的一部分交由GPU來做,而GPU的并行運算能力要遠遠高于CPU,這樣可以大大的降低對CPU的負載,CPU的占用率較低了之后就可以同時運行一些其他的程式了,
硬體碼優勢:更加省電,適合長時間的移動端視頻播放器和直播,手機電池有限的情況下,使用硬體解碼會更加好,減少CPU的占用,可以把CUP讓給別的執行緒使用,有利于手機的流暢度,
軟解碼優勢:具有更好的適應性,軟體解碼主要是會占用CUP的運行,軟解不考慮社備的硬體解碼支持情況,有CPU就可以使用了,但是占用了更多的CUP那就意味著很耗費性能,很耗電,在設備電量充足的情況下,或者設備硬體解碼支持不足的情況下使用軟體解碼更加好!
Android 音頻硬解碼的話那么當時是首先使用 MediaCodec 來實作,首先初始化MediaFormat做一些解碼前的配置,里面包含了決議SPS,PPS的引數,然后想 MediaCodec 填充 MediaFormat 以及資料Buffer等待解碼,
int i = mMC.dequeueInputBuffer(BUFFER_TIMEOUT);
然后等待解碼輸出到Buffer即可:
int i = mMC.dequeueOutputBuffer(mBI, BUFFER_TIMEOUT);
很簡單,只要處理好dequeueInputBuffer,dequeueOutputBuffer的順序以及Buffer變數的資料就可以實作這個功能了,如果你的資料源是MP4檔案那么只需要通過 MediaExtractor 來獲取音頻/視頻的軌道,單獨來進行解碼即可
Android音視頻的軟解碼: 軟解碼首推的就是ffmpeg,ffmpeg的使用還是很簡單的,簡單的來說你只需要一開始初始化 編解碼格式物件 AVCodecContext 與編解碼器 AVCodec ,然后把資料填充 AvPacket ,然后解碼成 AvFrame 就可以了
值得一說的是:ffmpeg自3.1版本加入了android mediacodec硬解支持, 使用方法:
- 首先在編譯ffmpeg期間要加上這些編譯選項:
--enable-jni
--enable-mediacodec
--enable-decoder=h264_mediacodec
--enable-hwaccel=h264_mediacodec(不知道有什么用,還是開了)
-
其次在JNI_OnLoad函式,或者使用解碼器之前呼叫 av_jni_set_java_vm(vm, NULL);(位于libavcodec/jni.h) 來設定java虛擬機(反調mediacodec時會用到)
-
再次,由于h264_mediacodec解碼器和h.264解碼器id相同所以,軟解時,通過 avcodec_find_decoder(id)來尋找解碼器 而想使用mediacodec硬解時,使用 avcodec_find_decoder_by_name("h264_mediacodec");尋找指定硬解解碼器
App 音視軟解的播放:
- 音頻的重采樣:有時候在音頻播放的時候,會出現你的音源與播放設備的硬體條件不匹配,例如播放每幀的樣本數不匹配,采樣位數不匹配的情況,那么這個時候需要用到對于音源PCM重采樣,重采樣以后才能正常播放,
int len = swr_convert(actx,outArr,frame->nb_samples,(const uint8_t **)frame->data,frame->nb_samples);
主要是通過 swr_convert 來進行轉換
/** Convert audio.
*
* in and in_count can be set to 0 to flush the last few samples out at the
* end.
*
* If more input is provided than output space, then the input will be buffered.
* You can avoid this buffering by using swr_get_out_samples() to retrieve an
* upper bound on the required number of output samples for the given number of
* input samples. Conversion will run directly without copying whenever possible.
*
* @param s allocated Swr context, with parameters set
* @param out output buffers, only the first one need be set in case of packed audio
* @param out_count amount of space available for output in samples per channel
* @param in input buffers, only the first one need to be set in case of packed audio
* @param in_count number of input samples available in one channel
*
* @return number of samples output per channel, negative value on error
*/
int swr_convert(struct SwrContext *s, uint8_t **out, int out_count,
const uint8_t **in , int in_count);
out表示的是輸出buffer的指標; out_count表示的是輸出的樣本大小; in表示的輸入buffer的指標; in_count表示的是輸入樣品的大小;
轉換成功后輸出的音頻資料再拿來播放就可以在指定的條件進行指定的播放
- 音頻軟解碼的播放:這種情況下一般我們推薦的還是利用 OpenSLES 來播放
//設定回呼函式,播放佇列空呼叫
(*pcmQue)->RegisterCallback(pcmQue,PcmCall,this);
//設定為播放狀態
(*iplayer)->SetPlayState(iplayer,SL_PLAYSTATE_PLAYING);
//啟動佇列回呼
(*pcmQue)->Enqueue(pcmQue,"",1);
- 音頻的硬解碼播放:這種情況下播放使用SDK自帶的 AudioTrack 來進行播放,大致步驟如下:
this.audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, 44100,
AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT,
audioData.length, AudioTrack.MODE_STATIC);
this.audioTrack.write(audioData, 0, audioData.length);
audioTrack.play();
App 視頻的播放:
- 視頻軟解播放:這個當然是首先 opengles ,拿到YUV資料,設定好貼圖坐標,使用YUV資料分別貼圖來播放顯示,例子如下:
sh.GetTexture(0,width,height,data[0]); // Y
if(type == XTEXTURE_YUV420P)
{
sh.GetTexture(1,width/2,height/2,data[1]); // U
sh.GetTexture(2,width/2,height/2,data[2]); // V
}
else
{
sh.GetTexture(1,width/2,height/2,data[1], true); // UV
}
sh.Draw();
- 視頻硬解播放:這種情況下一般使用SDK自帶的 SurfaceView 顯示控制元件,然后拿到他的 Surface
mDecoder.configure(sps_nal, pps_nal,surfaceViewDecode.getHolder().getSurface());
然后再配置解碼器的時候把這個配置進去,接完碼以后他就會把解碼資料填充到 Surface 來進行播放顯示
mMC.configure(mMF, surface, null, 0);
App實作截圖拍照: - 不論是硬解碼,還是軟解碼最后出來的資料應該都是YUV資料那么,利用YUV資料生成圖片方法很多,要看具體需求,例如可以直接生成圖片代碼:
/**
* 把一幀yuv資料保存為bitmap
* @param yuv 資料流
* @param mWidth 圖片的寬
* @param mHeight 圖片的高
* @return bitmap 物件
*
*/
public Bitmap saveYUV2Bitmap(byte[] yuv, int mWidth, int mHeight) {
YuvImage image = new YuvImage(yuv, ImageFormat.NV21, mWidth, mHeight, null);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
image.compressToJpeg(new Rect(0, 0, mWidth, mHeight), 100, stream);
Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
try {
stream.flush();
stream.close();
} catch (IOException e) {
e.printStackTrace();
}
return bmp;
}
也可以把YUV資料按照公式轉換為RGB,然后利用RGB再生成你想要的東西
App實作錄制視頻: 錄制視頻說白了就是封包,把編碼過的音頻AAC,視頻H.264封裝為一個資料格式,常見的格式Mp4,TS等等
- 如果是ffmpeg軟解碼的話那么封包相對來說還算好,因為ffmpeg的SDK里面就包含了封包的方法: 初始化三個 AVFormatContext 容器,一個音頻一個視頻的用來作為輸入的AAC,H.264的容器,另外一個作為輸出的容器,還有一個 AVOutputFormat 輸出格式化物件,簡單的來說就是讀出一個AvPacket然后處理好PTS,DTS以后往對應流的輸出容器去寫即可,涉及的函式:
avformat_open_input():打開輸入檔案,
avcodec_copy_context():賦值AVCodecContext的引數,
avformat_alloc_output_context2():初始化輸出檔案,
avio_open():打開輸出檔案,
avformat_write_header():寫入檔案頭,
av_compare_ts():比較時間戳,決定寫入視頻還是寫入音頻,這個函式相對要少見一些,
av_read_frame():從輸入檔案讀取一個AVPacket,
av_interleaved_write_frame():寫入一個AVPacket到輸出檔案,
av_write_trailer():寫入檔案尾,
- 如果是硬解碼實作封包的話:可以利用Android SDK提供的 MediaMuxer 封包類,以及 MediaFormat 音/視頻軌道格式化的物件,我們自己構造兩個 MediaFormat 來表示音視頻的格式化物件并且初始化他們,然后用addTrack添加進 MediaMuxer 表示支持的軌道
mVideoTrackIndex = mMediaMuxer.addTrack(format);
然后構造Buffer資料,以及做好音頻資料以及視頻資料的先后順序,分別寫入 MediaMuxer
mMediaMuxer.writeSampleData(mVideoTrackIndex, buffer, info);
(這種情況我用得比較少,如果有疑問大家可以給我留言)
App實作音視頻同步:
- 音視頻同步的話選擇一般來說有以下三種:
將視頻同步到音頻上:就是以音頻的播放速度為基準來同步視頻,
將音頻同步到視頻上:就是以視頻的播放速度為基準來同步音頻,
將視頻和音頻同步外部的時鐘上:選擇一個外部時鐘為基準,視頻和音頻的播放速度都以該時鐘為標準,
這三種是最基本的策略,考慮到人對聲音的敏感度要強于視頻,頻繁調節音頻會帶來較差的觀感體驗,且音頻的播放時鐘為線性增長,所以一般會以音頻時鐘為參考時鐘,視頻同步到音頻上,音頻作為主導視頻作為次要,用視頻流來同步音頻流,由于不論是哪一個平臺播放音頻的引擎,都可以保證播放音頻的時間長度與實際這段音頻所代表的時間長度是一致的,所以我們可以依賴于音頻的順序播放為我們提供的時間戳,當客戶端代碼請求發送視頻幀的時候,會先計算出當前視頻佇列頭部的視頻幀元素的時間戳與當前音頻播放幀的時間戳的差值,如果在閾值范圍內,就可以渲染這一幀視頻幀;如果不在閾值范圍內,則要進行對齊操作,具體的對齊操作方法就是:如果當前佇列頭部的視頻幀的時間戳小于當前播放音頻幀的時間戳,那么就進行跳幀操作(具體的跳幀操作可以是加快速度播放的實作,也可以是丟棄一部分視頻幀的實作 );如果大于當前播放音頻幀的時間戳,那么就進行等待(重復渲染上一幀或者不進行渲染)的操作,其優點是音頻可以連續地播放,缺點是視頻畫面有可能會有跳幀的操作,但是對于視頻畫面的丟幀和跳幀,用戶的眼睛是不太容易分辨得出來的
一般來說視頻丟幀是我們常見的處理視頻慢于音頻的方式,可以先計算出需要加快多少時間,然后根據一個GOP算出每一幀的時間是多少,可以得出需要丟多少幀,然后丟幀的時候要注意的是必須要判斷,不能把I幀丟了,否則接下來的P幀就根本用不了,而應該丟的是P幀,也就是一個GOP的后半部分,最合適的情況就是丟一整個GOP,如果是丟GOP后半部分的話你需要一開始播放GOP的時候弄一個變數記錄當前是第幾個P幀了,然后計算出需要丟幾個P幀才能和音頻同步,然后到了那一個需要丟的幀到來的時候直接拋棄,即到下一個I幀到來的時候才進行渲染(這里面有可能丟的不是那么準確,可能需要經過幾個的丟幀步驟才能準確同步)
好了,我們Android音視頻開發雜談就介紹到這了,如果大家喜歡歡迎留言討論
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/396238.html
標籤:其他
