简介
MediaCodec是 Android media 基础框架的一部分,通常和 MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface和AudioTrack 一起使用。
这张图描述了MediaCodec的整体工作流程,一般情况下,由客户端向MediaCodec申请一个空的输入ByteBuffer,进行数据填充,再将ByteBuffer发送给MediaCodec,MediaCodec会采用异步的方式处理这些输入的数据,并将处理后的数据填充到输出Buffer中,消费者取到输出Buffer进行消费后,需将缓冲区释放,返还给MediaCodec。
数据
数据载体模式:
ByteBuffer模式:3种数据类型都可使用ByteBuffer模式。这种模式下,通常可以使用Image类和getInput/OutputImage(int)获取原始视频帧,下面会具体列举到。
Surface模式:当处理原始视频数据时,应该考虑使用Surface,而不是ByteBuffer来作为数据载体,这样可以提高编解码器性能。因为,Surface使用的是更底层的视频缓冲区,而不是将数据映射或复制到ByteBuffer中,效率更高。这种模式下,可以使用ImageReader类来访问原始视频帧,并且它仍然比ByteBuffer模式高效。
数据类型:
1)压缩数据:作为解码器的输入数据或者编码器的输出数据
- 指定格式:通过MediaFormat.KEY_MIME来指定或获取格式,编解码器才知道如何处理这些数据。
- 视频数据:一般情况下,输入给解码器或从编码器得到的一个ByteBuffer,都会是完整的一帧数据。除非设置了BUFFER_FLAG_PARTIAL_FRAME标记,它表示了缓冲区只包含帧的一部分,解码器会对数据进行批处理,直到没有该标志的缓冲区出现,才开始解码。比如H264解码,必须将分割符和NALU单元作为一个完整的数据帧,传给解码器才能正确解码。
- 音频数据:音频的要求则要稍微宽松,一个ByteBuffer可能包含多个编码的音频访问单元。
2) 原始音频数据:ByteBuffer包含PCM音频数据的整个帧,这是每个声道按声道顺序的一个样本,每个PCM音频样本都是16位带符号整数或浮点数。格式为AudioFormat.ENCODING_PCM_16BIT才能做处理。
- 解码器输出:通过getOutputFormat()来获取MediaFormat
- 编码器输入:通过getInputFormat()来获取MediaFormat
获取音频采样数据的示例代码如下:
/**
* 根据声道获取采样数据
*/
short[] getSamplesForChannel(MediaCodec codec, int bufferId, int channelIx) {
//获取输出缓冲区
ByteBuffer outputBuffer = codec.getOutputBuffer(bufferId);
//获取音频编码格式
MediaFormat format = codec.getOutputFormat(bufferId);
//转换字节顺序和Short类型
ShortBuffer samples = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer();
//获取声道数
int numChannels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
if (channelIx < 0 || channelIx >= numChannels) {
return null;
}
//获取存储采样数据
short[] res = new short[samples.remaining() / numChannels];
for (int i = 0; i < res.length; ++i) {
res[i] = samples.get(i * numChannels + channelIx);
}
return res;
}
3) 原始视频数据:在ByteBuffer模式下,视频缓冲区根据MediaFormat.KEY_COLOR_FORMAT来进行布局。通过getCodecInfo().getCapabilitiesForType().colorFormats可以获取到支持的颜色格式数组,它包含3种颜色格式:
native raw video format: KEY_COLOR_FORMAT为COLOR_FormatSurface,表示该数据将是GraphicBuffer元数据的引用,在OMX中(即软编解码)中,被称为OMX_COLOR_FormatAndroidOpaque,这类格式的数据可以作为Surface模式的输入和输出。
flexible YUV buffers:比如CodecCapabilities.COLOR_FormatYUV420Flexible,Surface模式和ByteBuffer模式2中模式,都可以作为输入和输出。
other, specific formats: 除了以上2种,只支持ByteBuffer模式,在MediaCodecInfo.CodecCapabilities中可以看到许多不同的格式,只要后缀为flexible类型的,如,CodecCapabilities.COLOR_FormatRGBFlexible, 都可以使用Image类的getInput/OutputImage(int)来获取原始视频帧。
Build.VERSION_CODES.LOLLIPOP_MR1(5.1)以后,编解码器都支持YUV420P
生命周期和状态
MediaCodec有3种状态:Stopped,Executing和Released,其中Stopped和Released又各自细分成3种子状态,如下图所示:
1)Stopped:
- Uninitialized:当通过工厂方法了成功创建编解码器后,此时处于Uninitialized子状态
- Configured:通过configure方法配置编解码器,此时处于Configured子状态,接着,需要调用start方法来启动,让编解码器进入Executing状态的Flushed子状态,才能输入数据给编解码器处理
- Error:
2)Executing:
- Flushed:执行start方法后,此时处于Flushed子状态
- Running :当第一个输入缓冲区被出队,编解码器便进入Running子状态,这意味着,大部分时间编解码器都处于此状态
- End-of-Stream:当给编解码器发送一个带有End-of-Stream标记的Buffer后,编解码器就切换为End-of-Stream子状态,此时,编解码器不再接收输入数据,但仍旧会继续输出,直到end-of-stream标记输出
3)Released:
- Released:Stopped和Executing都可切换至此状态,当使用编码器操作完成后,应该调用release方法,使编解码器进入此状态
状态重置:
- flush:在Executing状态下,调用flush方法,来使编解码器回到Flushed子状态
- stop:在Executing状态下,调用stop方法,使编解码器进入Uninitialized子状态,此时可以调用configure方法来重新配置,进入下一轮循环
- reset:在某些情况下,编解码器会出现异常,此时应该使用reset而不是stop方法,使编解码进入Uninitialized状态,事实上,reset可以在任何时候被调用,如果异常发生后,不准备重新使用编解码器,那应该调用release进行释放
创建
通过编解码器类型和编解码器名字2种方式,可以创建编解码器,它们分别对应以下工厂方法:
//根据编码器类型创建解码器
public static MediaCodec createEncoderByType(@NonNull String type)
//根据解码器类型创建解码器
public static MediaCodec createDecoderByType(@NonNull String type)
//根据编解码器名字创建编解码器
public static MediaCodec createByCodecName(@NonNull String name)
MediaFormat包含了许多类型,如下:
public final class MediaFormat {
。。。。。
//视频类型
public static final String MIMETYPE_VIDEO_VP8 = "video/x-vnd.on2.vp8";
public static final String MIMETYPE_VIDEO_VP9 = "video/x-vnd.on2.vp9";
public static final String MIMETYPE_VIDEO_AV1 = "video/av01";
public static final String MIMETYPE_VIDEO_AVC = "video/avc";
public static final String MIMETYPE_VIDEO_HEVC = "video/hevc";
//音频类型
public static final String MIMETYPE_AUDIO_AMR_NB = "audio/3gpp";
public static final String MIMETYPE_AUDIO_AMR_WB = "audio/amr-wb";
public static final String MIMETYPE_AUDIO_MPEG = "audio/mpeg";
public static final String MIMETYPE_AUDIO_AAC = "audio/mp4a-latm";
public static final String MIMETYPE_AUDIO_QCELP = "audio/qcelp";
public static final String MIMETYPE_AUDIO_VORBIS = "audio/vorbis";
public static final String MIMETYPE_AUDIO_OPUS = "audio/opus";
。。。。。
}
在创建解码器时,如果是本地文件或者网络流,可以配合使用MediaExtractor解封装器来做格式提取,如下:
//mTrackIndex为视频轨索引获取格式
MediaFormat decoderFormat = mediaExtractor.getTrackFormat(mTrackIndex);
//创建解码器
MediaCodec decoder = MediaCodec.createDecoderByType(decoderFormat.getString(MediaFormat.KEY_MIME));
在创建编码器时,可以直接指定期望的编码类型,如下:
//指定格式
MediaFormat encoderFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
//创建编码器
MediaCodec encoder = MediaCodec.createEncoderByType(encoderFormat.getString(MediaFormat.KEY_MIME));
以上是硬编解码器的创建,如果创建失败,可以通过指定软解码器名字来创建,如下:
//创建软解码器
MediaCodec decoder = MediaCodec.createByCodecName("OMX.google.h264.decoder");
//创建软编码器
MediaCodec encoder = MediaCodec.createByCodecName("OMX.google.h264.encoder");
通常,以"OMX.google."为前缀的名字,即为软编解码类型
初始化配置
void configure (MediaFormat format, Surface surface, MediaCrypto crypto, int flags)
void configure (MediaFormat format, Surface surface, int flags, MediaDescrambler descrambler)
MediaCrypto和MediaDescrambler:用于加解密处理,都是可空参数,如果不涉及加密和解密,它们没有区别,会执行同一个重载方法
flags:传入MediaCodec.CONFIGURE_FLAG_ENCODE时(它的值为1),用来指定创建编码器,奇怪的是MediaCodec没有定义一个类似MediaCodec.CONFIGURE_FLAG_DECODE的常量用于创建解码器,这里通常直接传入0来指定创建解码器
Surface:调用releaseOutputBuffer(int index, boolean render)方法将render设置为true,解码器会将输出数据渲染到此处指定的Surface,当Surface不再使用或显示时,缓冲区会自动释放给编解码器
以下为编解码配置的示例代码:
//解码器配置
decoder.configure(decoderFormat, new Surface(new SurfaceTexture(getTextureId())), null, 0);
decoder.start()
这里指定了解码器最终输出渲染的表面,用来输出到开辟好的纹理空间
//编码器配置
encoder.configure(encoderFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface surface = encoder.createInputSurface()
encoder.start()
编码器创建的Surface只能用于硬件加速api的渲染输入,如OpenGL,这就是Surface模式的用法,比如编码器创建了自己的Surface,可以用来关联EGL做为编码器的数据输入,替代ByteBuffer输入方式。
还有另一种方式,也可以使用createPersistentInputSurface ()来创建Surface,其他的编码器随后可以调用setInputSurface(Surface)方法来继续使用这个Surface,但同一时间内,只能一个编解码器使用。
createInputSurface和setInputSurface方法必须在configure方法之后,start方法之前调用,否则抛IllegalStateException异常。
持久化表面必须使用createPersistentInputSurface创建,否则抛IllegalArgumentException异常。
特殊数据
某些格式如,AAC和MPEG4,H.264和H.265要求帧数据的前缀,包含设置数据或编解码器特定数据的缓冲区,如sps和pps。处理此类压缩格式时,必须在start方法之后且任何帧数据之前,将这些数据输送给编解码器。这类数据用BUFFER_FLAG_CODEC_CONFIG来做标记。
通常不使用ByteBuffer来直接提交,而是在configure方法时通过MediaFormat进行设置,它在start方法调用后,会直接提交给编解码器。编解码器同样会输出到输出缓冲区中,因此,在进行编码往Muxer写入数据时,携带BUFFER_FLAG_CODEC_CONFIG标记的数据不用再次写入,它应该通过MediaFormat传递给Muxer。
数据处理
1)同步方式:
系统版本5.0之前只能使用同步方式来处理,整个过程如开头的流程图所示,下面以解码流程为例:
/**
* 解封装
*/
private int drainExtractor(long timeoutUs) {
if (mIsExtractorEOS) return DRAIN_STATE_NONE;
int trackIndex = mExtractor.getSampleTrackIndex();
if (trackIndex >= 0 && trackIndex != mTrackIndex) {
return DRAIN_STATE_NONE;
}
//步骤1
int result = mDecoder.dequeueInputBuffer(timeoutUs);
if (result < 0) return DRAIN_STATE_NONE;
if (trackIndex < 0) {
mIsExtractorEOS = true;
mDecoder.queueInputBuffer(result, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
return DRAIN_STATE_NONE;
}
//步骤2
ByteBuffer buffer = mDecoder.getInputBuffer(result);
int sampleSize = mExtractor.readSampleData(buffer, 0);
boolean isKeyFrame = (mExtractor.getSampleFlags() & MediaExtractor.SAMPLE_FLAG_SYNC) != 0;
//步骤3
mDecoder.queueInputBuffer(result, 0, sampleSize, mExtractor.getSampleTime(), isKeyFrame ? MediaCodec.BUFFER_FLAG_KEY_FRAME : 0);
mExtractor.advance();
return DRAIN_STATE_CONSUMED;
}
步骤1:从解码器获取输入缓冲区的id
- timeoutUs == 0,立即返回
- timeoutUs < 0,无限期等待
- timeoutUs > 0,等待“timeoutUs”微秒超时
步骤2:根据缓冲id获取输入缓冲区
步骤3:将充满数据的输入缓冲求传递给解码器,最后的flags标记有几种类型
- BUFFER_FLAG_KEY_FRAME:是否关键帧
- BUFFER_FLAG_CODEC_CONFIG:是否sps和pps等特殊数据,通常应该用MediaFormat做传递
- BUFFER_FLAG_PARTIAL_FRAME:通常一个ByteBuffer包含一帧完整视频数据,除非指定该标志,出现该标记解码器会批量处理多个缓冲区,直到没有该标记出现,才进行解码,大多数情况下不会使用
- BUFFER_FLAG_END_OF_STREAM:结束输入数据给解码器,除非调用flush方法,否则不要再向编解码器输入缓冲区,可以在最后一个带有有效数据的缓冲区上加上此标记,也可以用一个空的缓冲区
来传递,此时的pts可为0
下面从解码器获取输出数据:
private int drainDecoder(long timeoutUs) throws InterruptedException {
if (mIsDecoderEOS) return DRAIN_STATE_NONE;
//1
int result = mDecoder.dequeueOutputBuffer(mBufferInfo, timeoutUs);
switch (result) {
case MediaCodec.INFO_TRY_AGAIN_LATER:
return DRAIN_STATE_NONE;
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
return DRAIN_STATE_SHOULD_RETRY_IMMEDIATELY;
}
//2
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
mEncoder.signalEndOfInputStream();
mIsDecoderEOS = true;
mBufferInfo.size = 0;
}
//3
boolean doRender = (mBufferInfo.size > 0);
mDecoder.releaseOutputBuffer(result, doRender);
......
return DRAIN_STATE_CONSUMED;
}
步骤1:从解码器获取输出缓冲区和缓冲区信息,result返回缓冲区索引或常量值
- INFO_TRY_AGAIN_LATER:获取超时
- INFO_OUTPUT_BUFFERS_CHANGED:表明输出缓冲区数据已更改,在5.0前,此时用getOutputBuffers方法获取缓冲区数组,5.0后,此方式已过期,可以忽略
- INFO_OUTPUT_FORMAT_CHANGED:表明输出格式已更改,后续数据将采用新的格式,此时可以通过getOutputFormat方法来获取新的格式,随后如果不出现超时,通常开始返回正确的输出缓冲区索引
步骤2:接受到带有BUFFER_FLAG_END_OF_STREAM标记的缓冲区,表明所有解码数据已输出完成,不会再有输出,signalEndOfInputStream方法通知编码器输入结束
步骤3:使用完输出缓冲区后,调用releaseOutputBuffer释放回给解码器,doRender为true,则缓冲区的数据会渲染到在configure方法中配置的Surface
可以不立即queueinputbuffer/releaseOutputBuffer到编解码器,但持有input/outputbuffer可能会使编解码器停止工作,并且此行为取决于设备。 编解码器有可能在产生输出缓冲区之前暂停,直到所有未完成的缓冲区queueinputbuffer/releaseOutputBuffer。 因此,用户最好每次获得缓冲区后执行释放操作。
2)异步方式:
在系统版本5.0及以上增加了异步处理方式,同步方式仍可以使用,但官方推荐首选异步方式,它的流程状态和同步方式稍微有些不同,再调用start方法后,状态自动切换为Running子状态,并且在调用flush方法后,必须再次调用start使状态流转为Running状态,才能开始输入数据。
异步方式的代码示例如下:
MediaCodec codec = MediaCodec.createByCodecName(name);
MediaFormat mOutputFormat; // member variable
codec.setCallback(new MediaCodec.Callback() {
@Override
void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
// fill inputBuffer with valid data
…
codec.queueInputBuffer(inputBufferId, …);
}
@Override
void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is equivalent to mOutputFormat
// outputBuffer is ready to be processed or rendered.
…
codec.releaseOutputBuffer(outputBufferId, …);
}
@Override
void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
mOutputFormat = format; // option B
}
@Override
void onError(…) {
…
}
});
codec.configure(format, …);
mOutputFormat = codec.getOutputFormat(); // option B
codec.start();
// wait for processing to complete
codec.stop();
codec.release();
setCallback方法必须在configure方法之前被调用,并且不应该再使用getInputBuffers,getOutputBuffers,dequeueInputBuffer和dequeueOutputBuffer方法。
最后
当编解码器使用Surface模式作为数据输入源或输出源时,对应的缓冲区无法访问:
输出源:使用输出Surface时,数据处理几乎与ByteBuffer模式相同。但是,输出缓冲区将不可访问,并表示为空值。例如,getOutputBuffer / Image(int)将返回null,getOutputBuffers将返回仅包含null的数组。
输入源:使用输入Surface时,没有可访问的输入缓冲区,因为缓冲区会自动从输入表面传递到编解码器。调用dequeueInputBuffer会抛出IllegalStateException,并且getInputBuffers返回一个不可写入的伪造ByteBuffer数组。调用signalEndOfInputStream以信号流结束后,输入表面将立即停止向编解码器提交数据。