引言:你以为简单,其实暗流涌动
MediaCodec 是 Android 视频开发绕不开的核心类。初学者往往觉得它"挺简单的"——创建一个、配置一下、喂数据、取输出——然后在第一个 IllegalStateException 面前愣住,接着花一下午研究为什么 Buffer 一直取不到,最后发现自己把 releaseOutputBuffer 写在了错误的地方……
MediaCodec 设计于 Android 4.1,是 Android 第一个直接暴露硬件加速编解码能力的公开 API。它的设计并不复杂,但有几个核心概念必须真正理解,否则代码跑得通但性能一塌糊涂,或者在特定设备上莫名崩溃。
本文把 MediaCodec 从状态机讲到 Buffer 队列,从同步模式讲到异步模式,再到 Surface 零拷贝的精髓,最后用两个实战案例(视频转码 + 实时编码)把所有知识串起来。
一、架构定位:MediaCodec 在哪一层?
App
│ java: android.media.MediaCodec
│
JNI (android_media_MediaCodec.cpp)
│
├── ACodec(OMX 通道,旧路径,Android 10 前主流)
│ └── OMXNodeInstance → libstagefright_omx.so
│
└── CCodec(Codec 2.0 通道,Android 10+ 主流)
└── C2Component → Vendor Codec2 HAL
└── 硬件编解码器(高通 MSM / MTK Vcodec / Google Tensor)
MediaCodec 是一个门面(Facade),底层实际由两条路径支撑:旧 OMX(ACodec) 和新 Codec2(CCodec)。Android 10 开始,Codec2 成为主推路径,但对应用层完全透明——你写的 MediaCodec 代码不需要感知底层走哪条路。
关键知识点:硬件编解码器(HW)优先于软件(SW)。MediaCodec.createEncoderByType("video/avc") 默认返回硬件 H.264 编码器;如果你非要软件,用 createByCodecName("OMX.google.h264.encoder"),但通常不该这么做。
二、状态机:MediaCodec 的生命周期
MediaCodec 有一套严格的状态机,在错误状态调用方法必然抛 IllegalStateException。把它记住,能省掉大量调试时间。
2.1 状态迁移的关键约束
| 当前状态 | 允许的操作 |
|---|---|
| Initialized | configure() |
| Configured | start()、setInputSurface()(编码器) |
| Executing | dequeueInputBuffer()、queueInputBuffer()、dequeueOutputBuffer()、releaseOutputBuffer()、flush()、stop() |
| Stopped | reset()(回到 Initialized)、release() |
| Any | release()(任意状态均可释放) |
flush() vs reset():
flush():清空所有 Buffer,留在 Executing 状态,适合 seek 后重新开始喂数据reset():回到 Initialized,需要重新 configure/start,适合格式变更
三、MediaFormat:把参数配对配全
MediaFormat 是 MediaCodec.configure() 的核心参数,配错了轻则编码质量差,重则直接报错。
3.1 视频编码参数
MediaFormat format = MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_AVC, // "video/avc",即 H.264
1920, 1080 // 宽 × 高
);
// 必填参数
format.setInteger(MediaFormat.KEY_BIT_RATE, 8_000_000); // 码率:8 Mbps
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); // 帧率:30fps
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // I帧间隔:每秒一个关键帧
// 直播建议 0.5,录像建议 1~2
// 颜色格式(Surface 输入时不需要手动设)
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); // Surface 输入
// 或 COLOR_FormatYUV420Flexible(ByteBuffer 输入时用)
// 可选但重要的参数
format.setInteger(MediaFormat.KEY_PROFILE, // 编码 Profile
MediaCodecInfo.CodecProfileLevel.AVCProfileHigh); // High Profile
format.setInteger(MediaFormat.KEY_LEVEL,
MediaCodecInfo.CodecProfileLevel.AVCLevel41); // Level 4.1(1080p@30fps)
// 码率控制模式(Android 8+)
format.setInteger(MediaFormat.KEY_BITRATE_MODE,
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR); // 可变码率
// BITRATE_MODE_CBR:恒定码率(直播/实时通话)
// BITRATE_MODE_CQ:恒定质量(不控码率,看画质)
3.2 视频解码参数
// 解码时 MediaFormat 来自 MediaExtractor,通常不需要手动构造
// 但如果手动构建,至少需要:
MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
// CSD(Codec-Specific Data):SPS/PPS,H.264 解码器必须有
format.setByteBuffer("csd-0", ByteBuffer.wrap(sps)); // Sequence Parameter Set
format.setByteBuffer("csd-1", ByteBuffer.wrap(pps)); // Picture Parameter Set
// H.265/HEVC
format.setByteBuffer("csd-0", ByteBuffer.wrap(vpsSpsPps)); // VPS+SPS+PPS 合并
// VP9 / AV1 无需 CSD,直接解码
3.3 常见参数错误
// ❌ 错误:Surface 输入时还设置了 COLOR_FormatYUV420Flexible
// 这两种模式互斥,用 Surface 就别设颜色格式
format.setInteger(KEY_COLOR_FORMAT, COLOR_FormatYUV420Flexible); // 多余且可能报错
// ❌ 错误:I 帧间隔设为 0
format.setInteger(KEY_I_FRAME_INTERVAL, 0);
// 0 在某些厂商 ROM 上表示"无限间隔"(全 P 帧),播放器无法 seek
// 如果真的需要每帧都是关键帧(如截屏录制),应该每帧手动触发关键帧:
Bundle params = new Bundle();
params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
encoder.setParameters(params);
// ❌ 错误:码率单位误用(填成 Kbps 而非 bps)
format.setInteger(KEY_BIT_RATE, 8000); // 8000 bps = 极低质量!
format.setInteger(KEY_BIT_RATE, 8_000_000); // ✅ 8 Mbps
四、同步模式:最直接的用法
同步模式是最经典的 MediaCodec 用法——通过 dequeueInputBuffer / dequeueOutputBuffer 主动轮询:
4.1 完整的同步编码流程
MediaCodec encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
MediaFormat format = buildVideoFormat(); // 见上节
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
boolean inputDone = false;
boolean outputDone = false;
while (!outputDone) {
// ─── 喂输入 ───
if (!inputDone) {
// 超时 10ms,-1 表示无限等待,0 表示立即返回
int inputIndex = encoder.dequeueInputBuffer(10_000);
if (inputIndex >= 0) {
ByteBuffer inputBuffer = encoder.getInputBuffer(inputIndex);
inputBuffer.clear();
// 填入一帧 YUV 数据
int sampleSize = fillYuvFrame(inputBuffer);
if (sampleSize < 0) {
// 没有更多数据了:发送 EOS
encoder.queueInputBuffer(inputIndex, 0, 0,
0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputDone = true;
} else {
// 提交数据,presentationTimeUs 是显示时间戳(微秒)
long pts = computePresentationTimeUs(frameIndex++);
encoder.queueInputBuffer(inputIndex, 0, sampleSize, pts, 0);
}
}
}
// ─── 取输出 ───
int outputIndex = encoder.dequeueOutputBuffer(bufferInfo, 10_000);
switch (outputIndex) {
case MediaCodec.INFO_TRY_AGAIN_LATER:
// 没有输出,继续循环
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
// 输出格式确定了(第一次)
// 编码器:可以在此获取 SPS/PPS(用于 MP4 容器)
MediaFormat newFormat = encoder.getOutputFormat();
muxer.setTrackFormat(videoTrack, newFormat);
break;
default:
if (outputIndex >= 0) {
ByteBuffer outputBuffer = encoder.getOutputBuffer(outputIndex);
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
// SPS/PPS 配置数据,写入 muxer 但不计入时长
bufferInfo.size = 0;
}
if (bufferInfo.size > 0) {
outputBuffer.position(bufferInfo.offset);
outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
muxer.writeSampleData(videoTrack, outputBuffer, bufferInfo);
}
// 必须释放,否则 Buffer 不归还队列,很快就没有 Buffer 可用
encoder.releaseOutputBuffer(outputIndex, false);
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
outputDone = true;
}
}
}
}
encoder.stop();
encoder.release();
最常见的 Bug:忘记调用 releaseOutputBuffer()
输出 Buffer 消费完后必须立即归还。如果忘记调用,Buffer 池会很快耗尽,后续的 dequeueOutputBuffer 永远返回 INFO_TRY_AGAIN_LATER,编码线程陷入假死。
五、异步模式:更现代的写法(推荐)
Android 5.0 引入了异步模式,通过 setCallback() 注册回调,无需轮询,逻辑更清晰,CPU 占用更低:
encoder.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
// 有空闲的输入 Buffer 可以使用,在此填充数据
ByteBuffer buffer = codec.getInputBuffer(index);
buffer.clear();
int size = fillYuvFrame(buffer);
if (size < 0) {
codec.queueInputBuffer(index, 0, 0, 0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
codec.queueInputBuffer(index, 0, size,
computePts(mFrameIndex++), 0);
}
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index,
MediaCodec.BufferInfo info) {
// 有已编码的输出 Buffer 可以消费
ByteBuffer buffer = codec.getOutputBuffer(index);
if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0
&& info.size > 0) {
buffer.position(info.offset);
buffer.limit(info.offset + info.size);
mMuxer.writeSampleData(mVideoTrack, buffer, info);
}
// 必须释放!
codec.releaseOutputBuffer(index, false);
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
onEncodingComplete();
}
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
// 格式确定,可以初始化 muxer
mVideoTrack = mMuxer.addTrack(format);
mMuxer.start();
}
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
Log.e(TAG, "编码器错误: " + e.getMessage());
// 通常需要重新初始化
}
});
// setCallback 必须在 configure 之前调用!
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();
5.1 异步模式的注意事项
回调线程不是主线程:onInputBufferAvailable 和 onOutputBufferAvailable 在 Codec 内部线程回调,禁止在此做耗时操作(如文件 IO),否则会阻塞整个 Codec Pipeline,导致帧率下降。
正确做法:回调中只做轻量操作,或把数据投递到生产者消费者队列由另一线程处理:
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index,
MediaCodec.BufferInfo info) {
// ✅ 轻量:复制数据到 ByteBuffer 再投递给 IO 线程
ByteBuffer copy = ByteBuffer.allocate(info.size);
ByteBuffer src = codec.getOutputBuffer(index);
src.position(info.offset).limit(info.offset + info.size);
copy.put(src);
copy.flip();
codec.releaseOutputBuffer(index, false); // 尽快释放
mIoExecutor.submit(() -> mMuxer.writeSampleData(mTrack, copy, info));
// ❌ 错误:在回调中直接做耗时 IO
// mMuxer.writeSampleData(...); // 持有 Codec 内部锁,会阻塞 Pipeline
}
六、Surface 模式:零拷贝的精髓
这是 MediaCodec 最重要的性能优化——通过 Surface 完全绕过 CPU 拷贝。
6.1 编码器:createInputSurface()
当视频来源是 Camera2、OpenGL ES 渲染、或屏幕录制时,可以用 createInputSurface() 创建一个 Surface 直接接收帧数据:
// 配置 Surface 输入模式(注意颜色格式必须是 COLOR_FormatSurface)
MediaFormat format = MediaFormat.createVideoFormat(MIMETYPE_VIDEO_AVC, width, height);
format.setInteger(KEY_COLOR_FORMAT, COLOR_FormatSurface); // ← 关键!
format.setInteger(KEY_BIT_RATE, 8_000_000);
format.setInteger(KEY_FRAME_RATE, 30);
format.setInteger(KEY_I_FRAME_INTERVAL, 1);
encoder.configure(format, null, null, CONFIGURE_FLAG_ENCODE);
// start() 之前获取 InputSurface
Surface inputSurface = encoder.createInputSurface();
encoder.start();
// 把 inputSurface 传给 Camera2 Session
// 这样相机拍到的每一帧直接进编码器,不经过 CPU
device.createCaptureSession(Arrays.asList(previewSurface, inputSurface), ...);
数据流:Sensor → ISP → Gralloc Buffer → EGL → Encoder,全程 GPU/ISP 搬运,CPU 只负责控制流。这在 4K 录像时差异最明显——CPU 占用从 40% 降到个位数。
6.2 解码器:Surface 输出
解码时把 Surface 传给 configure(),解码器直接渲染到 Surface 而无需 CPU 读回:
// 拿到 SurfaceView 的 Surface
Surface outputSurface = surfaceHolder.getSurface();
decoder.configure(format, outputSurface, null, 0); // ← 第二个参数是 Surface
decoder.start();
// 释放 OutputBuffer 时传 true:立即渲染到 Surface
decoder.releaseOutputBuffer(outputIndex, true); // ← true 表示渲染
// 如果传 false:不渲染,只归还 Buffer(用于纯解码不需显示的场景)
// Android 5.0+:支持指定渲染时间戳(用于音视频同步)
decoder.releaseOutputBuffer(outputIndex, renderTimeNs); // 纳秒精度
什么时候不能用 Surface 输出?
当你需要对解码后的 YUV 数据做处理时(人脸检测、滤镜、截图),必须用 ByteBuffer 模式(不传 Surface)才能在 CPU 侧读到像素数据。但要注意:ByteBuffer 读取涉及从 GPU 到 CPU 的数据搬运,开销很大,高帧率场景会成为瓶颈。
七、EOS 处理:正确结束最重要
EOS(End-of-Stream)处理不对,轻则最后几帧丢失,重则程序挂起。
7.1 编码器 EOS
// 方法一:通过最后一次 queueInputBuffer 发送 EOS 标志
encoder.queueInputBuffer(inputIndex, 0, 0, 0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
// 方法二(Surface 模式):调用 signalEndOfInputStream()
encoder.signalEndOfInputStream(); // 告知编码器不会再有新帧
// 之后仍需继续消费 OutputBuffer,直到收到带 EOS 标志的输出
// 如果不消费完就 stop(),编码器内部可能还有几帧没有输出,导致视频结尾丢帧
while (!outputDone) {
int idx = encoder.dequeueOutputBuffer(info, 10_000);
if (idx >= 0) {
encoder.releaseOutputBuffer(idx, false);
if ((info.flags & BUFFER_FLAG_END_OF_STREAM) != 0) {
outputDone = true;
}
}
}
7.2 解码器 EOS
// 当 MediaExtractor 读到文件末尾时(readSampleData 返回 -1):
decoder.queueInputBuffer(inputIndex, 0, 0, 0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
// 解码器会把所有缓存的帧都输出后,才在最后一帧的 BufferInfo.flags 中设置 EOS
// 必须一直消费到这个 EOS 帧,才表示解码完全结束
7.3 flush() 后的陷阱
调用 flush() 后(如 seek 操作),下一个 queueInputBuffer 必须带 BUFFER_FLAG_CODEC_CONFIG 或关键帧,否则解码器找不到参考帧,输出绿屏或花屏:
decoder.flush();
// flush 之后:
// 1. 重新送入 SPS/PPS(对 H.264)
decoder.queueInputBuffer(inputIdx, 0, spsData.length, 0,
MediaCodec.BUFFER_FLAG_CODEC_CONFIG);
// 2. 从关键帧(I帧)位置开始送数据
extractor.seekTo(seekTimeUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
八、实战案例一:视频转码(H.264 → H.265)
将一个 H.264 视频文件转码为 H.265(HEVC),可以在不改变分辨率的前提下将文件大小压缩约 40%:
public class VideoTranscoder {
public void transcode(String inputPath, String outputPath) throws Exception {
// 1. 初始化 Extractor(解封装)
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(inputPath);
int videoTrackIndex = selectVideoTrack(extractor);
extractor.selectTrack(videoTrackIndex);
MediaFormat inputFormat = extractor.getTrackFormat(videoTrackIndex);
// 2. 初始化 Decoder(H.264 解码)
MediaCodec decoder = MediaCodec.createDecoderByType(
inputFormat.getString(MediaFormat.KEY_MIME));
// 3. 初始化 Encoder(H.265 编码)
int width = inputFormat.getInteger(MediaFormat.KEY_WIDTH);
int height = inputFormat.getInteger(MediaFormat.KEY_HEIGHT);
MediaFormat encFormat = MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_HEVC, width, height);
encFormat.setInteger(MediaFormat.KEY_BIT_RATE,
(int)(inputFormat.getInteger(MediaFormat.KEY_BIT_RATE) * 0.6)); // 60% 码率
encFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
encFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
encFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
MediaCodec encoder = MediaCodec.createEncoderByType(
MediaFormat.MIMETYPE_VIDEO_HEVC);
encoder.configure(encFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface decoderOutputSurface = encoder.createInputSurface();
// 4. 解码器配置:输出到 Surface(零拷贝接 Encoder 输入)
decoder.configure(inputFormat, decoderOutputSurface, null, 0);
// 5. 初始化 Muxer(封装为 MP4)
MediaMuxer muxer = new MediaMuxer(outputPath,
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
encoder.start();
decoder.start();
// 6. 转码循环(实际项目应该在子线程中运行)
runTranscodeLoop(extractor, decoder, encoder, muxer);
// 7. 清理
decoder.stop(); decoder.release();
encoder.stop(); encoder.release();
extractor.release(); muxer.stop(); muxer.release();
}
private void runTranscodeLoop(MediaExtractor extractor, MediaCodec decoder,
MediaCodec encoder, MediaMuxer muxer) {
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
int muxTrackIndex = -1;
boolean inputDone = false, decodeDone = false, encodeDone = false;
while (!encodeDone) {
// ── 喂给 Decoder ──
if (!inputDone) {
int idx = decoder.dequeueInputBuffer(10_000);
if (idx >= 0) {
ByteBuffer buf = decoder.getInputBuffer(idx);
int size = extractor.readSampleData(buf, 0);
if (size < 0) {
decoder.queueInputBuffer(idx, 0, 0, 0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputDone = true;
} else {
decoder.queueInputBuffer(idx, 0, size,
extractor.getSampleTime(), 0);
extractor.advance();
}
}
}
// ── Decoder 输出(渲染到 Surface = Encoder 的 InputSurface)──
if (!decodeDone) {
int idx = decoder.dequeueOutputBuffer(info, 10_000);
if (idx >= 0) {
// render=true:解码帧直接送入 Encoder 的 InputSurface
decoder.releaseOutputBuffer(idx, true);
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
encoder.signalEndOfInputStream();
decodeDone = true;
}
}
}
// ── 从 Encoder 取输出 ──
int idx = encoder.dequeueOutputBuffer(info, 10_000);
if (idx == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
muxTrackIndex = muxer.addTrack(encoder.getOutputFormat());
muxer.start();
} else if (idx >= 0) {
if (muxTrackIndex >= 0 && info.size > 0
&& (info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
muxer.writeSampleData(muxTrackIndex,
encoder.getOutputBuffer(idx), info);
}
encoder.releaseOutputBuffer(idx, false);
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
encodeDone = true;
}
}
}
}
}
为什么用 Surface 连接 Decoder 和 Encoder?
这是转码性能的关键。Decoder 的输出通过 Surface(EGL Image)直接传给 Encoder 的 InputSurface,整个过程数据只在 GPU/VPU 内存中流转,不经过任何 CPU 内存拷贝。实测 1080p 转码在低端设备上也能跑到 3~4 倍速。
九、实战案例二:实时视频编码(屏幕录制)
屏幕录制是 MediaCodec 最典型的实时编码场景:
public class ScreenRecorder {
private MediaCodec mEncoder;
private MediaMuxer mMuxer;
private Surface mInputSurface;
private VirtualDisplay mVirtualDisplay;
private int mVideoTrackIndex = -1;
private boolean mMuxerStarted = false;
public void startRecording(MediaProjection projection, String outputPath,
int width, int height) throws IOException {
// 1. 创建编码器
MediaFormat format = MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
format.setInteger(KEY_COLOR_FORMAT, COLOR_FormatSurface);
format.setInteger(KEY_BIT_RATE, computeBitrate(width, height));
format.setInteger(KEY_FRAME_RATE, 30);
format.setInteger(KEY_I_FRAME_INTERVAL, 2);
mEncoder = MediaCodec.createEncoderByType(MIMETYPE_VIDEO_AVC);
// 使用异步模式
mEncoder.setCallback(new EncoderCallback(), mBackgroundHandler);
mEncoder.configure(format, null, null, CONFIGURE_FLAG_ENCODE);
// 2. 获取 InputSurface,MediaProjection 会把屏幕内容渲染到这个 Surface
mInputSurface = mEncoder.createInputSurface();
mEncoder.start();
// 3. 创建 VirtualDisplay:把屏幕内容输出到 mInputSurface
mVirtualDisplay = projection.createVirtualDisplay("ScreenRecorder",
width, height, dpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mInputSurface, // ← 直接传入 Encoder InputSurface
null, null
);
// 4. 初始化 Muxer
mMuxer = new MediaMuxer(outputPath, MUXER_OUTPUT_MPEG_4);
}
private class EncoderCallback extends MediaCodec.Callback {
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
// Surface 模式:屏幕内容自动填充到 InputSurface
// 不需要手动操作 InputBuffer
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index,
MediaCodec.BufferInfo info) {
ByteBuffer outputBuffer = codec.getOutputBuffer(index);
if ((info.flags & BUFFER_FLAG_CODEC_CONFIG) != 0) {
info.size = 0; // SPS/PPS 已通过 getOutputFormat 获取,不写入 muxer
}
if (info.size > 0 && mMuxerStarted) {
outputBuffer.position(info.offset);
outputBuffer.limit(info.offset + info.size);
mMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, info);
}
codec.releaseOutputBuffer(index, false);
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
mVideoTrackIndex = mMuxer.addTrack(format);
mMuxer.start();
mMuxerStarted = true;
}
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
Log.e(TAG, "编码错误,尝试重启", e);
}
}
private int computeBitrate(int width, int height) {
// 经验公式:宽 × 高 × 帧率 × BPP
// 1080p@30fps 约 8Mbps,720p@30fps 约 4Mbps
return (int)(width * height * 30 * 0.1);
}
public void stopRecording() {
mVirtualDisplay.release();
mEncoder.signalEndOfInputStream(); // 发送 EOS
// 等待 EOS 输出后(在 onOutputBufferAvailable 中检测)再 stop
}
}
9.1 实时编码的性能调优
降低编码延迟(实时推流场景):
// Android 12+ 低延迟模式
format.setInteger(MediaFormat.KEY_LATENCY, 0); // 0 = 最低延迟
// 减小 I 帧间隔(代价是码率波动增大)
format.setInteger(KEY_I_FRAME_INTERVAL, 0); // 每帧都是关键帧(极端情况)
// 关闭 B 帧(B 帧需要先看未来帧才能编码,天然引入延迟)
format.setInteger("vendor.rtc-ext-enc-low-latency", 1); // Qualcomm 扩展参数
// CBR 模式(码率稳定,便于网络传输)
format.setInteger(KEY_BITRATE_MODE, BITRATE_MODE_CBR);
温度控制(长时间录像):
// 监听 Thermal 状态,动态调整码率
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
// Android 10+ ThermalStatusListener
pm.addThermalStatusListener(executor, status -> {
if (status >= PowerManager.THERMAL_STATUS_MODERATE) {
// 降低码率 30%,避免过热降频导致丢帧
Bundle params = new Bundle();
params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE,
currentBitrate * 7 / 10);
mEncoder.setParameters(params);
}
});
十、调试工具
# 列出设备支持的所有编解码器
adb shell dumpsys media.codec
# 查看编解码器详细能力(格式、分辨率、帧率范围)
adb shell dumpsys media.codec | grep -A 20 "video/avc"
# 实时查看 MediaCodec 状态
adb logcat -s MediaCodec:V ACodec:V CCodec:V
# 抓取 Codec Pipeline 的 Perfetto trace
adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/codec.pftrace <<EOF
buffers: { size_kb: 32768 }
data_sources: { config { name: "android.media" } }
data_sources: { config { name: "linux.ftrace"
ftrace_config { ftrace_events: "media/*" }
} }
duration_ms: 10000
EOF
# 检查编解码器是否支持某种格式
# 代码层面:
MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS);
for (MediaCodecInfo info : list.getCodecInfos()) {
if (info.isEncoder() && info.getName().contains("hevc")) {
MediaCodecInfo.CodecCapabilities caps =
info.getCapabilitiesForType("video/hevc");
// 检查支持的分辨率、帧率、Profile 等
Log.d(TAG, "支持的分辨率范围: " + caps.getVideoCapabilities().getSupportedWidths());
}
}
总结
MediaCodec 的核心设计就两件事:状态机管理生命周期 + Buffer 队列异步传递数据。把这两件事搞清楚,剩下的细节都是具体配置。
本文核心要点回顾:
- 状态机:
Uninitialized → Initialized → Configured → Executing → Stopped,任何状态误操作都是IllegalStateException;flush()留在 Executing,reset()回到 Initialized - MediaFormat:I 帧间隔单位是秒、码率单位是 bps(不是 Kbps)、Surface 输入必须设
COLOR_FormatSurface - 同步 vs 异步:同步模式简单直接;异步模式(
setCallback)CPU 友好,但回调线程禁止耗时操作 - Surface 零拷贝:编码器用
createInputSurface(),解码器releaseOutputBuffer(index, true),GPU 全程搬运,拒绝 CPU 拷贝 - EOS 处理:必须消费完所有输出 Buffer 再
stop(),flush()后要从关键帧重新喂数据 - 实时编码调优:低延迟用
KEY_LATENCY=0+ CBR + 减小 I 帧间隔;长时间录像要监听 Thermal 动态调整码率
下一篇,我们深入 MediaCodec 背后的硬件通道:OMX 与 Codec2 框架——看看那些"vendor.xxx.ext"参数到底是什么、硬件编解码器是如何被加载和调度的。