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

原文地址
原創文章,未經做者容許不得轉載java

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

在Android開發方面,音視頻佔據了不小領域。對於想往這方面瞭解的小夥伴們,每每不知道從何處下手開始學習。
博主本人接觸音視頻開發有一段日子,做爲本身學習的回顧和補充,也一直在記錄一些音視頻開發的博客。
對往期博客有興趣的朋友們能夠先了解一二。github

MediaCodeC硬編碼將圖片集編碼爲視頻Mp4文件MediaCodeC編碼視頻
MediaCodeC將視頻完整解碼,並存儲爲圖片文件。使用兩種不一樣的方式,硬編碼解碼視頻
MediaCodeC解碼視頻指定幀硬編碼解碼指定幀算法

概述

最近再次回顧所學,以爲還有許多不足。遂決定寫幾篇小總結,以Android平臺錄製視頻項目爲例,整理本身的音視頻開發知識。若是有小夥伴想學習音視頻開發,但又不知道從何着手,能夠模仿博主作一個相關的Demo來學習。 本文的項目地址入口在Camera2Record入口界面,業務功能實如今Camera2Recorder緩存

API

項目中視頻方面採用的技術邏輯爲:安全

使用Camera2API,配合MediaCodeC + Surface + OpenGL將原始幀數據編碼爲H264碼流bash

音頻方面採用技術邏輯爲:架構

AudioRecord錄音,MediaCodeC將PCM數據編碼爲AAC數據app

音視頻編碼使用的是MediaMuxer,將視頻幀數據和音頻幀數據封裝爲MP4文件。總體而言涉及到的API有:ide

  • MediaCodeC
  • AudioRecord
  • MediaMuxer
  • OpenGL(不用詳細瞭解)

架構設計【注1】

做爲一個簡單的音視頻錄製應用,並無什麼花哨的功能(暫時沒有,之後會慢慢追加)。總體業務邏輯就是直截了當的錄製視頻 ——> 產出視頻。業務再細分的話,主要有三個部分:一是畫面,即視頻部分;二是聲音,即音頻部分;三是混合器,即將視頻和音頻混合,並生成視頻文件。
將業務略做區分後,咱們由結果向前反推,既然要生成MP4文件,那麼須要提供一些什麼數據呢?因此咱們根據輸出——即混合器部分,梳理各個模塊的詳細功能。

視頻封裝

混合器模塊,使用了Android提供的MediaMuxer做爲視頻封裝輸出工具。MediaMuxer支持三種輸出格式,分別爲MP四、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編碼時間戳問題

未完待續

因爲篇幅有限,這篇文章只分享了音頻的編碼,在下一篇文章裏博主會分享視頻的錄製和編碼~~
以上

相關文章
相關標籤/搜索