【转载】Android-->MediaMuxer,MediaCodec,AudioRecord及Camera实现音频视频混合MP4文件

来源:互联网 发布:钢铁力量6级车数据 编辑:程序博客网 时间:2024/05/16 08:46

本文属于转载原文地址

阅读之前,我喜欢你已经了解了以下内容:
1:https://github.com/saki4510t/AudioVideoRecordingSample
这个开源库介绍了, 音频和视频的录制, 其实已经够了~~~,不过视频的录制采用的是GLSurfaceView中的Surface方法, 并没有直接采用TextureView和Camera的PreviewCallback方法.

2:https://github.com/google/grafika
这个是谷歌的开源项目,里面介绍了很多关于GLSurfaceView和TextureView的操作,当然也有MediaCodec的使用.

3:https://developer.android.com/reference/android/media/MediaMuxer.html
这个是API文档介绍MediaMuxer混合器的文档,当然~~这个文档真的是”很详细”;

4:https://github.com/icylord/CameraPreview
这个开源库介绍了Camera的使用,还有TextureView,MediaCodec…and so on

能量补充完了,就该到我登场了…

本文的目的是通过Camera的PreviewCallback拿到帧数据,用MediaCodec编码成H264,添加到MediaMuxer混合器打包成MP4文件,并且使用TextureView预览摄像头. 当然使用AudioRecord录制音频,也是通过MediaCodec编码,一样是添加到MediaMuxer混合器和视频一起打包, 这个难度系数很低.

在使用MediaMuxer混合的时候,主要的难点就是控制视频数据和音频数据的同步添加,和状态的判断;

本文所有代码,采用片段式讲解,文章结尾会有源码下载:

1:视频录制和H264的数据获取

Camera mCamera = Camera.open();mCamera.addCallbackBuffer(mImageCallbackBuffer);//必须的调用1mCamera.setPreviewCallbackWithBuffer(mCameraPreviewCallback);...@Overridepublic 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);}

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);//读取音频数据到bufif (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); } }}

项目源代码: https://github.com/angcyo/PLDroidDemo/tree/master/audiovideorecordingdemo

0 0