Android硬編碼——音頻編碼、視頻編碼及音視頻混合

視頻編解碼對許多Android程序員來講都是Android中比較難的一個知識點。在Android 4.1之前,Android並無提供硬編硬解的API,因此以前基本上都是採用FFMpeg來作視頻軟件編解碼的,如今FFMpeg在Android的編解碼上依舊普遍應用。本篇博客主要講到的是利用Android4.1增長的API MediaCodec和Android 4.3增長的API MediaMuxer進行Mp4視頻的錄製。
概述
一般來講,對於同一平臺同一硬件環境,硬編硬解的速度是快於軟件編解碼的。並且相比軟件編解碼的高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=44100;   //採樣率,默認44.1k
private int channelCount=2;     //音頻採樣通道,默認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)*2;
buffer=new byte[bufferSize];
//實例化AudioRecord
mRecorder=new AudioRecord(MediaRecorder.AudioSource.MIC,sampleRate,channelConfig,
    audioFormat,bufferSize);
//開始錄製
mRecorder.startRecording();
//循環讀取數據到buffer中,並保存buffer中的數據到文件中
int length=mRecorder.read(buffer,0,bufferSize);
fos.write(buffer,0,length);
//停止循環並結束錄製
isRecording=false;
mRecorder.stop();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
按照上面的步驟,咱們就能成功的錄製PCM音頻文件了,可是處於傳輸和存儲方面的考慮,通常來講,咱們是不會直接錄製PCM音頻文件的。而是在錄製過程當中就對音頻數據進行編碼爲aac、mp三、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=256000;                    //編碼的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(-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;
//每次取出的時候,把全部加工好的都循環取出來
do{
    outIndex=mEnc.dequeueOutputBuffer(mInfo,0);
    if(outIndex>=0){
        ByteBuffer buffer=mEnc.getOutputBuffer(outIndex);
        buffer.position(mInfo.offset);
        //AAC編碼,須要加數據頭,AAC編碼數據頭固定爲7個字節
        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);
//編碼中止,發送編碼結束的標誌,循環結束後,中止並釋放編碼器
mEnc.stop();
mEnc.release();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
AAC編碼加文件頭的實現參照AAC編碼規則,將數據填入就行了,網上很容易找到,具體實現以下:
/**
* 給編碼出的aac裸流添加adts頭字段
* @param packet 要空出前7個字節,不然會搞亂數據
* @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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
這樣,獲得的文件就是AAC音頻文件了,通常Android系統自帶的播放器均可以直接播放。
視頻編碼實例
視頻的編碼和上面音頻的編碼也大同小異。攝像頭的數據回調時間並非肯定的,就算你設置了攝像頭FPS範圍爲30-30幀,它也不會每秒就必定給你30幀數據。Android攝像頭的數據回調,受光線的影響很是嚴重,這是由HAL層的3A算法決定的,你能夠將自動曝光補償、自動白平光等等給關掉,這樣你纔有可能獲得穩定的幀率。
而咱們錄製並編碼視頻的時候,確定是但願獲得一個固定幀率的視頻。因此在視頻錄製並進行編碼的過程當中,須要本身想些法子,讓幀率固定下來。最簡單也是最有效的作法就是,按照固定時間編碼,若是沒有新的攝像頭數據回調來就用上一幀的數據。
參考代碼以下:
private String mime="video/avc";    //編碼的MIME
private int rate=256000;            //波特率,256kb
private int frameRate=24;           //幀率,24幀
private int frameInterval=1;        //關鍵幀一秒一關鍵幀
//和音頻編碼同樣,設置編碼格式,獲取編碼器實例
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(-1);
if(index>=0){
    if(hasNewData){
        if(yuv==null){
            yuv=new byte[width*height*3/2];
        }
        //把傳入的rgba數據轉成yuv的數據,轉換在網上也是一大堆,不夠下面仍是一塊兒貼上吧
        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();
//嘗試取出加工好的數據,和音頻編碼同樣,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){
        //把編碼信息保存下來,關鍵幀上要用
        mHeadInfo=new byte[temp.length];
        mHeadInfo=temp;
    }else if(mInfo.flags%8==MediaCodec.BUFFER_FLAG_KEY_FRAME){
        //關鍵幀比普通幀是多了個幀頭的,保存了編碼的信息
        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){
        //結束的時候應該發送結束信號,在這裏處理
    }else{
        //寫入文件
        fos.write(temp,0,temp.length);
    }
    mEnc.releaseOutputBuffer(outIndex,false);
    outIndex=mEnc.dequeueOutputBuffer(mInfo,0);
}
//數據的來源,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 = 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));
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
對於其餘格式的音頻視頻編解碼也大同小異了,只要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();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
參照官方的說明和代碼示例,咱們能夠知道,音視頻混合(也能夠音頻和音頻混合),只須要將編碼器的MediaFormat加入到MediaMuxer中,獲得一個音軌視頻軌的索引,而後每次從編碼器中取出來的ByteBuffer,寫入(writeSampleData)到編碼器所在的軌道中就ok了。
這裏須要注意的是,必定要等編碼器設置編碼格式完成後,再將它加入到混合器中,編碼器編碼格式設置完成的標誌是dequeueOutputBuffer獲得返回值爲MediaCodec.INFO_OUTPUT_FORMAT_CHANGED。
音視頻錄製MP4文件
上面已經給出了音頻錄製的代碼和視頻錄製的代碼,利用MediaMuxer將其結合起來,就能夠和簡單的完成錄製有聲音有圖像的MP4文件的功能了。音頻錄製和視頻錄製的基本流程保持不變,在錄製編碼後,再也不將編碼的結果寫入到文件流中,而是寫入爲混合器的sample data。以視頻爲例,更改循環編碼的代碼爲:
//流程一直,無需更改
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);
    //結束時,發送結束標誌,在編碼完成後結束
    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);
        //編碼結束的標誌
        if((mInfo.flags&MediaCodec.BUFFER_FLAG_END_OF_STREAM)!=0){
            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==2){
            mMuxer.start();
            mTrackCount=3;
        }
    }
}while (outIndex>=0);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
固然是用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爲System.nanoTime()/1000,這樣作會致使編碼出來的音視頻,在播放時,總時長顯示的是錯誤的。應該記錄開始時候的nanoTime,而後設置presentationTimeUs爲(System.nanoTime()-nanoTime)/1000。
錄製結束時,應該發送結束標誌MediaCodec.BUFFER_FLAG_END_OF_STREAM,在編碼後區得到這個標誌時再終止循環,而不是直接終止循環。
應該還有其餘須要注意的問題。我暫時還沒遇到。
源碼
源碼在github中codec module下,有須要的小夥伴fork或者download。後續Android音視頻開發相關的Demo也會上傳到這個項目下。
————————————————
版權聲明:本文爲CSDN博主「湖廣午王」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連接及本聲明。
原文連接:https://blog.csdn.net/junzia/article/details/54018671
相關文章
相關標籤/搜索