Android MediaCodec 编解码

文章目录

  • 概述
  • 一、核心概念与工作原理
    • 1.编码器 (Encoder)
    • 2.解码器 (Decoder)
    • 3.MediaCodec 工作模型
    • 4. MediaCodec 生命周期
    • 5. 基本工作流程
  • 二、关键组件与 参数
    • 1.组件
    • 2.参数解析
  • 三、常见应用场景
  • 四、使用中的关键注意事项与难点

概述

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):
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() 释放缓冲区。
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() 释放资源。
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_MIMEString编码格式 MIME 类型“video/avc” (H.264), “video/hevc” (H.265), “video/x-vnd.on2.vp8” (VP8)必须与 createEncoderByType() 一致
KEY_WIDTHint视频帧宽度(像素)1920, 1280, 720必须
KEY_HEIGHTint视频帧高度(像素)1080, 720, 480必须
KEY_COLOR_FORMATint输入原始数据的颜色格式COLOR_FormatYUV420Flexible(推荐), COLOR_FormatYUV420SemiPlanar (NV12)必须格式不匹配会导致花屏!需转换相机 NV21 → NV12/I420
KEY_BIT_RATEint目标码率(单位:bps)2 * 1024 * 1024(2Mbps)核心影响画质与体积
KEY_FRAME_RATEint目标帧率(FPS)30, 25, 60核心应与输入帧率匹配
KEY_I_FRAME_INTERVALintI 帧(关键帧)间隔(单位:秒)1, 2, 3重要直播推荐 1~3 秒;影响拖动和容错
KEY_BITRATE_MODEint码率控制模式BITRATE_MODE_CBR, BITRATE_MODE_VBR, BITRATE_MODE_CQ重要CBR 适合直播,VBR 节省空间
KEY_PROFILEint编码 Profile(档次)AVCProfileBaseline, AVCProfileMain, AVCProfileHigh可选不设置则用默认,影响兼容性
KEY_LEVELint编码 Level(级别)AVCLevel3, AVCLevel4, AVCLevel5可选限制分辨率/码率上限

视频解码关键参数 (Decoder)

参数键 (Key)类型含义说明常用值 / 示例重要性备注
KEY_MIMEString解码格式 MIME 类型“video/avc”, “video/hevc”必须需与码流一致
KEY_WIDTHint视频帧宽度(从码流中解析)1920, 1280自动获取通常从容器读取
KEY_HEIGHTint视频帧高度(从码流中解析)1080, 720自动获取
KEY_COLOR_FORMATint通常被忽略,输出格式由 Surface 决定-无关解码到 ByteBuffer 时使用默认格式
KEY_MAX_INPUT_SIZEint建议输入缓冲区最大大小(字节)根据分辨率估算,如 width * height * 3 / 2 + 1024推荐设置避免 queueInputBuffer 失败

音频关键参数

参数键 (Key)类型含义说明常用值 / 示例重要性备注
KEY_MIMEString音频编码格式“audio/mp4a-latm” (AAC), “audio/opus”, “audio/vorbis”必须
KEY_SAMPLE_RATEint采样率(Hz)44100, 48000, 22050, 16000必须
KEY_CHANNEL_COUNTint声道数1 (单声道), 2 (立体声)必须
KEY_BIT_RATEint音频码率(bps)128000, 64000, 96000核心VBR 模式下为平均/最大码率
KEY_AAC_PROFILEintAAC 编码 ProfileAACObjectLC(最常用), 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 进行查询和适配。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木易 士心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值