OpenGL ES +MediaCodec音視頻采集錄制錄

對于一個視頻的錄制,包括以下幾個部分:
1,視頻圖像的采集
2,視頻錄像的編碼錄制
3,音頻的采集
4,音頻的編碼錄制
5,音視頻的合成

涉及到的技術(shù)包括OpenGL ES、EGL、MediaCodec、AudioRecord、MediaMuxer。

一、視頻的采集

這里使用OpenGL ES和GLSurfaceView進行攝像頭畫面的預(yù)覽,視頻的采集通過創(chuàng)建EGL環(huán)境。

通過MediaCodec創(chuàng)建一個surface,然后通過創(chuàng)建一個新的egl環(huán)境共享預(yù)覽的EglContext和這個surface綁定,渲染fbo綁定的紋理,即可錄制。

EGL14與EGL10區(qū)別:
EGL14是在Android 4.2(API 17)引入的,換言之API 17以下的版本不支持EGL14。
EGL10不支持OpenGL ES 2.x,因此在EGL10中某些相關(guān)常量參數(shù)只能用手寫硬編碼代替,例如EGL14.EGL_CONTEXT_CLIENT_VERSION以及EGL14.EGL_OPENGL_ES2_BIT等等。

1.1 EGL環(huán)境搭建

1.OpenGL ES 是Android繪圖API,但OpenGL ES是平臺通用的,在特定設(shè)備上使用需要一個中間層做適配,這個中間層就是EGL。

2.OpenGL ES 本質(zhì)上是一個圖形渲染管線的狀態(tài)機,而 EGL 則是用于監(jiān)控這些狀態(tài)以及維護 Frame buffer 和其他渲染 Surface 的外部層,它管理圖形上下文、表面/緩沖區(qū)綁定和呈現(xiàn)同步。


image.png

EGL架構(gòu)圖中的核心組成:
Display(EGLDisplay) :是對實際顯示設(shè)備的抽象。
Surface(EGLSurface):是對用來存儲圖像的內(nèi)存區(qū)域 FrameBuffer 的抽象,包括 Color Buffer, Stencil Buffer ,Depth Buffer。
Context (EGLContext) :存儲 OpenGL ES繪圖的一些狀態(tài)信息。

EGL環(huán)境創(chuàng)建主要關(guān)鍵步驟:
①、創(chuàng)建EGLDisplay,得到默認(rèn)的顯示設(shè)備(窗口)

mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);

②、初始化顯示設(shè)備

        boolean initResult = EGL14.eglInitialize(mEglDisplay,version,0,version,1);

③、配置顯示設(shè)備的屬性

        boolean configResult = EGL14.eglChooseConfig(mEglDisplay, eglConfigAttributes, 0, configs, 0, configs.length,

④、創(chuàng)建EglContext上下文

        int[] contextAttributeList = {
                EGL14.EGL_CONTEXT_CLIENT_VERSION,2,
                EGL14.EGL_NONE
        };

        mEglContext= EGL14.eglCreateContext(mEglDisplay,mEglConfig,eglContext ,contextAttributeList,0);

此處因為,視頻錄制需要創(chuàng)建的EGL上下文,需要獲取攝像頭預(yù)覽時的EglContext上下文來進行創(chuàng)建。
⑤、創(chuàng)建Surface

        mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, surfaceAttribList, 0);

此處的surface由MediaCodec創(chuàng)建得到。
⑥、綁定EglContext和Surface到顯示設(shè)備

        boolean curResult = EGL14.eglMakeCurrent(mEglDisplay,mEglSurface,mEglSurface,mEglContext);

1.2 MediaCodec創(chuàng)建Surface繪制編碼

private MediaCodec mMediaCodec;
private Surface mSurface;
mSurface = mMediaCodec.createInputSurface();

創(chuàng)建MediaCodec的輸入Surface,然后通過1.2中創(chuàng)建EGL上下文環(huán)境,OpenGL將數(shù)據(jù)渲染到這個Surface上,MediaCodec就可以在內(nèi)部拿到視頻數(shù)據(jù),進行視頻編碼了。

二、音頻的采集

音頻的采集使用AudioRecord進行。android中音頻的采集一般使用AudioRecord或者MediaRecord。AudioRecord可以采集到一幀幀的PCM數(shù)據(jù),而MediaRecorder可以將采集到的音頻數(shù)據(jù)轉(zhuǎn)化為編碼格式保存。而此處場景因為需要將音頻和視頻混合,所以使用AudioRecord。

AudioRecord的初始化需要先創(chuàng)建一個AudioRecord實例。

    public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,
            int bufferSizeInBytes)

構(gòu)造函數(shù)參數(shù)說明:
audioSource:音頻輸入源,一般使用MediaRecorder.AudioSource.MIC 麥克風(fēng)。
sampleRateInHz:音頻的采樣頻率,通常取值44100(44.1kHz)。
channelConfig:采集幾個聲道。雙聲道立體聲用AudioFormat.CHANNEL_IN_STEREO。
audioFormat:指定采樣PCM數(shù)據(jù)的采樣格式。常用值有 ENCODING_PCM_8BIT、ENCODING_PCM_16BIT和ENCODING_PCM_FLOAT,值得強調(diào)的是ENCODING_PCM_16BIT可以保證兼容大部分Andorid手機。
bufferSizeInBytes:音頻采集過程中需要的緩存區(qū)大小。一般根據(jù)采樣率,通道數(shù),已經(jīng)采樣格式來確定。

bufferSize = AudioRecord.getMinBufferSize(sampleRate, channels, audioFormat);

當(dāng)開啟音頻采集后,通過循環(huán)不停調(diào)用AudioRecord的read函數(shù)來獲取pcm數(shù)據(jù)

while(state == RECORDING){ //狀態(tài)判斷
    byte[] buffer = new byte[bufferSize];
                int readRecord = audioRecord.read(buffer, 0, bufferSize);
}

三、 視頻和音頻的編碼

音視頻的編碼使用MediaCodec。MediaCodec類為開發(fā)者提供了能訪問到Android底層媒體Codec(Encoder/Decoder)的能力,它是Android底層多媒體基礎(chǔ)架構(gòu)的一部分。

image.png

MediaCodec 架構(gòu)上采用了2個緩沖區(qū)隊列,異步處理數(shù)據(jù),并且使用了一組輸入輸出緩存。
你請求或接收到一個空的輸入緩存(input buffer),向其中填充滿數(shù)據(jù)并將它傳遞給編解碼器處理。編解碼器處理完這些數(shù)據(jù)并將處理結(jié)果輸出至一個空的輸出緩存(output buffer)。

MediaCodec的狀態(tài)轉(zhuǎn)換如下:

image.png

在MediaCodec的生命周期內(nèi)存在三種狀態(tài):Stopped, Executing or Released,其中
  Stopped狀態(tài)包含三種子狀態(tài):Uninitialized, Configured and Error
  Executing狀態(tài)包含三種子狀態(tài):Flushed, Running and End-of-Stream

  1. 當(dāng)通過 MediaCodec.createByCodecName(...) or MediaCodec.createDecoderByType(...) or MediaCodec.createEncoderByType(...)三種方法中的任一種創(chuàng)建一個MediaCodec對象實例后,Codec將會處于 Uninitialized 狀態(tài);

2. 當(dāng)你調(diào)用 MediaCodec.configure(...)方法對Codec進行配置后,Codec將進入 Configured 狀態(tài);

3. 之后可以調(diào)用 MediaCodec.start() 方法啟動Codec,Codec會轉(zhuǎn)入 Executing 狀態(tài),start后Codec立即進入 Flushed 子狀態(tài),此時的Codec擁有所有的input and output buffers,Client無法操作這些buffers;

4. 一旦第一個input buffer 出隊列,也即Client通過調(diào)用 MediaCodec.dequeueInputBuffer(...)請求得到了一個有效的input buffer index, Codec立即進入到了 Running 子狀態(tài),在這個狀態(tài)下Codec會進行實際的數(shù)據(jù)處理(解碼、編碼)工作,度過它生命周期的主要階段;

5. 當(dāng)輸入端入隊列一個帶有 end-of-stream 標(biāo)記的input buffer時(queueInputBuffer(EOS)),Codec將轉(zhuǎn)入 End of Stream 子狀態(tài)。在此狀態(tài)下,Codec不再接受新的input buffer數(shù)據(jù),但仍會處理之前入隊列而未處理完的input buffer并產(chǎn)生output buffer,直到end-of-stream 標(biāo)記到達輸出端,數(shù)據(jù)處理的過程也隨即終止;

6. 在 Executing狀態(tài)下可以調(diào)用 MediaCodec.flush()方法使Codec進入 Flushed 子狀態(tài);

7. 在 Executing狀態(tài)下可以調(diào)用 MediaCodec.stop()方法使Codec進入 Uninitialized 子狀態(tài),可以對Codec進行重新配置;

8. 極少數(shù)情況下Codec會遇到錯誤進入 Error 狀態(tài),可以調(diào)用 MediaCodec.reset() 方法使其再次可用;

9. 當(dāng)MediaCodec數(shù)據(jù)處理任務(wù)完成時或不再需要MediaCodec時,可使用 MediaCodec.release()方法釋放其資源。

主要調(diào)用流程:
1,初始化

        // 設(shè)置各種編碼參數(shù)
        MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,
                mWidth, mHeight);
        //顏色空間 從 surface當(dāng)中獲得
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities
                .COLOR_FormatSurface);
        int bitrate = mWidth *mHeight*2;
        //碼率
        format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
        //幀率
        format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
        //關(guān)鍵幀間隔-每秒關(guān)鍵幀數(shù)
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);

        //創(chuàng)建編碼器
        mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
        //配置編碼器
        mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

2,然后調(diào)用start()方法開啟,從輸入隊列塞入待編碼數(shù)據(jù)
視頻直接從Surface中獲取待編碼數(shù)據(jù)

        //這個surface顯示的內(nèi)容就是要編碼的畫面
        mSurface = mMediaCodec.createInputSurface();

音頻則通過AudioRecord采集到的pcm數(shù)據(jù),塞入到輸入隊列中

            int buffIndex = mAudioCodec.dequeueInputBuffer(0);
            presentationTimeUs += (long) (1.0 * buffSize / (sampleRate * channelCount * (audioFormat / 8)) * 1000000.0);
            Log.d(TAG, "pcm一幀時間戳 = " + presentationTimeUs / 1000000.0f);
            mAudioCodec.queueInputBuffer(buffIndex, 0, buffSize, presentationTimeUs, 0);

塞入pcm音頻數(shù)據(jù)時,需要進行音頻時間戳的計算。時間戳的單位是微秒。
PCM文件大小=采樣率x采樣時間x(采樣位深/8)x通道數(shù)(Bytes)
例如:
數(shù)據(jù)量Byte= 44100Hz×(16/8)×2×10s=1764 KByte
所以倒轉(zhuǎn)過來,時間戳(時長)計算就是:
presentationTimeUs = (totalBytes / sampleRate/ audioFormat / channelCount / 8 )x100000
最后乘以1000000,轉(zhuǎn)化成微秒。

3,從輸出隊列中獲得編碼后的數(shù)據(jù)

int outputBufferIndex = mAudioCodec.dequeueOutputBuffer(bufferInfo, 0);
if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 通過MediaMuxer添加音軌/視頻軌

}else{
  while (outputBufferIndex >= 0) {
    ByteBuffer outputBuffer = mAudioCodec.getOutputBuffer(outputBufferIndex);
     outputBuffer.position(bufferInfo.offset);
     outputBuffer.limit(bufferInfo.offset + bufferInfo.size);

      if (pts == 0) {
               pts = bufferInfo.presentationTimeUs;
       }
      bufferInfo.presentationTimeUs = bufferInfo.presentationTimeUs - pts;
// 音視頻合成
      mMuxer.writeSampleData(mCodecState.audioTrackIndex, outputBuffer, bufferInfo);

      mAudioCodec.releaseOutputBuffer(outputBufferIndex, false);
      outputBufferIndex = mAudioCodec.dequeueOutputBuffer(bufferInfo, 0);
  }
}

通過MediaCodec的dequeueOutputBuffer從輸出隊列中獲取編碼后的音視頻數(shù)據(jù)。然后通過MediaMuxer進行音視頻的合成。

四、音視頻的合成

在Android中,可以使用MediaMuxer來封裝編碼后的視頻流和音頻流到mp4容器中,MediaMuxer最多僅支持一個視頻track和一個音頻track。

MediaMuxer的使用必須嚴(yán)格遵循如下順序:
創(chuàng)建MediaMuxer->addTrack->start->writeSampleData->stop->release
對于音視頻的合成,需要當(dāng)音頻和視頻track都addTrack之后,才能調(diào)用start。
如果順序不對,或者多次調(diào)用start、stop、release,都會導(dǎo)致IllegalStateException。

使用MediaMuxer開發(fā)時,可能會經(jīng)常遇到類似如下錯誤。Muxer的狀態(tài)Illegal錯誤。

java.lang.IllegalStateException: Muxer is not initialized.
    at android.media.MediaMuxer.addTrack(MediaMuxer.java:617)
    at com.example.dj.record.MediaRecorder.codec(MediaRecorder.java:163)

可能的場景是在兩個不同的Thread中分別執(zhí)行音視頻的錄制+編碼,所以需要等待兩個線程都執(zhí)行完addTrack之后,再執(zhí)行start

代碼:
https://github.com/godtrace12/DOpenglTest.git
參考:https://blog.csdn.net/qq_34760508/article/details/107045337
https://juejin.cn/post/6844903904488996878
http://www.lxweimin.com/p/01b374acd6a4
https://blog.csdn.net/gb702250823/article/details/81627503
https://blog.csdn.net/u010126792/article/details/86580878/
https://www.pianshen.com/article/2768765911/
https://blog.csdn.net/liuyizhou95/article/details/89553013

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內(nèi)容