基于RK3568实现MPP+QT远程推拉流监控项目 (二)MPP解码
本文介绍了MPP解码模块的API接口及解码流程实现。MPP解码API主要包括创建上下文(mpp_create)、初始化(mpp_init)、数据包处理(mpp_packet_init/set_pts)、解码控制(decode_put_packet/get_frame)等函数,并详细说明了帧属性获取方法。解码流程包括:初始化→参数设置→读取压缩数据→解码处理→获取原始数据。具体实现部分展示了通过JS
·
文章目录
- 解码模块
-
- Api
-
- MPP_RET mpp_create(MppCtx *ctx, MppApi **mpi)
- MPP_RET mpp_init(MppCtx ctx, MppCtxType type, MppCodingType coding)
- MPP_RET (*decode_put_packet)(MppCtx ctx, MppPacket packet)
- MPP_RET (*decode_get_frame)(MppCtx ctx, MppFrame *frame)
- MPP_RET mpp_packet_init(MppPacket *packet, void data, size_t size)
- void mpp_packet_set_pts(MppPacket packet, RK_S64 pts)
- RK_U32 mpp_frame_get_width(const MppFrame frame)
- RK_U32 mpp_frame_get_height(const MppFrame frame)
- RK_U32 mpp_frame_get_hor_stride(const MppFrame frame)
- RK_U32 mpp_frame_get_ver_stride(const MppFrame frame)
- void mpp_buffer_get_ptr(MppBuffer buffer)
- 解码流程
- MPP解码远程摄像头数据具体实现
解码模块
Api
MPP_RET mpp_create(MppCtx *ctx, MppApi **mpi)
功能:
创建一个 MPP 上下文(Context)。这是使用 MPP 的第一步。
参数
ctx: 输出参数,用于接收创建的上下文句柄。
mpi: 输出参数,用于接收 MPP 的 API 函数指针集合。
MPP_RET mpp_init(MppCtx ctx, MppCtxType type, MppCodingType coding)
功能:
初始化 MPP 上下文,并指定其工作模式。
参数
ctx: mpp_create 创建的上下文句柄。
type: 指定是解码 (MPP_CTX_DEC)还是编码 (MPP_CTX_ENC)。这里我们用 MPP_CTX_DEC。
coding: 指定视频编码标准,例如 MPP_VIDEO_CodingAVC (H.264) 或 MPP_VIDEO_CodingHEVC (H.265)。
MPP_RET (*decode_put_packet)(MppCtx ctx, MppPacket packet)
功能:
将一个包含编码数据的 MppPacket 发送给解码器。这是一个异步接口,函数会立即返回,解码器在后台进行处理。
参数:
packet: 包含待解码视频码流的数据包。
MPP_RET (*decode_get_frame)(MppCtx ctx, MppFrame *frame)
功能:
从解码器中获取一帧已经解码完成的图像数据。这也是一个异步接口。如果当前没有解码完成的帧,它可能会阻塞或立即返回一个提示信息。
参数:
frame: 输出参数,用于接收解码后的 MppFrame。
MPP_RET mpp_packet_init(MppPacket *packet, void data, size_t size)
功能:
初始化一个 MppPacket,并将其与一块包含视频码流的内存关联起来。
参数
packet: 指向要初始化的 MppPacket 的指针。
data: 指向存放视频码流数据的内存地址。
size: 数据的大小。
void mpp_packet_set_pts(MppPacket packet, RK_S64 pts)
功能:
设置 MppPacket 的显示时间戳(Presentation Time Stamp)。这个时间戳会被传递给解码后的 MppFrame,用于同步音视频。
RK_U32 mpp_frame_get_width(const MppFrame frame)
功能:
获取解码后图像帧的 有效宽度。
RK_U32 mpp_frame_get_height(const MppFrame frame)
功能:
获取解码后图像帧的 有效高度。
RK_U32 mpp_frame_get_hor_stride(const MppFrame frame)
功能:
获取解码后图像帧的 水平跨距 (Horizontal Stride)。因为内存对齐的原因,stride 通常会大于或等于 width。例如,一个宽度为 1920 的图像,其 stride 可能是 1920 或者为了对齐而变成 1984。在读取 YUV 数据时必须使用 stride 而不是 width。
RK_U32 mpp_frame_get_ver_stride(const MppFrame frame)
功能:
获取解码后图像帧的 垂直跨距 (Vertical Stride)。类似地,它通常大于或等于 height。MppBuffer mpp_frame_get_buffer(MppFrame frame)功能: 获取 MppFrame 内部用于存储像素数据的 MppBuffer。这个 Buffer 是由 MPP 内部管理的。
void mpp_buffer_get_ptr(MppBuffer buffer)
功能:
获取 MppBuffer 指向的实际内存地址的指针。通过这个指针,你就可以直接访问解码后的 YUV 数据了。
解码流程

上图是MPP解码的大体流程:
- 调用mpp_create获取MppCtx实例和MppApi
- 调用mpp_init初始化MppCtx 的编解码类型与格式。
- 调用control设置MPP解码参数
- 调用avreadframe读取每一帧的视频压缩数据(这个数据可以是摄像头编码数据也可以是H264文件读取的数据)
- 调用mpp_packet_init赋值MppPacket
- 调用mpp_packet_set_pts设置MppPacket的PTS时间戳
- 调用decode_put_packet发送到解码器
- 调用decode_get_frame从解码器获取每一帧的视频原始数据
MPP解码远程摄像头数据具体实现
流程图

代码实现
读取json文件获取拉流地址字段
// --- 解析 JSON 配置文件 ---
FILE *boot_stream_file = fopen("/userdata/url_address_manage.json", "r"); // 以只读模式("r")打开 JSON 配置文件。
fseek(boot_stream_file, 0, SEEK_END); // 将文件指针移动到文件末尾。
long boot_length = ftell(boot_stream_file); // 获取文件当前位置(即文件长度)。
fseek(boot_stream_file, 0, SEEK_SET); // 将文件指针移回文件开头。
char *boot_data = (char *)malloc(boot_length + 1); // 分配足够的内存来存储整个文件内容,并多加 1 字节用于存放字符串结束符 '\0'。
fread(boot_data, 1, boot_length, boot_stream_file); // 从文件中读取 boot_length 字节数据到 boot_data 缓冲区。
boot_data[boot_length] = '\0'; // 在数据末尾添加字符串结束符。
cJSON *boot_json = cJSON_Parse(boot_data); // 使用 cJSON 库解析从文件中读取的字符串数据。
// 从 JSON 对象中提取配置信息
cJSON *recv_url_address_json = cJSON_GetObjectItem(boot_json, "recv_url_address"); // 获取键为 "recv_url_address" 的 JSON 对象。
char *recv_url_address = recv_url_address_json->valuestring; // 获取该对象的值(一个字符串),即接收流的地址。
ffmpeg拉流模块初始化
init_ffmpeg_recv_module(recv_url_address); // 调用自定义函数,初始化 FFmpeg 拉流(接收)模块。
// 定义一个函数,用于初始化 FFmpeg 的接收(拉流)模块
void init_ffmpeg_recv_module(char *recv_url_address)
{
AVDictionary *options = NULL; // 定义一个 FFmpeg 字典,用于设置额外的参数选项。
// 在 FFmpeg 4.0 之前,需要此函数来注册所有的编解码器、复用/解复用器等。在 4.0 及以上版本中已废弃,但为了兼容性保留。
av_register_all();
avformat_network_init(); // 初始化 FFmpeg 的网络模块,以便能访问网络流。
// 分配一个 AVFormatContext 结构体的内存并设置默认值。这是 FFmpeg 操作媒体文件的核心上下文。
pFormatCtx = avformat_alloc_context();
// --- 根据不同的流协议进行处理 ---
if (strstr(recv_url_address, "rtsp://") != NULL) // 检查 URL 地址是否包含 "rtsp://"
{
AVDictionary *options = NULL; // 创建一个局部的 options 字典。
// 设置 RTSP 传输协议为 UDP。相比 TCP,UDP 延迟更低,但可能丢包。
av_dict_set(&options, "rtsp_transport", "udp", 0);
av_dict_set(&options, "max_delay", "100", 0); // 设置最大延迟为 100 毫秒。
// 打开网络输入流。该函数会探测格式、连接服务器并读取头部信息。
if (avformat_open_input(&pFormatCtx, recv_url_address, NULL, &options) != 0)
{
printf("Couldn't open input rtsp stream.\n"); // 打开失败,打印错误信息。
return;
}
else
{
printf("Success open input rtsp stream.\n"); // 打开成功。
}
}
// 检查 URL 是否为 RTMP 或 SRT 协议
else if ((strstr(recv_url_address, "rtmp://") != NULL) || (strstr(recv_url_address, "srt://") != NULL))
{
is_flv_ts = true; // 设置全局标志,表示流的时间戳可能是基于 FLV/TS 格式的。
// 打开网络输入流,不带额外参数。
if (avformat_open_input(&pFormatCtx, recv_url_address, NULL, NULL) != 0)
{
printf("Couldn't open input other stream.\n"); // 打开失败。
return;
}
else
{
printf("Success open input other stream.\n"); // 打开成功。
}
}
else // 处理其他类型的流或本地文件
{
if (avformat_open_input(&pFormatCtx, recv_url_address, NULL, NULL) != 0)
{
printf("Couldn't open input other stream.\n"); // 打开失败。
return;
}
else
{
printf("Success open input other stream.\n"); // 打开成功。
}
}
// 读取数据包以获取流信息。此函数会填充 pFormatCtx->streams 数组。
if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
{
printf("Couldn't find stream information.\n"); // 查找失败。
return;
}
else
{
printf("Success find stream information.\n"); // 查找成功。
}
// --- 查找视频流并提取参数 ---
unsigned i = 0;
for (i = 0; i < pFormatCtx->nb_streams; i++) // 遍历所有媒体流。
{
// 检查当前流的编解码器类型是否为视频。
// 注意:pFormatCtx->streams[i]->codec 在新版 FFmpeg 中已废弃,应使用 codecpar。
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
videoindex = i; // 找到视频流,记录其索引。
break; // 退出循环。
}
}
if (videoindex == -1) // 如果循环结束后 videoindex 仍为 -1,说明没找到视频流。
{
printf("Didn't find a video stream.\n");
return;
}
// 将解析出的视频宽高参数写入共享内存的 camera_param 结构体中。
camera_param->width = pFormatCtx->streams[videoindex]->codecpar->width;
camera_param->height = pFormatCtx->streams[videoindex]->codecpar->height;
// 获取视频流的编解码器 ID。
int codec_id = pFormatCtx->streams[videoindex]->codecpar->codec_id;
if (codec_id == AV_CODEC_ID_H264) // 如果是 H.264
{
strcpy(camera_param->codec_type, "H264"); // 将 "H264" 字符串拷贝到共享内存。
}
else if (codec_id == AV_CODEC_ID_H265) // 如果是 H.265
{
strcpy(camera_param->codec_type, "H265"); // 拷贝 "H265"。
}
else if (codec_id == AV_CODEC_ID_MJPEG) // 如果是 MJPEG
{
strcpy(camera_param->codec_type, "MJPEG"); // 拷贝 "MJPEG"。
}
}
通过av_bitstream_filter_init初始化h264比特流过滤器
bsf = av_bitstream_filter_init("h264_mp4toannexb");
if (bsf != NULL)
{
printf("av_bitstream_filter_init success...\n");
}
else
{
printf("av_bitstream_filter_init failed...\n");
}
由于RTMP/SRT流媒体服务器存储的H264视频格式是AVCC存储, 但是AVCC格式无法直接播放, 需要转换成Annex B才能够正实时常播放。这里的过滤器用的是h264_mp4toannexb, 用于将 H.264 视频从 MP4 格式转换为 Annex B 格式。
mpp模块初始化
ret = mpp_create(&dec_ctx, &dec_mpi); // 创建 MPP 上下文和 API 接口,用于解码。
ret = mpp_init(dec_ctx, MPP_CTX_DEC, type); // 初始化解码器上下文,指定为解码模式(MPP_CTX_DEC)和对应的编码类型。
// --- 配置 MPP 解码器 ---
// NOTE: 解码器的分割模式需要在初始化(mpp_init)之前设置
dec_mpi_cmd = MPP_DEC_SET_PARSER_SPLIT_MODE; // 设置命令为“设置解析器分割模式”。
dec_param = &need_split; // 参数为 need_split 变量的地址 (值为 1),表示需要 MPP 自动按 NALU 分割码流。
ret = dec_mpi->control(dec_ctx, dec_mpi_cmd, dec_param); // 发送控制命令。
dec_mpi_cmd = MPP_SET_INPUT_BLOCK; // 设置命令为“设置输入为阻塞模式”。
dec_param = &need_split; // 参数为 need_split 的地址。
ret = dec_mpi->control(dec_ctx, dec_mpi_cmd, dec_param); // 发送控制命令,让解码输入接口在没有缓冲区时阻塞等待。
创建mpp_decode_ffmpeg_thread线程
pthread_create(&pid, NULL, mpp_decode_ffmpeg_thread, &dec_data);
// 这是一个线程函数,负责从 FFmpeg 拉取码流并送入 MPP 解码器。
// 参数 args: 一个 void 指针,实际指向一个 MpiDecLoopData 结构体,包含了该线程所需的所有数据。
void *mpp_decode_ffmpeg_thread(void *args)
{
// 将传入的 void 指针强制转换为 MpiDecLoopData 指针,以便访问其中的数据。
MpiDecLoopData *data = (MpiDecLoopData *)args;
while (1) // 进入一个无限循环,持续处理视频流。
{
// 从媒体流 (pFormatCtx) 中读取一帧(一个数据包),并将其存储在 data->av_packet 中。
if (av_read_frame(pFormatCtx, &data->av_packet) >= 0)
{
// 检查 is_flv_ts 标志。如果为 true,说明码流是 RTMP/SRT 等格式,
// 其 H.264/H.265 数据包是 MP4 格式(长度前缀),需要转换为 Annex B 格式(起始码00 00 00 01)。
if (is_flv_ts == true)
{
// 使用之前初始化的码流过滤器 (bsf, 即 "h264_mp4toannexb") 来转换数据包。
// 这个函数会原地修改 data->av_packet 的 data 和 size。
int ret = av_bitstream_filter_filter(bsf, pFormatCtx->streams[videoindex]->codec, NULL,
&data->av_packet.data, &data->av_packet.size,
data->av_packet.data, data->av_packet.size,
0);
// 调用辅助函数,将处理过的视频包送入 Rockchip MPP 硬件解码器。
decode_ffmpeg_venc(data, &data->av_packet);
}
else // 如果码流不需要转换(例如,来自 RTSP 的流通常已经是 Annex B 格式)。
{
// 直接调用辅助函数,将原始视频包送入解码器。
decode_ffmpeg_venc(data, &data->av_packet);
}
// 每次 av_read_frame 成功后,都需要在使用完 packet 后调用 av_packet_unref 来释放其引用的数据,否则会导致内存泄漏。
av_packet_unref(&data->av_packet);
}
}
}
decode_ffmpeg_venc函数的处理
// 定义一个函数,负责将单个 FFmpeg 数据包送入 MPP 解码器并获取解码后的帧。
// 参数 data: 指向 MpiDecLoopData 结构体,包含解码器上下文等信息。
// 参数 av_packet: 指向从 FFmpeg 读取到的、包含编码数据的 AVPacket。
int decode_ffmpeg_venc(MpiDecLoopData *data, AVPacket *av_packet)
{
MPP_RET ret = MPP_OK; // 初始化 MPP 返回值变量为成功。
MppPacket packet = NULL; // 初始化 MppPacket 指针为空,用于存放待解码的数据。
MppCtx ctx = data->ctx; // 从 data 结构体中获取 MPP 上下文。
MppApi *mpi = data->mpi; // 从 data 结构体中获取 MPP API 函数指针集。
RK_U32 pkt_done = 0; // 标志位,表示当前 packet 是否已成功送入解码器。0 表示未成功。
RK_U32 pkt_eos = 0; // 标志位,表示当前 packet 是否是码流的结束包 (End of Stream)。
RK_U32 err_info = 0; // 用于存储解码帧的错误信息。
MppFrame frame = NULL; // 初始化 MppFrame 指针为空,用于接收解码后的图像帧。
MppBuffer buffer = NULL; // 初始化 MppBuffer 指针,用于指向帧数据所在的缓冲区。
RK_U8 *base = NULL; // 初始化一个指针,用于指向解码后 YUV 数据的内存起始地址。
// 定义一些变量用于存储解码后帧的尺寸信息,此处未立即使用。
RK_U32 width = 0;
RK_U32 height = 0;
RK_U32 h_stride = 0;
RK_U32 v_stride = 0;
// 将 FFmpeg 的 av_packet 封装成 MPP 的 mpp_packet。
// 这个函数不会复制数据,只是创建一个指向 av_packet->data 的 packet 头。
ret = mpp_packet_init(&packet, av_packet->data, av_packet->size);
mpp_packet_set_pts(packet, av_packet->pts); // 设置 packet 的显示时间戳 (PTS)。
#if 1 // 预编译指令,使这部分代码生效。
// 使用 do-while 循环来确保即使解码器输入队列满时,也能重试发送。
do
{
RK_S32 times = 5; // 设置获取解码帧时的重试次数。
// 如果当前 packet 还没有送入解码器...
if (!pkt_done)
{
// 调用 mpi 接口,将 packet 送入解码器。
ret = mpi->decode_put_packet(ctx, packet);
if (MPP_OK == ret) // 如果成功送入...
pkt_done = 1; // 将标志位置为 1。
}
// 循环地从解码器获取所有已经解码完成的帧。
do
{
RK_S32 get_frm = 0; // 标志位,表示本次循环是否成功获取到一帧。
RK_U32 frm_eos = 0; // 标志位,表示获取到的帧是否是码流的最后一帧。
try_again: // 一个 goto 标签,用于超时重试。
// 调用 mpi 接口,尝试从解码器获取一帧。
ret = mpi->decode_get_frame(ctx, &frame);
if (MPP_ERR_TIMEOUT == ret) // 如果返回超时错误...
{
if (times > 0) // 如果还有重试次数...
{
times--; // 重试次数减 1。
msleep(2); // 等待 2 毫秒。
goto try_again; // 跳转到 try_again 标签处再次尝试。
}
mpp_err("decode_get_frame failed too much time\n"); // 重试多次后仍然超时,打印错误。
}
if (MPP_OK != ret) // 如果是其他类型的错误...
{
mpp_err("decode_get_frame failed ret %d\n", ret); // 打印错误信息。
break; // 退出内层循环。
}
if (frame) // 如果成功获取到一帧 (frame 不为 NULL)...
{
// 检查这一帧是否带有视频参数(如分辨率)变更的信息。
if (mpp_frame_get_info_change(frame))
{
// 获取新的视频宽度、高度、水平步长、垂直步长和缓冲区大小。
RK_U32 width = mpp_frame_get_width(frame);
RK_U32 height = mpp_frame_get_height(frame);
RK_U32 hor_stride = mpp_frame_get_hor_stride(frame);
RK_U32 ver_stride = mpp_frame_get_ver_stride(frame);
RK_U32 buf_size = mpp_frame_get_buf_size(frame);
printf("decode_get_frame get info changed found\n");
printf("decoder require buffer w:h [%d:%d] stride [%d:%d] buf_size %d",
width, height, hor_stride, ver_stride, buf_size);
// 如果分辨率变化,需要重新配置解码器使用的外部缓冲区组。
ret = mpp_buffer_group_get_internal(&data->frm_grp, MPP_BUFFER_TYPE_ION);
if (ret)
{
printf("get mpp buffer group failed ret %d\n", ret);
break;
}
// 将新的缓冲区组设置给解码器。
mpi->control(ctx, MPP_DEC_SET_EXT_BUF_GROUP, data->frm_grp);
// 通知解码器,信息变更已经处理完毕,可以继续解码。
mpi->control(ctx, MPP_DEC_SET_INFO_CHANGE_READY, NULL);
}
else // 如果是正常的图像帧...
{
// 检查帧是否有错误信息或者是否被标记为应丢弃。
err_info = mpp_frame_get_errinfo(frame) | mpp_frame_get_discard(frame);
if (err_info)
{
mpp_log("decoder_get_frame get err info:%d discard:%d.\n",
mpp_frame_get_errinfo(frame), mpp_frame_get_discard(frame));
}
data->frame_count++; // 解码帧计数器加 1。
buffer = mpp_frame_get_buffer(frame); // 获取帧数据所在的 MppBuffer。
base = (RK_U8 *)mpp_buffer_get_ptr(buffer); // 获取指向 YUV 数据内存的指针。
// 获取这一帧的实际尺寸信息。
RK_U32 width = mpp_frame_get_width(frame);
RK_U32 height = mpp_frame_get_height(frame);
RK_U32 hor_stride = mpp_frame_get_hor_stride(frame);
RK_U32 ver_stride = mpp_frame_get_ver_stride(frame);
RK_U32 buf_size = mpp_frame_get_buf_size(frame);
if (base != NULL) // 如果成功获取到数据指针...
{
// 将解码后的 YUV 数据从 MPP 缓冲区复制到我们之前创建的共享内存中。
memcpy(shareMemory_Video_Data, base, VIDEO_YUV_SIZE);
}
}
frm_eos = mpp_frame_get_eos(frame); // 检查这是否是码流的最后一帧。
mpp_frame_deinit(&frame); // 释放 MppFrame 结构体自身(注意:这不会释放 YUV 数据缓冲区)。
frame = NULL; // 将 frame 指针置空,防止重复释放。
get_frm = 1; // 标记本次循环成功获取到一帧。
}
// 如果已经发送了最后一包,但还没收到最后一帧,则继续等待。
if (pkt_eos && pkt_done && !frm_eos)
{
msleep(10);
continue;
}
if (frm_eos) // 如果收到了最后一帧...
{
printf("found last frame\n");
break; // 退出内层循环。
}
if (get_frm) // 如果本次循环获取到了帧,继续尝试获取下一帧。
continue;
break; // 如果没获取到帧,则退出内层循环。
} while (1);
// (这部分逻辑用于限制解码帧数,当前被注释)
if (data->frame_num > 0 && data->frame_count >= data->frame_num)
{
mpp_log("reach max frame number %d\n", data->frame_count);
break; // 退出外层循环。
}
if (pkt_done) // 如果 packet 已经成功发送,则退出外层循环。
break;
// 如果发送失败 (pkt_done=0),则等待 3 毫秒后重试。
msleep(3);
} while (1);
#endif
}
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)