視頻編解碼對許多Android程序員來說都是Android中比較難的一個知識點(diǎn)。在Android 4.1以前,Android并沒有提供硬編硬解的API,所以之前基本上都是采用FFMpeg來做視頻軟件編解碼的,現(xiàn)在FFMpeg在Android的編解碼上依舊廣泛應(yīng)用。本篇博客主要講到的是利用Android4.1增加的API MediaCodec和Android 4.3增加的API MediaMuxer進(jìn)行Mp4視頻的錄制。
概述
通常來說,對于同一平臺同一硬件環(huán)境,硬編硬解的速度是快于軟件編解碼的。而且相比軟件編解碼的高CPU占用率來說,硬件編解碼也有很大的優(yōu)勢,所以在硬件支持的情況下,一般硬件編解碼是我們的首選。
在Android中,我們可以直接使用MediaRecord來進(jìn)行錄像,但是在很多適合MediaRecord并不能滿足我們的需求,比如我們需要對錄制的視頻加水印或者其他處理后,所有的平臺都按照同一的大小傳輸?shù)椒?wù)器等。
在本篇博客中,將會講到的是利用AudioRecord錄音,利用OpenGL渲染相機(jī)數(shù)據(jù)并做處理。然后利用MediaCodec對音頻和視頻分別進(jìn)行編碼,使用MediaMuxer將編碼后的音視頻進(jìn)行混合保存為Mp4的編碼過程與代碼示例。
值得注意的是,音視頻編解碼用到的MediaCodec是Android 4.1新增的API,音視頻混合用到的MediaMuxer是Android 4.3新增的API,所以本篇博客的示例只實(shí)用于Android 4.3以上的設(shè)備。
AudioRecord(錄音API)
AudioRecord是相對MediaRecord更為底層的API,使用AudioRecord也可以很方便的完成錄音功能。AudioRecord錄音錄制的是原始的PCM音頻數(shù)據(jù),我們可以使用AudioTrack來播放PCM音頻文件。
AudioRecord最簡單的使用代碼如下:
private int sampleRate=44100; //采樣率,默認(rèn)44.1k
private int channelCount=2; //音頻采樣通道,默認(rèn)2通道
private int channelConfig=AudioFormat.CHANNEL_IN_STEREO; //通道設(shè)置,默認(rèn)立體聲
private int audioFormat=AudioFormat.ENCODING_PCM_16BIT; //設(shè)置采樣數(shù)據(jù)格式,默認(rèn)16比特PCM
private FileOutputStream fos; //用于保存錄音文件
//音頻錄制實(shí)例化和錄制過程中需要用到的數(shù)據(jù)
bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)*2;
buffer=new byte[bufferSize];
//實(shí)例化AudioRecord
mRecorder=new AudioRecord(MediaRecorder.AudioSource.MIC,sampleRate,channelConfig,
audioFormat,bufferSize);
//開始錄制
mRecorder.startRecording();
//循環(huán)讀取數(shù)據(jù)到buffer中,并保存buffer中的數(shù)據(jù)到文件中
int length=mRecorder.read(buffer,0,bufferSize);
fos.write(buffer,0,length);
//中止循環(huán)并結(jié)束錄制
isRecording=false;
mRecorder.stop();
按照上面的步驟,我們就能成功的錄制PCM音頻文件了,但是處于傳輸和存儲方面的考慮,一般來說,我們是不會直接錄制PCM音頻文件的。而是在錄制過程中就對音頻數(shù)據(jù)進(jìn)行編碼為aac、mp3、wav等其他格式的音頻文件。
MediaCodec(硬件編解碼API)
理解MediaCodec
MediaCodec的使用在Android Developer官網(wǎng)上有詳細(xì)的說明。官網(wǎng)上的圖能夠很好的說明MediaCodec的使用方式。我們只需理解這個圖,然后熟悉下MediaCodec的API就可以很快的上手使用MediaCodec來進(jìn)行音視頻的編解碼工作了。

針對于上圖,我們可以把InputBuffers和OutputBuffers簡單的理解為它們共同組成了一個環(huán)形的傳送帶,傳送帶上鋪滿了空盒子。編解碼開始后,我們需要得到一個空盒子(dequeueInputBuffer),然后往空盒子中填充原料(需要被編/解碼的音/視頻數(shù)據(jù)),并且放回到傳送帶你取出時候的那個位置上面(queueInputBuffer)。傳送帶經(jīng)過處理器(Codec)后,盒子里面的原料被加工成了你所期望的東西(編解碼后的數(shù)據(jù)),你就可以按照你放入原料時候的順序,連帶著盒子一起取出加工好的東西(dequeueOutputBuffer),并將取出來的東西貼標(biāo)簽(加數(shù)據(jù)頭之類的非必須)和裝箱(組合編碼后的幀數(shù)據(jù))操作,同樣之后也要把盒子放回到原來的位置(releaseOutputBuffer)。
音頻編碼實(shí)例
在官網(wǎng)上有更規(guī)范的使用示例,結(jié)合上面的音頻錄制,編碼為AAC音頻文件示例代碼如下:
private String mime = "audio/mp4a-latm"; //錄音編碼的mime
private int rate=256000; //編碼的key bit rate
//相對于上面的音頻錄制,我們需要一個編碼器的實(shí)例
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); //設(shè)置為編碼器
//同樣,在設(shè)置錄音開始的時候,也要設(shè)置編碼開始
mEnc.start();
//之前的音頻錄制是直接循環(huán)讀取,然后寫入文件,這里需要做編碼處理再寫入文件
//這里的處理就是和之前傳送帶取盒子放原料的流程一樣了,注意一般在子線程中循環(huán)處理
int index=mEnc.dequeueInputBuffer(-1);
if(index>=0){
final ByteBuffer buffer=mEnc.getInputBuffer(index);
buffer.clear();
int length=mRecorder.read(buffer,bufferSize);
if(length>0){
mEnc.queueInputBuffer(index,0,length,System.nanoTime()/1000,0);
}
}
MediaCodec.BufferInfo mInfo=new MediaCodec.BufferInfo();
int outIndex;
//每次取出的時候,把所有加工好的都循環(huán)取出來
do{
outIndex=mEnc.dequeueOutputBuffer(mInfo,0);
if(outIndex>=0){
ByteBuffer buffer=mEnc.getOutputBuffer(outIndex);
buffer.position(mInfo.offset);
//AAC編碼,需要加數(shù)據(jù)頭,AAC編碼數(shù)據(jù)頭固定為7個字節(jié)
byte[] temp=new byte[mInfo.size+7];
buffer.get(temp,7,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>=0);
//編碼停止,發(fā)送編碼結(jié)束的標(biāo)志,循環(huán)結(jié)束后,停止并釋放編碼器
mEnc.stop();
mEnc.release();
AAC編碼加文件頭的實(shí)現(xiàn)參照AAC編碼規(guī)則,將數(shù)據(jù)填入就好了,網(wǎng)上很容易找到,具體實(shí)現(xiàn)如下:
/**
* 給編碼出的aac裸流添加adts頭字段
* @param packet 要空出前7個字節(jié),否則會搞亂數(shù)據(jù)
* @param packetLen
*/
private void addADTStoPacket(byte[] packet, int packetLen) {
int profile = 2; //AAC LC
int freqIdx = 4; //44.1KHz
int chanCfg = 2; //CPE
packet[0] = (byte)0xFF;
packet[1] = (byte)0xF9;
packet[2] = (byte)(((profile-1)<<6) + (freqIdx<<2) +(chanCfg>>2));
packet[3] = (byte)(((chanCfg&3)<<6) + (packetLen>>11));
packet[4] = (byte)((packetLen&0x7FF) >> 3);
packet[5] = (byte)(((packetLen&7)<<5) + 0x1F);
packet[6] = (byte)0xFC;
}
這樣,得到的文件就是AAC音頻文件了,一般Android系統(tǒng)自帶的播放器都可以直接播放。
視頻編碼實(shí)例
視頻的編碼和上面音頻的編碼也大同小異。攝像頭的數(shù)據(jù)回調(diào)時間并不是確定的,就算你設(shè)置了攝像頭FPS范圍為30-30幀,它也不會每秒就一定給你30幀數(shù)據(jù)。Android攝像頭的數(shù)據(jù)回調(diào),受光線的影響非常嚴(yán)重,這是由HAL層的3A算法決定的,你可以將自動曝光補(bǔ)償、自動白平光等等給關(guān)掉,這樣你才有可能得到穩(wěn)定的幀率。
而我們錄制并編碼視頻的時候,肯定是希望得到一個固定幀率的視頻。所以在視頻錄制并進(jìn)行編碼的過程中,需要自己想些法子,讓幀率固定下來。最簡單也是最有效的做法就是,按照固定時間編碼,如果沒有新的攝像頭數(shù)據(jù)回調(diào)來就用上一幀的數(shù)據(jù)。
參考代碼如下:
private String mime="video/avc"; //編碼的MIME
private int rate=256000; //波特率,256kb
private int frameRate=24; //幀率,24幀
private int frameInterval=1; //關(guān)鍵幀一秒一關(guān)鍵幀
//和音頻編碼一樣,設(shè)置編碼格式,獲取編碼器實(shí)例
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
//實(shí)際上,并不是所有的手機(jī)都支持COLOR_FormatYUV420Planar顏色空間
//所以正確的做法應(yīng)該是,獲取當(dāng)前設(shè)備支持的顏色空間,并從中選取
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
mEnc=MediaCodec.createEncoderByType(mime);
mEnc.configure(format,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
//同樣,準(zhǔn)備好了后,開始編碼器
mEnc.start();
//編碼器正確開始后,在子線程中循環(huán)編碼,固定碼率的話,就是一個循環(huán)加上線程休眠的時間固定
//流程和音頻編碼一樣,取出空盒子,往空盒子里面加原料,放回盒子到原處,
//盒子中原料被加工,取出盒子,從盒子里面取出成品,放回盒子到原處
int index=mEnc.dequeueInputBuffer(-1);
if(index>=0){
if(hasNewData){
if(yuv==null){
yuv=new byte[width*height*3/2];
}
//把傳入的rgba數(shù)據(jù)轉(zhuǎn)成yuv的數(shù)據(jù),轉(zhuǎn)換在網(wǎng)上也是一大堆,不夠下面還是一起貼上吧
rgbaToYuv(data,width,height,yuv);
}
ByteBuffer buffer=getInputBuffer(index);
buffer.clear();
buffer.put(yuv);
//把盒子和原料一起放回到傳送帶上原來的位置
mEnc.queueInputBuffer(index,0,yuv.length,timeStep,0);
}
MediaCodec.BufferInfo mInfo=new MediaCodec.BufferInfo();
//嘗試取出加工好的數(shù)據(jù),和音頻編碼一樣,do while和while都行,覺得怎么好怎么寫
int outIndex=mEnc.dequeueOutputBuffer(mInfo,0);
while (outIndex>=0){
ByteBuffer outBuf=getOutputBuffer(outIndex);
byte[] temp=new byte[mInfo.size];
outBuf.get(temp);
if(mInfo.flags==MediaCodec.BUFFER_FLAG_CODEC_CONFIG){
//把編碼信息保存下來,關(guān)鍵幀上要用
mHeadInfo=new byte[temp.length];
mHeadInfo=temp;
}else if(mInfo.flags%8==MediaCodec.BUFFER_FLAG_KEY_FRAME){
//關(guān)鍵幀比普通幀是多了個幀頭的,保存了編碼的信息
byte[] keyframe = new byte[temp.length + mHeadInfo.length];
System.arraycopy(mHeadInfo, 0, keyframe, 0, mHeadInfo.length);
System.arraycopy(temp, 0, keyframe, mHeadInfo.length, temp.length);
Log.e(TAG,"other->"+mInfo.flags);
//寫入文件
fos.write(keyframe,0,keyframe.length);
}else if(mInfo.flags==MediaCodec.BUFFER_FLAG_END_OF_STREAM){
//結(jié)束的時候應(yīng)該發(fā)送結(jié)束信號,在這里處理
}else{
//寫入文件
fos.write(temp,0,temp.length);
}
mEnc.releaseOutputBuffer(outIndex,false);
outIndex=mEnc.dequeueOutputBuffer(mInfo,0);
}
//數(shù)據(jù)的來源,GL處理好后,readpix出來的RGBA數(shù)據(jù)喂進(jìn)來,
public void feedData(final byte[] data, final long timeStep){
hasNewData=true;
nowFeedData=data;
nowTimeStep=timeStep;
}
//RGBA轉(zhuǎn)YUV的方法,這是最簡單粗暴的方式,在使用的時候,一般不會選擇在Java層,用這種方式做轉(zhuǎn)換
private void rgbaToYuv(byte[] rgba,int width,int height,byte[] yuv){
final int frameSize = width * height;
int yIndex = 0;
int uIndex = frameSize;
int vIndex = frameSize + frameSize/4;
int R, G, B, Y, U, V;
int index = 0;
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
index = j * width + i;
if(rgba[index*4]>127||rgba[index*4]<-128){
Log.e("color","-->"+rgba[index*4]);
}
R = rgba[index*4]&0xFF;
G = rgba[index*4+1]&0xFF;
B = rgba[index*4+2]&0xFF;
Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128;
yuv[yIndex++] = (byte) ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y));
if (j % 2 == 0 && index % 2 == 0) {
yuv[uIndex++] = (byte) ((U < 0) ? 0 : ((U > 255) ? 255 : U));
yuv[vIndex++] = (byte) ((V < 0) ? 0 : ((V > 255) ? 255 : V));
}
}
}
}
對于其他格式的音頻視頻編解碼也大同小異了,只要MediaCodec支持就好。
MediaMuxer(音視頻混合API)
MediaMuxer的使用很簡單,在Android Developer官網(wǎng)上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了。
這里需要注意的是,一定要等編碼器設(shè)置編碼格式完成后,再將它加入到混合器中,編碼器編碼格式設(shè)置完成的標(biāo)志是dequeueOutputBuffer得到返回值為MediaCodec.INFO_OUTPUT_FORMAT_CHANGED。
音視頻錄制MP4文件
上面已經(jīng)給出了音頻錄制的代碼和視頻錄制的代碼,利用MediaMuxer將其結(jié)合起來,就可以和簡單的完成錄制有聲音有圖像的MP4文件的功能了。音頻錄制和視頻錄制的基本流程保持不變,在錄制編碼后,不再將編碼的結(jié)果寫入到文件流中,而是寫入為混合器的sample data。以視頻為例,更改循環(huán)編碼的代碼為:
//流程一直,無需更改
int index=mVideoEnc.dequeueInputBuffer(-1);
if(index>=0){
if(hasNewData){
if(yuv==null){
yuv=new byte[width*height*3/2];
}
rgbaToYuv(data,width,height,yuv);
}
ByteBuffer buffer=getInputBuffer(mVideoEnc,index);
buffer.clear();
buffer.put(yuv);
//結(jié)束時,發(fā)送結(jié)束標(biāo)志,在編碼完成后結(jié)束
mVideoEnc.queueInputBuffer(index,0,yuv.length,
mStartFlag?0:MediaCodec.BUFFER_FLAG_END_OF_STREAM);
}
MediaCodec.BufferInfo mInfo=new MediaCodec.BufferInfo();
int outIndex=mVideoEnc.dequeueOutputBuffer(mInfo,0);
do {
if(outIndex>=0){
ByteBuffer outBuf=getOutputBuffer(mVideoEnc,outIndex);
//里面不在是寫入到文件,而是寫入為混合器的sample data
if(mTrackCount==3&&mInfo.size>0){
mMuxer.writeSampleData(mVideoTrack,outBuf,mInfo);
}
mVideoEnc.releaseOutputBuffer(outIndex,false);
outIndex=mVideoEnc.dequeueOutputBuffer(mInfo,0);
Log.e("wuwang","outIndex-->"+outIndex);
//編碼結(jié)束的標(biāo)志
if((mInfo.flags&MediaCodec.BUFFER_FLAG_END_OF_STREAM)!=0){
return true;
}
}else if(outIndex==MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
//按照MediaMuxer中所說,加入軌道的時機(jī)在這里
mVideoTrack=mMuxer.addTrack(mVideoEnc.getOutputFormat());
Log.e("wuwang","video track-->"+mVideoTrack);
mTrackCount++;
//一定要音軌視頻軌都加入后,再開始混合
if(mTrackCount==2){
mMuxer.start();
mTrackCount=3;
}
}
}while (outIndex>=0);
當(dāng)然是用MediaMuxer前,肯定是需要創(chuàng)建一個MediaMuxer的實(shí)例的:
mMuxer=new MediaMuxer(path+"."+postfix, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
音頻的操作和視頻一樣更改,將音頻編碼也加入MeidaMuxer的軌道中,得到一個軌道索引,將編碼后的數(shù)據(jù)加入為MediaMuxer當(dāng)前音軌的sample data。音軌和上面的視軌各自做各自的,結(jié)束錄制時,都發(fā)送結(jié)束標(biāo)志,然后在編碼結(jié)束后,停止混合器就可以得到一個固定碼率的MP4文件了。
總結(jié)
至此,本篇博客就結(jié)束了。但是在實(shí)際使用MediaCodec和MediaMuxer的過程中,總會遇到這樣或者那樣的問題,硬編硬解,和硬件相關(guān)比較緊密,Android雖然提供了一個很好的API,但是各個廠商在實(shí)現(xiàn)的過程中,總是會做些讓自己變得獨(dú)特的事情。當(dāng)然他們的目的并不是為了獨(dú)特,有的是為了讓產(chǎn)品變得更優(yōu)秀(雖然最后可能會做砸了),有的是為了省錢,用軟件去彌補(bǔ)硬件的缺陷,最后的結(jié)果就是苦了做上層開發(fā)的碼農(nóng)們。
從博主在使用MediaCodec和MediaMuxer的過程中遇到的問題,總結(jié)下需要注意主要有以下幾點(diǎn):
- MediaCodec是Android4.1新增API,MediaMuxer是Android4.3新增API。
- 顏色空間。按照Android本身的意思,COLOR_FormatYUV420Planar應(yīng)該是所有硬件平臺都支持的。但是實(shí)際上并不是這樣。所以在設(shè)置顏色空間時,應(yīng)該獲取硬件平臺所支持的顏色空間,確保它是支持你打算使用的顏色空間,不支持的話應(yīng)該啟用備用方案(使用其他當(dāng)前硬件支持的顏色空間)。
- 視頻尺寸,在一些手機(jī)上,視頻錄制的尺寸可以是任意的。但是有些手機(jī),不支持的尺寸設(shè)置會導(dǎo)致錄制的視頻現(xiàn)錯亂。博主在使用Oppo R7測試,360*640的視頻,單獨(dú)錄制視頻沒問題,音視頻混合后,出現(xiàn)了顏色錯亂的情況,而在360F4手機(jī)上,卻都是沒問題的。將視頻寬高都設(shè)置為16的倍數(shù),可以解決這個問題。
- 編碼器格式設(shè)置,諸如音頻編碼的采樣率、比特率等,取值也需要結(jié)合硬件平臺來設(shè)置,否則也會導(dǎo)致崩潰或其他問題。這個其實(shí)和顏色空間的選擇一樣。
- 網(wǎng)上看到許多
queueInputBuffer中設(shè)置presentationTimeUs為System.nanoTime()/1000,這樣做會導(dǎo)致編碼出來的音視頻,在播放時,總時長顯示的是錯誤的。應(yīng)該記錄開始時候的nanoTime,然后設(shè)置presentationTimeUs為(System.nanoTime()-nanoTime)/1000。 - 錄制結(jié)束時,應(yīng)該發(fā)送結(jié)束標(biāo)志
MediaCodec.BUFFER_FLAG_END_OF_STREAM,在編碼后區(qū)獲得這個標(biāo)志時再終止循環(huán),而不是直接終止循環(huán)。
應(yīng)該還有其他需要注意的問題。我暫時還沒遇到。
源碼
源碼在github中codec module下,有需要的小伙伴fork或者download。后續(xù)Android音視頻開發(fā)相關(guān)的Demo也會上傳到這個項(xiàng)目下。
[http://blog.csdn.net/junzia/article/details/54018671]