一、硬解码与软解码:MediaCodec基础、硬解码流程、软解码(FFmpeg)集成、解码器选择策略
各位同学,今天我们来聊聊视频渲染中一个绕不开的话题——解码。说白了,就是把压缩过的视频数据,还原成我们能直接拿去渲染的原始图像帧。
解码这事儿,在Android上主要有两条路:硬解码和软解码。我刚开始做视频播放器的时候,也在这两条路上踩过不少坑。今天就把我的经验掰开揉碎了讲给你听。
1.1 MediaCodec基础
MediaCodec是Android官方提供的音视频编解码API。它从Android 4.1(API 16)开始就有了,到了Android 5.0以后基本成熟稳定。
我个人习惯把MediaCodec理解成一个「黑盒子」——你往里面塞压缩数据,它给你吐出原始数据。这个黑盒子的内部实现,可能是硬件加速的,也可能是软件模拟的,但API层面你不需要关心。
核心概念:
- InputBuffer:你往里放压缩数据(比如H.264的NAL单元)
- OutputBuffer:它往外吐原始数据(一般是YUV或RGB格式)
- Codec状态机:Stopped → Executing → Flushed → End-of-Stream
MediaCodec的工作模式有两种:
- 同步模式:dequeueInputBuffer/dequeueOutputBuffer 阻塞等待。适合简单场景。
- 异步模式:通过Callback回调通知。我建议你用这个,性能更好,也不容易卡UI线程。
// 创建一个解码器,以H.264为例
MediaCodec codec = MediaCodec.createDecoderByType("video/avc");
// 配置解码器
MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
codec.configure(format, surface, null, 0);
// 启动
codec.start();
// 循环处理输入输出
while (!isEOS) {
int inputIndex = codec.dequeueInputBuffer(TIMEOUT_US);
if (inputIndex >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(inputIndex);
// 把压缩数据塞进去
int size = readSampleData(inputBuffer);
codec.queueInputBuffer(inputIndex, 0, size, presentationTimeUs, flags);
}
int outputIndex = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US);
if (outputIndex >= 0) {
// 如果是Surface模式,数据已经渲染到Surface上了
codec.releaseOutputBuffer(outputIndex, true);
}
}
小提示:如果你用Surface模式,OutputBuffer里的数据会自动渲染到Surface上,你不需要手动处理YUV数据。但如果你需要拿到原始帧做滤镜或截图,就得用ByteBuffer模式。
1.2 硬解码流程
硬解码,说白了就是让GPU或专门的硬件模块来干活。Android上硬解码的典型路径是这样的:
- 数据源:从网络或本地文件读取压缩数据
- 解封装:用MediaExtractor把容器格式(MP4、TS等)拆成音视频流
- 送入解码器:把视频流塞给MediaCodec
- 输出渲染:解码后的帧直接输出到Surface,由SurfaceFlinger合成显示
我在项目中遇到过一个问题:有些低端手机硬解码H.265(HEVC)会直接崩溃。后来查了文档才发现,Android 5.0以下对HEVC的支持是「可选」的,不是所有芯片都实现了。
避坑指南:我曾经在某个项目里,直接硬解码4K视频,结果在小米某款手机上黑屏。后来发现是MediaCodec的Surface模式对分辨率有限制。解决办法是:先查询CodecCapabilities,确认支持再走硬解。
// 检查设备是否支持硬解某个格式
MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
String codecName = codecList.findDecoderForFormat(format);
if (codecName != null) {
// 有硬解能力
MediaCodecInfo codecInfo = codecList.getCodecInfoForName(codecName);
if (!codecInfo.isHardwareAccelerated()) {
// 虽然是找到了,但可能是软解实现
// 需要进一步判断
}
}
1.3 软解码(FFmpeg)集成
硬解码虽然快,但兼容性是个大问题。你想想看,Android设备千奇百怪,有的芯片不支持某些编码格式,有的驱动有bug。这时候就得靠软解码兜底了。
FFmpeg是目前最主流的软解方案。它支持几乎所有你能想到的编码格式:H.264、H.265、VP9、AV1……
在Android上集成FFmpeg,通常有两种方式:
- 预编译so库:从网上下载编译好的FFmpeg库,直接集成到项目中。省事,但体积大。
- 自己交叉编译:用NDK工具链编译FFmpeg源码,可以裁剪掉不需要的模块,减小体积。
我个人习惯用第二种方式。虽然麻烦一点,但能精确控制库的大小。比如我只解码H.264和H.265,那就只保留这两个解码器,其他全去掉。
// FFmpeg解码的核心流程(伪代码)
AVFormatContext *fmt_ctx = avformat_alloc_context();
avformat_open_input(&fmt_ctx, file_path, NULL, NULL);
avformat_find_stream_info(fmt_ctx, NULL);
// 找到视频流
int video_stream_index = -1;
for (int i = 0; i < fmt_ctx->nb_streams; i++) {
if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_index = i;
break;
}
}
// 打开解码器
AVCodec *codec = avcodec_find_decoder(fmt_ctx->streams[video_stream_index]->codecpar->codec_id);
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[video_stream_index]->codecpar);
avcodec_open2(codec_ctx, codec, NULL);
// 循环解码
AVPacket *pkt = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
while (av_read_frame(fmt_ctx, pkt) >= 0) {
if (pkt->stream_index == video_stream_index) {
avcodec_send_packet(codec_ctx, pkt);
while (avcodec_receive_frame(codec_ctx, frame) >= 0) {
// frame->data 里就是YUV数据
// 可以转成RGB,或者直接渲染到Surface
}
}
av_packet_unref(pkt);
}
注意:FFmpeg解码出来的数据默认是YUV420P格式。如果你要渲染到Surface上,需要先转成RGBA,或者用OpenGL的YUV着色器直接渲染。我一般用后者,省一次内存拷贝。
1.4 解码器选择策略
好了,现在硬解码和软解码你都会了。那问题来了:到底用哪个?
我的经验是:能硬解就硬解,硬解不行再软解。但具体怎么判断「不行」呢?
我总结了一套策略:
- 先查CodecCapabilities:看设备是否声明支持目标编码格式
- 试解码第一帧:如果MediaCodec返回错误,或者长时间没有输出,就切软解
- 监控解码耗时:如果单帧解码时间超过33ms(30fps),说明硬解性能不够,切软解
- 特殊格式兜底:比如AV1、VP9这些,很多老设备不支持硬解,直接走FFmpeg
我的推荐策略:
| 场景 | 推荐方案 |
|---|---|
| 主流格式(H.264/H.265) | 优先硬解,降级软解 |
| 小众格式(VP9/AV1) | 直接软解 |
| 低端设备(2GB RAM以下) | 硬解为主,软解容易卡顿 |
| 需要截图/滤镜 | 软解(方便获取原始帧数据) |
我曾经在一个直播项目里,遇到过一个奇葩问题:某款华为手机硬解H.264时,每隔几秒会跳一帧。查了半天,发现是MediaCodec的缓冲区设置问题。后来我加了一个「解码器健康检查」的机制——如果连续3帧解码时间超过50ms,就自动切到FFmpeg软解。
嗯,这里要注意:软解虽然兼容性好,但CPU占用高,发热也厉害。所以我的策略是:硬解为主,软解兜底,动态切换。
最后总结一下:硬解码和软解码不是二选一的关系,而是互补的。你需要在项目中实现一个「智能解码器选择器」,根据设备能力、视频格式、性能指标动态切换。这样用户才能获得最好的播放体验。
核心要点回顾:
- MediaCodec是Android官方解码API,支持硬解和软解
- 硬解性能好,但兼容性差;软解兼容性好,但CPU占用高
- FFmpeg是软解的首选方案,支持几乎所有编码格式
- 解码器选择策略:能硬解就硬解,不行再软解,动态切换