解码模块

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解码的大体流程:

  1. 调用mpp_create获取MppCtx实例和MppApi
  2. 调用mpp_init初始化MppCtx 的编解码类型与格式。
  3. 调用control设置MPP解码参数
  4. 调用avreadframe读取每一帧的视频压缩数据(这个数据可以是摄像头编码数据也可以是H264文件读取的数据)
  5. 调用mpp_packet_init赋值MppPacket
  6. 调用mpp_packet_set_pts设置MppPacket的PTS时间戳
  7. 调用decode_put_packet发送到解码器
  8. 调用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
}
Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐