原文地址
原創文章,未經作者允許不得轉載
山黛遠,月波長
暮雲秋影蘸瀟湘
醉魂應逐凌波夢,分付西風此夜涼
在Android開發方面,音視訊佔據了不小領域。對於想往這方面瞭解的小夥伴們,往往不知道從何處下手開始學習。
博主本人接觸音視訊開發有一段日子,作為自己學習的回顧和補充,也一直在記錄一些音視訊開發的部落格。
對往期部落格有興趣的朋友們可以先了解一二。
MediaCodeC硬編碼將圖片集編碼為視訊Mp4檔案MediaCodeC編碼視訊
MediaCodeC將視訊完整解碼,並儲存為圖片檔案。使用兩種不同的方式,硬編碼解碼視訊
MediaCodeC解碼視訊指定幀硬編碼解碼指定幀
概述
最近再次回顧所學,覺得還有許多不足。遂決定寫幾篇小總結,以Android平臺錄製視訊專案為例,整理自己的音視訊開發知識。如果有小夥伴想學習音視訊開發,但又不知道從何著手,可以模仿博主做一個相關的Demo來學習。 本文的專案地址入口在Camera2Record入口介面,業務功能實現在Camera2Recorder。
API
專案中視訊方面採用的技術邏輯為:
使用Camera2API,配合MediaCodeC + Surface + OpenGL將原始幀資料編碼為H264碼流
音訊方面採用技術邏輯為:
AudioRecord錄音,MediaCodeC將PCM資料編碼為AAC資料
音視訊編碼使用的是MediaMuxer
,將視訊幀資料和音訊幀資料封裝為MP4檔案。整體而言涉及到的API有:
- MediaCodeC
- AudioRecord
- MediaMuxer
- OpenGL(不用詳細瞭解)
架構設計【注1】
作為一個簡單的音視訊錄製應用,並沒有什麼花哨的功能(暫時沒有,以後會慢慢追加)。整體業務邏輯就是直截了當的錄製視訊 ——> 產出視訊
。業務再細分的話,主要有三個部分:一是畫面,即視訊部分;二是聲音,即音訊部分;三是混合器,即將視訊和音訊混合,並生成視訊檔案。
將業務略作區分後,我們由結果向前反推,既然要生成MP4檔案,那麼需要提供一些什麼資料呢?所以我們根據輸出——即混合器部分,梳理各個模組的詳細功能。
視訊封裝
在混合器
模組,使用了Android提供的MediaMuxer
作為視訊封裝輸出工具。MediaMuxer
支援三種輸出格式,分別為MP4、Webm和3GP檔案,本次專案的混合器輸出自然選擇的是MP4檔案。
MP4是MPEG-4的官方容器格式定義的廣義副檔名,可以流媒體化並支援眾多多媒體的內容:多音軌、視訊流、字幕、圖片、可變幀率、位元速率【注2】。
在製作MP4檔案時,應該優先選用MPEG-4標準下的視訊/音訊格式,一般來說,對於MP4容器的封裝,相對而言比較常見的有兩種編碼方式:
- H264視訊編碼,AAC音訊編碼
- Xvid視訊編碼,MP4音訊編碼
視訊編碼演算法
在本專案中,博主採用的視訊編碼演算法為H264。H264作為壓縮率最高的視訊壓縮格式,與其他編碼格式相比,同等畫面質量,體積最小。它有兩個名稱,一個是沿用ITU_T組織的H.26x名稱——H.264
;另一個是MPEG-4AVC,AVC即為高階視訊編碼,而MP4格式則是H264編碼制定使用的標準封裝格式【注3】。
音訊編碼演算法
博主採用的音訊編碼演算法為AAC。AAC可以同時支援48個音軌,15個低頻音軌,相比MP3,AAC可以在體積縮小30%的前提下提供更好的音質【注4】。
AAC最初是基於MPEG-2的音訊編碼技術,後來MPEG_4標準出臺,AAC重新整合了其他技術,變更為現在的MPEG-4 AAC標準。一般而言,目前常用的AAC編碼指代的就是MPEG-4 AAC。
MPEG-4 AAC有六種子規格:
- MPEG-4 AAC LC 低複雜度規格(Low Complexity)---現在的手機比較常見的MP4檔案中的音訊部份就包括了該規格音訊檔案
- MPEG-4 AAC Main 主規格 注:包含了除增益控制之外的全部功能,其音質最好
- MPEG-4 AAC SSR 可變取樣率規格(Scaleable SampleRate)
- MPEG-4 AAC LTP 長時期預測規格(Long TermPredicition)
- MPEG-4 AAC LD 低延遲規格(Low Delay)
- MPEG-4 AAC HE高效率規格(HighEfficiency)---這種規格用於低位元速率編碼,有NeroACC 編碼器支援
目前最流行的就是LC和HE了。需要注意的是MPEG-4 AAC LC這種規格為“低複雜度規格”,一般應用於中等位元速率。而中等位元速率,一般指96kbps~192kbps,所以如果使用了LC編碼,請將位元速率控制在這個範圍內會比較好一點。
工作流程
將業務邏輯梳理清楚之後,那麼各個模組更具體的功能就清晰了很多。這裡有一個大致的工作流程圖以作參考:
先從視訊模組開始,VideoRecorder
執行在一個獨立的工作執行緒,使用OpenGL+Surface+MediaCodeC
對接Camera2,接受相機回撥畫面並編碼為H264碼流。這個類對外回撥可用的視訊幀資料VideoPacket
物件。這個資料型別是工程中自行定義的物件,封裝了這一幀視訊的資料——ByteArray型別
,以及這一幀資料攜帶的資訊——BufferInfo:主要是這一幀的時間戳以及其他
。接下來是音訊模組,考慮到錄音模組或許日後有機會複用,所以將錄音模組單獨分離出來。
AudioRecorder
在開始錄製後不停執行,對外回撥PCM原始資料——ByteArray型別。
AudioRecord類可以對外提供兩種型別,ShortArray和ByteArray,因為視訊對外的資料型別為ByteArray,所以這裡也選擇了ByteArray。這一段PCM資料會被新增到一個外部的連結串列中,而AudioEncoder
音訊編碼模組,也持有PCM資料連結串列。在開始錄製後,AudioEncoder
不斷迴圈地從PCM連結串列中提取資料,編碼為AAC格式的原始幀資料。這裡的AAC原始資料,指的是沒有新增ADTS頭資訊的資料。與此同時,視訊模組輸出的視訊幀資料和音訊模組輸出的AAC音訊幀資料,會被提交到
Mux
模組中,在這個模組中,持有兩個視訊幀資料和音訊幀資料的連結串列。Mux
模組會不斷迴圈地從這兩個連結串列中提取資料,使用MediaMuxer
將幀資料封裝到各自的軌上,最終輸出MP4檔案。
音訊錄製及編碼
音訊模組分為錄音以及編碼兩個小模組,分別執行在兩個獨立的工作執行緒。錄音模組不用多提,完全是基於AudioRecord的二次封裝,這裡是程式碼地址AudioRecorder。
這裡主要說一下音訊編碼模組AudioEncoder,音訊錄製模組在執行後拿到可用PCM資料並回撥到外部,封裝到一個執行緒安全的連結串列中。而AudioEncoder
則會不停地從連結串列中提取資料,再使用MediaCodeC將PCM資料編碼為AAC格式的音訊幀資料。由於MediaMuxer
封裝AAC音訊軌,並不需要ADTS頭資訊,所以AudioEncoder
得到的AAC原始幀資料也無須再作二次處理了。
var presentationTimeUs = 0L
val bufferInfo = MediaCodec.BufferInfo()
// 迴圈的拿取PCM資料,編碼為AAC資料。
while (isRecording.isNotEmpty() || pcmDataQueue.isNotEmpty()) {
val bytes = pcmDataQueue.popSafe()
bytes?.apply {
val (id, inputBuffer) = codec.dequeueValidInputBuffer(1000)
inputBuffer?.let {
totalBytes += size
it.clear()
it.put(this)
it.limit(size)
// 當輸入資料全部處理完,需要向Codec傳送end——stream的Flag
codec.queueInputBuffer(id, 0, size
, presentationTimeUs,
if (isEmpty()) MediaCodec.BUFFER_FLAG_END_OF_STREAM else 0)
// 1000000L/ 總資料 / audio channel / sampleRate
presentationTimeUs = 1000000L * (totalBytes / 2) / format.sampleRate
}
}
loopOut@ while (true) {
// 獲取可用的輸出快取佇列
val outputBufferId = dequeueOutputBuffer(bufferInfo, defTimeOut)
if (outputBufferId == MediaCodec.INFO_TRY_AGAIN_LATER) {
break@loopOut
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// audio format changed
} else if (outputBufferId >= 0) {
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
break@loopOut
}
val outputBuffer = codec.getOutputBuffer(it)
if (bufferInfo.size > 0) {
frameCount++
dataCallback.invoke(outputBuffer, bufferInfo)
}
codec.releaseOutputBuffer(it, false)
}
}
}
複製程式碼
這裡的工作流程是這樣的:只有PCM連結串列中有資料,MediaCodeC就會將這些資料填入到可用的輸入佇列中。每一段PCM的資料長度並不一定是一幀音訊資料所對應的長度,所以工程要做的是,不停地想編碼器輸入資料,而編碼器也需要不停地往外輸出資料,直至將編碼器內部的輸入資料編碼完畢。
還有一個需要注意的點,就是MediaCodec當輸入資料全部填充完畢時,需要傳送一個==BUFFER_FLAG_END_OF_STREAM==標示,用來標示資料輸入END。如果沒有傳送這個標示的話,那麼編碼完後的音訊資料會丟失掉最後一小段時間的音訊。
除此之外,還有一個很重要的點,就是AAC編碼的時間戳計算問題,相關部分的知識請閱讀博主之前的部落格解決AAC編碼時間戳問題
未完待續
由於篇幅有限,這篇文章只分享了音訊的編碼,在下一篇文章裡博主會分享視訊的錄製和編碼~~
以上
注
- 1、本文的架構設計參考了《音視訊開發進階指南》—— 實現一款視訊錄製應用章節
- 2、參考資料Mp4編碼全介紹
- 3、參考資料音視訊封裝格式、編碼格式知識
- 4、參考資料AAC音訊編碼格式介紹