掘金 人工智能 07月24日 10:24
Android MediaCodec 的使用和源码实现分析
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

MediaCodec是Android提供的底层API,用于音视频数据的硬件编解码。本文详细介绍了MediaCodec的同步和异步使用流程,包括创建、配置、启动、缓冲区处理、停止与释放。文中给出了H.264解码的同步和异步示例,并强调了异步模式的推荐使用。此外,文章还涵盖了编码器的配置要点、常见使用注意事项,如线程安全、缓冲区管理、硬件限制、错误处理、性能优化以及输入数据帧对齐等关键细节。最后,文章简要阐述了MediaCodec的底层架构,包括与Stagefright、OMX、HAL的交互,以及调试和性能优化的实用技巧。

🎬 MediaCodec的核心使用流程是通用的,无论是编码器还是解码器,都遵循“Configure → Start → (queueInputBuffer / dequeueOutputBuffer) → Stop → Release”的步骤。开发者可以根据MIME类型(如"video/avc")创建实例,通过MediaFormat配置分辨率、码率等参数,并可选择同步或推荐的异步模式进行数据处理。

💡 异步模式(API 21+)通过`setCallback()`设置回调,能有效避免阻塞主线程,提高应用响应性。在`onInputBufferAvailable`中填充数据,在`onOutputBufferAvailable`中处理解码后的数据,这种事件驱动的方式更符合现代Android开发实践。

🔧 在使用MediaCodec时,需注意线程安全问题,所有操作应在同一线程或通过同步机制处理。同时,要确保输入数据的“帧”对齐,例如H.264流中的NALU单元,以及正确处理`dequeueOutputBuffer()`返回的`INFO_OUTPUT_FORMAT_CHANGED`等特殊状态,并妥善处理EOS(End Of Stream)标志。

🚀 为实现最佳性能,应优先选择硬件编解码器,并合理配置`MediaFormat`参数,如码率、帧率和I帧间隔。视频解码时,使用`Surface`输出可以显著提高效率,减少数据拷贝。复用`MediaCodec`实例而非频繁创建销毁,也是重要的性能优化手段。

MediaCodec 是 Android 提供的底层多媒体编解码 API,允许开发者使用系统硬件编解码器对音视频数据进行压缩/解压。

使用流程(图像/音频通用)

MediaCodec 提供了同步和异步两种使用模式,推荐使用异步模式(API 21 及以上)。无论是编码器(Encoder)还是解码器(Decoder),MediaCodec 的典型使用流程是:

Configure → Start → (queueInputBuffer / dequeueOutputBuffer) → Stop → Release

    创建 MediaCodec 实例

    通过 MediaCodec.createByCodecNameMediaCodec.createEncoderByType / MediaCodec.createDecoderByType 创建编码器或解码器,指定 MIME 类型(如 "video/avc""audio/mp4a-latm")。配置 MediaFormat,设置参数如分辨率、码率、采样率等。

    配置 MediaCodec

    调用 configure() 方法,传入 MediaFormat、Surface(视频解码时可选)等参数,指定编码或解码模式。

    启动 MediaCodec

    调用 start() 进入执行状态。

    处理输入输出缓冲区

    输入缓冲区:通过 dequeueInputBuffer() 获取可用的输入缓冲区索引,将数据(如原始 PCM 或 YUV 数据)写入缓冲区,然后调用 queueInputBuffer() 提交。

    输出缓冲区:通过 dequeueOutputBuffer() 获取处理后的数据,处理后调用 releaseOutputBuffer() 释放。

    停止和释放

    调用 stop()release() 停止并释放资源。

异步模式(API 21+):使用 setCallback() 设置回调,处理输入输出缓冲区事件,避免手动轮询。在 onInputBufferAvailable 中填充数据,在 onOutputBufferAvailable 中处理结果。

同步示例:

以解码 H.264 视频为例(原始数据为裸流 .h264

val codec = MediaCodec.createDecoderByType("video/avc")val format = MediaFormat.createVideoFormat("video/avc", width, height)format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0)codec.configure(format, surface, null, 0)codec.start()val inputBuffers = codec.inputBuffersval outputBuffers = codec.outputBufferswhile (running) {    val inIndex = codec.dequeueInputBuffer(10000)    if (inIndex >= 0) {        val inputBuffer = inputBuffers[inIndex]        inputBuffer.clear()        val sampleSize = readH264Frame(inputBuffer)        if (sampleSize > 0) {            codec.queueInputBuffer(inIndex, 0, sampleSize, presentationTimeUs, 0)        } else {            codec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)            break        }    }    val bufferInfo = MediaCodec.BufferInfo()    val outIndex = codec.dequeueOutputBuffer(bufferInfo, 10000)    if (outIndex >= 0) {        // 解码帧已就绪,在 Surface 上显示        codec.releaseOutputBuffer(outIndex, true)    } else if (outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {        val newFormat = codec.outputFormat        Log.d("MediaCodec", "Output format changed: $newFormat")    }}codec.stop()codec.release()

异步示例:

使用 MediaCodec 异步模式进行视频解码的示例( H.264 视频解码到 Surface ):

import android.media.MediaCodec;import android.media.MediaFormat;import android.view.Surface;import java.nio.ByteBuffer;public class VideoDecoder {    private MediaCodec mediaCodec;    private static final String MIME_TYPE = "video/avc"; // H.264    private static final int TIMEOUT_US = 10000; // 10ms 超时    public void startDecoder(Surface surface, byte[] sps, byte[] pps) {        try {            // 创建解码器            mediaCodec = MediaCodec.createDecoderByType(MIME_TYPE);            // 配置 MediaFormat            MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, 1920, 1080); // 分辨率            format.setByteBuffer("csd-0", ByteBuffer.wrap(sps)); // SPS            format.setByteBuffer("csd-1", ByteBuffer.wrap(pps)); // PPS            // 设置异步回调            mediaCodec.setCallback(new MediaCodec.Callback() {                @Override                public void onInputBufferAvailable(MediaCodec codec, int index) {                    // 获取输入缓冲区                    ByteBuffer inputBuffer = codec.getInputBuffer(index);                    // 从数据源获取数据(示例中假设有数据)                    byte[] data = getVideoData(); // 需实现                    if (data != null) {                        inputBuffer.put(data);                        codec.queueInputBuffer(index, 0, data.length, getPresentationTimeUs(), 0);                    } else {                        // 结束标志                        codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);                    }                }                @Override                public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {                    // 释放输出缓冲区到 Surface                    if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {                        codec.stop();                        codec.release();                        return;                    }                    codec.releaseOutputBuffer(index, true); // 渲染到 Surface                }                @Override                public void onError(MediaCodec codec, MediaCodec.CodecException e) {                    e.printStackTrace();                }                @Override                public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {                    // 输出格式变化                    // 可处理新的 MediaFormat                }            });            // 配置并启动            mediaCodec.configure(format, surface, null, 0);            mediaCodec.start();        } catch (Exception e) {            e.printStackTrace();        }    }    // 模拟获取视频数据    private byte[] getVideoData() {        // 替换为实际数据源(如文件、网络流)        return null;    }    // 模拟获取时间戳    private long getPresentationTimeUs() {        return System.nanoTime() / 1000;    }    // 停止解码    public void stop() {        if (mediaCodec != null) {            mediaCodec.stop();            mediaCodec.release();            mediaCodec = null;        }    }}

使用方法

    创建 Surface(如通过 SurfaceView 获取)。提供 H.264 流的 SPS(Sequence Parameter Set,序列参数集) 和 PPS(Picture Parameter Set,图像参数集)数据。调用 startDecoder() 传入 Surface 和 SPS/PPS。数据源需实现 getVideoData(),提供 H.264 帧数据。调用 stop() 释放资源。

编码示例:

与解码类似,只是配置为 CONFIGURE_FLAG_ENCODE

format.setInteger(MediaFormat.KEY_BIT_RATE, 125000)format.setInteger(MediaFormat.KEY_FRAME_RATE, 15)format.setInteger(MediaFormat.KEY_COLOR_FORMAT,                  MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5)codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)

然后从 inputBuffer 填入 YUV,读取 outputBuffer 得到压缩后的 H264。

常见使用注意点

    线程安全

    MediaCodec 本身非线程安全,所有操作应在同一线程或通过同步机制处理。异步模式中,回调运行在内部线程,需确保数据处理线程安全。

    缓冲区管理

    输入缓冲区需确保数据格式正确(如 YUV 帧格式、PCM 采样率)。输出缓冲区可能返回 INFO_ 状态(如 INFO_OUTPUT_FORMAT_CHANGED),需处理这些状态。不要长时间持有缓冲区,避免阻塞。

    硬件限制

    不同设备支持的编解码器和参数(如分辨率、帧率)不同,需通过 MediaCodecInfo 查询支持的功能。硬件解码器可能有缓冲区数量限制,需合理管理。

    配置 MediaFormat

    编码器需设置合理的码率、帧率、I 帧间隔等,避免过高参数导致性能问题。解码器需匹配输入数据的格式(如 H.264 的 SPS/PPS)。

    错误处理

    处理 MediaCodec.CodecException,可能由硬件或配置错误引发。检查 isRecoverable()isTransient(),决定是否重试或重启。

    异步 vs 同步

    异步模式更适合现代应用,避免阻塞主线程。同步模式适合简单场景,但需手动轮询,可能影响性能。

    Surface 使用

    视频解码时,输出到 Surface 可提高效率,但需确保 Surface 有效。编码器不支持 Surface 输出,需手动处理输出数据。

    性能优化

    使用硬件加速(优先选择硬件编解码器)。避免频繁创建/销毁 MediaCodec 实例,复用实例以降低开销。

    输入数据必须是“帧”对齐的

    如果你是从网络流(如 RTP/RTMP)中接收数据,确保每次 queueInputBuffer() 的数据是一帧(NALU),否则容易出现花屏或丢帧。

    MediaCodec 必须按严格顺序调用

    configure()start()queue/dequeuestop()release()

    调用顺序错了会抛出 IllegalStateException

    Surface 与 ByteBuffer 二选一

    解码可以输出到 Surface(适合显示)或 ByteBuffer(适合处理图像)。若使用 Surface 解码,不可访问 outputBuffer

    解码时可能遇到 format change

dequeueOutputBuffer() 返回 INFO_OUTPUT_FORMAT_CHANGED 时必须处理新的输出格式:

val newFormat = codec.outputFormat// 处理新的分辨率等信息
    记得处理 EOS(End Of Stream)
queueInputBuffer(..., MediaCodec.BUFFER_FLAG_END_OF_STREAM)

当 EOS 输入后,dequeueOutputBuffer 最终返回的 bufferInfo.flags 也会包含 EOS,代表流结束。

    使用 setCallback() 进行异步处理(可选)

从 Android 5.0 开始,支持异步 API:

codec.setCallback(object : MediaCodec.Callback() {    override fun onInputBufferAvailable(codec: MediaCodec, index: Int) { /*...*/ }    override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: BufferInfo) { /*...*/ }    override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) { /*...*/ }    override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) { /*...*/ }})

源码实现

MediaCodec 的底层实现基于 Android 的多媒体框架,主要依赖 AOSP 的 stagefright 和硬件抽象层(HAL)。

架构概览

MediaCodec 是 Java 层 API,通过 JNI 调用 C++ 层的 MediaCodecframeworks/av/media/libstagefright/MediaCodec.cpp)。底层通过 OMX(OpenMAX IL)与硬件编解码器交互,或使用软件编解码器(如 FFmpeg 或 Google 的软件编解码器)。

初始化与配置

MediaCodec::CreateByTypeCreateByName 调用 OMXClient 初始化硬件解码器,查询设备支持的编解码器列表(MediaCodecList)。configure() 方法将 MediaFormat 转换为 OMX 参数,设置编码/解码参数(如分辨率、码率)。

缓冲区处理

输入输出缓冲区通过 AMessageABuffer 在 C++ 层管理,映射到 Java 层的 ByteBuffer。异步模式使用 LooperHandler 机制分发回调事件。

硬件加速

硬件解码通过 OMX 层与芯片厂商提供的驱动交互(如 Qualcomm、MediaTek 的硬件编解码器)。源码中,OMXCodecframeworks/av/media/libstagefright/OMXCodec.cpp)负责与硬件通信,处理缓冲区和帧数据。

错误处理

底层通过 status_t 返回错误码,映射到 Java 层的 MediaCodec.CodecException。常见错误包括硬件资源不足、格式不支持等。

关键类与文件

调用链(解码器)

Android MediaCodec 底层通过 JNI 调用了 C++ 层的 MediaCodec,最终与硬件编解码器通信:

MediaCodec.configure() → native_configure() → MediaCodec.cpp::configure()  ↓CodecLooper 创建 Codec(如 OMXCodec、C2Codec)  ↓SurfaceTexture 或 BufferQueue 创建输出目标

源码路径(Android AOSP)

frameworks/av/media/libstagefright/    MediaCodec.cpp    MediaCodecBuffer.cpp    ...frameworks/av/services/mediacodec/    MediaCodecService.cpp

补充说明

调试技巧

使用 adb logcat 查看 MediaCodec 日志,定位错误。检查 MediaCodecInfo 确保设备支持目标 MIME 类型和参数。验证输入数据的完整性(如 H.264 的 NAL 单元)。

性能优化

使用 Surface 输出视频解码结果,减少数据拷贝。批量处理缓冲区,降低调用频率。优先选择硬件编解码器(名称通常包含厂商标识,如 OMX.qcom)。

常见问题

黑屏:可能是 Surface 无效或输入数据格式错误。卡顿:检查帧率、码率是否过高,或缓冲区处理不及时。崩溃:检查是否正确释放资源,或硬件不支持指定参数。

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

MediaCodec Android 音视频编解码 硬件加速 多媒体开发
相关文章