Camera2錄製視訊(一):音訊的錄製及編碼

AiLo發表於2019-06-26

原文地址
原創文章,未經作者允許不得轉載

山黛遠,月波長
暮雲秋影蘸瀟湘
醉魂應逐凌波夢,分付西風此夜涼

在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編碼,請將位元速率控制在這個範圍內會比較好一點。

工作流程

將業務邏輯梳理清楚之後,那麼各個模組更具體的功能就清晰了很多。這裡有一個大致的工作流程圖以作參考:

Camera2錄製視訊(一):音訊的錄製及編碼
先從視訊模組開始,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編碼時間戳問題

未完待續

由於篇幅有限,這篇文章只分享了音訊的編碼,在下一篇文章裡博主會分享視訊的錄製和編碼~~
以上

相關文章