第三章 toybrick mpp mpi_dec_nt_test.c 讲解
前言
该示例程序主要演示了如何使用MPP库中的解码器进行视频解码,并且提供了三种不同的缓冲模式供选择。这三种模式分别是:
纯内部模式:解码器会在内部创建缓冲区,用户不需要调用MPP_DEC_SET_EXT_BUF_GROUP函数来设置解码器的外部缓冲区,只需要调用MPP_DEC_SET_INFO_CHANGE_READY函数通知解码器可以开始解码即可。但是该模式存在一些缺点,例如解码器创建的缓冲区可能无法在解码器关闭前释放,可能导致内存泄漏或崩溃;解码器的内存使用量也无法控制,可能会占用所有可用内存;另外该模式不易实现零拷贝显示路径。
半内部模式:用户需要根据解码器返回的信息创建MppBufferGroup,并为每个MppFrame分配缓冲区。用户可以使用mpp_buffer_group_limit_config函数来限制解码器的内存使用量。该模式相对容易使用,并且用户可以在解码器关闭后释放MppBufferGroup,从而使内存安全的保存更长时间。但是该模式仍然存在一些缺点,例如缓冲区限制仍不准确,内存使用量仍然是100%固定的;另外该模式仍不易实现零拷贝显示路径。
纯外部模式:用户需要创建一个空的MppBufferGroup,并通过文件句柄从外部分配器中导入内存。在Android平台上,SurfaceFlinger会创建缓冲区,然后将文件句柄从SurfaceFlinger传递给MediaServer,最后提交到解码器的MppBufferGroup中。该模式最适合实现零拷贝显示,但是不易学习和使用,同时可能需要外部解析器来获取外部分配器正确的缓冲区大小。
示例程序还提供了缓冲区大小的计算方法,并根据不同的编解码器推荐了不同数量的缓冲区。
一、dec_nt_decode
使用rockchip平台的mpp库进行视频解码的代码。代码使用MPP库来创建上下文、控制和解码视频流。MPP库的创建完成后,代码调用了mpp_dec_cfg_init(&cfg)来初始化解码器的配置。然后使用MPP_DEC_GET_CFG命令从解码器上下文中获取默认配置,并设置了MPP_DEC_SET_CFG命令以应用该配置。代码还创建了一个数据结构,其中包括输入和输出数据,以及指向解码器上下文和MPP API的指针。接下来,代码使用线程来执行解码操作。
int dec_nt_decode(MpiDecTestCmd *cmd)
{
// base flow context
MppCtx ctx = NULL; //解码库的上下文
MppApi *mpi = NULL; //API结构体
// input / output //解码视频流
MppPacket packet = NULL; //输入的压缩数据包
MppFrame frame = NULL; //输出的解码数据帧
// paramter for resource malloc
RK_U32 width = cmd->width;
RK_U32 height = cmd->height;
MppCodingType type = cmd->type; //编码格式
// config for runtime mode
MppDecCfg cfg = NULL; //cfg是MPP解码器的配置结构体
RK_U32 need_split = 1; //need_split表示是否需要分割压缩数据包
// resources
MppBuffer frm_buf = NULL; //frm_buf是解码数据帧的缓冲区
pthread_t thd; //解码的线程
pthread_attr_t attr; //线程属性
MpiDecLoopData data; //data是解码器的参数结构体
MPP_RET ret = MPP_OK; //ret是MPP解码库返回值。
//对一些变量进行初始化和设置:
mpp_log("mpi_dec_test start\n");
memset(&data, 0, sizeof(data));
pthread_attr_init(&attr); //attr结构体初始化
cmd->simple = (cmd->type != MPP_VIDEO_CodingMJPEG) ? (1) : (0); //cmd->simple用于判断是否为MJPEG编码格式
//如果需要输出解码结果,则打开输出文件和验证文件。
if (cmd->have_output) {
data.fp_output = fopen(cmd->file_output, "w+b");
if (NULL == data.fp_output) {
mpp_err("failed to open output file %s\n", cmd->file_output);
goto MPP_TEST_OUT;
}
}
if (cmd->file_slt) {
data.fp_verify = fopen(cmd->file_slt, "wt");
if (!data.fp_verify)
mpp_err("failed to open verify file %s\n", cmd->file_slt);
}
//根据视频编码格式的不同,分别对packet和frame进行初始化
if (cmd->simple) {
ret = mpp_packet_init(&packet, NULL, 0);
mpp_err_f("mpp_packet_init get %p\n", packet);
if (ret) {
mpp_err("mpp_packet_init failed\n");
goto MPP_TEST_OUT;
}
} else {
RK_U32 hor_stride = MPP_ALIGN(width, 16);
RK_U32 ver_stride = MPP_ALIGN(height, 16);
ret = mpp_buffer_group_get_internal(&data.frm_grp, MPP_BUFFER_TYPE_ION);
if (ret) {
mpp_err("failed to get buffer group for input frame ret %d\n", ret);
goto MPP_TEST_OUT;
}
ret = mpp_frame_init(&frame); /* output frame */
if (ret) {
mpp_err("mpp_frame_init failed\n");
goto MPP_TEST_OUT;
}
/*
* NOTE: For jpeg could have YUV420 and YUV422 the buffer should be
* larger for output. And the buffer dimension should align to 16.
* YUV420 buffer is 3/2 times of w*h.
* YUV422 buffer is 2 times of w*h.
* So create larger buffer with 2 times w*h.
*
*/
//该注释解释了对于JPEG图像格式,输出缓冲区的大小需要考虑到可能出现的YUV420和YUV422两种格式,
//且缓冲区的尺寸需要对齐到16个像素。对于YUV420格式,缓冲区大小应该是图像宽度和高度的1.5倍,
//对于YUV422格式,缓冲区大小应该是图像宽度和高度的2倍。因此,在这种情况下,创建的缓冲区应该
//比所需的尺寸大,大小应该是图像宽度和高度的2倍。
ret = mpp_buffer_get(data.frm_grp, &frm_buf, hor_stride * ver_stride * 4);
if (ret) {
mpp_err("failed to get buffer for input frame ret %d\n", ret);
goto MPP_TEST_OUT;
}
mpp_frame_set_buffer(frame, frm_buf);
}
// decoder demo
ret = mpp_create(&ctx, &mpi);
if (ret) {
mpp_err("mpp_create failed\n");
goto MPP_TEST_OUT;
}
mpp_log("%p mpi_dec_test decoder test start w %d h %d type %d\n",
ctx, width, height, type);
ret = mpi->control(ctx, MPP_SET_DISABLE_THREAD, NULL);
ret = mpp_init(ctx, MPP_CTX_DEC, type);
if (ret) {
mpp_err("%p mpp_init failed\n", ctx);
goto MPP_TEST_OUT;
}
//mpp_dec_cfg_init(&cfg)来初始化解码器的配置。
mpp_dec_cfg_init(&cfg);
/* get default config from decoder context */ //使用MPP_DEC_GET_CFG命令从解码器上下文中获取默认配置
ret = mpi->control(ctx, MPP_DEC_GET_CFG, cfg);
if (ret) {
mpp_err("%p failed to get decoder cfg ret %d\n", ctx, ret);
goto MPP_TEST_OUT;
}
/*
* split_parse is to enable mpp internal frame spliter when the input
* packet is not aplited into frames.
*/
ret = mpp_dec_cfg_set_u32(cfg, "base:split_parse", need_split);
if (ret) {
mpp_err("%p failed to set split_parse ret %d\n", ctx, ret);
goto MPP_TEST_OUT;
}
//应用该配置
ret = mpi->control(ctx, MPP_DEC_SET_CFG, cfg);
if (ret) {
mpp_err("%p failed to set cfg %p ret %d\n", ctx, cfg, ret);
goto MPP_TEST_OUT;
}
data.cmd = cmd;
data.ctx = ctx;
data.mpi = mpi;
data.loop_end = 0;
data.packet = packet;
data.frame = frame;
data.frame_count = 0;
data.frame_num = cmd->frame_num;
data.quiet = cmd->quiet;
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
ret = pthread_create(&thd, &attr, thread_decode, &data);
if (ret) {
mpp_err("failed to create thread for input ret %d\n", ret);
goto MPP_TEST_OUT;
}
if (cmd->frame_num < 0) {
// wait for input then quit decoding
mpp_log("*******************************************\n");
mpp_log("**** Press Enter to stop loop decoding ****\n");
mpp_log("*******************************************\n");
getc(stdin);
data.loop_end = 1;
}
pthread_join(thd, NULL);
cmd->max_usage = data.max_usage;
ret = mpi->reset(ctx);
if (ret) {
mpp_err("%p mpi->reset failed\n", ctx);
goto MPP_TEST_OUT;
}
MPP_TEST_OUT:
if (data.packet) {
mpp_packet_deinit(&data.packet);
data.packet = NULL;
}
if (frame) {
mpp_frame_deinit(&frame);
frame = NULL;
}
if (ctx) {
mpp_destroy(ctx);
ctx = NULL;
}
if (!cmd->simple) {
if (frm_buf) {
mpp_buffer_put(frm_buf);
frm_buf = NULL;
}
}
if (data.frm_grp) {
mpp_buffer_group_put(data.frm_grp);
data.frm_grp = NULL;
}
if (data.fp_output) {
fclose(data.fp_output);
data.fp_output = NULL;
}
if (data.fp_verify) {
fclose(data.fp_verify);
data.fp_verify = NULL;
}
if (cfg) {
mpp_dec_cfg_deinit(cfg);
cfg = NULL;
}
pthread_attr_destroy(&attr);
return ret;
}
二、dec_loop
这是一个视频解码器的函数,主要作用是从输入二进制文件读取数据包并进行解码,直到数据包全部处理完毕。
具体实现流程如下:
读取输入二进制文件并获取数据包。
对于每个数据包,调用解码函数进行解码,然后获取可用的帧并释放。
如果帧信息发生了变化,则根据变化信息创建MppBufferGroup缓冲区组,并限制解码器内存使用。
如果数据包处理完毕,且当前帧数小于0或者大于总帧数,则重复读取数据包;否则,将循环结束标志设置为1。
另外,该函数中还包含了一些注释,介绍了解码器的三种缓冲模式及其优缺点。
介绍解码器的缓冲模式,有三种模式可供选择:
纯内部模式(Mode 1):用户不需要调用 MPP_DEC_SET_EXT_BUF_GROUP 控制解码器,只需调用 MPP_DEC_SET_INFO_CHANGE_READY 让解码器继续运行。解码器会在内部创建缓冲区,用户需要释放他们获取的每个帧。该模式的优点是易于使用且快速实现,缺点是解码器返回的缓冲区可能在解码器关闭前没有返回,导致内存泄漏或崩溃;解码器的内存使用无法控制,随意消耗系统内存;难以实现零拷贝显示路径。
半内部模式(Mode 2):用户需要根据返回的信息更改 MppFrame 创建 MppBufferGroup。用户可以使用 mpp_buffer_group_limit_config 函数限制解码器的内存使用。该模式的优点是易于使用;用户可以在解码器关闭后释放 MppBufferGroup,使内存更安全地保留;可以通过 mpp_buffer_group_limit_config 限制内存使用。缺点是缓冲区限制仍然不精确,内存使用是100%固定的;难以实现零拷贝显示路径。
纯外部模式(Mode 3):在此模式下,用户需要创建一个空的 MppBufferGroup,并通过文件句柄从外部分配器导入内存。在Android上,surfaceflinger将创建缓冲区。然后mediaserver从surfaceflinger获取文件句柄并提交到解码器的MppBufferGroup。该模式的优点是最高效的零拷贝显示方式。缺点是难以学习和使用;播放器的工作流程可能限制这种用法;可能需要一个外部解析器来获取外部分配器的正确缓冲区大小。
对于缓冲区大小的计算,对于像素数据,需要计算 hor_stride * ver_stride * 3 / 2 的大小;对于额外信息,需要计算 hor_stride * ver_stride / 2 的大小;总的缓冲区大小需要 hor_stride * ver_stride * 2。
对于H.264/H.265,20个以上的缓冲区就足够了;对于其他编解码器,10个缓冲区就足够了。
* NOTE: We can choose decoder's buffer mode here.
* There are three mode that decoder can support:
*
* Mode 1: Pure internal mode
* In the mode user will NOT call MPP_DEC_SET_EXT_BUF_GROUP
* control to decoder. Only call MPP_DEC_SET_INFO_CHANGE_READY
* to let decoder go on. Then decoder will use create buffer
* internally and user need to release each frame they get.
*
* Advantage:
* Easy to use and get a demo quickly
* Disadvantage:
* 1. The buffer from decoder may not be return before
* decoder is close. So memroy leak or crash may happen.
* 2. The decoder memory usage can not be control. Decoder
* is on a free-to-run status and consume all memory it can
* get.
* 3. Difficult to implement zero-copy display path.
*
* Mode 2: Half internal mode
* This is the mode current test code using. User need to
* create MppBufferGroup according to the returned info
* change MppFrame. User can use mpp_buffer_group_limit_config
* function to limit decoder memory usage.
*
* Advantage:
* 1. Easy to use
* 2. User can release MppBufferGroup after decoder is closed.
* So memory can stay longer safely.
* 3. Can limit the memory usage by mpp_buffer_group_limit_config
* Disadvantage:
* 1. The buffer limitation is still not accurate. Memory usage
* is 100% fixed.
* 2. Also difficult to implement zero-copy display path.
*
* Mode 3: Pure external mode
* In this mode use need to create empty MppBufferGroup and
* import memory from external allocator by file handle.
* On Android surfaceflinger will create buffer. Then
* mediaserver get the file handle from surfaceflinger and
* commit to decoder's MppBufferGroup.
*
* Advantage:
* 1. Most efficient way for zero-copy display
* Disadvantage:
* 1. Difficult to learn and use.
* 2. Player work flow may limit this usage.
* 3. May need a external parser to get the correct buffer
* size for the external allocator.
*
* The required buffer size caculation:
* hor_stride * ver_stride * 3 / 2 for pixel data
* hor_stride * ver_stride / 2 for extra info
* Total hor_stride * ver_stride * 2 will be enough.
*
* For H.264/H.265 20+ buffers will be enough.
* For other codec 10 buffers will be enough.
*/
三 thread_decode
首先初始化了data中的checkcrc结构体,然后分配了luma和chroma的sum空间。接着,记录了开始时间t_s,并且在while循环内调用dec_loop函数进行解码操作。当data->loop_end为真时,跳出循环。记录结束时间t_e,并计算了elapsed_time、frame_count、frame_rate和delay,其中elapsed_time是解码用时,frame_count是解码的总帧数,frame_rate是解码帧率,delay是第一帧解码前的延迟。最后输出解码的帧数、用时、延迟和帧率等信息,并释放luma和chroma的sum空间。函数最终返回NULL。
void *thread_decode(void *arg)
{
MpiDecLoopData *data = (MpiDecLoopData *)arg;
RK_S64 t_s, t_e;
memset(&data->checkcrc, 0, sizeof(data->checkcrc));
data->checkcrc.luma.sum = mpp_malloc(RK_ULONG, 512);
data->checkcrc.chroma.sum = mpp_malloc(RK_ULONG, 512);
t_s = mpp_time();
while (!data->loop_end)
dec_loop(data);
t_e = mpp_time();
data->elapsed_time = t_e - t_s;
data->frame_count = data->frame_count;
data->frame_rate = (float)data->frame_count * 1000000 / data->elapsed_time;
data->delay = data->first_frm - data->first_pkt;
mpp_log("decode %d frames time %lld ms delay %3d ms fps %3.2f\n",
data->frame_count, (RK_S64)(data->elapsed_time / 1000),
(RK_S32)(data->delay / 1000), data->frame_rate);
MPP_FREE(data->checkcrc.luma.sum);
MPP_FREE(data->checkcrc.chroma.sum);
return NULL;
}