1. 程式人生 > >Android-->MediaMuxer,MediaCodec,AudioRecord及Camera實現音訊視訊混合MP4檔案

Android-->MediaMuxer,MediaCodec,AudioRecord及Camera實現音訊視訊混合MP4檔案

本文相當長,讀者請注意…

閱讀之前,我喜歡你已經瞭解了以下內容:

這個開源庫介紹了, 音訊和視訊的錄製, 其實已經夠了~~~,不過視訊的錄製採用的是GLSurfaceView中的Surface方法, 並沒有直接採用TextureView和Camera的PreviewCallback方法.


這個是谷歌的開源專案,裡面介紹了很多關於GLSurfaceView和TextureView的操作,當然也有MediaCodec的使用.

能量補充完了,就該到我登場了…

本文的目的是通過Camera的PreviewCallback拿到幀資料,用MediaCodec編碼成H264,新增到MediaMuxer混合器打包成MP4檔案,並且使用TextureView預覽攝像頭. 當然使用AudioRecord錄製音訊,也是通過MediaCodec編碼,一樣是新增到MediaMuxer混合器和視訊一起打包, 這個難度係數很低.

在使用MediaMuxer混合的時候,主要的難點就是控制視訊資料和音訊資料的同步新增,和狀態的判斷;

本文所有程式碼,採用片段式講解,文章結尾會有原始碼下載:

1:視訊錄製和H264的資料獲取

Camera mCamera = Camera.open();
mCamera.addCallbackBuffer(mImageCallbackBuffer);//必須的呼叫1
mCamera.setPreviewCallbackWithBuffer(mCameraPreviewCallback);
...
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
    //通過回撥,拿到的data資料是原始資料
videoRunnable.add(data);//丟給videoRunnable執行緒,使用MediaCodec進行h264編碼操作 camera.addCallbackBuffer(data);//必須的呼叫2 }

1.1:H264的編碼操作

編碼器的配置:

private static final String MIME_TYPE = "video/avc"; // H.264的mime型別
MediaCodecInfo codecInfo = selectCodec(MIME_TYPE);//選擇系統用於編碼H264的編碼器資訊,固定的呼叫
mColorFormat = selectColorFormat(codecInfo, MIME_TYPE);//根據MIME格式,選擇顏色格式,固定的呼叫
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, this.mWidth, this.mHeight);//根據MIME建立MediaFormat,固定 //以下引數的設定,儘量固定.當然,如果你非常瞭解,也可以自行修改 mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);//設定位元率 mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);//設定幀率 mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, mColorFormat);//設定顏色格式 mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);//設定關鍵幀的時間 try { mMediaCodec = MediaCodec.createByCodecName(codecInfo.getName());//這裡就是根據上面拿到的編碼器建立一個MediaCodec了;//MediaCodec還有一個方法可以直接用MIME型別,建立 } catch (IOException e) { e.printStackTrace(); } //第二個引數用於播放MP4檔案,顯示影象的Surface; //第四個引數,編碼H264的時候,固定CONFIGURE_FLAG_ENCODE, 播放的時候傳入0即可;API文件有解釋 mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);//關鍵方法 mMediaCodec.start();//必須

開始H264的編碼:

private void encodeFrame(byte[] input) {//這個引數就是上面回撥拿到的原始資料
    NV21toI420SemiPlanar(input, mFrameData, this.mWidth, this.mHeight);//固定的方法,用於顏色轉換

    ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();//拿到輸入緩衝區,用於傳送資料進行編碼
    ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();//拿到輸出緩衝區,用於取到編碼後的資料
    int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC);//得到當前有效的輸入緩衝區的索引
    if (inputBufferIndex >= 0) {//當輸入緩衝區有效時,就是>=0
        ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
        inputBuffer.clear();
        inputBuffer.put(mFrameData);//往輸入緩衝區寫入資料,關鍵點
        mMediaCodec.queueInputBuffer(inputBufferIndex, 0,
                mFrameData.length, System.nanoTime() / 1000, 0);//將緩衝區入隊
    } else {
    }

    int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);//拿到輸出緩衝區的索引
    do {
        if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
        } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
            outputBuffers = mMediaCodec.getOutputBuffers();
        } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            //特別注意此處的呼叫
            MediaFormat newFormat = mMediaCodec.getOutputFormat();
            MediaMuxerRunnable mediaMuxerRunnable = this.mediaMuxerRunnable.get();
            if (mediaMuxerRunnable != null) {
            //如果要合成視訊和音訊,需要處理混合器的音軌和視軌的新增.因為只有新增音軌和視軌之後,寫入資料才有效
                mediaMuxerRunnable.addTrackIndex(MediaMuxerRunnable.TRACK_VIDEO, newFormat);
            }
        } else if (outputBufferIndex < 0) {
        } else {
            //走到這裡的時候,說明資料已經編碼成H264格式了
            ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];//outputBuffer儲存的就是H264資料了
            if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                mBufferInfo.size = 0;
            }
            if (mBufferInfo.size != 0) {
                MediaMuxerRunnable mediaMuxerRunnable = this.mediaMuxerRunnable.get();

                //因為上面的addTrackIndex方法不一定會被呼叫,所以要在此處再判斷並新增一次,這也是混合的難點之一
                if (mediaMuxerRunnable.isAudioAdd()) {
                    MediaFormat newFormat = mMediaCodec.getOutputFormat();
                    mediaMuxerRunnable.addTrackIndex(MediaMuxerRunnable.TRACK_VIDEO, newFormat);
                }

                // adjust the ByteBuffer values to match BufferInfo (not needed?)
                outputBuffer.position(mBufferInfo.offset);
                outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);

                if (mediaMuxerRunnable != null) {
                //這一步就是新增視訊資料到混合器了,在呼叫新增資料之前,一定要確保視軌和音軌都新增到了混合器
                    mediaMuxerRunnable.addMuxerData(new MediaMuxerRunnable.MuxerData(
                            MediaMuxerRunnable.TRACK_VIDEO, outputBuffer, mBufferInfo
                    ));
                }
            }
            mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);//釋放資源
        }
        outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
    } while (outputBufferIndex >= 0);
}

群友補充1:
上段程式碼中的NV21toI420SemiPlanar實現方法, 這個編碼的視訊是黑白的,把這個方法的實現,改為:

private void NV21toI420SemiPlanar(byte[] nv21bytes, byte[] i420bytes, int width, int height) {
    final int iSize = width * height;
    System.arraycopy(nv21bytes, 0, i420bytes, 0, iSize);

    for (int iIndex = 0; iIndex < iSize / 2; iIndex += 2) {
        i420bytes[iSize + iIndex / 2 + iSize / 4] = nv21bytes[iSize + iIndex]; // U
        i420bytes[iSize + iIndex / 2] = nv21bytes[iSize + iIndex + 1]; // V
    }
}

就會是彩色;

群友補充2:
上段程式碼中的

int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);

改為

int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, System.nanoTime() / 1000);

即可提高成功率.

如果專案中遇到這兩個問題,大可以拿去。感謝群友 明天的現在.

2:音訊的錄製和編碼

和視訊一樣,需要配置編碼器:

private static final String MIME_TYPE = "audio/mp4a-latm";
audioCodecInfo = selectAudioCodec(MIME_TYPE);//是不是似曾相識?沒錯,一樣是通過MIME拿到系統對應的編碼器資訊
final MediaFormat audioFormat = MediaFormat.createAudioFormat(MIME_TYPE, SAMPLE_RATE, 1);
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, AudioFormat.CHANNEL_IN_MONO);//CHANNEL_IN_STEREO 立體聲
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
//      audioFormat.setLong(MediaFormat.KEY_MAX_INPUT_SIZE, inputFile.length());
//      audioFormat.setLong(MediaFormat.KEY_DURATION, (long)durationInMs );
mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);

mMediaCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mMediaCodec.start();
//過程都差不多~不解釋了;

獲取音訊裝置,用於獲取音訊資料:

android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
try {
 final int min_buffer_size = AudioRecord.getMinBufferSize(
         SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO,
         AudioFormat.ENCODING_PCM_16BIT);
 int buffer_size = SAMPLES_PER_FRAME * FRAMES_PER_BUFFER;
 if (buffer_size < min_buffer_size)
     buffer_size = ((min_buffer_size / SAMPLES_PER_FRAME) + 1) * SAMPLES_PER_FRAME * 2;

 audioRecord = null;
 for (final int source : AUDIO_SOURCES) {
     try {
         audioRecord = new AudioRecord(
                 source, SAMPLE_RATE,
                 AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, buffer_size);
         if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED)
             audioRecord = null;
     } catch (final Exception e) {
         audioRecord = null;
     }
     if (audioRecord != null) break;
 }
} catch (final Exception e) {
 Log.e(TAG, "AudioThread#run", e);
}

開始音訊資料的採集:

audioRecord.startRecording();//固定寫法
while (!isExit) {
    buf.clear();
    readBytes = audioRecord.read(buf, SAMPLES_PER_FRAME);//讀取音訊資料到buf
    if (readBytes > 0) {
        buf.position(readBytes);
        buf.flip();
        encode(buf, readBytes, getPTSUs());//開始編碼
    }
}

開始音訊編碼:

private void encode(final ByteBuffer buffer, final int length, final long presentationTimeUs) {
 if (isExit) return;
 final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
 final int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC);
     /*向編碼器輸入資料*/
 if (inputBufferIndex >= 0) {
     final ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
     inputBuffer.clear();
     if (buffer != null) {
         inputBuffer.put(buffer);
     }
         mMediaCodec.queueInputBuffer(inputBufferIndex, 0, 0,
                 presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
     } else {
         mMediaCodec.queueInputBuffer(inputBufferIndex, 0, length,
                 presentationTimeUs, 0);
     }
 } else if (inputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
 }
 //上面的過程和視訊是一樣的,都是向輸入緩衝區輸入原始資料

 /*獲取解碼後的資料*/
 ByteBuffer[] encoderOutputBuffers = mMediaCodec.getOutputBuffers();
 int encoderStatus;
 do {
     encoderStatus = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
     if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
     } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
         encoderOutputBuffers = mMediaCodec.getOutputBuffers();
     } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
      //特別注意此處, 此處和視訊編碼是一樣的
         final MediaFormat format = mMediaCodec.getOutputFormat(); // API >= 16
         MediaMuxerRunnable mediaMuxerRunnable = this.mediaMuxerRunnable.get();
         if (mediaMuxerRunnable != null) {
             //新增音軌,和新增視軌都是一樣的呼叫
             mediaMuxerRunnable.addTrackIndex(MediaMuxerRunnable.TRACK_AUDIO, format);
         }

     } else if (encoderStatus < 0) {
     } else {
         final ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
         if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
             mBufferInfo.size = 0;
         }

         if (mBufferInfo.size != 0) {
             mBufferInfo.presentationTimeUs = getPTSUs();
            //當保證視軌和音軌都新增完成之後,才可以新增資料到混合器
             muxer.addMuxerData(new MediaMuxerRunnable.MuxerData(
                     MediaMuxerRunnable.TRACK_AUDIO, encodedData, mBufferInfo));
             prevOutputPTSUs = mBufferInfo.presentationTimeUs;
         }
         mMediaCodec.releaseOutputBuffer(encoderStatus, false);
     }
 } while (encoderStatus >= 0);
}

3:混合器的操作

private Vector<MuxerData> muxerDatas;//緩衝傳輸過來的資料
public void start(String filePath) throws IOException {
    isExit = false;
    isVideoAdd = false;
//建立混合器
    mediaMuxer = new MediaMuxer(filePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
    if (audioRunnable != null) {
        //音訊準備工作
        audioRunnable.prepare();
        audioRunnable.prepareAudioRecord();
    }
    if (videoRunnable != null) {
         //視訊準備工作
        videoRunnable.prepare();
    }
    new Thread(this).start();
    if (audioRunnable != null) {
        new Thread(audioRunnable).start();//開始音訊解碼執行緒
    }
    if (videoRunnable != null) {
        new Thread(videoRunnable).start();//開始視訊解碼執行緒
    }

}

//混合器,最重要的就是保證再新增資料之前,要先新增視軌和音軌,並且儲存響應軌跡的索引,用於新增資料的時候使用
public void addTrackIndex(@TrackIndex int index, MediaFormat mediaFormat) {
  if (isMuxerStart()) {
      return;
  }
  int track = mediaMuxer.addTrack(mediaFormat);
  if (index == TRACK_VIDEO) {
      videoTrackIndex = track;
      isVideoAdd = true;
      Log.e("angcyo-->", "新增視軌");
  } else {
      audioTrackIndex = track;
      isAudioAdd = true;
      Log.e("angcyo-->", "新增音軌");
  }
  requestStart();
}

private void requestStart() {
   synchronized (lock) {
       if (isMuxerStart()) {
           mediaMuxer.start();//在start之前,確保視軌和音軌已經添加了
           lock.notify();
       }
   }
}

while (!isExit) {
 if (muxerDatas.isEmpty()) {
     synchronized (lock) {
         try {
             lock.wait();
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     }
 } else {
     if (isMuxerStart()) {
         MuxerData data = muxerDatas.remove(0);
         int track;
         if (data.trackIndex == TRACK_VIDEO) {
             track = videoTrackIndex;
         } else {
             track = audioTrackIndex;
         }
         //新增資料...
         mediaMuxer.writeSampleData(track, data.byteBuf, data.bufferInfo);
     }
 }
}

如果您喜歡這篇文章,您也可以進行打賞, 金額不限.

聯絡作者

請使用QQ掃碼加群, 小夥伴們在等著你哦!

關注我的公眾號, 每天都能一起玩耍哦!