摘自:https://zhuanlan.zhihu.com/p/455196527
https://blog.csdn.net/weiwei9363/article/details/136412810
https://blog.csdn.net/lidec/article/details/107006760 Android MediaCodec+OpenGL视频编解码实践笔记
MediaCodec处理的类型
MediaCodec 支持处理三种数据类型,分别是压缩数据(compressed data)、原始音频数据(raw audio data)、原始视频数据(raw video data),可以使用 ByteBuffer 处理这三种数据,也就是后文中提到的缓冲区,对于原始视频数据,可以使用 Surface 来提高编解码器性能,但是不能访问原始视频数据,但是可以通过 ImageReader 访问原始视频帧,通过 Image 进而获取到与之对应的 YUV 数据等其他信息。
压缩缓冲区:用于解码器的输入缓冲区和用于编码器的输出缓冲区会包含 MediaFormat 的 KEY_MIME 对应类型的压缩数据,对于视频类型,通常是单个压缩视频帧,对于音频数据,这通常是一个编码的音频段,通常包含几毫秒的音频,因格式类型而定。
原始音频缓冲区:原始音频缓冲区包含 PCM 音频数据的整个帧,这是每一个通道按照通道顺序的一个样本,每个 PCM 音频样本都是 16 位带符号整数或浮点数(以本机字节顺序),如果要使用浮点 PCM 编码的原始音频缓冲区,需要如下配置:
mediaFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_FLOAT);
检查 MediaFormat 中的浮点 PCM 的方法如下:
static boolean isPcmFloat(MediaFormat format) {
return format.getInteger(MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_16BIT)
== AudioFormat.ENCODING_PCM_FLOAT;
}
原始视频缓冲区:在 ByteBuffer 模式下,视频缓冲区根据其 MediaFormat 的 KEY_COLOR_FORMAT 设置的值进行布局,可以从通过 MediaCodecInfo 相关方法获取设备受支持的颜色格式,视频编解码器可能支持三种颜色格式:
- native raw video format:原始原始视频格式,由CodecCapabilities 的 COLOR_FormatSurface 常量标记,可以与输入或输出Surface一起使用。
- flexible YUV buffers:灵活的 YUV 缓冲区,如 CodecCapabilities 的 COLOR_FormatYUV420Flexible 常量对应的颜色格式,可以通过 getInput、OutputImage 等于与输入、输出 Surface 以及 ByteBuffer 模式一起使用。
- other specific formats:其他特定格式:通常仅在 ByteBuffer 模式下支持这些格式, 某些颜色格式是特定于供应商的,其他在均在 CodecCapabilities 中定义。
自 Android 5.1 开始,所有视频编解码器均支持灵活的 YUV 4:2:0 缓冲区。其中 MediaFormat#KEY_WIDTH 和 MediaFormat#KEY_HEIGHT 键指定视频帧的大小,在大多数情况下,视频仅占据视频帧的一部分,具体表示如下:

需要使用以下键从输出格式获取原始输出图像的裁剪矩形,如果输出格式中不存在这些键,则视频将占据整个视频帧,在使用任何 MediaFormat#KEY_ROTATION 之前,也就是在设置旋转之前,可以使用下面的方式计算视频帧的大小,参考如下:
MediaFormat format = decoder.getOutputFormat(…);
int width = format.getInteger(MediaFormat.KEY_WIDTH);
if (format.containsKey("crop-left") && format.containsKey("crop-right")) {
width = format.getInteger("crop-right") + 1 - format.getInteger("crop-left");
}
int height = format.getInteger(MediaFormat.KEY_HEIGHT);
if (format.containsKey("crop-top") && format.containsKey("crop-bottom")) {
height = format.getInteger("crop-bottom") + 1 - format.getInteger("crop-top");
}
MediaCodec编解码的流程
MediaCodec 首先获取一个空的输入缓冲区,填充要编码或解码的数据,再将填充数据的输入缓冲区送到 MediaCodec 进行处理,处理完数据后会释放这个填充数据的输入缓冲区,最后获取已经编码或解码的输出缓冲区,使用完毕后释放输出缓冲区,其编解码的流程示意图如下:

各个阶段对应的 API 如下:
// 获取可用的输入缓冲区的索引
public int dequeueInputBuffer (long timeoutUs)
// 获取输入缓冲区
public ByteBuffer getInputBuffer(int index)
// 将填满数据的inputBuffer提交到编码队列
public final void queueInputBuffer(int index,int offset, int size, long presentationTimeUs, int flags)
// 获取已成功编解码的输出缓冲区的索引
public final int dequeueOutputBuffer(BufferInfo info, long timeoutUs)
// 获取输出缓冲区
public ByteBuffer getOutputBuffer(int index)
// 释放输出缓冲区
public final void releaseOutputBuffer(int index, boolean render)
MediaCodec生命周期
MediaCodec 有三种状态,分别是执行(Executing)、停止(Stopped)和释放(Released),其中执行和停止分别有三个子状态,执行的三个字状态分别是 Flushed、Running 和 Stream-of-Stream,停止的三个子状态分别是 Uninitialized、Configured 和 Error,MediaCodec 生命周期示意图如下:

如上图所示,三种状态的切换都是由 start、stop、reset、release 等触发,根据 MediaCodec 处理数据方式的不同,其生命周期会略有不同,如在异步模式下 start 之后立即进入 Running 子状态,如果已经处于 Flushed 子状态,则需再次调用 start 进入 Running 子状态,下面是各个子状态切换对应的关键 API 如下:
- 停止状态(Stopped)
// 创建MediaCodec进入Uninitialized子状态
public static MediaCodec createByCodecName (String name)
public static MediaCodec createEncoderByType (String type)
public static MediaCodec createDecoderByType (String type)
// 配置MediaCodec进入Configured子状态,crypto和descrambler会在后文中进行说明
public void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags)
public void configure(MediaFormat format, @Nullable Surface surface,int flags, MediaDescrambler descrambler)
// Error
// 编解码过程中遇到错误进入Error子状态
- 执行状态(Executing)
// start之后立即进入Flushed子状态
public final void start()
// 第一个输入缓冲区出队的时候进入Running子状态
public int dequeueInputBuffer (long timeoutUs)
// 输入缓冲区与流结束标记排队时,编解码器将转换为End-of-Stream子状态
// 此时MediaCodec将不接受其他输入缓冲区,但会生成输出缓冲区
public void queueInputBuffer (int index, int offset, int size, long presentationTimeUs, int flags)
- 释放状态(Released)
// 编解码完成结束后释放MediaCodec进入释放状态(Released)
public void release ()
MediaCodec的创建
前面已经提到过当创建 MediaCodec 的时候进入Uninitialized 子状态,其创建方式如下:
// 创建MediaCodec
public static MediaCodec createByCodecName (String name)
public static MediaCodec createEncoderByType (String type)
public static MediaCodec createDecoderByType (String type)
MediaCodec初始化
创建 MediaCodec 之后进入 Uninitialized 子状态,此时需要对其进行一些设置如指定 MediaFormat。
如果使用的是异步处理数据的方式,在 configure 之前要设置 MediaCodec.Callback,关键 API 如下:
// 1. MediaFormat
// 创建MediaFormat
public static final MediaFormat createVideoFormat(String mime,int width,int height)
// 开启或关闭功能,具体参见MediaCodeInfo.CodecCapabilities
public void setFeatureEnabled(@NonNull String feature, boolean enabled)
// 参数设置
public final void setInteger(String name, int value)
// 2. setCallback
// 如果使用的是异步处理数据的方式,在configure 之前要设置 MediaCodec.Callback
public void setCallback (MediaCodec.Callback cb)
public void setCallback (MediaCodec.Callback cb, Handler handler)
// 3. 配置
public void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags)
public void configure(MediaFormat format, @Nullable Surface surface,int flags, MediaDescrambler descrambler)
上面 configure 配置中涉及到几个参数,其中 surface 表示解码器要渲染的 Surface,flags 则是指定当前编解码器是作为编码器还是解码器来使用的,crypto 和 descrambler 都和解密有关,比如某些 vip 视频就需要特定的密钥来配合解码,只有用户登录校验后才会对视频内容进行解密,要不然某些需要付费才能观看的视频下载之后就能随意传播了,更多细节可以查看音视频中的数字版权技术。
此外某些特定格式比如 AAC 音频以及 MPEG4、H.264、H.265 视频格式,这些格式包含一些用于 MediaCodec 的初始化特定的数据,当解码处理这些压缩格式时,必须在 start 之后且在任何帧数据处理之前将这些特定数据提交给 MediaCodec,即在对 queueInputBuffer 的调用中使用标志 BUFFER_FLAG_CODEC_CONFIG 标记此类数据,这些特定的数据也可以通过 MediaFormat 设置 ByteBuffer 的方式进行配置,如下
// csd-0、csd-1、csd-2同理
val bytes = byteArrayOf(0x00.toByte(), 0x01.toByte())
mediaFormat.setByteBuffer("csd-0", ByteBuffer.wrap(bytes))
其中 csd-0、csd-1 这些键可以从 MediaExtractor#getTrackFormat 获取的MediaFormat中获取,这些特定的数据会在start 时自动提交给 MediaCodec,无需直接提交此数据,如果在输出缓冲区或格式更改之前调用了 flush,则会丢失提交的特定数据,就需要在 queueInputBuffer 的调用中使用标志 BUFFER_FLAG_CODEC_CONFIG 标记这类数据。
Android 使用以下特定于编解码器的数据缓冲区,为了正确配置 MediaMuxer 轨道,还需要将它们设置为轨道格式,每个参数集和标有(*)的编解码器专用数据部分必须以“ \ x00 \ x00 \ x00 \ x01”的起始代码开头,参考如下:

编码器在收到这些信息后将会同样输出带有BUFFER_FLAG_CODEC_CONFIG标记的 outputbuffer,此时这些数据就是特定数据,不是媒体数据。
MediaCodec数据处理方式
每个创建已经创建的编解码器都维护一组输入缓冲区,有两种处理数据的方式,同步和异步方式,根据 API 版本不同有所区别,在 API 21 也就是从 Android5.0 开始,推荐使用 ButeBuffer 的方式进行数据的处理,在此之前只能使用 ButeBuffer 数组的方式进行数据的处理,如下:

MediaCodec,也就是编解码器的数据处理,主要是获取输入、输出缓冲区、提交数据给编解码器、释放输出缓冲区这几个过程,同步方式和异步方式的不同点在于输入缓冲区和输出缓冲区的其关键 API 如下:
// 获取输入缓冲区(同步)
public int dequeueInputBuffer (long timeoutUs)
public ByteBuffer getInputBuffer (int index)
// 获取输出缓冲区(同步)
public int dequeueOutputBuffer (MediaCodec.BufferInfo info, long timeoutUs)
public ByteBuffer getOutputBuffer (int index)
// 输入、输出缓冲区索引从MediaCodec.Callback的回调中获取,在获取对应的输入、输出缓冲区(异步)
public void setCallback (MediaCodec.Callback cb)
public void setCallback (MediaCodec.Callback cb, Handler handler)
// 提交数据
public void queueInputBuffer (int index, int offset, int size, long presentationTimeUs, int flags)
public void queueSecureInputBuffer (int index, int offset, MediaCodec.CryptoInfo info, long presentationTimeUs, int flags)
// 释放输出缓冲区
public void releaseOutputBuffer (int index, boolean render)
public void releaseOutputBuffer (int index, long renderTimestampNs)
同步处理模式
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
MediaFormat outputFormat = codec.getOutputFormat(); // option B
codec.start();
for (;;) {
int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(…);
// 使用有效数据填充输入缓冲区
…
codec.queueInputBuffer(inputBufferId, …);
}
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat与outputFormat是相同的
// 输出缓冲区已准备后被处理或渲染了
…
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 输出格式改变,后续采用新格式,此时使用getOutputFormat()获取新格式
// 如果使用getOutputFormat(outputBufferId)获取特定缓冲区的格式,则无需监听格式变化
outputFormat = codec.getOutputFormat(); // option B
}
}
codec.stop();
codec.release();
异步处理模式
只要设置callback,就等于使用了异步模式,此时一个线程queue数据,而这个回调可以再单独设置一个线程托管。我们在回调onOutputBufferAvailable中就可以得到编码好的数据。
val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC
val encoder = MediaCodec.createEncoderByType(encodeCodecName)
encoder.setCallback(object: MediaCodec.Callback(){
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
//
}
override fun onOutputBufferAvailable(
codec: MediaCodec,
index: Int,
info: MediaCodec.BufferInfo
) {
//
}
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
//
}
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
//
}
})
MediaCodec类中的setCallback()方法用于设置一个回调接口,这个接口将在编解码操作的各个阶段被调用。这个方法接收一个MediaCodec.Callback对象作为参数。
MediaCodec.Callback是一个抽象类,它定义了四个方法:
- onInputBufferAvailable(MediaCodec codec, int index):当输入缓冲区可用时,此方法被调用。参数index指示了哪个输入缓冲区已经变得可用。
- onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info):当输出缓冲区可用时,此方法被调用。参数index指示了哪个输出缓冲区已经变得可用,info包含了关于这个缓冲区的元数据,如其包含的数据的大小,时间戳等。
- onError(MediaCodec codec, MediaCodec.CodecException e):当编解码器发生错误时,此方法被调用。参数e是一个MediaCodec.CodecException对象,包含了关于错误的详细信息。
- onOutputFormatChanged(MediaCodec codec, MediaFormat format):当输出格式发生变化时,此方法被调用。参数format是一个MediaFormat对象,包含了新的输出格式。
- muxer 何时启动
在启动 muxer 之前我们需要明确知道 output format 的信息。
在使用MediaCodec进行编码时,onOutputFormatChanged 方法会在开始编码后首次调用。这是因为在开始编码后,MediaCodec 会根据你设置的参数(如分辨率、比特率等)来确定最终的输出格式。一旦输出格式确定,就会触发onOutputFormatChanged方法。
这个方法的调用表示编码器的输出格式已经准备好,你可以获取到这个新的输出格式,并用它来配置你的MediaMuxer。这是必要的,因为MediaMuxer需要知道它正在混合的音频和视频的具体格式。
基于上述原因,在异步模式下我们可以在 onOutputFormatChanged 回调函数中启动 muxer:
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
videoTrackIndex = muxer.addTrack(format)
muxer.start()
}
- 循环地编码视频帧
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
val pts = computePresentationTime(generateIndex)
// input eos
if(generateIndex == NUM_FRAMES)
{
codec.queueInputBuffer(index, 0, 0, pts, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
}else
{
val frameData = ByteArray(videoWidth * videoHeight * 3 / 2)
generateFrame(generateIndex, codec.inputFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT), frameData)
val inputBuffer = codec.getInputBuffer(index)
inputBuffer.put(frameData)
codec.queueInputBuffer(index, 0, frameData.size, pts, 0)
generateIndex++
}
}
override fun onOutputBufferAvailable(
codec: MediaCodec,
index: Int,
info: MediaCodec.BufferInfo
) {
// output eos
val isDone = (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0
if(isDone)
{
outputEnd.set(true)
info.size = 0
}
if(info.size > 0){
val encodedData = codec.getOutputBuffer(index)
muxer.writeSampleData(videoTrackIndex, encodedData!!, info)
codec.releaseOutputBuffer(index, false)
}
}
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
e.printStackTrace()
}
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
//...
}
- val pts = computePresentationTime(generateIndex):这行代码计算了当前帧的显示时间,通常是根据帧率和当前帧的索引来计算的。
- if(generateIndex == NUM_FRAMES):这行代码检查是否已经处理完所有的帧。如果是,那么就需要向编码器发送一个表示输入结束的标志。
- codec.queueInputBuffer(index, 0, 0, pts, MediaCodec.BUFFER_FLAG_END_OF_STREAM):这行代码向编码器的输入队列中添加一个空的缓冲区,并设置了一个表示输入结束的标志。这告诉编码器不会有更多的数据输入了。
- val frameData = ByteArray(videoWidth * videoHeight * 3 / 2):这行代码创建了一个字节数组,用于存储一帧的数据。这里假设的是YUV420格式的数据,所以大小是宽度乘以高度的1.5倍。
- generateFrame(generateIndex, codec.inputFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT), frameData):这行代码生成了一帧的数据。
- val inputBuffer = codec.getInputBuffer(index):这行代码获取了编码器的一个输入缓冲区。
- inputBuffer.put(frameData):这行代码将生成的帧数据放入输入缓冲区。
- codec.queueInputBuffer(index, 0, frameData.size, pts, 0):这行代码将填充了数据的输入缓冲区添加到编码器的输入队列中。
- generateIndex++:这行代码将帧的索引加一,准备处理下一帧的数据。
onOutputBufferAvailable 回调逻辑:
- val isDone = (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0:这行代码检查编码器是否已经处理完所有的输入数据并生成了所有的输出数据。如果是,那么isDone会被设置为true。
- if(isDone) {…}:这个if语句检查是否已经完成了所有的编码工作。如果是,那么就设置outputEnd为true,表示输出结束,并将info.size设置为0,表示没有更多的输出数据。
- if(info.size > 0){…}:这个if语句检查是否有输出数据。如果有,那么就处理这些数据。
- val encodedData = codec.getOutputBuffer(index):这行代码获取了编码器的一个输出缓冲区,这个缓冲区包含了编码后的数据。
- muxer.writeSampleData(videoTrackIndex, encodedData!!, info):这行代码将编码后的数据写入到媒体混合器中。这里的videoTrackIndex是视频轨道的索引,encodedData是编码后的数据,info包含了这些数据的元信息,如显示时间、大小等。
- codec.releaseOutputBuffer(index, false):这行代码释放了编码器的输出缓冲区,让编码器可以继续使用这个缓冲区来存储新的输出数据。这里的false表示不需要将这个缓冲区的数据显示出来,因为我们是在编码数据,而不是播放数据。
opengl编码数据直接给到MediaCodec的方式
必须调用MediaCodec的 createInputSurface()方法,拿出MediaCodec内部的Surface,这个Surface用于接收视频帧数据,具体操作就是以这个Surface为画布,创建一个opengl环境,在这个opengl环境中做任何绘制都等于画出了视频画面,别忘了调用eglSwapBuffers,这时就可以阻塞读取MediaCodec,读出的数据就是编码好的视频流。此时可以使用同步方式,也可以使用异步方式。
如果还有预览的需求,此时可以在一个线程中创建两个opengl环境,一个使用屏幕Surface,一个就是上面的MediaCodec的Surface,然后通过eglMakeCurrent切换环境进行两次绘制,同时进行预览和录像。
编码结束
当要处理的数据结束时(End-of-stream),需要标记流的结束,可以在最后一个有效的输入缓冲区上使用 queueInputBuffer 提交数据的时候指定 flags 为 BUFFER_FLAG_END_OF_STREAM 标记其结束,也可以在最后一个有效输入缓冲区之后提交一个空的设置了流结束标志的输入缓冲区来标记其结束,此时不能够再提交输入缓冲区,除非编解码器被 flush、stop、restart,输出缓冲区继续返回直到最终通过在 dequeueOutputBuffer 或通过 Callback#onOutputBufferAvailable 返回的 BufferInfo 中指定相同的流结束标志,最终通知输出流结束为止。
如果使用了一个输入 Surface 作为编解码器的输入,此时没有可访问的输入缓冲区,输入缓冲区会自动从这个 Surface 提交给编解码器,相当于省略了输入的这个过程,这个输入 Surface 可由 createInputSurface 方法创建,此时调用 signalEndOfInputStream 将发送流结束的信号,调用后,输入表面将立即停止向编解码器提交数据,关键 API 如下:
// 创建输入Surface,需在configure之后、start之前调用
public Surface createInputSurface ()
// 设置输入Surface
public void setInputSurface (Surface surface)
// 发送流结束的信号
public void signalEndOfInputStream ()
同理如果使用了输出 Surface,则与之相关的输出缓冲区的相关功能将会被代替,可以通过 setOutputSurface 设置一个 Surface 作为编解码器的输出,可以选择是否在输出 Surface 上渲染每一个输出缓冲区,关键 API 如下:
// 设置输出Surface
public void setOutputSurface (Surface surface)
// false表示不渲染这个buffer,true表示使用默认的时间戳渲染这个buffer
public void releaseOutputBuffer (int index, boolean render)
// 使用指定的时间戳渲染这个buffer
public void releaseOutputBuffer (int index, long renderTimestampNs)
MediaCodec的异常处理
关于 MediaCodec 使用过程中的异常处理,这里提一下 CodecException 异常,一般是由编解码器内部异常导致的,比如媒体内容损坏、硬件故障、资源耗尽等,可以通过如下方法判断以做进一步的处理:
// true表示可以通过stop、configure、start来恢复
public boolean isRecoverable ()
// true表示暂时性问题,编码或解码操作会在后续重试进行
public boolean isTransient ()
如果 isRecoverable 和 isTransient 都是返回 false,则需要通过 reset 或 release 操作释放资源后重新工作,两者不可能同时返回 true。关于 MediaCodec 的介绍到此为止。