test/ 下有 4 个解码 demo,它们的差别只在线程模型和 API 选择,业务逻辑(读流 → 喂解码器 → 写 YUV)完全一致。先把基础那个吃透,其它三个都是变体。
| 文件 | 行数 | 流水线类型 | 一句话 |
|---|---|---|---|
mpi_dec_test.c | 656 | A: simple async / B: advanced | 教科书。读懂这个,其它 3 个一眼就过 |
mpi_dec_mt_test.c | 443 | A: simple async(put/get 分线程) | 流式低延迟参考 |
mpi_dec_nt_test.c | 521 | C: sync decode() | “没线程”模式:禁用 MPP 内部线程 |
mpi_dec_multi_test.c | 649 | A or B(多实例) | 多路并发性能测试 |
1. mpi_dec_test.c —— 解码教科书
1.1 顶层结构
main() // line 628
└─ dec_decode(cmd) // line 409: 核心入口
├─ memset/init data, attr
├─ cmd->simple = (type != JPEG) // line 439: 关键分支
├─ open output file // line 441
├─ dec_buf_mgr_init() // line 455
├─ if simple: mpp_packet_init(空) // line 461
│ else: mpp_frame_init + 预分配 frm_buf // line 468
├─ mpp_create / mpp_init // line 501
├─ MppDecCfg: split_parse=1 // line 526
├─ pthread_create(thread_decode) // line 553
├─ if frame_num<0: getc(stdin) // line 559
├─ pthread_join // line 569
├─ mpi->reset // line 573
└─ MPP_TEST_OUT: 集中 deinit // line 579
thread_decode(void *arg) // line 367: 线程主体
├─ crc_frm_init
├─ if simple: while (loop dec_simple) // line 380
│ else: mpi->control(SET_OUTPUT_FORMAT);
│ while (loop dec_advanced) // line 392
└─ 计算 fps / delay 并打印
dec_simple(data) // line 51: simple 模式(一次循环送一包)
├─ reader_read 读输入文件 // line 65
├─ if pkt_eos 且帧数已够: 退出
│ 否则 reader_rewind 重读
├─ mpp_packet_set_data/size/pos/length // line 87
├─ if eos: mpp_packet_set_eos
└─ do {
if (!pkt_done) decode_put_packet
do {
decode_get_frame
if info_change: 申请 buffer group + INFO_CHANGE_READY
else: 写文件 / 算 CRC / 计数
} while (有更多帧或还没等到)
if pkt_done break
msleep(1) 重试 put
} while (1)
dec_advanced(data) // line 271: advanced 模式(一次解一帧 JPEG)
├─ reader_index_read(0) 一次性读整文件 // line 286
├─ mpp_packet_init_with_buffer(buf) // line 290
├─ meta = packet 的 meta
│ meta_set_frame(KEY_OUTPUT_FRAME, 预分配的 frame) // line 297-299
├─ decode_put_packet // line 301
├─ decode_get_frame // line 311
├─ 写文件 / 算 CRC // line 326-330
├─ 检查 KEY_INPUT_PACKET 一致性 // line 343-350
└─ mpp_packet_deinit
1.2 关键代码段拆解
(A) simple 模式的双层循环 —— dec_simple()
外层每次”喂一包”,内层每次”取所有可取的帧”:
do { // 外层:包级别
if (!pkt_done) {
ret = mpi->decode_put_packet(ctx, packet);
if (MPP_OK == ret) pkt_done = 1;
}
do { // 内层:帧级别
try_again:
ret = mpi->decode_get_frame(ctx, &frame);
if (MPP_ERR_TIMEOUT == ret) {
if (times-- > 0) { msleep(1); goto try_again; }
}
if (frame) {
if (info_change(frame)) ... 申请 buffer group ...
else ... 处理真帧 ...
mpp_frame_deinit(&frame);
get_frm = 1;
}
if (pkt_eos && pkt_done && !frm_eos) { msleep(1); continue; }
if (frm_eos) break;
if (达到帧数限制) break;
if (get_frm) continue;
break;
} while (1);
if (达到帧数限制) { loop_end = 1; break; }
if (pkt_done) break;
msleep(1); // put 失败时让出
} while (1);
为什么是双层?
- 一个 packet 进来不一定立刻出帧(B 帧需要前后参考帧凑齐才能解出)。
- 一个 packet 也可能出多帧(B 帧延迟解码,到关键时刻一次性吐 3 帧)。
所以:”送一次包,把当前能取的帧都取干净,再送下一包”。
(B) info_change 处理(line 130–177)
第一帧通常是 info_change,要做三件事:
if (mpp_frame_get_info_change(frame)) {
RK_U32 buf_size = mpp_frame_get_buf_size(frame);
// 1. FBC/Tile 输出格式:要先 SET_FRAME_INFO 让解码器知道目标格式
if (MPP_FRAME_FMT_IS_FBC(cmd->format) || MPP_FRAME_FMT_IS_TILE(cmd->format)) {
MppFrame frm = NULL;
mpp_frame_init(&frm);
mpp_frame_set_width(frm, width);
mpp_frame_set_height(frm, height);
mpp_frame_set_fmt(frm, cmd->format);
mpi->control(ctx, MPP_DEC_SET_FRAME_INFO, frm);
mpp_frame_deinit(&frm);
}
// 2. 申请 buffer group,按 info_change 给的 buf_size,最多 24 个 buffer
grp = dec_buf_mgr_setup(data->buf_mgr, buf_size, 24, cmd->buf_mode);
mpi->control(ctx, MPP_DEC_SET_EXT_BUF_GROUP, grp);
data->frm_grp = grp;
// 3. 告诉解码器"准备好了,继续"
mpi->control(ctx, MPP_DEC_SET_INFO_CHANGE_READY, NULL);
}
为什么 24 个 buffer? —— 经验值。H.264 reference 链最多 16 帧 + reorder 余量 + 输出排队 ≈ 20+。少了会反压住硬件。 mpi_dec_utils.h 注释也说 “For H.264/H.265 20+ buffers will be enough.”
(C) 真帧处理(line 177–216)
} else {
RK_U32 err_info = mpp_frame_get_errinfo(frame); // 解码错误标志
RK_U32 discard = mpp_frame_get_discard(frame); // 标记此帧应丢弃
if (!data->first_frm) data->first_frm = mpp_time(); // 记首帧时间
// 拼日志(如带 meta 的 temporal id)
if (mpp_frame_has_meta(frame)) {
MppMeta meta = mpp_frame_get_meta(frame);
RK_S32 temporal_id = 0;
mpp_meta_get_s32(meta, KEY_TEMPORAL_ID, &temporal_id);
}
data->frame_count++;
if (data->fp_output && !err_info)
dump_mpp_frame_to_file(frame, data->fp_output); // 写 YUV
if (data->fp_verify) {
crc_frm_calc(checkcrc, frame);
crc_frm_write(checkcrc, data->fp_verify); // 写 CRC
}
fps_calc_inc(cmd->fps);
}
dump_mpp_frame_to_file() 在 utils/utils.c,会按 hor_stride/ver_stride 正确写出 Y/U/V 平面。
(D) advanced 模式 —— dec_advanced()(仅 JPEG)
ret = reader_index_read(cmd->reader, 0, &slot); // 整个 JPEG 文件作为一个 buffer
mpp_packet_init_with_buffer(&packet, slot->buf); // 用 buffer 创 packet
if (slot->eos) mpp_packet_set_eos(packet);
// 把"输出 frame 应该写哪个预分配 buffer"通过 meta 告诉解码器
meta = mpp_packet_get_meta(packet);
if (meta) mpp_meta_set_frame(meta, KEY_OUTPUT_FRAME, frame);
ret = mpi->decode_put_packet(ctx, packet);
ret = mpi->decode_get_frame(ctx, &frame_ret);
if (frame_ret != frame) // 应该是同一个 frame
mpp_err_f("mismatch frame %p -> %p\n", frame_ret, frame);
dump_mpp_frame_to_file(frame_ret, data->fp_output);
mpp_packet_deinit(&packet);
关键差异:advanced 模式下调用者预分配了输入 packet 的 buffer 和输出 frame 的 buffer,通过 meta 关联。解码器不会自己申请帧 buffer,因此没有 info_change 流程——你已经告诉它输出去哪。
适合 JPEG 因为:
- JPEG 一文件一帧,不需要 reference 链
- 每张图尺寸/格式可能不同(解 1000 张 jpg),调用者每次预分配一张就行
- 无 reorder,简化时序
(E) 主线程的”按回车停”(line 559–567)
if (cmd->frame_num < 0) {
mpp_log("**** Press Enter to stop loop decoding ****\n");
(void)getc(stdin);
data.loop_end = 1;
}
pthread_join(thd, NULL);
只在 frame_num < 0(无限循环模式)时启用。否则解码线程靠 frame_count >= frame_num 或 EOS 自己退出。
1.3 资源生命周期总结
申请 释放(MPP_TEST_OUT 段)
───────────────────────────────── ──────────────────────────────
fopen(fp_output, "w+b") fclose(fp_output)
fopen(fp_verify, "wt") fclose(fp_verify)
dec_buf_mgr_init dec_buf_mgr_deinit
mpp_packet_init / mpp_frame_init mpp_packet_deinit / mpp_frame_deinit
dec_buf_mgr_setup → frm_grp (group 由 buf_mgr_deinit 间接释放)
mpp_buffer_get → frm_buf mpp_buffer_put
mpp_create → ctx mpp_destroy
mpp_dec_cfg_init → cfg mpp_dec_cfg_deinit
pthread_attr_init pthread_attr_destroy
MPP_TEST_OUT 标签是经典 C 错误处理:任何中间步骤失败都 goto MPP_TEST_OUT,集中清理。
2. mpi_dec_mt_test.c —— 输入输出分线程
2.1 与 mpi_dec_test 的差异
mpi_dec_test:
单线程: while (read → put → get → write)
mpi_dec_mt_test:
线程 in: while (read → put) ← 死等 put 成功
线程 out: while (get → write) ← 死等 get 到帧(block 模式)
2.2 关键代码
// 主流程相同的部分省略
// 关键差异 1:设置输出阻塞模式
MppPollType timeout = MPP_POLL_BLOCK;
mpi->control(ctx, MPP_SET_OUTPUT_TIMEOUT, &timeout);
// 关键差异 2:起两个线程
pthread_create(&thd_in, &attr, thread_input, &data);
pthread_create(&thd_out, &attr, thread_output, &data);
pthread_join(thd_in, NULL);
pthread_join(thd_out, NULL);
2.3 thread_input()(line 50)
do {
reader_read(reader, &slot);
mpp_packet_set_data/size/pos/length(packet, ...);
if (slot->eos) {
if (要循环) reader_rewind, 不设 eos
else mpp_packet_set_eos(packet)
}
do { // 死等 put 成功
ret = mpi->decode_put_packet(ctx, packet);
if (MPP_OK == ret) {
mpp_assert(0 == mpp_packet_get_length(packet));
break;
}
msleep(1);
} while (!data->loop_end);
if (pkt_eos) break;
} while (!data->loop_end);
注意 mpp_assert(0 == mpp_packet_get_length(packet))——put 成功的语义是”内容已被消费”,length 必须归零,否则后续会重复送。
2.4 thread_output()(line 107)
do {
ret = mpi->decode_get_frame(ctx, &frame); // BLOCK 模式下会等到帧
if (NULL == frame) { msleep(1); continue; }
if (info_change) ... 同 dec_simple ...
else ... 写文件、计数 ...
frm_eos = mpp_frame_get_eos(frame);
mpp_frame_deinit(&frame);
if (达到帧数限制) data->loop_end = 1;
} while (!data->loop_end);
2.5 为什么这样写
适合实时解码低延迟场景:网络收到一包就立刻 put,解码线程独立按硬件节奏 get。生产者消费者解耦,CPU 利用率更高。
mpi_dec_test 单线程的代码相对低效,因为内层”取帧”循环里 hardware 还在工作时主线程在 sleep;mt 版本里 in 线程 sleep 时 out 线程还在工作。
2.6 局限
if (cmd->type == MPP_VIDEO_CodingMJPEG) {
mpp_log("mpi_dec_mt_test not support mjpeg yet\n");
goto RET;
}
—— 不支持 JPEG。因为 JPEG 走 advanced 模式,不是 put/get 异步队列。
3. mpi_dec_nt_test.c —— 一步到位 decode()
3.1 名字解析
nt = no thread。意思是:禁用 MPP 内部解码线程,调用者一次 mpi->decode() 完成 put + 等硬件 + get。
ret = mpi->control(ctx, MPP_SET_DISABLE_THREAD, NULL); // line 373
...
ret = mpi->decode(ctx, packet, &frame); // line 115
3.2 与 mpi_dec_test 的对比
// mpi_dec_test 的 dec_simple:
ret = mpi->decode_put_packet(ctx, packet); // 异步,可能 ERR_TIMEOUT
ret = mpi->decode_get_frame(ctx, &frame); // 异步,可能 ERR_TIMEOUT,可能 NULL
// mpi_dec_nt_test 的 dec_loop:
ret = mpi->decode(ctx, packet, &frame); // 同步:返回时帧已就绪(或 info_change)
3.3 适用场景
- 嵌入式低端 SoC:内部线程开销大,宁愿同步
- 需要严格控制流水线节奏(每帧解多久要可预测)
- Debug 时栈更清晰(异步多线程栈很难调)
3.4 关键差异
do {
RK_U32 frm_eos = 0;
RK_S32 get_frm = 0;
MppFrame frame = NULL;
ret = mpi->decode(ctx, packet, &frame); // 同步,会 block
if (frame) {
if (info_change) ...
else ...
mpp_frame_deinit(&frame);
get_frm = 1;
}
// 解码后 packet 的 length 可能没归零(一包多帧时),需要继续 decode
if (packet) {
if (mpp_packet_get_length(packet)) {
msleep(1);
continue; // 还有内容,下次循环再 decode
}
packet = NULL;
pkt_done = 1;
}
if (pkt_eos && !frm_eos) { msleep(1); continue; }
if (pkt_done) break;
} while (1);
注意 mpi->decode() 返回后 packet 的 length 可能不为零——这意味着这包还有未消费的字节,需要再 decode。这就是 line 242–253 那段”多余”逻辑的来由。
3.5 输入支持 JPEG buffer
if (!slot->buf) { // line 87
/* non-jpeg decoding */
mpp_packet_set_data/size/pos/length(packet, slot->data, slot->size);
} else {
/* jpeg decoding */
void *buf = mpp_buffer_get_ptr(slot->buf);
mpp_packet_set_data/size/pos/length(packet, buf, mpp_buffer_get_size(slot->buf));
mpp_packet_set_buffer(packet, slot->buf); // 用 MppBuffer
}
reader_init 在打开 JPEG 流时会预分配 MppBuffer 装每个 JPEG,普通流则直接 malloc 字节数组。nt 版本两条路都支持。
4. mpi_dec_multi_test.c —— 多实例并发
4.1 与 mpi_dec_mt 的本质差别
| mpi_dec_mt | mpi_dec_multi | |
|---|---|---|
| MppCtx 数量 | 1 | N(每实例一个) |
| 线程数量 | 2(in + out) | N(每实例 1) |
| 适用场景 | 低延迟单流 | 多路并发吞吐 |
4.2 数据结构
typedef struct {
MpiDecTestCmd *cmd; // 共享命令行
pthread_t thd; // 这一路的线程
MpiDecMultiCtx ctx; // 这一路的解码上下文
MpiDecMultiCtxRet ret; // 这一路的返回值
} MpiDecMultiCtxInfo;
ctxs = mpp_calloc(MpiDecMultiCtxInfo, cmd->nthreads); // 申请 N 个
for (i = 0; i < cmd->nthreads; i++) {
ctxs[i].cmd = cmd;
pthread_create(&ctxs[i].thd, NULL, multi_dec_decode, &ctxs[i]);
}
每个线程跑 multi_dec_decode(),里面是缩水版的 dec_decode + thread_decode:
multi_dec_decode(arg):
open output file (per-thread)
dec_buf_mgr_init
if simple: mpp_packet_init else mpp_frame_init + 预分配
mpp_create / mpp_init
set split_parse cfg
t_s = mpp_time()
while (!loop_end):
if simple: multi_dec_simple
else: multi_dec_advanced
t_e = mpp_time()
计算这一路的 fps / delay
清理
4.3 multi_dec_simple() 与 dec_simple() 的差别
很小:
- 用
reader_index_read(reader, packet_count++, &slot)而不是reader_read,因为多线程共享同一个 reader 时不能用游标式 read(会互相干扰),改成显式索引 - 循环时
data->packet_count = 0而不是reader_rewind(同样原因:reader 是共享的)
ret = reader_index_read(reader, data->packet_count++, &slot);
reader_index_read 是线程安全的(实现见 utils/mpi_dec_utils.c)。
4.4 multi_dec_advanced() —— 用 task 队列
mpi_dec_multi_test 是唯一用 mpi->poll/dequeue/enqueue 的解码 demo:
mpp_packet_init_with_buffer(&packet, slot->buf);
if (slot->eos) mpp_packet_set_eos(packet);
mpi->poll(ctx, MPP_PORT_INPUT, MPP_POLL_BLOCK);
mpi->dequeue(ctx, MPP_PORT_INPUT, &task);
mpp_task_meta_set_packet(task, KEY_INPUT_PACKET, packet);
mpp_task_meta_set_frame (task, KEY_OUTPUT_FRAME, frame);
mpi->enqueue(ctx, MPP_PORT_INPUT, task);
mpi->poll(ctx, MPP_PORT_OUTPUT, MPP_POLL_BLOCK);
mpi->dequeue(ctx, MPP_PORT_OUTPUT, &task);
mpp_task_meta_get_frame(task, KEY_OUTPUT_FRAME, &frame_out);
... 处理 frame_out ...
mpi->enqueue(ctx, MPP_PORT_OUTPUT, task);
// 还要把输入 task 重新走一遍 dequeue/enqueue 来释放
mpi->dequeue(ctx, MPP_PORT_INPUT, &task);
mpp_task_meta_get_packet(task, KEY_INPUT_PACKET, &packet_out);
mpp_packet_deinit(&packet_out);
mpi->enqueue(ctx, MPP_PORT_INPUT, task);
注释(line 347–351):
The following input port task dequeue and enqueue is to make sure that the input packet can be released. We can directly deinit the input packet after frame output in most cases.
意思是:输入端 task 跑完后还得”再 dequeue 一次”才能拿到引用并释放 packet。如果不这么做,packet 会被 task 队列扣住直到 ctx 销毁。
4.5 输出与统计
for (i = 0; i < cmd->nthreads; i++) {
pthread_join(ctxs[i].thd, NULL);
}
for (i = 0; i < cmd->nthreads; i++) {
MpiDecMultiCtxRet *r = &ctxs[i].ret;
mpp_log("chn %2d decode %d frames time %lld ms delay %3d ms fps %3.2f\n",
i, r->frame_count, ..., r->frame_rate);
total_rate += r->frame_rate;
}
mpp_log("average frame rate %.2f\n", total_rate / cmd->nthreads);
return (int)total_rate; —— 进程退出码就是平均 fps,方便脚本读。
5. 横向对比一览
| 特性 | dec_test | dec_mt | dec_nt | dec_multi |
|---|---|---|---|---|
| MppCtx 数 | 1 | 1 | 1 | N |
| 用户线程数 | 1(解码) | 2(in + out) | 1(解码) | N |
| 内部 MPP 线程 | 默认开 | 默认开 | 关(DISABLE_THREAD) | 默认开 |
| API 入口 | put/get | put/get | decode() | put/get 或 task |
| 支持 JPEG | 是(advanced) | 否 | 是 | 是(advanced) |
| 支持按回车停 | 是(frame_num<0) | 是 | 是 | 是 |
| 写多个输出文件 | 否 | 否 | 否 | 是(按 chn) |
| 用途 | 学习起点 | 单流低延迟 | 极端可控/低端 | 多路并发压测 |
6. 共同的”坑”
不论哪个变体,下面这些坑都得当心:
6.1 buffer group 的 24 数量
grp = dec_buf_mgr_setup(data->buf_mgr, buf_size, 24, cmd->buf_mode);
24 是经验值。如果你的流有大量 reorder(B 帧很多)或者你在 hold 解出帧给后处理,可能不够,会导致解码反压;如果是 P-only 流,可以减到 8 省内存。
6.2 first_pkt / first_frm 的赋值时机
代码里都是这样:
注意 if (!data->first_pkt) 防止后续被覆盖。如果你想测 “P-only 流的稳态延迟”,应该清零再测,不要被首帧污染。
6.3 EOS 处理
EOS 不是一个特殊帧,而是一个标志位:
- 输入:
mpp_packet_set_eos(packet),告诉解码器”流到此为止” - 输出:解码器吐出最后一帧时
mpp_frame_get_eos(frame) == 1,可能伴随空 buffer
代码里这段(dec_simple line 230–233):
if (pkt_eos && pkt_done && !frm_eos) {
msleep(1);
continue; // 已经发了 eos packet,但还没收到 eos frame,等
}
是必需的。少了它,你最后一帧会被漏掉。
6.4 reset 时机
ret = mpi->reset(ctx); // pthread_join 之后
reset 会清空内部队列。在线程还活着时调 reset 会和 put/get 打架。代码顺序:先让线程结束 → 再 reset → 再 destroy。
本网站尊重知识产权,如有侵权,请及时联系我们删除。
本站所有原创内容仅用于学习和交流目的,未经作者和本站授权不得进行商业使用或盈利行为。












暂无评论内容