做嵌入式音视频开发的人,几乎躲不开Rockchip平台上的MPP(Media Process Platform)库。这篇文章要解决的问题很具体:怎么用MPP把一根H264编码的视频流解出来,最终拿到YUV裸数据并落成文件。适用对象包括刚上手Rockchip开发板、想把视频解码从软解换成硬解、或者在做低功耗视频采集与显示链路的工程师。我会把整个流程从编译库、理解核心数据结构,到线程模型、packet发送、帧获取,再到YUV格式与Qt显示路线的完整逻辑写清楚。读完后你能直接照着搭出一个可跑的H264硬解demo。
1. 硬解H264,为什么非Rockchip MPP不可
1.1 H264编码的核心结构:从SPS/PPS到NAL单元的旅程
在做解码之前,我建议先花十分钟把H264码流结构捋一遍,不然后面遇到花屏、宽高错误、解码不出帧这类问题,你会完全摸不着头脑。
H264码流的基本单位是NAL单元(Network Abstraction Layer Unit)。每个NAL单元一般以起始码开头,常见的是00 00 00 01,少部分场景用00 00 01。起始码之后紧跟一个字节的NAL Header,低5位就是NAL Type。我们最关心的几种类型是:
- SPS(Sequence Parameter Set,NAL Type 7):序列参数集,里面记录了解码需要的宽、高、帧率、参考帧数量等关键信息。解码器必须先拿到SPS才能开始解。
- PPS(Picture Parameter Set,NAL Type 8):图像参数集,记录熵编码方式、片组划分等更细的参数。
- IDR帧(Instantaneous Decoder Refresh,NAL Type 5):关键帧,也叫I帧,解码器看到IDR就知道可以刷新所有参考帧状态,从这个位置开始解必能出完整画面。
- 非IDR的Slice(NAL Type 1或2):普通图像帧,可能是P帧也可能是B帧,解码依赖前面的帧。
硬件解码器在工作前,同样需要这些参数。好消息是MPP内置了split解析,可以自动从裸码流里切出NAL单元并识别头部信息,我们不需要像做协议栈那样自己一字节一字节去解析。但你至少要理解:正确码流的SPS/PPS一定会在第一个IDR帧之前出现。我踩过最典型的坑,就是从视频中间截了一段数据喂给解码器,结果解码器一直不输出帧,查了半天才发现是SPS缺失。
NAL里面装的压缩数据是怎么变成图像的?简单说,H264用帧内预测和帧间预测大幅去掉空间和时间冗余,剩下的残差再过DCT变换、量化、熵编码。这些运算量极大,尤其在高分辨率高帧率场景下,CPU软解经常撑不住,这就引出了硬解的必要性。
1.2 软解与硬解的差距,MPP到底帮你省了什么
软解的意思是用CPU的通用算术单元去算反变换、反量化、运动补偿和环路滤波,每一步都是实打实的整数/浮点计算。拿一块主频1.8GHz左右的ARM Cortex-A55来算,软解1080P H264 30fps时CPU占用能跑到80%以上,如果同时还要跑Qt界面、网络传输,系统基本就卡成幻灯片了。而4K H264软解在多数嵌入式芯片上根本跑不动。
硬解则不同。Rockchip芯片内部有一块专门的视频解码模块,常见叫VDPU(Video Decoder Processing Unit)或VPU,本质是一颗专用硬件加速器。用户态通过MPP库把压缩码流和参数下发,内核驱动负责把任务配置到硬件寄存器上,硬件完成后把YUV数据写进内存。整个过程CPU只需要做控制和搬运,解码计算几乎不占主核。实测在RK3568上,一路1080P H264 30fps硬解,CPU占用通常只有5%左右,整机功耗也能压住。
MPP就是这个过程的用户态统一入口。它把不同平台的差异封装在库里,对外提供一套接近C语言风格的API。只要代码写得规范,从RK3399换到RK3568再换到RK3588,基本不用改业务逻辑。这也是我建议新手直接学MPP而不是去翻VDPU寄存器手册的原因,硬件抽象和内存管理这些脏活累活,MPP已经替你做掉了。
2. 环境准备:编译MPP库与理解关键数据结构
2.1 本地编译、交叉编译与工具链选择
MPP官方仓库在GitHub上,仓库名是rockchip-linux/mpp。拿到代码后第一步是编译,这一步看似简单,坑其实不少。
如果你是在开发板上直接编译,那最简单,确认系统里有cmake和gcc就行:
git clone https://github.com/rockchip-linux/mpp.git cd mpp mkdir build && cd build cmake .. make -j4 sudo make install编译产物会生成两个关键库文件:librockchip_mpp.so(主库)和librockchip_mpp_vepu.so(编码相关)。解码只依赖主库,链接的时候写-lrockchip_mpp就够了。
如果你的目标平台是ARM开发板,但习惯在x86主机上开发,那就需要交叉编译。Rockchip官方提供了build/cmake下的交叉编译脚本,比如arm.linux.cross.cmake。使用方式:
cd mpp/build cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/arm.linux.cross.cmake \ -DCMAKE_INSTALL_PREFIX=$PWD/install \ .. make -j8交叉编译有几个细节要注意。工具链路径如果不在系统PATH里,要在cmake文件里改CMAKE_C_COMPILER的绝对路径。Rockchip各平台ARMv7和ARMv8不同,AArch64的板子就要用aarch64的工具链,否则编译出的库根本跑不起来。这里我建议优先用开发板配套的SDK里自带的MPP源码,比如Buildroot或Debian SDK里通常已经有编译好的版本,能省不少事。
编译完成后,把librockchip_mpp.so拷贝到板子的/usr/lib或应用同目录,并确保/usr/lib路径能被动态链接器找到。最简单的验证方式:
ldconfig -p | grep rockchip_mpp能看到输出,说明库已经可以被链接。
2.2 核心API与数据结构速查:MppCtx、MppApi、MppPacket、MppFrame
MPP的API设计比较直接,核心抽象就这么几个,先建立概念再写代码会顺手很多。
MppCtx:解码器上下文句柄,代表一个解码实例。一个视频解码器一个ctx。MppApi:操作接口集合,是个结构体,里面装着decode_put_packet、decode_get_frame、control这些函数指针。拿到ctx之后通过mpi->xxx()调用。MppPacket:输入数据包,用来包装要送给解码器的H264压缩码流。可以理解成一个"装着数据的快递盒",盒子里有数据指针、长度、时间戳这些信息。MppFrame:输出帧,解码器产出的YUV图像数据都挂在它上面。通过mpp_frame_get_buffer拿到MppBuffer,再通过mpp_buffer_get_ptr拿到可读的内存起始地址。MppBuffer:MPP管理的内存块。如果你自己用malloc分配内存给解码器用,性能和兼容性都会打折扣,MPP内部推荐用自己管理的buffer池,这一点后面细说。
还有一个比较重要的配置结构体是MppDecCfg,它用于在创建解码器时设置参数,比如split解析开关、参考缓冲数量、是否禁用错误帧等。常用方式是通过mpp_dec_cfg_init初始化后,用mpp_dec_cfg_set_u32设置键值,再通过mpi->control下发。
代码里最常见的一段初始化长这样:
MppCtx ctx = NULL; MppApi *mpi = NULL; MppDecCfg cfg = NULL; mpp_create(&ctx, &mpi); mpp_dec_cfg_init(&cfg); mpp_dec_cfg_set_u32(cfg, "base:split_parse", 1); mpp_init(ctx, MPP_CTX_DEC, MPP_VIDEO_CodingAVC); mpi->control(ctx, MPP_DEC_SET_CFG, cfg);初始化完成后,ctx和mpi就绑定了解码器状态。MPP_VIDEO_CodingAVC就是H264的编码类型常量,对应值为7。后面所有操作都通过mpi里的函数指针发起。
3. H264文件解码到YUV的完整实现
3.1 模块划分与线程模型设计
把解码做成一个真正可用的模块,而不是跑一次就结束的测试脚本,第一步就是设计线程模型。
最简单的单线程模型长这样:读文件、发packet、循环取帧、处理帧数据、直到EOS。这种模式适合验证功能,跑短视频没问题,但一旦码流大、帧率高,就会出现"取帧阻塞发送"的情况,整体吞吐上不去。MPP官方测试工具mpi_dec_test里,其实保留了一个更推荐的模式:生产者和消费者分离。
生产者线程负责从文件或网络读取码流,切成合理的packet喂给解码器。消费者线程单独调用decode_get_frame把解码结果取出来处理。MPP的接口对这两个操作是做了并发保护的,允许put和get在不同线程同时执行,这也是官方推荐的用法。两个线程之间不需要额外加锁,因为解码器内部已经管理了任务队列和buffer。
实际项目中我建议再加一个管理线程做状态控制和资源回收,比如告诉生产者停止、检查解码错误、释放buffer。解码模块对外暴露的就三个接口:初始化、写入数据、取输出帧。这样上层逻辑很干净,后续接RTSP、接摄像头都方便。
线程模型定下来之后,内存管理策略也要想清楚。解码器内部会维护buffer池,输出帧如果不及时归还,池子会被占满,解码就会卡住。所以消费者线程拿到帧之后要尽快处理,处理完调用mpp_frame_deinit归还帧资源,而不是无脑累积。
3.2 解码器创建和初始化细节
创建解码器的完整代码我先贴出来,再解释每一处关键点:
#include "rk_mpi.h" #include "mpp_buffer.h" #include "mpp_packet.h" #include "mpp_frame.h" #include "mpp_dec_cfg.h" #define MAX_READ_SIZE (1024 * 1024) static MppCtx ctx = NULL; static MppApi *mpi = NULL; int decoder_init(void) { MPP_RET ret; MppDecCfg cfg = NULL; ret = mpp_create(&ctx, &mpi); if (ret != MPP_OK) { printf("mpp_create failed\n"); return -1; } ret = mpp_init(ctx, MPP_CTX_DEC, MPP_VIDEO_CodingAVC); if (ret != MPP_OK) { printf("mpp_init failed\n"); return -1; } mpp_dec_cfg_init(&cfg); mpp_dec_cfg_set_u32(cfg, "base:split_parse", 1); ret = mpi->control(ctx, MPP_DEC_SET_CFG, cfg); if (ret != MPP_OK) { printf("set cfg failed\n"); return -1; } return 0; }mpp_init时指定编码类型为MPP_VIDEO_CodingAVC,这里不能传错。MPP_VIDEO_CodingAVC对应H264,MPP_VIDEO_CodingHEVC对应H265,MPP_VIDEO_CodingVP9对应VP9。
base:split_parse这个配置是我强烈建议开启的。它的作用是让MPP自己从码流里识别NAL起始码,自动把数据切片成帧。如果不开启,你必须自己保证每次发送的MppPacket恰好是完整的一帧数据,否则解码器很可能报错或输出烂帧。开了split_parse之后,哪怕你每次只读几KB塞进去,MPP也会自己凑齐一个完整帧才开始解码,这在处理网络流或者文件流时省了大量工作。
初始化的control调用里,MPP_DEC_SET_CFG负责把配置下发。除了split_parse,还可以配置base:pre_alloc_buf_num(预分配参考帧buffer数量)等参数。记住一点:配置尽量早做,放在第一帧发送之前,否则某些字段可能不生效。
3.3 文件读取与packet发送:流式喂数据,不要一口吃成胖子
很多MPP示例代码喜欢一次性把整个文件读进内存再发出去,这种方式在demo里没问题,但放到真实项目里就是个隐患。一个100MB的文件一次read进内存,嵌入式设备本来内存就紧张,再加上解码器内部buffer开销,很可能直接OOM。所以我推荐流式读取,一次读一小块,边读边发。
发送数据的核心函数是mpi->decode_put_packet。它接收一个MppPacket,每个packet代表一段码流数据。流程是先把数据放进MppBuffer,再用mpp_packet_init_with_buffer把buffer包装成packet,设置好长度后发送。
MppBuffer pkt_buf = NULL; MppPacket packet = NULL; uint8_t *pkt_ptr = NULL; size_t read_len = fread(tmp_buf, 1, MAX_READ_SIZE, fp_in); if (read_len <= 0) break; // 从MPP buffer池申请一块内存 mpp_buffer_get(NULL, &pkt_buf, read_len); pkt_ptr = mpp_buffer_get_ptr(pkt_buf); memcpy(pkt_ptr, tmp_buf, read_len); // 包装成packet mpp_packet_init_with_buffer(&packet, pkt_buf); mpp_packet_set_length(packet, read_len); mpp_packet_set_pts(packet, pts); // 发送给解码器 mpi->decode_put_packet(ctx, packet); // 发送完可以立即回收buffer mpp_packet_deinit(&packet); mpp_buffer_put(pkt_buf);这里有个容易被忽略的细节:mpp_buffer_get(NULL, &pkt_buf, size)的第一个参数传NULL,表示让MPP从它自己管理的默认buffer组里分配。刚接触时容易直接用malloc分配内存再填数据,这在某些平台上会导致解码器无法访问内存,出现奇怪的花屏或崩溃。正确做法是始终用MPP的buffer接口。
mpp_packet_set_pts设置的是显示时间戳。如果数据来源是文件,你可以按帧率估算一个pts;如果来源是摄像头或网络协议,直接从封装层取真实时间戳。MPP解码默认不会自动填充时间戳,需要上游传入。如果只是做离线的YUV转储,pts填0也行。
发送完一段数据后,packet和buffer可以立刻释放。MPP内部已经把数据拷贝到自己的解码buffer里了吗?并不是所有情况都拷贝,但如果解码器正在使用这个buffer,MPP内部有自己的引用计数管理,你主动归还buffer并不会导致正在解码的数据失效。这一点比我预想的要安全,可即便如此,我依然建议你在发送完并确认没用到之后立刻归还,避免buffer池被耗尽。
文件读到末尾后,需要发送一个EOS标记,告诉解码器流结束了。方法是用mpp_packet_set_eos(packet, 1)发一个空数据包,或者在最后一个包上设置EOS。不设置EOS的话,消费者线程会永远阻塞在decode_get_frame上,等到超时才发现问题。
3.4 输出帧获取与YUV数据落盘
消费者线程的核心循环是decode_get_frame。它和decode_put_packet成对出现,一个负责往里塞数据,一个负责往外取结果。
int decode_output_loop(FILE *fp_out) { MppFrame frame = NULL; MPP_RET ret; while (1) { ret = mpi->decode_get_frame(ctx, &frame); if (ret != MPP_OK) { printf("decode_get_frame failed: %d\n", ret); break; } if (frame == NULL) { // 缓冲池暂时没有输出帧,继续等待 usleep(500); continue; } // 分辨率变化时的处理 if (mpp_frame_get_info_change(frame)) { uint32_t w = mpp_frame_get_width(frame); uint32_t h = mpp_frame_get_height(frame); printf("resolution changed: %dx%d\n", w, h); // 根据新分辨率调整输出buffer } // 判断是否结束 if (mpp_frame_get_eos(frame)) { mpp_frame_deinit(&frame); break; } // 获取YUV数据 MppBuffer frame_buf = mpp_frame_get_buffer(frame); if (frame_buf) { uint8_t *base = mpp_buffer_get_ptr(frame_buf); uint32_t width = mpp_frame_get_width(frame); uint32_t height = mpp_frame_get_height(frame); uint32_t hor_stride = mpp_frame_get_hor_stride(frame); uint32_t ver_stride = mpp_frame_get_ver_stride(frame); // 按stride写文件,不是按width/height size_t y_size = hor_stride * ver_stride; size_t uv_size = y_size / 2; fwrite(base, 1, y_size, fp_out); fwrite(base + y_size, 1, uv_size, fp_out); printf("saved frame: %ux%u, stride %ux%u, pts %lld\n", width, height, hor_stride, ver_stride, mpp_frame_get_pts(frame)); } mpp_frame_deinit(&frame); } return 0; }mpp_frame_deinit这个地方极其重要。MPP的buffer池有限,每取出一帧如果不归还,很快池子就满了,后面解码器不再输出新帧。我发现很多人第一次写都会漏掉这一步,然后来问为什么解码几十帧后卡死。如果帧数据需要长期使用,正确做法是把YUV数据拷贝到自己的内存里,再归还frame,而不是保留frame不放。
判断EOS这一步也别漏。如果不判断EOS,文件读完后消费者线程可能一直空转,frame永远返回NULL,程序看起来就像卡死了。
最终输出的YUV文件是裸数据,没有文件头,也没有宽高信息。后续要用ffplay查看时,必须手工指定宽高和格式:
ffplay -f rawvideo -pixel_format nv12 -video_size 1920x1080 output.yuv4. YUV数据格式细节与Qt5.15显示链路
4.1 NV12布局与stride对齐问题
MPP硬解H264默认输出格式一般是NV12,也就是YUV420SP的一种。NV12的内存布局是:一整块Y平面,紧跟着一整块交错排列的U和V。UV是交错存放的,每两个像素共用一对UV,所以叫"半平面"布局。
内存布局可以画成这样:
- Y平面:
hor_stride * ver_stride字节,每个像素一个字节,取值0~255。 - UV平面:
(hor_stride / 2) * (ver_stride / 2) * 2字节,也就是每两个水平像素共用一对UV,每两行像素共用一行UV。
很多人在这一步踩坑,原因是不理解stride。解码器输出的hor_stride和ver_stride不一定是实际分辨率的宽高,而是硬件对齐后的值。比如实际1920x1080的画面,hor_stride可能是1920,ver_stride可能是1088,因为硬件按16像素对齐。你如果按1920x1080去读Y平面,读出来的数据是对的,但如果按1080去计算UV平面起始地址,就会错位,直接导致画面底部出现一条花屏带。
我写文件时坚持用hor_stride * ver_stride作为Y平面的实际大小,用这个值再除以2得到UV平面大小。如果你希望最终得到紧凑且无对齐的YUV文件,可以在拷贝时逐行把数据从stride宽度截断到实际宽度,代码如下:
for (uint32_t row = 0; row < height; row++) { memcpy(dst + row * width, src + row * hor_stride, width); }UV平面同理,按行拷贝时宽度是width / 2,行数是height / 2,步长是hor_stride / 2。处理完之后文件就能直接用-video_size 1920x1080播放了。
4.2 从YUV到可视画面的三种转换路线
拿到YUV裸数据后,下一步通常是要显示。常见场景是在嵌入式Qt5.15界面上实时预览视频,这时候有几条路线可以选。
路线一,用libyuv软件转换。libyuv是Google开源的图像缩放和格式转换库,性能比手写循环好得多。用法是调用NV12ToI420或者NV12ToARGB把YUV转成RGB,再用QImage包一层交给Qt绘制。
#include "libyuv.h" // NV12 -> BGRA,方便QImage使用 uint8_t *dst_argb = (uint8_t *)malloc(width * height * 4); NV12ToARGB(y_ptr, hor_stride, uv_ptr, hor_stride, dst_argb, width * 4, width, height); QImage img(dst_argb, width, height, QImage::Format_ARGB32);软件转换的优点是简单、跨平台、无硬编码依赖,缺点是RGB转换本身也要算不少乘法和加法。在较低端的Cortex-A7平台,1080P转换一帧可能耗时十几毫秒,连续解码就来不及了。
路线二,用Rockchip RGA硬件转换。RGA是Rockchip的2D图形加速单元,可以做格式转换、缩放、旋转。它把YUV转RGB的运算搬到硬件上,CPU只负责提交任务。MPP和RGA在同一个SDK里共存,接口比较统一。这种方式是我在正式项目里的首选,帧率再高都不怕。
路线三,用OpenGL着色器做实时转换。把NV12分成Y纹理和UV纹理上传到GPU,在fragment shader里直接做矩阵运算,把YUV映射成RGB再绘制。Qt5.15里可以用QOpenGLWidget配合自定义shader实现,性能同样很高,而且避免了GPU->CPU->GPU的拷贝。缺点是代码量最大,调试门槛也高,适合对渲染链路有较高要求的团队。
三种路线我用一个表格总结一下:
| 方案 | 实现成本 | 性能 | 适用场景 |
|---|---|---|---|
| libyuv软件转换 | 低 | 中等,CPU参与计算 | 快速验证、低帧率预览 |
| RGA硬件转换 | 中 | 高,不占CPU | 量产产品、高帧率显示 |
| OpenGL shader转换 | 高 | 很高,GPU参与 | 需要叠加OSD、动效的场景 |
Qt5.15环境下,如果只是简单预览,直接走libyuv就够了;如果是产品化,我强烈建议用RGA,整体链路省心很多。
5. 排障实录与性能优化笔记
5.1 解码异常与花屏问题速查
我在不同平台、不同码流上跑了很久MPP,把最常碰到的问题整理成一张速查表,可以直接当排查手册用。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
一直取不到帧,decode_get_frame返回空 | SPS/PPS缺失或错误 | 检查文件是不是从流中间截取的;先发SPS/PPS再发IDR |
| 前几帧正常,后面花屏 | 输入码流有丢包或损坏 | 开启split_parse;发送时不要拆散帧数据 |
| 画面下方有彩条 | stride和width没分清 | 写文件或拷贝时使用hor_stride * ver_stride |
| 颜色整体偏绿或偏紫 | YUV格式理解错误 | 确认输出是NV12还是I420;NV12是UV交错,I420是三个平面 |
| 解码几十帧后卡死 | 没有调用mpp_frame_deinit归还帧 | 确保每帧处理完都归还buffer |
程序崩溃在mpp_buffer_get_ptr | buffer被提前释放 | 检查发送packet后是否过早释放了源buffer |
| 分辨率变化后画面错乱 | 没有处理info_change事件 | 检测mpp_frame_get_info_change并重建输出逻辑 |
mpp_init返回失败 | 平台不支持该编码格式 | 确认芯片VDPU支持H264;检查MPP_VIDEO_CodingAVC宏定义 |
这里面最容易忽略的是EOS问题,我单独说一下。文件读完后如果不发EOS标记,消费者线程可能永远得不到退出信号。但EOS也不要乱发,mpp_packet_set_eos(packet, 1)必须放在最后一个packet之后单独发送,或者放在最后一个packet上。有几次我在发送中途不小心设了EOS,结果解码器提前结束,后面的帧全部丢弃。
另一个值得提醒的坑是pts。如果你的H264数据是从MP4等容器里解封装出来的,时间戳不一定从0开始,且存在DTS/PTS两个时间戳。MPP的packet只接收PTS,送错时间戳会导致后续显示顺序错乱。离线转YUV文件时影响不大,但做实时播放就必须处理。
5.2 性能优化方向与实测心得
解码性能瓶颈不一定在解码器本身,有时候是内存和显示环节拖了后腿。我在RK3568上做过一轮优化,记录几个关键心得。
内存分配是第一优先优化点。MPP官方推荐用内部buffer组管理内存,因为硬件解码器通过IOMMU/DMA访问内存,普通用户态malloc出来的内存可能需要额外映射,效率低且可能失败。尽量做到buffer复用,不要每帧都重新分配再释放。比如输入侧用固定的几个buffer轮转,输出侧用固定大小的YUV缓冲区。
第二点是减少无谓拷贝。解码器输出的YUV数据,如果是给软件做算法处理,没办法必须拷贝;如果只是显示,尽量走零拷贝路线。Rockchip平台上,MPP buffer和RGA、DRM显示框架之间可以做buffer共享,避免YUV先拷进用户态再拷给显示层。这个优化做下来,1080P 60fps的链路延迟能降低将近一半。
第三点是控制消费者线程的处理时间。decode_get_frame返回的帧必须在下一帧解码完成前处理完,否则buffer池会积压。我习惯在消费者线程里只做"取帧、快速处理、归还"三步,复杂的图像算法放到独立线程,用队列衔接,避免拖慢解码主链路。
在视频尺寸上也有讲究。硬解H264对分辨率的适配很宽,但某些分辨率会触发不必要的内部缩放。比如1920x1080和1920x1088这样细微的差异,解码性能几乎没有区别,但如果你自己做了非对齐的裁剪,反而增加额外开销。所以编码端尽量用标准分辨率,MPP省事,后面显示也省事。
还有一个很多人没意识到的点:H264码流Level和RefFrame数量直接影响硬解性能。如果码流里reference frame数量大于芯片支持的最大值,硬解会出现"能解但很慢"或偶发花屏的情况。这时候最简单的办法是在编码端限制参考帧数量,或者用FFmpeg转码时加参数:
ffmpeg -i input.mp4 -c:v libx264 -refs 4 -level 4.0 output.h264在低端芯片如RK3308这种不带VDPU的型号上,MPP硬解H264根本不生效,会回退到软解或者直接报错。选型之前务必确认芯片规格里是否有硬件解码单元。
整条链路跑通之后,我的习惯是先用mpi_dec_test快速验证码流和硬件是否正常:
mpi_dec_test -t 7 -i input.h264 -o output.yuv -w 1920 -h 1080 -n 100参数里-t 7表示H264,-w和-h是宽高,-n 100表示只解前100帧。这个工具非常稳,如果它都跑不正常,那就不是业务代码的问题,而是码流、板子或SDK版本的问题。把这把工具用熟,排查效率会高很多。
最后再分享一个我自己的习惯:每次改了解码流程,我都会把同一份H264样本文件拿出来回归测试,保证输出YUV可以稳定落盘。音频视频工程里,解码链路是最底层的地基,这里稳了,上层才能放心干活。