【转载】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
- 【转载】Android-->MediaMuxer,MediaCodec,AudioRecord及Camera实现音频视频混合MP4文件
- Android-->MediaMuxer,MediaCodec,AudioRecord及Camera实现音频视频混合MP4文件
- Android-->MediaMuxer,MediaCodec,AudioRecord及Camera实现音频视频混合MP4文件
- MediaMuxer+MediaCodec生成MP4视频报错
- ffmpeg开发之旅(3):AAC编码格式分析与MP4文件封装(MediaCodec+MediaMuxer)
- ffmpeg开发之旅(3):AAC编码格式分析与MP4文件封装(MediaCodec+MediaMuxer)
- Android中如何提取和生成mp4文件(MediaMUxer)
- Android中MediaCodec和MediaMuxer的使用
- Android中MediaMuxer和MediaCodec用例
- Android Multimedia框架总结(二十三)MediaCodec补充及MediaMuxer引入(附案例)
- Android Multimedia框架总结(二十三)MediaCodec补充及MediaMuxer引入(附案例)
- EasyPlayer实现Android MediaMuxer录像MP4(支持G711/AAC/G726音频)
- Android中MediaMuxer和MediaCodec用例 - audio+video
- Android中MediaMuxer和MediaCodec用例 - audio+video
- Android中MediaMuxer和MediaCodec用例 - audio+video
- Android中MediaMuxer和MediaCodec用例 - audio+video
- Android中MediaMuxer和MediaCodec用例 - audio+video
- Android在MediaMuxer和MediaCodec录制视频示例 - audio+video
- 第六章 永无止境:网站的伸缩性架构
- 最新版本RAD Studio – 现在购买可享九折优惠
- 常见Android路线图
- malloc到未初始化的内存
- 结构体struct和typedef后面接指针的含义
- 【转载】Android-->MediaMuxer,MediaCodec,AudioRecord及Camera实现音频视频混合MP4文件
- Android 广播大全 Intent Action 事件
- 【Java数据结构与算法04】 | 第2章--数组
- android开发中通过aidl实现远程方法调用
- CentOS6.6 删除已安装的MySQL数据库
- 2016.09.19回顾
- Failed to create the java virtual machine解决
- Mybatis 单个参数的if判断(针对异常:There is no getter for property..)
- mybatis用log4j打印sql日志