首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Android MediaCodec 编解码

Android MediaCodec 编解码

作者头像
木易士心
发布2025-11-30 09:21:45
发布2025-11-30 09:21:45
140
举报

概述

Android MediaCodec 是 Android 系统提供的底层 API,用于访问设备的硬件(或软件)编解码器,实现高效、低功耗的音视频编码和解码。它是构建高性能多媒体应用(如视频播放器、视频录制、直播推流、视频编辑等)的核心组件。

一、核心概念与工作原理

MediaCodec 采用异步的生产者 - 消费者模型,通过输入和输出缓冲区队列处理数据。

1.编码器 (Encoder)

生产者:应用(提供原始数据,如 YUV 格式的视频帧或 PCM 格式的音频采样)。 消费者:MediaCodec 编码器(接收原始数据,输出压缩后的数据,如 H.264 视频流或 AAC 音频流)。

2.解码器 (Decoder)

生产者:应用(提供压缩数据)。 消费者:MediaCodec 解码器(接收压缩数据,输出原始数据)。

3.MediaCodec 工作模型

MediaCodec 采用双缓冲区队列(输入/输出)实现异步数据处理,其架构可分为以下三层:

客户端(Client)

  • 输入端:填充待编解码的原始数据(如 YUV 视频帧、PCM 音频)到输入缓冲区队列。
  • 输出端:从输出缓冲区队列读取编解码后的数据(如 H.264 流、AAC 音频)并进行渲染或播放。

编解码器(Codec)

  • 硬件加速层:优先调用设备专属编解码器(如高通 DSP、ARM Mali),显著降低 CPU 负载。
  • 处理逻辑:从输入队列取出数据,执行编码/解码后,将结果存入输出队列,并回收缓冲区供复用。

缓冲区队列(Buffer Queue)

  • 输入队列:存储待处理的原始数据缓冲区(ByteBuffer 数组)。
  • 输出队列:存储处理后的数据缓冲区,供客户端消费。
在这里插入图片描述
在这里插入图片描述

4. MediaCodec 生命周期

生命周期执行顺序和各各声明周期详解如下:

在这里插入图片描述
在这里插入图片描述
  1. Uninitialized(未初始化) 创建方式:调用 MediaCodec.createEncoderByType() 或 createDecoderByType() 后进入。 允许操作: configure(…) release() → 跳转到 Released 禁止操作:调用 start(), dequeueInputBuffer(), getInputBuffer() 等会抛出 IllegalStateException ⚠️ 此时还未配置参数,不能进行任何数据处理。
  2. Configured(已配置) 进入方式:在 Uninitialized 状态下调用 configure(mediaFormat, surface, crypto, flags)。 允许操作: start() → 跳转到 Executing release() → 跳转到 Released 禁止操作:调用 dequeueInputBuffer() 等数据操作会抛异常。 注意:如果使用 Surface 输入/输出(如相机或播放器),Surface 必须在 configure 时传入,之后不能更改。
  3. Executing(执行中) ← 核心工作状态 进入方式:在 Configured 状态下调用 start()。 子状态: Flushed(刚启动或调用 flush() 后) Running(正常处理数据中) End-of-Stream(收到 EOS 信号,正在清空缓冲区) ➤ Executing - Flushed 刚调用 start() 或 flush() 后进入。 输入/输出缓冲区队列为空。 第一次调用 dequeueInputBuffer() 会返回有效索引。 ➤ Executing - Running 正常编解码状态。 可以反复调用: dequeueInputBuffer() + queueInputBuffer() → 提交数据 dequeueOutputBuffer() + releaseOutputBuffer() → 获取并释放结果 ➤ Executing - End-of-Stream 当你调用 queueInputBuffer(…, …, BUFFER_FLAG_END_OF_STREAM) 后进入。 编解码器会继续输出剩余数据,直到 dequeueOutputBuffer() 返回带有 BUFFER_FLAG_END_OF_STREAM 的 buffer。 此时仍需继续处理输出缓冲区,直到收到 EOS。 在 Executing 状态下可以调用:

flush() → 回到 Flushed 子状态(清空所有缓冲区,用于 seek 或重新开始) stop() → 跳转到 Uninitialized release() → 跳转到 Released 4. Released(已释放) 进入方式:在任何状态下调用 release()。 特点: 所有资源被释放,包括底层硬件编解码器实例。 对象不可再使用,任何方法调用都会抛出 IllegalStateException。 GC 会回收 Java 对象,但 native 资源必须手动 release() 才能释放。 实践:在 Activity/Fragment 销毁、Surface 被销毁、或编码完成时,必须调用 release()

5. 基本工作流程

  • 创建 (Create):通过 MediaCodec.createEncoderByType() 或MediaCodec.createDecoderByType() 创建实例。
  • 配置 (Configure):使用 MediaFormat 对象设置编解码参数(如分辨率、码率、帧率、颜色格式、MIME类型等),然后调用 configure() 方法。
  • 启动 (Start):调用 start() 方法,使编解码器进入运行状态,此时可开始访问输入和输出缓冲区。 处理数据 (Process):
代码语言:javascript
复制
import android.media.MediaCodec;
import android.media.MediaFormat;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;

public class H264Encoder {
   private static final String MIME_TYPE = "video/avc"; // H.264 MIME 类型
   private MediaCodec mEncoder;
   private FileOutputStream mOutputStream; // 用于写入 H.264 文件

   public void configure(int width, int height, int bitrate, int frameRate, int iframeInterval) {
       try {
           // 1. 创建编码器
           mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);

           // 2. 创建 MediaFormat 并设置参数
           MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);

           // 关键参数设置
           format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                   MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible); // 推荐使用 Flexible
           format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); // 码率,单位 bps
           format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); // 帧率
           format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iframeInterval); // I帧间隔,单位秒
           format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR); // 码率模式,可选 CBR/VBR/CQ

           // 3. 配置编码器(指定为编码器,传入 null 表示不关联 Surface)
           mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

           // 4. 启动编码器
           mEncoder.start();

           // 5. 打开输出文件
           mOutputStream = new FileOutputStream("output.h264");

       } catch (IOException e) {
           e.printStackTrace();
       }
   }
}
  • 输入:调用 dequeueInputBuffer() 获取可用输入缓冲区索引,通过 getInputBuffer() 获取缓冲区,写入数据后调用 queueInputBuffer() 提交给编解码器。
  • 输出:调用 dequeueOutputBuffer() 获取包含处理后数据的输出缓冲区索引,通过getOutputBuffer()获取数据,处理完毕后调用 releaseOutputBuffer() 释放缓冲区。
代码语言:javascript
复制
public void encodeFrame(byte[] yuvData, long presentationTimeUs) {
    try {
        // --- 处理输入 ---
        // 1. 获取输入缓冲区的索引
        int inputBufferIndex = mEncoder.dequeueInputBuffer(10000); // 超时 10ms
        if (inputBufferIndex >= 0) {
            ByteBuffer inputBuffer = mEncoder.getInputBuffer(inputBufferIndex);
            inputBuffer.clear();

            // 2. 将 YUV 数据复制到输入缓冲区
            // 注意:这里假设 yuvData 的格式与 MediaFormat 中设置的 COLOR_FORMAT 一致!
            // 如果格式不匹配,需要先进行转换(例如 NV21 -> I420 或 NV12)
            inputBuffer.put(yuvData);

            // 3. 提交输入缓冲区给编码器
            // presentationTimeUs 是此帧的时间戳,单位微秒 (us)
            // 如果是最后一帧,需要加上 BUFFER_FLAG_END_OF_STREAM 标志
            mEncoder.queueInputBuffer(inputBufferIndex, 0, yuvData.length, presentationTimeUs, 0);
        }

        // --- 处理输出 ---
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputBufferIndex;
        while ((outputBufferIndex = mEncoder.dequeueOutputBuffer(bufferInfo, 10000)) >= 0) {
            if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                // 这是编解码器配置信息 (SPS/PPS),通常需要保存并在文件开头或每个 I 帧前写入
                // 对于 H.264 文件,通常将 SPS/PPS 写在文件最开头
                ByteBuffer outputBuffer = mEncoder.getOutputBuffer(outputBufferIndex);
                byte[] spsPps = new byte[bufferInfo.size];
                outputBuffer.get(spsPps);
                // 将 spsPps 写入文件
                mOutputStream.write(spsPps);
                // 释放输出缓冲区
                mEncoder.releaseOutputBuffer(outputBufferIndex, false);
                continue;
            }

            if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                // 编码结束
                break;
            }

            // 获取包含编码后 H.264 数据的输出缓冲区
            ByteBuffer outputBuffer = mEncoder.getOutputBuffer(outputBufferIndex);
            byte[] encodedData = new byte[bufferInfo.size];
            outputBuffer.get(encodedData);

            // 将编码后的 H.264 NAL 单元写入文件
            // 通常需要在每个 NAL 单元前添加起始码 0x00000001
            mOutputStream.write(new byte[]{0, 0, 0, 1});
            mOutputStream.write(encodedData);

            // 释放输出缓冲区
            mEncoder.releaseOutputBuffer(outputBufferIndex, false);
        }

    } catch (IOException e) {
        e.printStackTrace();
    }
}
  • 结束 (Stop & Release):处理完所有数据后,发送结束信号(在 queueInputBuffer() 时设置 BUFFER_FLAG_END_OF_STREAM),等待并处理完所有输出缓冲区,最后调用 stop() 和 release() 释放资源。
代码语言:javascript
复制
public void stopAndRelease() {
    try {
        // 1. 发送结束信号
        int inputBufferIndex = mEncoder.dequeueInputBuffer(-1); // 阻塞等待
        if (inputBufferIndex >= 0) {
            mEncoder.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
        }

        // 2. 处理所有剩余的输出数据
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputBufferIndex;
        do {
            outputBufferIndex = mEncoder.dequeueOutputBuffer(bufferInfo, 10000);
            if (outputBufferIndex >= 0) {
                if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    break; // 确认结束
                }
                ByteBuffer outputBuffer = mEncoder.getOutputBuffer(outputBufferIndex);
                byte[] encodedData = new byte[bufferInfo.size];
                outputBuffer.get(encodedData);

                mOutputStream.write(new byte[]{0, 0, 0, 1});
                mOutputStream.write(encodedData);

                mEncoder.releaseOutputBuffer(outputBufferIndex, false);
            }
        } while (outputBufferIndex >= 0 || outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER);

        // 3. 关闭文件流
        if (mOutputStream != null) {
            mOutputStream.close();
        }

        // 4. 停止并释放编码器资源
        mEncoder.stop();
        mEncoder.release();
        mEncoder = null;

    } catch (IOException e) {
        e.printStackTrace();
    }
}

二、关键组件与 参数

1.组件

  • MediaCodec:核心类,负责编解码操作。
  • MediaForma:用于描述媒体数据的格式。在配置编解码器时,必须提供正确的 MediaFormat。
  • MediaCodec.BufferInfo:伴随输出缓冲区返回,包含数据大小、时间戳、偏移量和标志位(如 BUFFER_FLAG_CODEC_CONFIG,BUFFER_FLAG_END_OF_STREAM)。
  • MediaCodecList:用于查询设备支持的编解码器及其能力。从 Android 10 (API 29) 开始,可以查询编解码器是否为硬件加速 (isHardwareAccelerated()) 以及支持的性能点 (getSupportedPerformancePoints())。

2.参数解析

视频编码关键参数 (Encoder) 这些参数在创建视频编码器(如 H.264, H.265)时配置,决定了输出视频流的特性和质量。

参数键 (Key)

类型

含义说明

常用值 / 示例

重要性

备注

KEY_MIME

String

编码格式 MIME 类型

“video/avc” (H.264), “video/hevc” (H.265), “video/x-vnd.on2.vp8” (VP8)

必须

与 createEncoderByType() 一致

KEY_WIDTH

int

视频帧宽度(像素)

1920, 1280, 720

必须

KEY_HEIGHT

int

视频帧高度(像素)

1080, 720, 480

必须

KEY_COLOR_FORMAT

int

输入原始数据的颜色格式

COLOR_FormatYUV420Flexible(推荐), COLOR_FormatYUV420SemiPlanar (NV12)

必须

格式不匹配会导致花屏!需转换相机 NV21 → NV12/I420

KEY_BIT_RATE

int

目标码率(单位:bps)

2 * 1024 * 1024(2Mbps)

核心

影响画质与体积

KEY_FRAME_RATE

int

目标帧率(FPS)

30, 25, 60

核心

应与输入帧率匹配

KEY_I_FRAME_INTERVAL

int

I 帧(关键帧)间隔(单位:秒)

1, 2, 3

重要

直播推荐 1~3 秒;影响拖动和容错

KEY_BITRATE_MODE

int

码率控制模式

BITRATE_MODE_CBR, BITRATE_MODE_VBR, BITRATE_MODE_CQ

重要

CBR 适合直播,VBR 节省空间

KEY_PROFILE

int

编码 Profile(档次)

AVCProfileBaseline, AVCProfileMain, AVCProfileHigh

可选

不设置则用默认,影响兼容性

KEY_LEVEL

int

编码 Level(级别)

AVCLevel3, AVCLevel4, AVCLevel5

可选

限制分辨率/码率上限

视频解码关键参数 (Decoder)

参数键 (Key)

类型

含义说明

常用值 / 示例

重要性

备注

KEY_MIME

String

解码格式 MIME 类型

“video/avc”, “video/hevc”

必须

需与码流一致

KEY_WIDTH

int

视频帧宽度(从码流中解析)

1920, 1280

自动获取

通常从容器读取

KEY_HEIGHT

int

视频帧高度(从码流中解析)

1080, 720

自动获取

KEY_COLOR_FORMAT

int

通常被忽略,输出格式由 Surface 决定

-

无关

解码到 ByteBuffer 时使用默认格式

KEY_MAX_INPUT_SIZE

int

建议输入缓冲区最大大小(字节)

根据分辨率估算,如 width * height * 3 / 2 + 1024

推荐设置

避免 queueInputBuffer 失败

音频关键参数

参数键 (Key)

类型

含义说明

常用值 / 示例

重要性

备注

KEY_MIME

String

音频编码格式

“audio/mp4a-latm” (AAC), “audio/opus”, “audio/vorbis”

必须

KEY_SAMPLE_RATE

int

采样率(Hz)

44100, 48000, 22050, 16000

必须

KEY_CHANNEL_COUNT

int

声道数

1 (单声道), 2 (立体声)

必须

KEY_BIT_RATE

int

音频码率(bps)

128000, 64000, 96000

核心

VBR 模式下为平均/最大码率

KEY_AAC_PROFILE

int

AAC 编码 Profile

AACObjectLC(最常用), AACObjectHE, AACObjectHE_PS

推荐设置

LC 兼容性好,HE 低码率高效

三、常见应用场景

  • 视频录制与编码:从相机获取 YUV 数据,使用 MediaCodec 编码为 H.264/H.265 格式,然后封装成 MP4 文件(通常需要配合 MediaMuxer)。
  • 视频播放与解码:读取 MP4 文件中的 H.264 数据,使用 MediaCodec 解码为 YUV/RGB 数据,然后渲染到 SurfaceView 或 TextureView 上。
  • 直播推流:实时采集音视频数据,编码后通过网络协议(如 RTMP)发送到服务器。
  • 视频转码/编辑:解码源视频,进行裁剪、滤镜等处理,再重新编码。
  • 音频处理:录制 PCM 音频并编码为 AAC,或解码 AAC 为 PCM 进行播放或分析。

四、使用中的关键注意事项与难点

  • YUV 格式匹配:相机输出的格式(通常是 NV21)与编码器期望的格式(通常是 NV12 或 I420)往往不同。必须进行格式转换,否则会出现花屏。可以使用 libyuv 库或 OpenGL ES 进行高效转换。
  • SPS/PPS 处理:对于 H.264/H.265 编码,编解码器会在开始时输出包含 SPS/PPS 的 BUFFER_FLAG_CODEC_CONFIG 数据。这些数据是解码的关键,必须保存并在文件开头或流中正确发送。
  • 时间戳管理:正确设置每一帧的 presentationTimeUs(单位微秒)对于音视频同步和流畅播放至关重要。
  • 异步处理:虽然 MediaCodec 本身是同步 API,但为了不阻塞主线程,通常需要在子线程中进行编解码循环。
  • 资源释放:务必在使用完毕后调用 stop() 和 release(),否则可能导致内存泄漏或后续编解码失败。
  • 设备兼容性:不同设备支持的编解码器、颜色格式和性能可能不同。建议使用 MediaCodecList 进行查询和适配。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-09-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
  • 一、核心概念与工作原理
    • 1.编码器 (Encoder)
    • 2.解码器 (Decoder)
    • 3.MediaCodec 工作模型
    • 4. MediaCodec 生命周期
    • 5. 基本工作流程
  • 二、关键组件与 参数
    • 1.组件
    • 2.参数解析
  • 三、常见应用场景
  • 四、使用中的关键注意事项与难点
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档