對于一個視頻的錄制,包括以下幾個部分:
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)同步。
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)的一部分。
MediaCodec 架構(gòu)上采用了2個緩沖區(qū)隊列,異步處理數(shù)據(jù),并且使用了一組輸入輸出緩存。
你請求或接收到一個空的輸入緩存(input buffer),向其中填充滿數(shù)據(jù)并將它傳遞給編解碼器處理。編解碼器處理完這些數(shù)據(jù)并將處理結(jié)果輸出至一個空的輸出緩存(output buffer)。
MediaCodec的狀態(tài)轉(zhuǎn)換如下:
在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
- 當(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