做Android音视频的,早晚会在MediaCodec上栽几跤。这API表面上很简单——几十行代码就够把一段视频解出来丢到Surface上;但真到了产品里,它又是另一副面孔:状态机没理清、buffer时序不对、厂商设备的编解码器实现有差异,都可能让原本正常的画面变成黑屏、花屏、音画不同步。我最早也把MediaCodec单纯理解成“系统提供的硬件加速编解码接口”,直到被线上反馈和实机测试连着教育了几次,才踏踏实实把这套机制啃明白。

这篇文章想跟你聊的,就是我在实际项目中反复琢磨出来的那套东西:从MediaCodec在Android媒体栈里的位置、状态机和buffer模型,到手写一个可用的解码循环、再用Camera做一条硬编录制管线,最后是那些“不踩一遍绝对想不到”的兼容性坑。全程用真实可运行的思路讲,不空谈原理。

1. MediaCodec不是孤军奋战的API:它前面有Extractor后面有Muxer和Surface

1.1 一次视频播放的完整调用链

很多新手一开始就用MediaCodec,结果发现“这玩意儿怎么还要自己喂数据、自己取数据”?这其实是因为MediaCodec本身只干一件事:吃进去压缩数据,吐出来原始图像(或者反过来的编码)。它不负责读文件、不负责解封装、不负责音频播放,也不负责把画面画到屏幕上。

一条典型的本地视频播放链路是这样的:

MediaExtractor 负责把MP4/MKV等封装格式里的ES流(Elementary Stream,也就是H.264/H.265裸流)一帧一帧读出来, MediaCodec 负责把这些ES流解码成YUV画面,最后把YUV渲染到Surface上。如果走ByteBuffer输出模式,你还需要自己处理格式转换和上屏。顺序大概是:

文件 → MediaExtractor → MediaCodec解码器 → Surface/ByteBuffer → 显示

所以你在写代码时的第一步,往往不是new MediaCodec,而是先有一个MediaExtractor去拿数据。反过来做视频录制/编码时,链路就变成了:

Camera/屏幕 → MediaCodec编码器 → MediaMuxer → MP4文件

你会发现,MediaCodec永远处在中间那一环,它的输入输出都是“流”,不关心来源和去向。想清楚这一点,后面写代码才不会觉得“为什么数据要我一帧一帧取”。

1.2 Codec2/OMX与硬件VPU:所谓“硬解”到底硬在哪

说到“硬件加速编解码”,就绕不开VPU这个词。VPU是Video Processing Unit,即SoC上专门处理视频编解码的硬件单元。你在做视频处理时经常听到“这个机器支持VPU硬解H.264”,意思是这款SoC的芯片里专门设计了一块电路,可以高效完成H.264的帧间预测、DCT变换、熵编码等重活,比纯CPU做软解更快、更省电。

Android系统里,MediaCodec要访问这块VPU,中间还有一层抽象。Android 8以前是OpenMAX IL(OMX-IL),之后逐渐过渡到Codec2(C2)。OMX/C2框架本身不负责解码,它只是定义了“编解码器组件”的统一接口,让系统和厂商HAL能对得上话。真正干活的,还是VPU、GPU或DSP里那些固定功能单元。

从开发者视角看,这些底层变化都被MediaCodec封装掉了。你能感知到差异的地方只有两个:一是通过 MediaCodecList 拿到codec的名字和类型,二是通过 MediaCodecInfo.isHardwareAccelerated() (API 29+)判断当前选中的是硬件解码器还是软件解码器。

这也是为什么同一个视频流,在骁龙平台上硬解没问题,换到另一颗SoC上就花屏的原因——码流本身是对的,但各家VPU固件对某些H.264 Level/Profile的支持程度、参考帧数量、错误容忍策略都不一样。排查这类问题时,别急着怀疑自己的代码,先查一下设备上真正选中的是哪个解码器。

2. 先过两关:状态机认识和buffer流转逻辑

2.1 状态机设计意图与常见误用

MediaCodec内部是一套显式的状态机,官方文档里画了张图,初看觉得绕,实际使用时核心就几个状态:

  • Uninitialized :刚创建出来,比如 createDecoderByType() 之后。
  • Configured :调用了 configure() ,参数已设置好,但还没开始干活。
  • Executing :调用了 start() ,开始正常收发buffer。
  • Flushed :在Executing状态下执行 flush() ,把队列里还没处理的数据都清掉。
  • End-of-Stream :输入侧收到了带 BUFFER_FLAG_END_OF_STREAM 标志的数据。
  • Released : release() 之后,整个实例作废。

常见误用是把MediaCodec当成一把即用即扔的“媒体快门”——有人会反复 start() 而不等内部状态稳定,或者用完直接不调用 stop() 和 release() 就丢给GC。MediaCodec底层持有的是系统编解码器资源,不显式释放,短时间内大量创建实例,会直接把 OMX/Codec2 实例耗尽,表现就是后续创建codec时抛出 MediaCodec.CodecException 。

我的建议是:把MediaCodec当成一个“重量级对象”来管理。一个解码器实例尽量复用,不要频繁创建;如果确实要切换码流,尽量通过 flush() 清理后复用,实在不行才走 stop() → configure() → start() 的重配流程。

2.2 Input/Output Buffer的前世今生

MediaCodec没有直接让你把压缩数据塞进方法的参数里,它用的是“缓冲区队列”模型。简单理解:系统内部维护了一组输入缓冲区和一组输出缓冲区,你通过 dequeueInputBuffer() 申请一个可写的输入桶,把数据填进去,再用 queueInputBuffer() 把这个桶交还给编解码器;编解码器处理完之后,把结果填到某个输出桶里,你通过 dequeueOutputBuffer() 取到它,用完再用 releaseOutputBuffer() 归还。

这个模型的好处是,数据在编解码器内部流转时可以减少拷贝。尤其是Surface模式,解码器的输出buf可以直接落到GPU纹理里,完全绕开Java层的ByteBuffer内存拷贝。这也是为什么很多讲性能优化的文章反复强调:解码输出能用Surface就别用ByteBuffer。

但很多人会在这里犯一个认知错误:以为拿到 dequeueInputBuffer() 之后只能按FIFO顺序处理。实际不是,你可以同时拿到多个可写的input buffer,自己管理投递顺序。不过绝大多数场景不需要这么搞,一个简单的“取一个、填一个、交一个”循环就够了。

2.3 同步还是异步:依据你的业务选型

MediaCodec从API 21开始支持异步模式,通过 setCallback() 注册回调:

  • onInputBufferAvailable :系统通知你input buffer空了,可以投数据。
  • onOutputBufferAvailable :系统通知你有解码结果可以取。
  • onOutputFormatChanged :输出格式变了,比如刚拿到SPS/PPS后分辨率确定下来。
  • onError :解码器异常。

那同步和异步到底怎么选?我的经验是:如果你在写一个业务相对独立的“解码线程”,同步模式反而更好控制——整个逻辑就在一个while循环里,行号能对得上,出问题也好定位。如果你在做一个复杂的播放引擎,解码、渲染、音频时钟、seek逻辑互相耦合,那就用异步回调,代码结构更清晰,也不会因为一个循环阻塞整个线程。

不过异步模式有一个坑:回调的线程不是随随便便拿一个就用。官方推荐给 setCallback() 传一个单独的 HandlerThread ,避免回调跑到主线程里做编解码操作。很多人偷懒直接在主线程setCallback,结果 onInputBufferAvailable 里一跑重活,主线程就卡了。

3. 从零手写解码循环:把H.264裸流放到屏幕上

3.1 喂数据之前的准备:configure关键参数与CSD

无论同步还是异步,写解码器之前都要先 configure 。常见参数包括:

  • key :取MIME类型,如 video/avc 。
  • height/width :视频宽高。
  • csd-0 、 csd-1 :Codec-Specific Data,具体到H.264就是SPS和PPS。

如果你是配合 MediaExtractor 使用,CSD可以不用自己解析,从extractor取到的 MediaFormat 里直接getByteBuffer("csd-0")和"csd-1"就行。比如:

MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(filePath);
MediaFormat format = extractor.getTrackFormat(0);
// format里已经带了 csd-0/csd-1/key/width/height

这里特别提醒:字节流有两种封装方式,很多人在这一步踩坑。

  • Annex-B 格式:每个NALU前面是 00 00 00 01 或者 00 00 01 起始码,SPS/PPS直接内嵌在码流里。这种格式一般给解码器喂裸流就行。
  • AVCC 格式:每个NALU前面是4字节长度前缀,SPS/PPS一般在解封装阶段单独提取到 csd-0/csd-1 里,码流本身的NALU不会重复携带。

MediaCodec对这两种格式的处理逻辑是:如果你在configure时给了CSD,它就用你给的SPS/PPS;如果没给,它尝试自己从码流里探测。所以如果你手动拆流,一定要搞清楚当前码流是Annex-B还是AVCC,别把CSD搞丢了,否则解码器经常报“无法解码”或者一直不出帧。

3.2 同步解码循环的标准写法

我习惯把解码逻辑放在一个单独的while循环里,循环条件一般是“没有收到EOS”。下面这段代码是从MediaExtractor读数据、解码到Surface的最简版本:

// codec已经通过createDecoderByType + configure创建并start
BufferInfo info = new BufferInfo();
boolean inputDone = false;
boolean outputDone = false;

while (!outputDone) {
    // --- 输入侧:只要有空位就塞数据
    if (!inputDone) {
        int inIndex = codec.dequeueInputBuffer(10_000); // 10ms超时
        if (inIndex >= 0) {
            ByteBuffer inputBuffer = codec.getInputBuffer(inIndex);
            int sampleSize = extractor.readSampleData(inputBuffer, 0);
            if (sampleSize < 0) {
                // 没有更多数据了,发个EOS
                codec.queueInputBuffer(inIndex, 0, 0, 0L,
                        MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                inputDone = true;
            } else {
                codec.queueInputBuffer(inIndex, 0, sampleSize,
                        extractor.getSampleTime(), 0);
                extractor.advance();
            }
        }
    }

    // --- 输出侧:取解码结果
    int outIndex = codec.dequeueOutputBuffer(info, 10_000);
    if (outIndex >= 0) {
        boolean render = (info.size > 0); // 空buffer不需要渲染
        codec.releaseOutputBuffer(outIndex, render);
        if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
            outputDone = true;
        }
    } else if (outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
        // 通常表示拿到了解析后的格式信息
        MediaFormat outputFormat = codec.getOutputFormat();
        // 可以在这里读outputFormat的宽高、颜色格式等
    }
}

codec.stop();
codec.release();

注意两个细节:

第一, readSampleData 返回-1时表示extractor已经读到文件尾,这时需要发送一个带 BUFFER_FLAG_END_OF_STREAM 的空buffer。不要直接跳出循环,不然解码器里缓存的帧不会再吐出来。

第二, releaseOutputBuffer(outIndex, true) 的第二个参数代表“渲染”。如果你用的是Surface输出模式,传 true 会把当前帧提交到Surface;如果是ByteBuffer输出模式,官方要求必须传 false ,传true在某些机型上会直接抛异常。

3.3 ByteBuffer输出与Surface输出两条路

一条路是Surface输出。这种模式下,解码器内部直接操作图形内存,GPU纹理和Surface绑定,你不需要碰YUV数据,性能最好。适合播放器:把 SurfaceHolder 的Surface塞进configure,然后无限 releaseOutputBuffer(..., true) 就可以了。

另一条路是ByteBuffer输出。这种模式下, dequeueOutputBuffer 出来的 ByteBuffer 里是原始YUV数据,你需要自己处理颜色格式转换。比如做视频帧抽帧、做缩略图、做人脸检测、做OpenGL滤镜,都需要先拿到YUV。

但ByteBuffer模式下有个隐藏问题:颜色格式不统一。有的设备输出 COLOR_FormatYUV420Planar (I420,即Y平面+U平面+V平面),有的输出 COLOR_FormatYUV420SemiPlanar (NV12,Y平面+UV交错平面),有的甚至输出 COLOR_FormatYUV420Flexible 。你如果直接按I420去读NV12的数据,画面颜色就会偏色。最稳的办法是用 RenderScript / OpenGL 去转换,或者自己去解析 Image / ByteBuffer 的plane信息,不要写死一种排布。

3.4 EOS处理与安全释放

EOS(End of Stream)是MediaCodec模型里一个容易出错的知识点。你在输入侧发送EOS后,解码器会继续输出缓冲区里缓存的帧,直到把最后一帧都吐出来,然后再输出一个带 BUFFER_FLAG_END_OF_STREAM 标志的空buffer。所以EOS不是同步的,它有一个“排空”过程。

开发播放器时,很多bug出现在这里:发送EOS后,立刻 stop() 再 release() ,导致屏幕最后几帧没渲染出来,画面直接闪一下黑屏。这不是故障,是代码时序错了。正确做法是:发送EOS后,继续跑输出循环,直到 dequeueOutputBuffer 返回带 BUFFER_FLAG_END_OF_STREAM 的buffer,再退出循环,再stop/release。

另外,如果你要中断解码(比如用户点了取消),直接 stop() 没问题,但同一个codec实例想复用来解码下一段视频时,要记得重新 configure() 。有的同学只调 start() ,结果在底层状态还没完全回到Configured时又start,直接收到 CodecException 。

4. 编码场景:把Camera帧塞给MediaCodec做硬编

4.1 编码器input颜色格式:首先确认YUV排列

编码器与解码器在buffer模型上几乎对称,区别在于编码器的输入是原始图像(YUV/RGB),输出是压缩码流。所以第一步要搞清楚:编码器希望收到什么颜色格式?

MediaCodec编码器常见的输入颜色格式包括:

  • COLOR_FormatYUV420Flexible (通用VU420,具体排列不固定)
  • COLOR_FormatYUV420SemiPlanar (NV12,最常见)
  • COLOR_FormatYUV420Planar (I420)

很多新手直接往编码器input buffer里塞一帧NV12数据,结果发现部分机型颜色偏绿或偏紫,就是因为这台机器的编码器不能直接吃NV12,它要的是I420,或反过来。查格式的方法是:

MediaCodecInfo codecInfo = codec.getCodecInfo();
MediaCodecInfo.CodecCapabilities caps = codecInfo.getCapabilitiesForType("video/avc");
int[] colorFormats = caps.colorFormats;

把这几个颜色格式打出来,跟摄像头输出的YUV格式比对一下,再决定要不要在中间加一次转换。Android相机最常用的YUV是 YUV_420_888 ,它可能是planar也可能是semi-planar,必须用 Image.getPlanes() 去解析,不能假设plane数量。

4.2 码率模式和关键帧间隔的参数设定

配置编码器时,除了MIME和宽高外,还有三个参数对最终效果影响很大。

第一是码率模式。MediaCodec支持三种模式:

模式 特点 适用场景
BITRATE_MODE_CQ 恒定质量,码率可变 离线转码、对体积不敏感
BITRATE_MODE_VBR 可变码率,动态分配码率 普通录制,追求画质与体积平衡
BITRATE_MODE_CBR 恒定码率,带宽稳定 直播、实时传输

我做过一个视频录制App,最初为了“省流”选了CBR+低码率,结果静止画面还好,画面一动就满屏马赛克。后来改成VBR,码率上限放宽,肉眼观感好了很多。如果走直播协议(比如RTMP),那就必须CBR,否则网络抖动会被放大。

第二是关键帧间隔。 setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1) 表示每隔1秒插一个关键帧。关键帧间隔太大会导致seek响应很慢——用户拖动进度条后,要等很久才能等到下一个I帧才能开始解码。但I帧特别大,频率太密又会拉高码率。一般本地录制设1~2秒,直播设1秒左右,内部可以配合码率控制器再动态插入。

第三是profile和level。比如H.264的高Profile(High Profile)有些老设备硬编不支持,强行设置会导致编码器start失败。稳妥的做法是先用 MediaCodecInfo 查询支持的 profileLevels ,再决定用Baseline/Main/High。

4.3 录制App的最小管线:ImageReader→编码器→MediaMuxer

一个最小可跑的录制管线大概是:

  1. 用 MediaCodec.createEncoderByType("video/avc") 创建编码器。
  2. 设置好MediaFormat,调用 configure ,最后一个flag传 CONFIGURE_FLAG_ENCODE 。
  3. 用 codec.createInputSurface() 创建一个输入Surface(这是编码器的输入入口)。
  4. 把这个Surface交给Camera2或CameraX作为预览目标之一。
  5. Camera每帧落到input Surface上后,编码器会自动消费,不需要手动填充input buffer。
  6. 编码器实时输出H.264码流,用 MediaMuxer 封装成MP4。

这里最关键的是第3步: createInputSurface() + Camera预览 的做法。它最大的优势是零拷贝——Camera的YUV数据直接进编码器的硬编通路,不经过CPU,省电也省内存。当初我以为一定要用ImageReader拿YUV再手动queue到编码器,后来才发现Surface输入模式是最优解。

如果要做滤镜,套路也类似:不用Camera直接target编码器的input surface,而是改用一个 SurfaceTexture 接收Camera预览,然后通过 OpenGL ES 把纹理画到一个EGL Surface上,这个EGL Surface再作为编码器的input surface。滤镜逻辑全在fragment shader里,效率非常高。

4.4 编码器输出的CSD和MediaFormat如何喂给Muxer

编码器工作后,第一次 dequeueOutputBuffer 返回 INFO_OUTPUT_FORMAT_CHANGED 时,会拿到一个包含编码器输出格式的MediaFormat,里面带着SPS/PPS(csd-0/csd-1)和宽高、颜色格式等信息。这个MediaFormat要原封不动传给 MediaMuxer.addTrack() :

MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
// 循环取编码输出
int outIndex = codec.dequeueOutputBuffer(info, 10_000);
if (outIndex >= 0) {
    ByteBuffer encodedData = codec.getOutputBuffer(outIndex);
    if (info.flags != 0) {
        // 首次拿到csd时,通过INFO_OUTPUT_FORMAT_CHANGED分支处理
    }
    muxer.writeSampleData(trackIndex, encodedData, info);
    codec.releaseOutputBuffer(outIndex, false);
} else if (outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
    MediaFormat newFormat = codec.getOutputFormat();
    trackIndex = muxer.addTrack(newFormat);
    muxer.start();
}

有人会问:为什么不能直接自己组装一个MediaFormat给Muxer?因为Muxer写MP4文件头时,需要知道准确的SPS/PPS,这个只能从编码器输出里拿。如果你拿不到csd,文件虽然能写出来,但播放器要么黑屏要么报“不支持的数据格式”。

另外,写文件时buffer的 presentationTimeUs 必须单调递增,否则MediaMuxer会直接抛异常。如果你自己生成PTS,务必保证它是微秒单位且严格递增。

5. 实测里最磨人的兼容性问题:从MediaCodecInfo查起

5.1 拿MediaCodecList做解码器/编码器探测

Android上“同一个版本系统,不同型号设备”的编解码器能力差异,远超你的想象。有的设备支持4K 60fps H.265硬解,有的设备只支持1080p 30fps H.264硬解。写死任何假设都可能在真机上翻车。

可靠的做法是运行时探测。先遍历 MediaCodecList ,找到支持指定MIME type的codec:

MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS);
for (MediaCodecInfo info : list.getCodecInfos()) {
    if (info.isEncoder()) continue;
    String[] types = info.getSupportedTypes();
    for (String type : types) {
        if (type.equalsIgnoreCase("video/avc")) {
            // 这就是一个可以解码H.264的codec
        }
    }
}

拿到codec后,再通过 MediaCodecInfo.VideoCapabilities 查询分辨率、帧率上限等,确认目标设备能不能跑你预定的分辨率。比如:

MediaCodecInfo.CodecCapabilities caps = info.getCapabilitiesForType("video/avc");
MediaCodecInfo.VideoCapabilities vc = caps.getVideoCapabilities();
vc.isSizeSupported(1920, 1080);      // 是否支持1080p
vc.getSupportedFrameRatesFor(1920, 1080); // 拿到帧率范围

这里有个容易忽略的点: isSizeSupported() 并不能完全代表实际编码性能。有些设备返回支持4K,但实际跑到4K时帧率只有个位数。最好再结合码率和帧率范围做一次估算,必要时直接限制分辨率。

5.2 同一码流不同设备表现不一致的排查思路

我遇到过一个很典型的案例:同一段H.264码流,在A设备上解码流畅、画面正常;在B设备上直接黑屏,logcat里刷 CodecException: Error 0x80000000 。那段码流是我自己用x264压的,参数比较激进,用了 high profile + 8x8 DCT + 多参考帧 。问题根源就在B设备的硬解码器不支持这个profile的某些特性。

排查思路其实就三步:

第一步,确认选中的是硬解还是软解。通过 dumpsys media.player 或者logcat里的codec名称判断,如果名字里带 c2.android.* 多半是软解,带 c2.qti.* 、 OMX.MTK.* 等厂商名字多半是硬解。

第二步,对比支持的profile和level。找到设备支持的 profileLevels ,把码流的SPS解析出来对比。解析SPS不需要自己写二进制解析,用MediaExtractor拿 csd-0 ,再配合 MediaFormat 的 KEY_PROFILE / KEY_LEVEL 查看。不满足就把码流降到 Baseline profile 再试,一般就能解。

第三步,实在查不到就强制软解。系统上一般存在 c2.android.avc.decoder 这个软件解码器,遇到硬件不兼容时降级用软解,功能上没问题,代价是CPU占用高、耗电增加。所以在做播放器时,一定要预留软解降级开关,不能把硬解当成“必须成功”的路径。

5.3 PTS抖动和花屏:两个真实的坑

PTS(Presentation Time Stamp)是另一个大坑。理论上,每帧输入都给一个微秒级的时间戳,按时间顺序递增地喂给解码器就行。但实际开发中很多播放器把PTS计算错成了“每条线程各自用 System.nanoTime() 减去某个绝对时间”,结果不同来源的时间戳起点不一致,导致播放器渲染节奏紊乱,看起来就是“音画逐渐不同步”。

我在做Camera录制的编码器时也踩过:直接用 System.nanoTime()/1000 作为PTS,结果画面倒是能编码,但封装到MP4后播放时,画面明显比声音快或慢。后来改成“以首帧时间为0,后续帧按帧间隔累加”的方式,问题立刻消失。原因是编码器/封装器期望PTS是一个相对连续的线性递增序列,而不是一个绝对时间戳。

花屏则常见于seek处理不完善。比如用MediaExtractor直接 seekTo() 到某个时间点,再往解码器里喂数据。虽然extractor会seek到前面最近的关键帧,但如果播放器直接把seek后解出来的第一批帧渲染上屏,用户就会看到“从关键帧到目标时间点之间的碎画面”一闪而过。正确做法是“解码但先不渲染”——继续解,直到 presentationTimeUs >= 目标时间点 ,才允许 releaseOutputBuffer(..., true) 。

5.4 低延迟与性能优化:能Surface就不ByteBuffer

最后说性能。很多低端设备上,同一个1080p视频用Surface模式解码能稳定60fps,改用ByteBuffer模式后就掉到30fps,还发热严重。差别主要出在拷贝次数上:Surface模式解码器直接把画面输出到GPU纹理,ByteBuffer模式要把解码后的数据从硬件内存复制到Java堆,再复制一次给渲染器,整条链路的带宽开销翻倍。

所以我的原则是:解码输出能走Surface绝不走ByteBuffer;编码输入能走 createInputSurface() 绝不走ImageReader+手动queue。只有在必须拿到像素数据做算法处理的场景(滤镜、人脸、裁剪、缩略图),才考虑ByteBuffer/ImageReader,而且能复用buffer就复用,不要在每帧里new对象。

另外,设置 MediaFormat.KEY_LOW_LATENCY (API 30+)可以降低编码器/解码器的内部缓冲延迟,对实时通信类应用很有用。但它会牺牲一点压缩效率,离线转码就别开了。

6. 写给自己的MediaCodec调优记录

最后分享几条我自己的实操经验,就当是备忘录了。

第一,调试MediaCodec问题时,logcat的Tag非常重要。系统层的 MediaCodec 、 ACodec 、 Codec2 、 NuPlayer 、 OMX 这些Tag都要开着。看到 SoftAVC 说明在用软解,看到 c2.qti 、 OMX.MTK 之类的说明在用硬解。很多诡异问题,光靠看这两个Tag就能定位一半。

第二,发生 CodecException 时不要把异常打印一下就算了,要把它携带的错误码和vendor message一起记录下来。同一段码流在不同设备上抛出的错误码不一样,这些信息对后续复现和定位非常关键。我们一般会在崩溃上报和日志系统里专门留一个media字段,存codec name、错误码、当前输入帧数这些。

第三,写demo时先跑最简单的通路,比如 MediaExtractor → MediaCodec → Surface 播放一段官方测试视频,通了再叠加自己的功能。很多人喜欢一开始就把滤镜、倍速、多音轨全接上去,结果出了问题根本不知道是哪一层坏了。我是把解码链路拆成模板方法,每次加一层,就单独验证一层。

第四,MediaCodec官方API文档其实写得很全,只是不好读。推荐去翻Android源码里的 frameworks/av/media/mediacodec 相关代码,尤其是 MediaCodec.java 的注释块,很多边界行为在文档里没有,注释里反而说清楚了。

这些年用下来,MediaCodec虽然坑多,但只要把它的状态机、buffer流转、厂商差异这三件事想明白,它反而是Android上最值得信任的媒体组件。你现在踩到的那些花屏、黑屏、不同步,基本都能从上面这几条里找到答案。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐