原文地址
原創文章,未經做者容許不得轉載java
山黛遠,月波長
暮雲秋影蘸瀟湘
醉魂應逐凌波夢,分付西風此夜涼git
在Android開發方面,音視頻佔據了不小領域。對於想往這方面瞭解的小夥伴們,每每不知道從何處下手開始學習。
博主本人接觸音視頻開發有一段日子,做爲本身學習的回顧和補充,也一直在記錄一些音視頻開發的博客。
對往期博客有興趣的朋友們能夠先了解一二。github
MediaCodeC硬編碼將圖片集編碼爲視頻Mp4文件MediaCodeC編碼視頻
MediaCodeC將視頻完整解碼,並存儲爲圖片文件。使用兩種不一樣的方式,硬編碼解碼視頻
MediaCodeC解碼視頻指定幀硬編碼解碼指定幀算法
最近再次回顧所學,以爲還有許多不足。遂決定寫幾篇小總結,以Android平臺錄製視頻項目爲例,整理本身的音視頻開發知識。若是有小夥伴想學習音視頻開發,但又不知道從何着手,能夠模仿博主作一個相關的Demo來學習。 本文的項目地址入口在Camera2Record入口界面,業務功能實如今Camera2Recorder。緩存
項目中視頻方面採用的技術邏輯爲:安全
使用Camera2API,配合MediaCodeC + Surface + OpenGL將原始幀數據編碼爲H264碼流bash
音頻方面採用技術邏輯爲:架構
AudioRecord錄音,MediaCodeC將PCM數據編碼爲AAC數據app
音視頻編碼使用的是MediaMuxer
,將視頻幀數據和音頻幀數據封裝爲MP4文件。總體而言涉及到的API有:ide
做爲一個簡單的音視頻錄製應用,並無什麼花哨的功能(暫時沒有,之後會慢慢追加)。總體業務邏輯就是直截了當的錄製視頻 ——> 產出視頻
。業務再細分的話,主要有三個部分:一是畫面,即視頻部分;二是聲音,即音頻部分;三是混合器,即將視頻和音頻混合,並生成視頻文件。
將業務略做區分後,咱們由結果向前反推,既然要生成MP4文件,那麼須要提供一些什麼數據呢?因此咱們根據輸出——即混合器部分,梳理各個模塊的詳細功能。
在混合器
模塊,使用了Android提供的MediaMuxer
做爲視頻封裝輸出工具。MediaMuxer
支持三種輸出格式,分別爲MP四、Webm和3GP文件,本次項目的混合器輸出天然選擇的是MP4文件。
MP4是MPEG-4的官方容器格式定義的廣義文件擴展名,能夠流媒體化並支持衆多多媒體的內容:多音軌、視頻流、字幕、圖片、可變幀率、碼率【注2】。
在製做MP4文件時,應該優先選用MPEG-4標準下的視頻/音頻格式,通常來講,對於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有六種子規格:
目前最流行的就是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頭信息的數據。
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編碼時間戳問題
因爲篇幅有限,這篇文章只分享了音頻的編碼,在下一篇文章裏博主會分享視頻的錄製和編碼~~
以上