實(shí)現(xiàn)錄制音視頻也有兩種方案,分別是MediaRecorder和MediaCodec
什么是MediaRecorder
MediaRecorder是安卓提供的一個用于音視頻采集的類
MediaRecorder的優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
可以實(shí)現(xiàn)直接錄制視頻 使用方便,得到就是編碼和封裝好的音視頻文件暴浦,可以直接使用
缺點(diǎn)
無法獲取原始數(shù)據(jù)乎赴,不能對每一幀數(shù)據(jù)進(jìn)行處理,無法支持我們程序中自己需要的一些邏輯纫雁,比方需要錄制灰度視頻纲辽。
由于不滿足我的需求缨睡,所以這里就不再對MediaRecorder講解了和二,那接下來我們來說說MediaCodec
什么是MediaCodec
MediaCodec
類是Android平臺提供的用于訪問低層多媒體硬件編/解碼器接口徘铝,它是Android低層多媒體架構(gòu)的一部分,通常與MediaExtractor惯吕、MediaMuxer惕它、AudioTrack結(jié)合使用,能夠編解碼諸如H.264废登、H.265淹魄、AAC、3gp等常見的音視頻格式堡距。一般來說H.264的AVC視頻編碼和AAC的音頻編碼是最常見的甲锡。
MediaCodec工作原理
MediaCodec的工作原理就是處理輸入數(shù)據(jù)以產(chǎn)生輸出數(shù)據(jù)兆蕉。具體來說,MediaCodec在編解碼的過程中使用了一組輸入/輸出緩存區(qū)來同步或異步處理數(shù)據(jù):首先缤沦,客戶端向獲取到的編解碼器輸入緩存區(qū)寫入要編解碼的數(shù)據(jù)并將其提交給編解碼器虎韵,待編解碼器處理完畢后將其轉(zhuǎn)存到編碼器的輸出緩存區(qū),同時收回客戶端對輸入緩存區(qū)的所有權(quán)缸废;然后包蓝,客戶端從獲取到編解碼輸出緩存區(qū)讀取編碼好的數(shù)據(jù)進(jìn)行處理,待處理完畢后編解碼器收回客戶端對輸出緩存區(qū)的所有權(quán)企量。不斷重復(fù)整個過程测萎,直至編碼器停止工作或者異常退出。
MediaCodec生命周期中的狀態(tài)
mediacodec分為三種狀態(tài)届巩,Stopped
, Executing
和Released
硅瞧。一張圖表示(這張圖是從網(wǎng)上直接下載下來使用的):
Stopped狀態(tài)包含三個子狀態(tài):Uninitialized
, Configured
和Error
,Executing同樣包含三個狀態(tài):Flushed
, Running
和End-of-Stream
恕汇。
在mediacodec的使用過程中必須遵守圖里標(biāo)出的流程腕唧,否則會發(fā)生錯誤。
比方拇勃,沒有調(diào)用start()方法四苇,就開始sotp()會報(bào)錯。
以解碼器為例方咆,講解一下使用流程月腋。當(dāng)使用工廠方法創(chuàng)建mediacodec并且指定為解碼后,進(jìn)入U(xiǎn)ninitialized狀態(tài)瓣赂,調(diào)用configure方法后榆骚,進(jìn)入Configured狀態(tài),然后調(diào)用start方法進(jìn)入Executing狀態(tài)煌集。
進(jìn)入Executing狀態(tài)后妓肢,首先到達(dá)Flush
狀態(tài),此時mediacodec會持有所有的數(shù)據(jù)苫纤,當(dāng)?shù)谝粋€inputbufffer從隊(duì)列中取出時碉钠,立即進(jìn)入Running
狀態(tài)局扶,這個時間很短怠苔。然后就可以調(diào)用dequeueInputBuffer和getInputBuffer來獲取用戶可用的緩沖區(qū)少漆,用戶填滿數(shù)據(jù)后調(diào)用queueinputbuffer方法返回給解碼器戴卜,解碼器大部分時間都會工作在Running
狀態(tài)。當(dāng)想inputbufferqueue中輸入一幀標(biāo)記EndOfStream的時候悍手,進(jìn)入End-of-Stream
狀態(tài)证薇,在這種狀態(tài)下桥氏,解碼器不再接受任何新的數(shù)據(jù)輸入乍赫,緩沖區(qū)中的數(shù)據(jù)和標(biāo)記EndOfStream最終會執(zhí)行完畢瓣蛀。在任何時候都可以調(diào)用flush方法回到Flush
狀態(tài)陆蟆。
調(diào)用stop方法會使mediacode進(jìn)入 Uninitialized
狀態(tài),這時候可以執(zhí)行configure方法來進(jìn)入下一循環(huán)惋增。當(dāng)mediacodec使用完畢后必須調(diào)用release
方法來釋放所有的資源叠殷。
在某些情況下,例如取出緩沖區(qū)索引時器腋,mediacodec會發(fā)生錯誤進(jìn)入Error
狀態(tài)溪猿,此時調(diào)用reset方法來是mediacodec重新處于Uninitialized
狀態(tài),或者調(diào)用release來結(jié)束解碼纫塌。
MediaCodec API 說明
MediaCodec 主要的API做一個介紹:
- MediaCodec創(chuàng)建:
- createDecoderByType/createEncoderByType:根據(jù)特定MIME類型
(如"video/avc")
創(chuàng)建codec。 - createByCodecName:知道組件的確切名稱(如OMX.google.mp3.decoder)的時候讲弄,根據(jù)組件名創(chuàng)建codec措左。使用MediaCodecList可以獲取組件的名稱。
- createDecoderByType/createEncoderByType:根據(jù)特定MIME類型
-
configure
:配置解碼器或者編碼器避除。 -
start
:成功配置組件后調(diào)用start怎披。 -
buffer處理的接口:
- dequeueInputBuffer:從輸入流隊(duì)列中取數(shù)據(jù)進(jìn)行編碼操作。
- queueInputBuffer:輸入流入隊(duì)列瓶摆。
-
dequeueOutputBuffer
:從輸出隊(duì)列中取出編碼操作之后的數(shù)據(jù)凉逛。 -
releaseOutputBuffer
:處理完成,釋放ByteBuffer數(shù)據(jù)群井。 - getInputBuffers:獲取需要編碼數(shù)據(jù)的輸入流隊(duì)列状飞,返回的是一個ByteBuffer數(shù)組。
-
getOutputBuffers
:獲取編解碼之后的數(shù)據(jù)輸出流隊(duì)列书斜,返回的是一個ByteBuffer數(shù)組诬辈。
-
flush
:清空的輸入和輸出端口。 -
stop
:終止decode/encode會話 -
release
:釋放編解碼器實(shí)例使用的資源荐吉。
MediaCodec創(chuàng)建編/解碼器
MediaCodec主要提供了createEncoderByType(String type)焙糟、createDecoderByType(String type)兩個方法來創(chuàng)建編解碼器,它們均需要傳入一個MIME類型多媒體格式样屠。常見的MIME類型多媒體格式如下:
● “video/x-vnd.on2.vp8” - VP8 video (i.e. video in .webm)
● “video/x-vnd.on2.vp9” - VP9 video (i.e. video in .webm)
● “video/avc” - H.264/AVC video
● “video/mp4v-es” - MPEG4 video
● “video/3gpp” - H.263 video
● “audio/3gpp” - AMR narrowband audio
● “audio/amr-wb” - AMR wideband audio
● “audio/mpeg” - MPEG1/2 audio layer III
● “audio/mp4a-latm” - AAC audio (note, this is raw AAC packets, not packaged in LATM!)
● “audio/vorbis” - vorbis audio
● “audio/g711-alaw” - G.711 alaw audio
● “audio/g711-mlaw” - G.711 ulaw audio
MediaCodec參數(shù)配置
private void initVideoCodec(int width, int height) {
try {
// https://developer.android.google.cn/reference/android/media/MediaCodec mediacodec官方介紹
// 比方MediaCodec的幾種狀態(tài)
// avc即h264編碼
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,width,height);
// 設(shè)置顏色格式
// 本地原始視頻格式(native raw video format):這種格式通過COLOR_FormatSurface標(biāo)記穿撮,并可以與輸入或輸出Surface一起使用
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
// 設(shè)置碼率,通常碼率越高痪欲,視頻越清晰悦穿,但是對應(yīng)的視頻也越大
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE,width * height * 4);
// 設(shè)置幀率 三星s21手機(jī)camera預(yù)覽時,支持的幀率為10-30
// 通常這個值越高勤揩,視頻會顯得越流暢咧党,一般默認(rèn)設(shè)置成30,你最低可以設(shè)置成24陨亡,不要低于這個值傍衡,低于24會明顯卡頓深员,微信為28
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE,30);
// 設(shè)置 I 幀間隔的時間
// 通常的方案是設(shè)置為 1s,對于圖片電影等等特殊情況蛙埂,這里可以設(shè)置為 0倦畅,表示希望每一幀都是 KeyFrame
// IFRAME_INTERVAL是指的幀間隔,這是個很有意思的值绣的,它指的是叠赐,關(guān)鍵幀的間隔時間。通常情況下屡江,你設(shè)置成多少問題都不大芭概。
// 比如你設(shè)置成10,那就是10秒一個關(guān)鍵幀惩嘉。但是罢洲,如果你有需求要做視頻的預(yù)覽,那你最好設(shè)置成1
// 因?yàn)槿绻阍O(shè)置成10文黎,那你會發(fā)現(xiàn)惹苗,10秒內(nèi)的預(yù)覽都是一個截圖
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,5);
// 創(chuàng)建編碼器
// https://www.codercto.com/a/41316.html MediaCodec 退坑指南
mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
mMediaCodec.configure(mediaFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
// 相機(jī)的像素?cái)?shù)據(jù)繪制到該 surface 上面
mSurface = mMediaCodec.createInputSurface();
videoEncoderThread = new VideoEncoderThread(videoRecorderReference);
} catch (Exception e) {
e.printStackTrace();
}
}
可以用微信錄制一個短視頻,然后看下參數(shù):
比較有參照意思的參數(shù):
//錄制了4.917秒
Duration/String : 4 秒 917 毫秒
//每秒一幀
Format_Settings_RefFrames/String : 4 幀
//視頻寬高
Width/String : 288 像素
Height/String : 640 像素
//1601*1024除以288*640=8.9耸峭,KEY_BIT_RATE和寬高比接近9
OverallBitRate/String : 1 601 kb/s
//格式avc桩蓉,和MIMETYPE_VIDEO_AVC對應(yīng)
Format/String : AVC
"FileExtension" : "mp4",
//avc也是mpeg-4
"Format" : "MPEG-4",
//幀率
FrameRate/String : 28.067 FPS
//yuv420,在camera預(yù)覽時設(shè)定的nv21對應(yīng)
ColorSpace : YUV
ChromaSubsampling/String : 4:2:0
//聲音的格式劳闹,再補(bǔ)充
Format/String : AAC LC
Channel(s)/String : 1 聲道
SamplingRate/String : 44.1 kHz
FrameRate/String : 43.066 FPS (1024 SPF)
configure
public void configure(
MediaFormat format,
Surface surface, MediaCrypto crypto, int flags);
-
MediaFormat format
:輸入數(shù)據(jù)的格式(解碼器)或輸出數(shù)據(jù)的所需格式(編碼器)院究。傳null等同于傳遞MediaFormat#MediaFormat作為空的MediaFormat。 -
Surface surface
:指定Surface玷或,用于解碼器輸出的渲染儡首。如果編解碼器不生成原始視頻輸出(例如,不是視頻解碼器)和/或想配置解碼器輸出ByteBuffer偏友,則傳null蔬胯。 -
MediaCrypto crypto
:指定一個crypto對象,用于對媒體數(shù)據(jù)進(jìn)行安全解密位他。對于非安全的編解碼器氛濒,傳null。 -
int flags
:當(dāng)組件是編碼器時鹅髓,flags指定為常量CONFIGURE_FLAG_ENCODE舞竿。
MediaFormat:封裝描述媒體數(shù)據(jù)格式的信息(包括音頻或視頻),以及可選的特性元數(shù)據(jù)窿冯。
- 媒體數(shù)據(jù)的格式指定為key/value對骗奖。key是字符串。值可以integer、long执桌、float鄙皇、String或ByteBuffer。
- 特性元數(shù)據(jù)被指定為string/boolean對仰挣。
開始錄制
我們通過對MediaCodec參數(shù)進(jìn)行配置伴逸,然后得到一個MediaCodec
mMediaCodec.start();
結(jié)束錄制
這里需要注意一下釋放的順序,一定得是按照下面的順序進(jìn)行資源釋放的
mMediaCodec.signalEndOfInputStream();
mMediaCodec.stop();
mMediaCodec.release();
● 下面看一段源碼:當(dāng)編解碼器start后膘壶,會進(jìn)入一個for(;;)循環(huán)错蝴,該循環(huán)是一個死循環(huán),以實(shí)現(xiàn)不斷地去從編解碼器的輸入緩存池中獲取包含數(shù)據(jù)的一個緩存區(qū)颓芭,然后再從輸出緩存池中獲取編解碼好的輸出數(shù)據(jù)顷锰。
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
MediaFormat outputFormat = codec.getOutputFormat(); // option B
codec.start();
for (;;) {
int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(…);
// fill inputBuffer with valid data
…
codec.queueInputBuffer(inputBufferId, …);
}
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is identical to outputFormat
// outputBuffer is ready to be processed or rendered.
…
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
outputFormat = codec.getOutputFormat(); // option B
}
}
codec.stop();
codec.release();
上層數(shù)據(jù)獲取
do {
if (mMediaCodec != null) {
int outBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, -1);
if (outBufferIndex >= 0) {
ByteBuffer bb = mMediaCodec.getOutputBuffer(outBufferIndex);
//這里獲取到原始數(shù)據(jù),然后根據(jù)相關(guān)需求可對數(shù)據(jù)進(jìn)行處理
}
if (outBufferIndex >= 0) {
mMediaCodec.releaseOutputBuffer(outBufferIndex, false);
}
}
}
while (isStarted);