本文相当长,读者请注意…
阅读之前,我喜欢你已经了解了以下内容:
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);
}
群友补充1:
上段代码中的NV21toI420SemiPlanar
实现方法, 这个编码的视频是黑白的,把这个方法的实现,改为:
private void NV21toI420SemiPlanar(byte[] nv21bytes, byte[] i420bytes, int width, int height) {
final int iSize = width * height;
System.arraycopy(nv21bytes, 0, i420bytes, 0, iSize);
for (int iIndex = 0; iIndex < iSize / 2; iIndex += 2) {
i420bytes[iSize + iIndex / 2 + iSize / 4] = nv21bytes[iSize + iIndex]; // U
i420bytes[iSize + iIndex / 2] = nv21bytes[iSize + iIndex + 1]; // V
}
}
就会是彩色;
群友补充2:
上段代码中的
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
改为
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, System.nanoTime() / 1000);
即可提高成功率.
如果项目中遇到这两个问题,大可以拿去。感谢群友 明天的现在
.
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 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扫码加群, 小伙伴们在等着你哦!
关注我的公众号, 每天都能一起玩耍哦!