MediaCodec类可以访问底层媒体编解码框架(StageFright 或 OpenMAX),即编解码组件,它是Android基本的多媒体支持基础架构的一部分,通常和MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface和AudioTrack一起使用。它本身并不是Codec,它通过调用底层编解码组件获得了Codec的能力。
MediaCodec的工作方式
MediaCodec处理输入数据产生输出数据。当异步处理数据时,使用一组输入和输出Buffer队列。通常,在逻辑上,客户端请求(或接收)数据后填入预先设定的空输入缓冲区,输入Buffer填满后将其传递到MediaCodec并进行编解码处理。之后MediaCodec编解码后的数据填充到一个输出Buffer中。最后,客户端请求(或接收)输出Buffer,消耗输出Buffer中的内容,用完后释放,给回MediaCodec重新填充输出数据。
必须保证输入和输出队列同时非空,即至少有一个输入Buffer和输出Buffer才能工作。
MediaCodec状态周期图
在MediaCodec的生命周期中存在三种状态 :Stopped、Executing、Released。
Stopped状态实际上还可以处在三种状态:Uninitialized、Configured、Error。
Executing状态也分为三种子状态:Flushed, Running、End-of-Stream。
从上图可以看出:
- 当创建编解码器的时候处于未初始化状态。首先你需要调用configure(…)方法让它处于Configured状态,然后调用start()方法让其处于Executing状态。在Executing状态下,你就可以使用上面提到的缓冲区来处理数据。
- Executing的状态下也分为三种子状态:Flushed, Running、End-of-Stream。在start() 调用后,编解码器处于Flushed状态,这个状态下它保存着所有的缓冲区。一旦第一个输入buffer出现了,编解码器就会自动运行到Running的状态。当带有end-of-stream标志的buffer进去后,编解码器会进入End-of-Stream状态,这种状态下编解码器不在接受输入buffer,但是仍然在产生输出的buffer。此时你可以调用flush()方法,将编解码器重置于Flushed状态。
- 调用stop()将编解码器返回到未初始化状态,然后可以重新配置。 完成使用编解码器后,您必须通过调用release()来释放它。
- 在极少数情况下,编解码器可能会遇到错误并转到错误状态。 这是使用来自排队操作的无效返回值或有时通过异常来传达的。 调用reset()使编解码器再次可用。 您可以从任何状态调用它来将编解码器移回未初始化状态。 否则,调用 release()动到终端释放状态。
MediaCodec 基本使用流程:
- createEncoderByType/createDecoderByType
- configure
- start
- while(true) {
- dequeueInputBuffer
- queueInputBuffer
- dequeueOutputBuffer
- releaseOutputBuffer
}
- stop
- release
- 实时采集音频并编码
为了保证兼容性,推荐的配置是 44.1kHz、单通道、16 位精度。首先创建并配置 AudioRecord 和 MediaCodec
// 输入源 麦克风
private final static int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
// 采样率 44.1kHz,所有设备都支持
private final static int SAMPLE_RATE = 44100;
// 通道 单声道,所有设备都支持
private final static int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
// 精度 16 位,所有设备都支持
private final static int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
// 通道数 单声道
private static final int CHANNEL_COUNT = 1;
// 比特率
private static final int BIT_RATE = 96000;
public void createAudio() {
mBufferSizeInBytes = AudioRecord.getMinBufferSize(AudioEncoder.SAMPLE_RATE, AudioEncoder.CHANNEL_CONFIG, AudioEncoder.AUDIO_FORMAT);
if (mBufferSizeInBytes <= 0) {
throw new RuntimeException("AudioRecord is not available, minBufferSize: " + mBufferSizeInBytes);
}
Log.i(TAG, "createAudioRecord minBufferSize: " + mBufferSizeInBytes);
mAudioRecord = new AudioRecord(AudioEncoder.AUDIO_SOURCE, AudioEncoder.SAMPLE_RATE, AudioEncoder.CHANNEL_CONFIG, AudioEncoder.AUDIO_FORMAT, mBufferSizeInBytes);
int state = mAudioRecord.getState();
Log.i(TAG, "createAudio state: " + state + ", initialized: " + (state == AudioRecord.STATE_INITIALIZED));
}
public void createMediaCodec() throws IOException {
MediaCodecInfo mediaCodecInfo = CodecUtils.selectCodec(MIMETYPE_AUDIO_AAC);
if (mediaCodecInfo == null) {
throw new RuntimeException(MIMETYPE_AUDIO_AAC + " encoder is not available");
}
Log.i(TAG, "createMediaCodec: mediaCodecInfo " + mediaCodecInfo.getName());
MediaFormat format = MediaFormat.createAudioFormat(MIMETYPE_AUDIO_AAC, SAMPLE_RATE, CHANNEL_COUNT);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
mMediaCodec = MediaCodec.createEncoderByType(MIMETYPE_AUDIO_AAC);
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
}
然后开始录音,得到原始音频数据,再编码为 AAC 格式。这个地方会阻塞调用的线程,而且编码比较耗时,一定要在主线程之外调用。
public void offerEncoder(AudioRecord record, boolean endOfStream) throws Exception {
try {
int e = this.mEncoder.dequeueInputBuffer(0L);
// 当输入缓冲区有效时,就是>=0
if(e >= 0) {
// 输入Buffer 队列,用于传送数据进行编码
ByteBuffer[] inputBuffers = this.mEncoder.getInputBuffers();
ByteBuffer bufferCache = inputBuffers[e];
int audioSize = record.read(bufferCache, bufferCache.remaining());
if(audioSize != AudioRecord.ERROR_INVALID_OPERATION
&& audioSize != AudioRecord.ERROR_BAD_VALUE) {
int flag = endOfStream?4:0;
// 通知编码器编码
this.mEncoder.queueInputBuffer(e, 0, audioSize, this.mLastPresentationTimeUs, flag);
if(this.presentationInterval == 0) {
this.presentationInterval = (int)((float)audioSize / (float)this.sampleByteSizeInSec * 1000000.0F);
}
// 时间戳保证递增就是
this.mLastPresentationTimeUs += (long)this.presentationInterval;
} else {
Log.w(TAG, "offerEncoder : error audioSize = " + audioSize);
}
} else if(endOfStream) {
this.unExpectedEndOfStream = true;
}
} catch (Exception e) {
if(null != this.mCallback) {
this.mCallback.onStatus(AudioWorker.ENCODE_OFFER_ERROR, new Object[]{e});
}
}
}
public void drainEncoder(boolean endOfStream) throws Exception {
while(true) {
// 输出Buffer队列, 用于取到编码后的数据
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
// 拿到输出缓冲区的索引
int bufferIndex = this.mEncoder.dequeueOutputBuffer(info, 0L);
ByteBuffer[] buffers = this.mEncoder.getOutputBuffers();
if(bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat data1 = this.mEncoder.getOutputFormat();
if(null != this.mCallback) {
this.mCallback.onStatus(AudioWorker.STATUS_START, new Object[]{data1});
}
} else if(bufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
this.mEncoder.getOutputBuffers();
} else {
if(bufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
if(endOfStream && !this.unExpectedEndOfStream) {
continue;
}
} else {
if(bufferIndex < 0) {
Log.w(TAG, "AudioEncoderCore.drainEncoder : bufferIndex < 0 ");
continue;
}
ByteBuffer data = buffers[bufferIndex];
if(null != data) {
data.position(info.offset);
data.limit(info.offset + info.size);
}
if (info.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
if (null != this.mCallback) {
this.mCallback.onStatus(AudioWorker.STATUS_HEAD, new Object[]{data, info});
}
} else if(null != this.mCallback) {
this.mCallback.onStatus(AudioWorker.STATUS_DATA, new Object[]{data, info});
}
this.mEncoder.releaseOutputBuffer(bufferIndex, false);
if((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == 0) {
continue;
}
}
return;
}
}
}
dequeueInputBuffer 返回缓冲区索引,如果索引小于 0 ,则表示当前没有可用的缓冲区。它的参数 timeoutUs 表示超时时间 ,毕竟用的是 MediaCodec 的同步模式,如果没有可用缓冲区,就会阻塞指定参数时间,如果参数为负数,则会一直阻塞下去。
queueInputBuffer 方法将数据入队时,除了要传递出队时的索引值,然后还需要传入当前缓冲区的时间戳 presentationTimeUs 和当前缓冲区的一个标识 flag 。
其中,时间戳通常是缓冲区渲染的时间,而标识则有多种标识,标识当前缓冲区属于那种类型:
BUFFER_FLAG_CODEC_CONFIG
标识当前缓冲区携带的是编解码器的初始化信息,并不是媒体数据
BUFFER_FLAG_END_OF_STREAM
结束标识,当前缓冲区是最后一个了,到了流的末尾
BUFFER_FLAG_KEY_FRAME
表示当前缓冲区是关键帧信息,也就是 I 帧信息
在编码的时候可以计算当前缓冲区的时间戳,也可以直接传递 0 就好了,对于标识也可以直接传递 0 作为参数。
把数据传入给 MediaCodec 之后,通过 dequeueOutputBuffer 方法取出编解码后的数据,除了指定超时时间外,还需要传入 MediaCodec.BufferInfo 对象,这个对象里面有着编码后数据的长度、偏移量以及标识符。
取出 MediaCodec.BufferInfo 内的数据之后,根据不同的标识符进行不同的操作:
BUFFER_FLAG_CODEC_CONFIG
表示当前数据是一些配置数据,在 H264 编码中就是 SPS 和 PPS 数据,也就是 00 00 00 01 67 和 00 00 00 01 68 开头的数据,这个数据是必须要有的,它里面有着视频的宽、高信息。
BUFFER_FLAG_KEY_FRAME
关键帧数据,对于 I 帧数据,也就是开头是 00 00 00 01 65 的数据,
BUFFER_FLAG_END_OF_STREAM
表示结束,MediaCodec 工作结束
对于返回的 flags ,不符合预定义的标识,则可以直接写入,那些数据可能代表的是 H264 中的 P 帧 或者 B 帧。