1. 程式人生 > >Android硬編碼——音訊編碼、視訊編碼及音視訊混合

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
  • 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、mp3、wav等其他格式的音訊檔案。

MediaCodec(硬體編解碼API)

理解MediaCodec

MediaCodec的使用在Android Developer官網上有詳細的說明。官網上的圖能夠很好的說明MediaCodec的使用方式。我們只需理解這個圖,然後熟悉下MediaCodec的API就可以很快的上手使用MediaCodec來進行音視訊的編解碼工作了。 
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
  • 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
  • 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
  • 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
  • 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
  • 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
  • 1

音訊的操作和視訊一樣更改,將音訊編碼也加入MeidaMuxer的軌道中,得到一個軌道索引,將編碼後的資料加入為MediaMuxer當前音軌的sample data。音軌和上面的視軌各自做各自的,結束錄製時,都發送結束標誌,然後在編碼結束後,停止混合器就可以得到一個固定位元速率的MP4檔案了。

總結

至此,本篇部落格就結束了。但是在實際使用MediaCodec和MediaMuxer的過程中,總會遇到這樣或者那樣的問題,硬編硬解,和硬體相關比較緊密,Android雖然提供了一個很好的API,但是各個廠商在實現的過程中,總是會做些讓自己變得獨特的事情。當然他們的目的並不是為了獨特,有的是為了讓產品變得更優秀(雖然最後可能會做砸了),有的是為了省錢,用軟體去彌補硬體的缺陷,最後的結果就是苦了做上層開發的碼農們。 
從博主在使用MediaCodec和MediaMuxer的過程中遇到的問題,總結下需要注意主要有以下幾點:

  1. MediaCodec是Android4.1新增API,MediaMuxer是Android4.3新增API。
  2. 顏色空間。按照Android本身的意思,COLOR_FormatYUV420Planar應該是所有硬體平臺都支援的。但是實際上並不是這樣。所以在設定顏色空間時,應該獲取硬體平臺所支援的顏色空間,確保它是支援你打算使用的顏色空間,不支援的話應該啟用備用方案(使用其他當前硬體支援的顏色空間)。
  3. 視訊尺寸,在一些手機上,視訊錄製的尺寸可以是任意的。但是有些手機,不支援的尺寸設定會導致錄製的視訊現錯亂。博主在使用Oppo R7測試,360*640的視訊,單獨錄製視訊沒問題,音視訊混合後,出現了顏色錯亂的情況,而在360F4手機上,卻都是沒問題的。將視訊寬高都設定為16的倍數,可以解決這個問題。
  4. 編碼器格式設定,諸如音訊編碼的取樣率、位元率等,取值也需要結合硬體平臺來設定,否則也會導致崩潰或其他問題。這個其實和顏色空間的選擇一樣。
  5. 網上看到許多queueInputBuffer中設定presentationTimeUsSystem.nanoTime()/1000,這樣做會導致編碼出來的音視訊,在播放時,總時長顯示的是錯誤的。應該記錄開始時候的nanoTime,然後設定presentationTimeUs(System.nanoTime()-nanoTime)/1000
  6. 錄製結束時,應該傳送結束標誌MediaCodec.BUFFER_FLAG_END_OF_STREAM,在編碼後區獲得這個標誌時再終止迴圈,而不是直接終止迴圈。

應該還有其他需要注意的問題。我暫時還沒遇到。

原始碼

原始碼在github中codec module下,有需要的小夥伴fork或者download。後續Android音視訊開發相關的Demo也會上傳到這個專案下。