当前位置: 代码迷 >> Android >> Android->MediaMuxer,MediaCodec,AudioRecord及Camera实现音频视频混同MP4文件
  详细解决方案

Android->MediaMuxer,MediaCodec,AudioRecord及Camera实现音频视频混同MP4文件

热度:149   发布时间:2016-04-24 11:17:23.0
Android-->MediaMuxer,MediaCodec,AudioRecord及Camera实现音频视频混合MP4文件

本文相当长,读者请注意…


阅读之前,我喜欢你已经了解了以下内容:
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);//读取音频数据到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);     } }}

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


如果您喜欢这篇文章,您也可以进行打赏, 金额不限.

至此: 文章就结束了,如有疑问: QQ群:274306954 欢迎您的加入.

  相关解决方案