【转载】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);//必须的调用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);
}

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值