MediaCodec 编解码基础:Buffer 队列、状态机与零拷贝的艺术

引言:你以为简单,其实暗流涌动

MediaCodec 是 Android 视频开发绕不开的核心类。初学者往往觉得它"挺简单的"------创建一个、配置一下、喂数据、取输出------然后在第一个 IllegalStateException 面前愣住,接着花一下午研究为什么 Buffer 一直取不到,最后发现自己把 releaseOutputBuffer 写在了错误的地方......

MediaCodec 设计于 Android 4.1,是 Android 第一个直接暴露硬件加速编解码能力的公开 API。它的设计并不复杂,但有几个核心概念必须真正理解,否则代码跑得通但性能一塌糊涂,或者在特定设备上莫名崩溃。

本文把 MediaCodec 从状态机讲到 Buffer 队列,从同步模式讲到异步模式,再到 Surface 零拷贝的精髓,最后用两个实战案例(视频转码 + 实时编码)把所有知识串起来。


一、架构定位:MediaCodec 在哪一层?

yaml 复制代码
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:把参数配对配全

MediaFormatMediaCodec.configure() 的核心参数,配错了轻则编码质量差,重则直接报错。

3.1 视频编码参数

java 复制代码
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 视频解码参数

java 复制代码
// 解码时 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 常见参数错误

java 复制代码
// ❌ 错误: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 完整的同步编码流程

java 复制代码
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 占用更低:

java 复制代码
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 异步模式的注意事项

回调线程不是主线程onInputBufferAvailableonOutputBufferAvailable 在 Codec 内部线程回调,禁止在此做耗时操作(如文件 IO),否则会阻塞整个 Codec Pipeline,导致帧率下降。

正确做法:回调中只做轻量操作,或把数据投递到生产者消费者队列由另一线程处理:

java 复制代码
@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 直接接收帧数据:

java 复制代码
// 配置 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 读回:

java 复制代码
// 拿到 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

java 复制代码
// 方法一:通过最后一次 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

java 复制代码
// 当 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 或关键帧,否则解码器找不到参考帧,输出绿屏或花屏:

java 复制代码
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%:

java 复制代码
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 最典型的实时编码场景:

java 复制代码
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 实时编码的性能调优

降低编码延迟(实时推流场景)

java 复制代码
// 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);

温度控制(长时间录像)

java 复制代码
// 监听 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);
    }
});

十、调试工具

bash 复制代码
# 列出设备支持的所有编解码器
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 队列异步传递数据。把这两件事搞清楚,剩下的细节都是具体配置。

本文核心要点回顾:

  1. 状态机Uninitialized → Initialized → Configured → Executing → Stopped,任何状态误操作都是 IllegalStateExceptionflush() 留在 Executing,reset() 回到 Initialized
  2. MediaFormat :I 帧间隔单位是秒、码率单位是 bps(不是 Kbps)、Surface 输入必须设 COLOR_FormatSurface
  3. 同步 vs 异步 :同步模式简单直接;异步模式(setCallback)CPU 友好,但回调线程禁止耗时操作
  4. Surface 零拷贝 :编码器用 createInputSurface(),解码器 releaseOutputBuffer(index, true),GPU 全程搬运,拒绝 CPU 拷贝
  5. EOS 处理 :必须消费完所有输出 Buffer 再 stop()flush() 后要从关键帧重新喂数据
  6. 实时编码调优 :低延迟用 KEY_LATENCY=0 + CBR + 减小 I 帧间隔;长时间录像要监听 Thermal 动态调整码率

下一篇,我们深入 MediaCodec 背后的硬件通道:OMX 与 Codec2 框架------看看那些"vendor.xxx.ext"参数到底是什么、硬件编解码器是如何被加载和调度的。


参考资料

相关推荐
光影少年2 小时前
Android和iOS原生开发的基础知识对RN开发的重要性,RN打包发布时原生端需要做哪些配置?
android·前端·react native·react.js·ios
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.2 小时前
MySQL主从复制实战配置详解,以及企业级相关优化说明
android·mysql·adb
simpleeeeee2 小时前
安卓开发者应该知道的 10 个安卓性能优化秘诀
android
simpleeeeee2 小时前
Android 17:API 级别 37 的开发者指南——现在你需要构建什么
android
zhaoyufei1332 小时前
RK3566 EDP屏幕背光闪修改pwm
android·java
simpleeeeee2 小时前
Android 17 正在改写规则——以下是每位开发者都需要了解的内容
android
summerkissyou19872 小时前
Android-Audio-根据音频焦点控制播放
android·audio
brahmsjiang2 小时前
Java类加载机制解析:从JVM启动到双亲委派,再到Android的特殊实现
android·java·jvm
fire-flyer3 小时前
ClickHouse系列(九):慢查询、内存 OOM 与稳定性治理
android·clickhouse