本文相当长,读者请注意…
阅读之前,我喜欢你已经了解了以下内容:
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 欢迎您的加入.