目錄
1 整體步驟
2 MediaCodec 使用的基本流程
3 設計
4 編解碼實例分析
5 Android FFmpeg視頻解碼播放
6 從零實現一個H.264碼流解析器
7 音視頻同步
8 OpenSL ES音頻解碼播放
正文
1 整體步驟
模塊之間數據(buffer)流程圖。
1.1 編碼:dequeueinputBuffer(從input緩沖隊列申請empty buffer.左邊的上虛線)---inputbuffer(拷貝mp4文件的一幀到empty buffer)
--queueInputBuffe(將inputbuffer放回codec)---dequeueOutputBuffer(從output緩沖區隊列申請編解碼后的buffer)--編碼后的數據渲染--releaseOutputBuffer(放回到output緩沖區隊列)
1.2 解碼:dequeueinputBuffer(從input緩沖隊列申請empty buffer)---inputbuffer(拷貝mp4文件的數據到empty buffer)---mediacodec(從input buffer 取一幀)
--dequeueOutputBuffer(從output緩沖區隊列申請編解碼后的buffer)--解碼后的數據播放--releaseOutputBuffer(放回到output緩沖區隊列)
2 MediaCodec 使用的基本流程
MediaCodec 的生命周期有三種狀態:停止態-Stopped、執行態-Executing、釋放態-Released。
停止狀態(Stopped)包括了三種子狀態:未初始化(Uninitialized)、配置(Configured)、錯誤(Error)。
執行狀態(Executing)會經歷三種子狀態:刷新(Flushed)、運行(Running)、流結束(End-of-Stream
理解了queueInputBuffe作用,就理解了編解碼流程.
1) mediaExtractor.setDataSource(path);
//extractor讀取sampleData
int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);
2 ) decodec.queueInputBuffer(index, 0, sampleSize, mediaExtractor.getSampleTime(), 0);
//一直沒搞懂queueInputBuffer的作用,主要是因為inputBuffer沒搞懂。codec抽取了文件不就是解碼了嗎?都已經放入inputbuffer中了,還需要queueInputBuffer干什么?
實際這是兩件事,兩個主體,mediaExtractor and decodec 操作同一個inputBuffer;
mediaExtractor僅僅負責抽取sample到 input buffer,比如aac是1024. 沒有編解碼操作.僅僅是拷貝部分文件。
decodec是真正的編解碼,將一幀中的aac,去header,變成pcm.并且將pcm放入queueoutputbuffer。
queueInputBuffer將input buffer放回codec,實現兩件事,1) codec 開始編解碼,結果放入outputbuffer 2)將inputbuffer重新入隊
mediaextractor& queueInputBuffer
mediaextractor:input-source file,output--inputbuffer
queueInputBuffer:inputbuffer--outputbuffer (!!抽取sample的不是codec.也不是放入codec的inputbuffer就自動開始編解碼,而是需要queueInputBuffer通知codec開始編解碼)
notes: dequeueInputBuffer是典型的dequeue 隊列的用法,從header取一個buffer.
queue & dequeue 是入隊出隊.兩者都是codec做的.
3 設計
流程圖
偽代碼
- createByCodeName/createEncoderByType/createDecoderByType: (靜態工廠構造MediaCodec對象)--生成MediaCodec,Uninitialized狀態
- configure:(配置) -- configure狀態
- start (啟動)--處于Excuting狀態 Flushed子狀態,然后進入Running狀態
- while(1) {
try{
- dequeueInputBuffer (從編解碼器獲取輸入緩沖區buffer)
- queueInputBuffer (buffer被生成方client填滿之后提交給編解碼器)
- dequeueOutputBuffer (從編解碼器獲取輸出緩沖區buffer)
- releaseOutputBuffer (消費方client消費之后釋放給編解器)
} catch(Error e){
- error (出現異常 進入error狀態)
}
}
- stop (編解碼完成后,釋放codec)
- release
MediaCodec 有已下解碼相關的 API:
1)dequeueInputBuffer:若大于 0,則是返回填充編碼數據的緩沖區的索引,該操作為同步操作;
返回一個input index
2)getInputBuffer:填充編碼數據的 ByteBuffer 數組,結合 dequeueInputBuffer 返回值,可獲取一個可填充編碼數據的 ByteBuffer;
根據index 返回一個input ByteBuffer
3)queueInputBuffer :應用將編碼數據拷貝到 ByteBuffer 后,通過該方法告知 MediaCodec 已經填寫的編碼數據的緩沖區索引;
拷貝操作
4)dequeueOutputBuffer:若大于 0,則是返回填充解碼數據的緩沖區的索引,該操作為同步操作;
返回一個output index
5)getOutputBuffer:填充解碼數據的 ByteBuffer 數組,結合 dequeueOutputBuffer 返回值,可獲取一個可填充解碼數據的 ByteBuffer;
根據index,返回一個output ByteBuffer
6)releaseOutputBuffer:告訴編碼器數據處理完成,釋放 ByteBuffer 數據。
release output buffer。
補充:
a) release queueoutputbuffer
沒有release queue inputbuffer,從上圖可知,release inputbuffer是codec的工作.
b) //讀取下一幀
mediaExtractor.advance();
在實踐當中發現,發送端發送的視頻寬高需要 16 字節對齊,因為在某些 Android 手機上解碼器需要 16 字節對齊。
other:若非16字節對齊,dequeueOutputBuffer會有一次MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED.
而不是一上來就能成功解碼一幀.
經測試發現:幀寬高非 16 字節對齊會比 16 字節對齊的慢 100 ms 左右
4 編解碼實例分析
編解碼分成同步和異步兩種實現方法。
流程圖比較給力.屬于詳細設計
//【解碼步驟:1. 初始化,并啟動解碼器】
//【解碼步驟:2. 將數據壓入解碼器輸入緩沖】
//【解碼步驟:3. 將解碼好的數據從緩沖區拉取出來】
//【解碼步驟:4. 渲染】
//【解碼步驟:5. 釋放輸出緩沖】
//【解碼步驟:6. 判斷解碼是否完成】
音視頻硬解碼流程:封裝基礎解碼框架: http://www.lxweimin.com/p/ff65ef5207ce 解碼器
MediaCodec進行編解碼AAC(文件格式轉換) https://hejunlin.blog.csdn.net/article/details/103771289.
Android 音視頻學習:使用 MediaCodec API 完成音頻 AAC 硬編、硬解 https://my.oschina.net/u/4582396/blog/4384448
音視頻開發之旅(六)MediaCodec硬編解流程與實踐 https://zhuanlan.zhihu.com/p/268441151
5 Android FFmpeg視頻編解碼播放
FFmpeg 開發(02):FFmpeg + ANativeWindow 實現視頻解碼播放 http://www.lxweimin.com/p/1efd030e8cf7
Android FFmpeg視頻解碼播放:http://www.lxweimin.com/p/d7c8f49d9ea4
6 從零實現一個H.264碼流解析器
【從零實現一個H.264碼流解析器】(一):從碼流中找到NALU https://mp.weixin.qq.com/s/7E0z-mRi4uURICm0RVXuxw
【從零實現一個H.264碼流解析器】(二):導入指數哥倫布解碼實現并初步解析NALU https://mp.weixin.qq.com/s?__biz=MzI5Njc3OTk5NA==&mid=2247483958&idx=1&sn=7eeb49fa874115abd9237db2d4175fcd&chksm=ecbe6b83dbc9e295c058dea5c5915792377118b8d670fd6059a8b62c5ee70da69554e9f49edb&scene=21#wechat_redirect
【從零實現一個H.264碼流解析器】(三):解析序列參數集SPS的句法元素 https://mp.weixin.qq.com/s?__biz=MzI5Njc3OTk5NA==&mid=2247483961&idx=1&sn=175dcc1ce65659b24f4e09e2dfa11c2d&chksm=ecbe6b8cdbc9e29a57831664a9b89074a4e9829556601196c05b370858787519c807416f3cc3&scene=21#wechat_redirect
【從零實現一個H.264碼流解析器】(四):生成句法元素跟蹤trace文件 https://mp.weixin.qq.com/s/ZPUg4wWU0CWlN7pp0DbspQ
【從零實現一個H.264碼流解析器】(五):解析圖像參數集PPS的句法元素 https://mp.weixin.qq.com/s/GXkq88XrHW8Pug-376T5BA
【從零實現一個H.264碼流解析器】(六):解析片頭部Slice_Header的句法元素 https://mp.weixin.qq.com/s/01-JTlph0mduW76N-zubaA
7 音視頻同步
http://www.lxweimin.com/p/ba8db84f8fe8
時間同步的原理如下:
codec 出來的pts 播放時間戳和system current time保存一致
進入解碼前,獲取當前系統時間,存放在mStartTimeForSync,一幀數據解碼出來以后,計算當前系統時間和mStartTimeForSync的距離,也就是已經播放的時間,如果當前幀的PTS大于流失的時間,進入sleep,否則直接渲染
8 OpenSL ES音頻解碼播放
OpenSL 是音頻編解碼,ffmpeg是視頻編解碼. ffmpeg也可以音頻編解碼.
Android FFmpeg+OpenSL ES音頻解碼播放 http://www.lxweimin.com/p/28fc978721b4
用ffmpeg解碼視頻,用opensl es解碼音頻。用opensl 渲染音頻,eg:混音(增加音效?)。小結opensl的使用steps good
other:常用格式YUV 4:2:0 下面link說的清楚: https://www.cnblogs.com/leisure_chn/p/10290575.html
QA:
codec 注意碼率設置不成功,可能是幀率設置有問題
每當切換到前置攝像頭的時候,界面會有部分卡在上一個界面.每當切換到前置攝像頭的時候,界面會有部分卡在上一個界面
附錄:
old:用MediaCodec解碼->取出ByteBuffer->用OpenGLES處理->處理完畢后readPixels->得到圖像數據->將圖像數據推入MediaCodec編碼.(readPixels非常耗時。480*840的視頻,一幀耗時基本是40ms+)
new:MediaCodec解碼視頻直接解碼到Surface上->通過OpenGLES處理->又通過Surface進行編碼
(無需關注解碼出來的數據的格式了,而且應用層也不必自己去將原始數據導入GPU以及將處理后的數據導出GPU了,這些工作可以都丟給Android SDK去做)
5 Android音視頻硬編碼:生成一個MP4:
http://www.lxweimin.com/p/bfdeac7da147
code實現完全符合標準設計流程:先變量,接口。然后基類核心函數。然后實現av子類. 然后render獨立線程。最后activity調用線程池運行子類。
實現:
1 基類實現
1). 定義編碼器變量
2) 初始化編碼器 (定義接口)
3). 開啟編碼循環 :encode實現的核心,使用codec實現。這樣子類負責配置就可以了.
4). 拉取數據 :將codec出來的數據,writeData到mp4文件
2、音頻/視頻編碼器
configEncoder ,addTrack,writeVideoData
3、整合
1 ) 主線程
從系統獲取surface
override fun surfaceCreated(holder: SurfaceHolder) {
mSurface = holder.surface
mThread.onSurfaceCreate()
}
decodeOneFrame --notifySwap通知渲染線程render --渲染之后encodeOneFrame
2 ) 渲染線程:initEGL,createEGLSurface
mDrawers.forEach { it.draw() }
mEGLSurface?.setTimestamp(mCurTimestamp)
mEGLSurface?.swapBuffers()
3 ) activity:
threadPool.execute(videoEncoder) //線程池運行子類