在安卓执法仪开发过程中,我们发现有些设备的编码器性能较低,后来发现MediaCodec库支持异步模式了, 切换到异步模式果然会提高一些编码帧率.
同步模式MediaCodec 同步模式调用图如下:
创建,配置,启动AAC编码器:
MediaFormat format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, spec.getSample(), spec.getChannel());
format.setInteger(MediaFormat.KEY_BIT_RATE, 16000);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 1920); // 1920 = 16k的采样,60毫秒长度.
Timber.i("audio codec:mediaFormat = " + videoFormat);
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mediaCodec.start();
获取输入队列并塞入PCM数据:
int inputBufferIndex = mCodec.dequeueInputBuffer(100);
if (inputBufferIndex >= 0) {
if (pcmBuf.capacity() != length) {
pcmBuf = ByteBuffer.allocate(length * 2).order(ByteOrder.LITTLE_ENDIAN);
}
pcmBuf.asShortBuffer().put(pcm, 0, length);
pcmBuf.clear();
ByteBuffer inputBuffer;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
inputBuffer = mCodec.getInputBuffer(inputBufferIndex);
} else {
final ByteBuffer[] inputBuffers = mCodec.getInputBuffers();
inputBuffer = inputBuffers[inputBufferIndex];
}
inputBuffer.clear();
inputBuffer.put(pcmBuf);
mCodec.queueInputBuffer(inputBufferIndex, 0, length * 2, timestamp / 1000, 0);// , needKeyFrm
}
inputBufferIndex小于0表示没有可用的输出帧(可能编码缓冲已满).
获取输出的AAC音频帧:
int aacOutputBufferIndex = aacCodec.dequeueOutputBuffer(mBufferInfo, 0);
long timeSpend2 = SystemClock.elapsedRealtime() - begin;
if (timeSpend2 > 200) {
Timber.w("aac dequeueOutputBuffer spend too much time....%d ms", timeSpend2);
}
if (aacOutputBufferIndex >= 0) {
try {
ByteBuffer outputBuffer;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
outputBuffer = aacCodec.getOutputBuffer(aacOutputBufferIndex);
} else {
outputBuffer = audioOutputBuffers[aacOutputBufferIndex];//outputBuffer保存的就是H264数据了
}
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {// sps
mBufferInfo.size = 0;
}
if (mBufferInfo.size != 0) {
outputBuffer.position(mBufferInfo.offset);
outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
onAudioEncoded(outputBuffer, mBufferInfo);
}
} finally {
aacCodec.releaseOutputBuffer(aacOutputBufferIndex, false);//释放资源
}
} else if (aacOutputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat outputFormat = aacCodec.getOutputFormat();
ByteBuffer sps = outputFormat.getByteBuffer("csd-0");
int sps_size = sps.remaining();
extra2 = new byte[sps_size];
sps.get(extra2);
if (recordReady) {
startMediaMuxer();
}
}
aacOutputBufferIndex小于0表示还没编出一帧数据;或者没有待编码的输入帧了.
同步模式的输入输出相互影响,其中一方过慢,都会导致另一方无法取到有效Buffer,CPU无意义地消耗在轮训上面.效率较低
异步模式工作模式如下:
异步模式下,我们需要先把PCM数据先存到缓冲里面,在编码器可用时再存入编码器.
相比同步模式,异步模式要设置一个回调,该调用发生在create之后,configure之前:
mediaCodec.setCallback(mAudioCallback, mAudioHandler);
其中mAudioCallback是回调对象,mAudioHandler是回调函数执行的线程的Handler.
回调函数如下:
private MediaCodec.Callback mAudioCallback = new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(@NonNull MediaCodec mediaCodec, int id) {
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(id);
inputBuffer.clear();
Pair pair = mInputAudioQueue.poll();
Timber.d("AudioAvailable.queue size=%d",mInputAudioQueue.size());
long presentationTimeUs = 0;
if (pair != null) {
ByteBuffer dataSources = (ByteBuffer) pair.second;
presentationTimeUs = (long) pair.first;
inputBuffer.put(dataSources);
}
mediaCodec.queueInputBuffer(id, 0, inputBuffer.position(), presentationTimeUs / 1000, (mExit && pair == null)? MediaCodec.BUFFER_FLAG_END_OF_STREAM:0);
}
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec, int id, @NonNull MediaCodec.BufferInfo bufferInfo) {
try {
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0){
// 等输出回调中遇到该标志位时,表示缓冲帧已消耗完毕.
int flag = codecFlag.decrementAndGet();
Timber.i("audio codec EOS,flag goto:%d.", flag);
if (flag == 0) {
// 当音频,视频的缓冲帧都消耗完毕时,停止录像.
Timber.i("收到音频BUFFER_FLAG_END_OF_STREAM标志,即将停止录像.");
PublishSubject x = recordSubject;
if (x != null) x.onComplete();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
quitSafely();
} else {
quit();
}
}
return;
}
// Timber.i("MediaCodec onOutputBufferAvailable:"+bufferInfo.size+","+id);
//走到这里的时候,说明数据已经编码成AAC格式了
ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(id);
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {// sps
bufferInfo.size = 0;
}
if (bufferInfo.size != 0) {
outputBuffer.position(bufferInfo.offset);
outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
onAudioEncoded(outputBuffer, bufferInfo);
}
} finally {
mediaCodec.releaseOutputBuffer(id, false);//释放资源
// Timber.i("MediaCodec releaseOutputBuffer:"+id);
}
}
@Override
public void onError(@NonNull MediaCodec mediaCodec, @NonNull MediaCodec.CodecException e) {
Timber.e(e);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
quitSafely();
} else quit();
}
@Override
public void onOutputFormatChanged(@NonNull MediaCodec mediaCodec, @NonNull MediaFormat outputFormat) {
int flag = codecFlag.incrementAndGet();
Timber.i("Audio MediaCodec onOutputFormatChanged.flag:%d,format:%s",flag, outputFormat);
//特别注意此处的调用
ByteBuffer sps = outputFormat.getByteBuffer("csd-0");
int sps_size = sps.remaining();
extra2 = new byte[sps_size];
sps.get(extra2);
if (recordReady) {
startMediaMuxer();
}
}
};
其中,onInputBufferAvailable表示编码器空闲了,输入端可输入数据了.因此我们从缓冲区取出一个音频PCM帧存入编码器;此外onOutputBufferAvailable表示编码器编码出一个音频帧了.我们从编码器里面取出帧数据进行muxing.onError表示编码器异常,一般不会出现,出错后停止编码器即可.onOutputFormatChanged表示编码器内部状态更改了,在这里我们取出metaData.
经过测试异步模式的优势基本符合预期,其CPU消耗低,且效率要高于同步模式.
异步模式录像停止时,我们先把缓存在编码器内部的帧都消耗掉后,再优雅停止,否则缓存在编码器中的媒体帧被丢失,录像会不完整.停止录像正确操作应该这样:
- 设置EOF标志位,不再输入.等输出回调中遇到该标志位时,表示缓冲帧已消耗完毕.停止编码器等音频,视频编码器都停止时,停止录像.
具体见上面onOutputBufferAvailable里的注释.
我们得到编码器输出帧后,下面要进行的就是写入录像文件内.这个过程被称为muxing.
参考资料vlog清新互联执法记录仪介绍设计篇清新互联执法记录仪功能篇MP4文件格式 https://developer.apple.com/standards/qtff-2001.pdfFFMPEG



