概述
通常來說,對于同一平台同一硬體環境,硬編硬解的速度是快于軟體編解碼的。而且相比軟體編解碼的高CPU占用率來說,硬體編解碼也有很大的優勢,是以在硬體支援的情況下,一般硬體編解碼是我們的首選。
在Android中,我們可以直接使用MediaRecord來進行錄像,但是在很多适合MediaRecord并不能滿足我們的需求,比如我們需要對錄制的視訊加水印或者其他處理後,所有的平台都按照同一的大小傳輸到伺服器等。
在本篇部落格中,将會講到的是利用AudioRecord錄音,利用OpenGL渲染相機資料并做處理。然後利用MediaCodec對音頻和視訊分别進行編碼,使用MediaMuxer将編碼後的音視訊進行混合儲存為Mp4的編碼過程與代碼示例。
值得注意的是,音視訊編解碼用到的MediaCodec是Android 4.1新增的API,音視訊混合用到的MediaMuxer是Android 4.3新增的API,是以本篇部落格的示例隻實用于Android 4.3以上的裝置。
AudioRecord(錄音API)
AudioRecord是相對MediaRecord更為底層的API,使用AudioRecord也可以很友善的完成錄音功能。AudioRecord錄音錄制的是原始的PCM音頻資料,我們可以使用AudioTrack來播放PCM音頻檔案。
AudioRecord最簡單的使用代碼如下:
private int sampleRate=; //采樣率,預設44.1k
private int channelCount=; //音頻采樣通道,預設2通道
private int channelConfig=AudioFormat.CHANNEL_IN_STEREO; //通道設定,預設立體聲
private int audioFormat=AudioFormat.ENCODING_PCM_16BIT; //設定采樣資料格式,預設16比特PCM
private FileOutputStream fos; //用于儲存錄音檔案
//音頻錄制執行個體化和錄制過程中需要用到的資料
bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)*;
buffer=new byte[bufferSize];
//執行個體化AudioRecord
mRecorder=new AudioRecord(MediaRecorder.AudioSource.MIC,sampleRate,channelConfig,
audioFormat,bufferSize);
//開始錄制
mRecorder.startRecording();
//循環讀取資料到buffer中,并儲存buffer中的資料到檔案中
int length=mRecorder.read(buffer,,bufferSize);
fos.write(buffer,,length);
//中止循環并結束錄制
isRecording=false;
mRecorder.stop();
按照上面的步驟,我們就能成功的錄制PCM音頻檔案了,但是處于傳輸和存儲方面的考慮,一般來說,我們是不會直接錄制PCM音頻檔案的。而是在錄制過程中就對音頻資料進行編碼為aac、mp3、wav等其他格式的音頻檔案。
MediaCodec(硬體編解碼API)
了解MediaCodec
MediaCodec的使用在Android Developer官網上有詳細的說明。官網上的圖能夠很好的說明MediaCodec的使用方式。我們隻需了解這個圖,然後熟悉下MediaCodec的API就可以很快的上手使用MediaCodec來進行音視訊的編解碼工作了。

針對于上圖,我們可以把InputBuffers和OutputBuffers簡單的了解為它們共同組成了一個環形的傳送帶,傳送帶上鋪滿了空盒子。編解碼開始後,我們需要得到一個空盒子(dequeueInputBuffer),然後往空盒子中填充原料(需要被編/解碼的音/視訊資料),并且放回到傳送帶你取出時候的那個位置上面(queueInputBuffer)。傳送帶經過處理器(Codec)後,盒子裡面的原料被加工成了你所期望的東西(編解碼後的資料),你就可以按照你放入原料時候的順序,連帶着盒子一起取出加工好的東西(dequeueOutputBuffer),并将取出來的東西貼标簽(加資料頭之類的非必須)和裝箱(組合編碼後的幀資料)操作,同樣之後也要把盒子放回到原來的位置(releaseOutputBuffer)。
音頻編碼執行個體
在官網上有更規範的使用示例,結合上面的音頻錄制,編碼為AAC音頻檔案示例代碼如下:
private String mime = "audio/mp4a-latm"; //錄音編碼的mime
private int rate=; //編碼的key bit rate
//相對于上面的音頻錄制,我們需要一個編碼器的執行個體
MediaFormat format=MediaFormat.createAudioFormat(mime,sampleRate,channelCount);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_BIT_RATE, rate);
mEnc=MediaCodec.createEncoderByType(mime);
mEnc.configure(format,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE); //設定為編碼器
//同樣,在設定錄音開始的時候,也要設定編碼開始
mEnc.start();
//之前的音頻錄制是直接循環讀取,然後寫入檔案,這裡需要做編碼處理再寫入檔案
//這裡的處理就是和之前傳送帶取盒子放原料的流程一樣了,注意一般在子線程中循環處理
int index=mEnc.dequeueInputBuffer(-);
if(index>=){
final ByteBuffer buffer=mEnc.getInputBuffer(index);
buffer.clear();
int length=mRecorder.read(buffer,bufferSize);
if(length>){
mEnc.queueInputBuffer(index,,length,System.nanoTime()/,);
}
}
MediaCodec.BufferInfo mInfo=new MediaCodec.BufferInfo();
int outIndex;
//每次取出的時候,把所有加工好的都循環取出來
do{
outIndex=mEnc.dequeueOutputBuffer(mInfo,);
if(outIndex>=){
ByteBuffer buffer=mEnc.getOutputBuffer(outIndex);
buffer.position(mInfo.offset);
//AAC編碼,需要加資料頭,AAC編碼資料頭固定為7個位元組
byte[] temp=new byte[mInfo.size+];
buffer.get(temp,,mInfo.size);
addADTStoPacket(temp,temp.length);
fos.write(temp);
mEnc.releaseOutputBuffer(outIndex,false);
}else if(outIndex ==MediaCodec.INFO_TRY_AGAIN_LATER){
//TODO something
}else if(outIndex==MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
//TODO something
}
}while (outIndex>=);
//編碼停止,發送編碼結束的标志,循環結束後,停止并釋放編碼器
mEnc.stop();
mEnc.release();
AAC編碼加檔案頭的實作參照AAC編碼規則,将資料填入就好了,網上很容易找到,具體實作如下:
/**
* 給編碼出的aac裸流添加adts頭字段
* @param packet 要空出前7個位元組,否則會搞亂資料
* @param packetLen
*/
private void addADTStoPacket(byte[] packet, int packetLen) {
int profile = ; //AAC LC
int freqIdx = ; //44.1KHz
int chanCfg = ; //CPE
packet[] = (byte);
packet[] = (byte);
packet[] = (byte)(((profile-)<<) + (freqIdx<<) +(chanCfg>>));
packet[] = (byte)(((chanCfg&)<<) + (packetLen>>));
packet[] = (byte)((packetLen&) >> );
packet[] = (byte)(((packetLen&)<<) + );
packet[] = (byte);
}
這樣,得到的檔案就是AAC音頻檔案了,一般Android系統自帶的播放器都可以直接播放。
視訊編碼執行個體
視訊的編碼和上面音頻的編碼也大同小異。攝像頭的資料回調時間并不是确定的,就算你設定了攝像頭FPS範圍為30-30幀,它也不會每秒就一定給你30幀資料。Android攝像頭的資料回調,受光線的影響非常嚴重,這是由HAL層的3A算法決定的,你可以将自動曝光補償、自動白平光等等給關掉,這樣你才有可能得到穩定的幀率。
而我們錄制并編碼視訊的時候,肯定是希望得到一個固定幀率的視訊。是以在視訊錄制并進行編碼的過程中,需要自己想些法子,讓幀率固定下來。最簡單也是最有效的做法就是,按照固定時間編碼,如果沒有新的攝像頭資料回調來就用上一幀的資料。
參考代碼如下:
private String mime="video/avc"; //編碼的MIME
private int rate=; //波特率,256kb
private int frameRate=; //幀率,24幀
private int frameInterval=; //關鍵幀一秒一關鍵幀
//和音頻編碼一樣,設定編碼格式,擷取編碼器執行個體
MediaFormat format=MediaFormat.createVideoFormat(mime,width,height);
format.setInteger(MediaFormat.KEY_BIT_RATE,rate);
format.setInteger(MediaFormat.KEY_FRAME_RATE,frameRate);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,frameInterval);
//這裡需要注意,為了簡單這裡是寫了個固定的ColorFormat
//實際上,并不是所有的手機都支援COLOR_FormatYUV420Planar顔色空間
//是以正确的做法應該是,擷取目前裝置支援的顔色空間,并從中選取
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
mEnc=MediaCodec.createEncoderByType(mime);
mEnc.configure(format,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
//同樣,準備好了後,開始編碼器
mEnc.start();
//編碼器正确開始後,在子線程中循環編碼,固定碼率的話,就是一個循環加上線程休眠的時間固定
//流程和音頻編碼一樣,取出空盒子,往空盒子裡面加原料,放回盒子到原處,
//盒子中原料被加工,取出盒子,從盒子裡面取出成品,放回盒子到原處
int index=mEnc.dequeueInputBuffer(-);
if(index>=){
if(hasNewData){
if(yuv==null){
yuv=new byte[width*height*/];
}
//把傳入的rgba資料轉成yuv的資料,轉換在網上也是一大堆,不夠下面還是一起貼上吧
rgbaToYuv(data,width,height,yuv);
}
ByteBuffer buffer=getInputBuffer(index);
buffer.clear();
buffer.put(yuv);
//把盒子和原料一起放回到傳送帶上原來的位置
mEnc.queueInputBuffer(index,,yuv.length,timeStep,);
}
MediaCodec.BufferInfo mInfo=new MediaCodec.BufferInfo();
//嘗試取出加工好的資料,和音頻編碼一樣,do while和while都行,覺得怎麼好怎麼寫
int outIndex=mEnc.dequeueOutputBuffer(mInfo,);
while (outIndex>=){
ByteBuffer outBuf=getOutputBuffer(outIndex);
byte[] temp=new byte[mInfo.size];
outBuf.get(temp);
if(mInfo.flags==MediaCodec.BUFFER_FLAG_CODEC_CONFIG){
//把編碼資訊儲存下來,關鍵幀上要用
mHeadInfo=new byte[temp.length];
mHeadInfo=temp;
}else if(mInfo.flags%==MediaCodec.BUFFER_FLAG_KEY_FRAME){
//關鍵幀比普通幀是多了個幀頭的,儲存了編碼的資訊
byte[] keyframe = new byte[temp.length + mHeadInfo.length];
System.arraycopy(mHeadInfo, , keyframe, , mHeadInfo.length);
System.arraycopy(temp, , keyframe, mHeadInfo.length, temp.length);
Log.e(TAG,"other->"+mInfo.flags);
//寫入檔案
fos.write(keyframe,,keyframe.length);
}else if(mInfo.flags==MediaCodec.BUFFER_FLAG_END_OF_STREAM){
//結束的時候應該發送結束信号,在這裡處理
}else{
//寫入檔案
fos.write(temp,,temp.length);
}
mEnc.releaseOutputBuffer(outIndex,false);
outIndex=mEnc.dequeueOutputBuffer(mInfo,);
}
//資料的來源,GL處理好後,readpix出來的RGBA資料喂進來,
public void feedData(final byte[] data, final long timeStep){
hasNewData=true;
nowFeedData=data;
nowTimeStep=timeStep;
}
//RGBA轉YUV的方法,這是最簡單粗暴的方式,在使用的時候,一般不會選擇在Java層,用這種方式做轉換
private void rgbaToYuv(byte[] rgba,int width,int height,byte[] yuv){
final int frameSize = width * height;
int yIndex = ;
int uIndex = frameSize;
int vIndex = frameSize + frameSize/;
int R, G, B, Y, U, V;
int index = ;
for (int j = ; j < height; j++) {
for (int i = ; i < width; i++) {
index = j * width + i;
if(rgba[index*]>||rgba[index*]<-){
Log.e("color","-->"+rgba[index*]);
}
R = rgba[index*]&;
G = rgba[index*+]&;
B = rgba[index*+]&;
Y = (( * R + * G + * B + ) >> ) + ;
U = ((- * R - * G + * B + ) >> ) + ;
V = (( * R - * G - * B + ) >> ) + ;
yuv[yIndex++] = (byte) ((Y < ) ? : ((Y > ) ? : Y));
if (j % == && index % == ) {
yuv[uIndex++] = (byte) ((U < ) ? : ((U > ) ? : U));
yuv[vIndex++] = (byte) ((V < ) ? : ((V > ) ? : V));
}
}
}
}
對于其他格式的音頻視訊編解碼也大同小異了,隻要MediaCodec支援就好。
MediaMuxer(音視訊混合API)
MediaMuxer的使用很簡單,在Android Developer官網上MediaMuxer的API說明中,也有其簡單的使用示例代碼:
MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
// More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat()
// or MediaExtractor.getTrackFormat().
MediaFormat audioFormat = new MediaFormat(...);
MediaFormat videoFormat = new MediaFormat(...);
int audioTrackIndex = muxer.addTrack(audioFormat);
int videoTrackIndex = muxer.addTrack(videoFormat);
ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
boolean finished = false;
BufferInfo bufferInfo = new BufferInfo();
muxer.start();
while(!finished) {
// getInputBuffer() will fill the inputBuffer with one frame of encoded
// sample from either MediaCodec or MediaExtractor, set isAudioSample to
// true when the sample is audio data, set up all the fields of bufferInfo,
// and return true if there are no more samples.
finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);
if (!finished) {
int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;
muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
}
};
muxer.stop();
muxer.release();
參照官方的說明和代碼示例,我們可以知道,音視訊混合(也可以音頻和音頻混合),隻需要将編碼器的MediaFormat加入到MediaMuxer中,得到一個音軌視訊軌的索引,然後每次從編碼器中取出來的ByteBuffer,寫入(writeSampleData)到編碼器所在的軌道中就ok了。
這裡需要注意的是,一定要等編碼器設定編碼格式完成後,再将它加入到混合器中,編碼器編碼格式設定完成的标志是
dequeueOutputBuffer
得到傳回值為
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED
。
音視訊錄制MP4檔案
上面已經給出了音頻錄制的代碼和視訊錄制的代碼,利用MediaMuxer将其結合起來,就可以和簡單的完成錄制有聲音有圖像的MP4檔案的功能了。音頻錄制和視訊錄制的基本流程保持不變,在錄制編碼後,不再将編碼的結果寫入到檔案流中,而是寫入為混合器的sample data。以視訊為例,更改循環編碼的代碼為:
//流程一直,無需更改
int index=mVideoEnc.dequeueInputBuffer(-);
if(index>=){
if(hasNewData){
if(yuv==null){
yuv=new byte[width*height*/];
}
rgbaToYuv(data,width,height,yuv);
}
ByteBuffer buffer=getInputBuffer(mVideoEnc,index);
buffer.clear();
buffer.put(yuv);
//結束時,發送結束标志,在編碼完成後結束
mVideoEnc.queueInputBuffer(index,,yuv.length,
mStartFlag?:MediaCodec.BUFFER_FLAG_END_OF_STREAM);
}
MediaCodec.BufferInfo mInfo=new MediaCodec.BufferInfo();
int outIndex=mVideoEnc.dequeueOutputBuffer(mInfo,);
do {
if(outIndex>=){
ByteBuffer outBuf=getOutputBuffer(mVideoEnc,outIndex);
//裡面不在是寫入到檔案,而是寫入為混合器的sample data
if(mTrackCount==&&mInfo.size>){
mMuxer.writeSampleData(mVideoTrack,outBuf,mInfo);
}
mVideoEnc.releaseOutputBuffer(outIndex,false);
outIndex=mVideoEnc.dequeueOutputBuffer(mInfo,);
Log.e("wuwang","outIndex-->"+outIndex);
//編碼結束的标志
if((mInfo.flags&MediaCodec.BUFFER_FLAG_END_OF_STREAM)!=){
return true;
}
}else if(outIndex==MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
//按照MediaMuxer中所說,加入軌道的時機在這裡
mVideoTrack=mMuxer.addTrack(mVideoEnc.getOutputFormat());
Log.e("wuwang","video track-->"+mVideoTrack);
mTrackCount++;
//一定要音軌視訊軌都加入後,再開始混合
if(mTrackCount==){
mMuxer.start();
mTrackCount=;
}
}
}while (outIndex>=);
當然是用MediaMuxer前,肯定是需要建立一個MediaMuxer的執行個體的:
mMuxer=new MediaMuxer(path+"."+postfix, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
- 1
音頻的操作和視訊一樣更改,将音頻編碼也加入MeidaMuxer的軌道中,得到一個軌道索引,将編碼後的資料加入為MediaMuxer目前音軌的sample data。音軌和上面的視軌各自做各自的,結束錄制時,都發送結束标志,然後在編碼結束後,停止混合器就可以得到一個固定碼率的MP4檔案了。
總結
至此,本篇部落格就結束了。但是在實際使用MediaCodec和MediaMuxer的過程中,總會遇到這樣或者那樣的問題,硬編硬解,和硬體相關比較緊密,Android雖然提供了一個很好的API,但是各個廠商在實作的過程中,總是會做些讓自己變得獨特的事情。當然他們的目的并不是為了獨特,有的是為了讓産品變得更優秀(雖然最後可能會做砸了),有的是為了省錢,用軟體去彌補硬體的缺陷,最後的結果就是苦了做上層開發的碼農們。
從部落客在使用MediaCodec和MediaMuxer的過程中遇到的問題,總結下需要注意主要有以下幾點:
- MediaCodec是Android4.1新增API,MediaMuxer是Android4.3新增API。
- 顔色空間。按照Android本身的意思,COLOR_FormatYUV420Planar應該是所有硬體平台都支援的。但是實際上并不是這樣。是以在設定顔色空間時,應該擷取硬體平台所支援的顔色空間,確定它是支援你打算使用的顔色空間,不支援的話應該啟用備用方案(使用其他目前硬體支援的顔色空間)。
- 視訊尺寸,在一些手機上,視訊錄制的尺寸可以是任意的。但是有些手機,不支援的尺寸設定會導緻錄制的視訊現錯亂。部落客在使用Oppo R7測試,360*640的視訊,單獨錄制視訊沒問題,音視訊混合後,出現了顔色錯亂的情況,而在360F4手機上,卻都是沒問題的。将視訊寬高都設定為16的倍數,可以解決這個問題。
- 編碼器格式設定,諸如音頻編碼的采樣率、比特率等,取值也需要結合硬體平台來設定,否則也會導緻崩潰或其他問題。這個其實和顔色空間的選擇一樣。
- 網上看到許多
中設定queueInputBuffer
為presentationTimeUs
,這樣做會導緻編碼出來的音視訊,在播放時,總時長顯示的是錯誤的。應該記錄開始時候的nanoTime,然後設定System.nanoTime()/1000
為presentationTimeUs
。(System.nanoTime()-nanoTime)/1000
- 錄制結束時,應該發送結束标志
,在編碼後區獲得這個标志時再終止循環,而不是直接終止循環。MediaCodec.BUFFER_FLAG_END_OF_STREAM
應該還有其他需要注意的問題。我暫時還沒遇到。
源碼
源碼在github中codec module下,有需要的小夥伴fork或者download。後續Android音視訊開發相關的Demo也會上傳到這個項目下。
此文轉載于:http://lib.csdn.net/article/liveplay/57099。