RV1126+FFMPEG多路码流监控项目——高低分辨率码流推送流媒体

一、内容概要

本章的内容主要是讲解如何通过高低分辨率队列的每一帧数据,并且通过FFMPEG推流器分别传输到高低码流分辨率流媒体服务器,本章节代码在rkmedia_assignment_manage.cpp和rkmedia_data_process.cpp里面。低分辨率编码码流推送的过程和高分辨率编码码流的推送的过程基本上一致,唯一的区别在于分辨率的设置。

二、推流过程

推流过程总共分成6个步骤。分别是初始化RKMEDIA_FFMPEG_CONFIG结构体、调用init_rkmedia_ffmpeg_context设置高低分辨率的推流器、创建video_push_thread和low_video_push_thread线程、分别从VIDEO_QUEUELOW_VIDEO_QUEUE队列获取每一帧视频数据 、每一帧AVPacket计算PTS并进行时间基转换、利用FFMPEG的API推送每一帧视频数据到流媒体服务器,下面我们来具体看每一个步骤的实现过程:

2.1 初始化RKMEDIA_FFMPEG_CONFIG结构体

rkmedia_ffmpeg_config.h中创建结构体

复制代码
typedef struct
{
    int width;
    int height;
    unsigned int config_id;
    int protocol_type; //流媒体TYPE
    char network_addr[NETWORK_ADDR_LENGTH];//流媒体地址
    enum AVCodecID video_codec; //视频编码器ID
    enum AVCodecID audio_codec; //音频编码器ID
    OutputStream video_stream; //VIDEO的STREAM配置
    OutputStream audio_stream; //AUDIO的STREAM配置
    AVFormatContext *oc; //是存储音视频封装格式中包含的信息的结构体,也是FFmpeg中统领全局的结构体,对文件的封装、编码操作从这里开始。

} RKMEDIA_FFMPEG_CONFIG; //FFMPEG配置

这段代码定义了项目自定义的RKMedia 对接 FFmpeg 推流的全局配置结构体 RKMEDIA_FFMPEG_CONFIG,是整个推流模块的顶层管理容器。它把业务侧的分辨率、协议、地址等自定义参数,和 FFmpeg 原生的格式上下文、音视频流对象全部封装在一起,统一管理一路推流的全部配置与资源句柄。

在这个项目的双码流推流项目中,每一路推流(1080P 高清 / 720P 标清)都会对应一个独立的该结构体实例,所有初始化、推流、释放操作都以这个结构体指针作为核心入参传递,避免零散变量满天飞。

我们来看看RKMEDIA_FFMPEG_CONFIG的成员变量

  1. 1. width **:**推流器的width,width和rv1126编码器的width一致
  2. 2. height **:**推流器的height,height和rv1126编码器的height一致
  3. 3. config_id **:**config_id,暂时没用到
  4. 4. protocol_type **:**流媒体的类型
  5. 5.network_addr **:**流媒体地址
  6. 6.video_codec **:**视频编码器ID
  7. 7.audio_codec **:**音频编码器ID
  8. 8.video_stream **:**自定义VIDEO的STREAM结构体配置
  9. 9.audio_stream **:**自定义AUDIO的STREAM结构体配置
  10. 在rkmedia_assignment_manage.cpp中进行参数配置
复制代码
    RKMEDIA_FFMPEG_CONFIG *ffmpeg_config = (RKMEDIA_FFMPEG_CONFIG *)malloc(sizeof(RKMEDIA_FFMPEG_CONFIG));
        if (ffmpeg_config == NULL)
        {
            printf("malloc ffmpeg_config failed\n");
        }
        ffmpeg_config->width = 1920;
        ffmpeg_config->height = 1080;
        ffmpeg_config->config_id = 0;
        ffmpeg_config->protocol_type = protocol_type;
        ffmpeg_config->video_codec = AV_CODEC_ID_H264;
        ffmpeg_config->audio_codec = AV_CODEC_ID_AAC;
        memcpy(ffmpeg_config->network_addr, network_address, strlen(network_address));
        //初始化ffmpeg输出模块
        init_rkmedia_ffmpeg_context(ffmpeg_config);

    RKMEDIA_FFMPEG_CONFIG *low_ffmpeg_config = (RKMEDIA_FFMPEG_CONFIG *)malloc(sizeof(RKMEDIA_FFMPEG_CONFIG));
        if (ffmpeg_config == NULL)
        {
            printf("malloc ffmpeg_config failed\n");
        }

        //char  * low_network_address = "rtmp://192.168.1.66:1935/live/02";
        low_ffmpeg_config->width = 1280;
        low_ffmpeg_config->height = 720;
        low_ffmpeg_config->config_id = 1;
        low_ffmpeg_config->protocol_type = protocol_type;
        low_ffmpeg_config->video_codec = AV_CODEC_ID_H264;
        low_ffmpeg_config->audio_codec = AV_CODEC_ID_AAC;
        //memcpy(low_ffmpeg_config->network_addr, low_network_address, strlen(low_network_address));
        memcpy(low_ffmpeg_config->network_addr, low_url_address, strlen(low_url_address));

        init_rkmedia_ffmpeg_context(low_ffmpeg_config);
  1. 这两段代码是 1080P和720P的推流通道的启动入口,完整执行「配置结构体内存分配 → 高清通道业务参数填充 → FFmpeg 推流器初始化」三步核心逻辑,是整个推流模块从无到有的第一步。执行完成后,推流器的参数配置与网络通道就全部就绪,下一步即可创建推流线程、循环送入硬件编码帧进行推流。

2.2 创建 推流 线程

在rkmedia_assignment_manage.cpp中进行线程的创建

复制代码
//创建HIGH_PUSH线程
ret = pthread_create(&pid, NULL, high_video_push_thread, (void *)ffmpeg_config);
if (ret != 0)
{
    printf("push_server_thread error\n");
}

//创建LOW_PUSH线程
ret = pthread_create(&pid, NULL, low_video_push_thread, (void *)low_ffmpeg_config);
if (ret != 0)
{
    printf("push_server_thread error\n");
}

在rkmeida_data_process.cpp中写线程函数

复制代码
void *high_video_push_thread(void *args)
{
    pthread_detach(pthread_self());
    RKMEDIA_FFMPEG_CONFIG ffmpeg_config = *(RKMEDIA_FFMPEG_CONFIG *)args;
    free(args);
    AVOutputFormat *fmt = NULL;
    int ret;

    while (1)
    {
        ret = deal_high_video_avpacket(ffmpeg_config.oc, &ffmpeg_config.video_stream); // 处理FFMPEG视频数据
        if (ret == -1)
        {
            printf("deal_video_avpacket error\n");
            break;
        }
    }

    av_write_trailer(ffmpeg_config.oc);                         // 写入AVFormatContext的尾巴
    free_stream(ffmpeg_config.oc, &ffmpeg_config.video_stream); // 释放VIDEO_STREAM的资源
    free_stream(ffmpeg_config.oc, &ffmpeg_config.audio_stream); // 释放AUDIO_STREAM的资源
    avio_closep(&ffmpeg_config.oc->pb);                         // 释放AVIO资源
    avformat_free_context(ffmpeg_config.oc);                    // 释放AVFormatContext资源
    return NULL;
}

void *low_video_push_thread(void *args)
{
    pthread_detach(pthread_self());
    RKMEDIA_FFMPEG_CONFIG ffmpeg_config = *(RKMEDIA_FFMPEG_CONFIG *)args;
    free(args);
    AVOutputFormat *fmt = NULL;
    int ret;

    while (1)
    {
        ret = deal_low_video_avpacket(ffmpeg_config.oc, &ffmpeg_config.video_stream); // 处理FFMPEG视频数据
        if (ret == -1)
        {
            printf("deal_video_avpacket error\n");
            break;
        }
    }

    av_write_trailer(ffmpeg_config.oc);                         // 写入AVFormatContext的尾巴
    free_stream(ffmpeg_config.oc, &ffmpeg_config.video_stream); // 释放VIDEO_STREAM的资源
    free_stream(ffmpeg_config.oc, &ffmpeg_config.audio_stream); // 释放AUDIO_STREAM的资源
    avio_closep(&ffmpeg_config.oc->pb);                         // 释放AVIO资源
    avformat_free_context(ffmpeg_config.oc);                    // 释放AVFormatContext资源
    return NULL;
}

这段代码是1080P 和720P通道的推流线程主入口,在队列获取每一帧H264编码视频流,然后再把每一帧H264的码流数据先赋值到AVPacket,再调用FFMPEG的API把视频流传输到流媒体服务器。是双码流推流架构中高清支路的核心运行载体。 它以独立子线程的形式常驻运行

  1. 循环执行「从队列取帧 → 时间戳转换 → FLV 封装 → RTMP 推送」的核心业务;
  2. 承担异常兜底:推流异常终止时,自动按照规范顺序完成全套 FFmpeg 资源回收,避免内存泄漏、网络连接残留、服务器挂死流等问题。

2.3 队列 获取每一帧 H264 数据码流并且赋值到 AVPacket

在rkmeida_data_process.cpp中写核心处理函数

复制代码
int deal_high_video_avpacket(AVFormatContext *oc, OutputStream *ost)
{
    int ret;
    AVCodecContext *c = ost->enc;
    AVPacket *video_packet = get_high_ffmpeg_video_avpacket(ost->packet); // 从RV1126视频编码数据赋值到FFMPEG的Video AVPacket中
    if (video_packet != NULL)
    {
        video_packet->pts = ost->next_timestamp++; // VIDEO_PTS按照帧率进行累加
    }

    ret = write_ffmpeg_avpacket(oc, &c->time_base, ost->stream, video_packet); // 向复合流写入视频数据
    if (ret != 0)
    {
        printf("write video avpacket error");
        return -1;
    }

    return 0;
}

int deal_low_video_avpacket(AVFormatContext *oc, OutputStream *ost)
{
    int ret;
    AVCodecContext *c = ost->enc;
    AVPacket *video_packet = get_low_ffmpeg_video_avpacket(ost->packet); // 从RV1126视频编码数据赋值到FFMPEG的Video AVPacket中
    if (video_packet != NULL)
    {
        video_packet->pts = ost->next_timestamp++; // VIDEO_PTS按照帧率进行累加
    }

    ret = write_ffmpeg_avpacket(oc, &c->time_base, ost->stream, video_packet); // 向复合流写入视频数据
    if (ret != 0)
    {
        printf("write video avpacket error");
        return -1;
    }

    return 0;
}

这是推流链路的单帧核心处理函数,运行在推流线程的主循环中。 它的核心职责是串联「取帧 → 打时间戳 → 封装推送」三步:从码流队列中获取 RV1126 硬件编码好的 H.264 裸流帧,通过计数累加的方式给数据包打上 PTS 时间戳,最终调用 FFmpeg 封装接口将数据包写入复合流,完成 FLV 封装与 RTMP 推送。

复制代码
// 从RV1126视频编码数据赋值到FFMPEG的Video AVPacket中
AVPacket *get_high_ffmpeg_video_avpacket(AVPacket *pkt)
{
    video_data_packet_t *video_data_packet = high_video_queue->getVideoPacketQueue(); // 从视频队列获取数据

    if (video_data_packet != NULL)
    {
        /*
     重新分配给定的缓冲区
   1.  如果入参的 AVBufferRef 为空,直接调用 av_realloc 分配一个新的缓存区,并调用 av_buffer_create 返回一个新的 AVBufferRef 结构;
   2.  如果入参的缓存区长度和入参 size 相等,直接返回 0;
   3.  如果对应的 AVBuffer 设置了 BUFFER_FLAG_REALLOCATABLE 标志,或者不可写,再或者 AVBufferRef data 字段指向的数据地址和 AVBuffer 的 data 地址不同,递归调用 av_buffer_realloc 分配一个新
的 buffer,并将 data 拷贝过去;
   4.  不满足上面的条件,直接调用 av_realloc 重新分配缓存区。
 */
        int ret = av_buffer_realloc(&pkt->buf, video_data_packet->video_frame_size + 70);
        if (ret < 0)
        {
            return NULL;
        }
        pkt->size = video_data_packet->video_frame_size;                                        // rv1126的视频长度赋值到AVPacket Size
        memcpy(pkt->buf->data, video_data_packet->buffer, video_data_packet->video_frame_size); // rv1126的视频数据赋值到AVPacket data
        pkt->data = pkt->buf->data;                                                             // 把pkt->buf->data赋值到pkt->data
        pkt->flags |= AV_PKT_FLAG_KEY;                                                          // 默认flags是AV_PKT_FLAG_KEY
        if (video_data_packet != NULL)
        {
            free(video_data_packet);
            video_data_packet = NULL;
        }

        return pkt;
    }
    else
    {
        return NULL;
    }
}

AVPacket *get_low_ffmpeg_video_avpacket(AVPacket *pkt)
{
    video_data_packet_t *video_data_packet = low_video_queue->getVideoPacketQueue(); // 从视频队列获取数据

    if (video_data_packet != NULL)
    {
        /*
     重新分配给定的缓冲区
   1.  如果入参的 AVBufferRef 为空,直接调用 av_realloc 分配一个新的缓存区,并调用 av_buffer_create 返回一个新的 AVBufferRef 结构;
   2.  如果入参的缓存区长度和入参 size 相等,直接返回 0;
   3.  如果对应的 AVBuffer 设置了 BUFFER_FLAG_REALLOCATABLE 标志,或者不可写,再或者 AVBufferRef data 字段指向的数据地址和 AVBuffer 的 data 地址不同,递归调用 av_buffer_realloc 分配一个新
的 buffer,并将 data 拷贝过去;
   4.  不满足上面的条件,直接调用 av_realloc 重新分配缓存区。
 */
        int ret = av_buffer_realloc(&pkt->buf, video_data_packet->video_frame_size + 70);
        if (ret < 0)
        {
            return NULL;
        }
        pkt->size = video_data_packet->video_frame_size;                                        // rv1126的视频长度赋值到AVPacket Size
        memcpy(pkt->buf->data, video_data_packet->buffer, video_data_packet->video_frame_size); // rv1126的视频数据赋值到AVPacket data
        pkt->data = pkt->buf->data;                                                             // 把pkt->buf->data赋值到pkt->data
        pkt->flags |= AV_PKT_FLAG_KEY;                                                          // 默认flags是AV_PKT_FLAG_KEY
        if (video_data_packet != NULL)
        {
            free(video_data_packet);
            video_data_packet = NULL;
        }

        return pkt;
    }
    else
    {
        return NULL;
    }
}

这段代码是推流链路中,硬件编码层与 FFmpeg 封装层的核心数据衔接函数 ,是推流主循环里 "从队列取帧" 步骤的具体实现。 它的核心作用是:从标清码流队列中取出 RV1126 硬件 VENC 编码输出的自定义 H.264 裸流数据包,把数据拷贝、转换为 FFmpeg 标准的AVPacket结构体,完成业务层数据结构到 FFmpeg 标准结构的格式转换,供后续时间戳计算、FLV 封装、RTMP 推送使用。

这里面有几个比较核心的地方:video_data_packet的视频数据包赋值到AVPacket,这里要赋值两部分:一部分是AVPacket缓冲区数据的赋值,另外一个是AVPacket的长度赋值。

2.3.1 AVPacket 缓冲区的赋值

首先用av_buffer_realloc 分配每一个缓冲区数据。要注意的是AVPacket中缓冲区的buf是不能直接赋值的,如:memcpy(pkt->data, video_data_packet->buffer, video_data_packet->frame_size) 否则程序就会出现core_dump 情况。我们需要先把video_data_packet_t的视频数据(video_data_packet->buffer)先拷贝到pkt->buf->data,然后再把pkt->buf->data的数据赋值到pkt->data。

2.3.2 AVPacket 缓冲区长度的赋值

把video_data_packet的video_frame_size长度直接赋值给AVPacket的pkt->size。

2.3.3 AVPacket 关键帧标识符的赋值

添加了这个标识符后,每个AVPacket中都进行关键帧设置,这个标识符必须要加,否则播放器则无法正常解码出视频。

2.5. 每一帧 AVPacket 计算 PTS 时间戳

根据AVPacket的数据去计算视频的PTS,若AVPacket的数据不为空。则让视频pts = ost->next_timestamp++

把视频PTS进行时间基的转换,调用av_packet_rescale_ts把采集的视频时间基转换成复合流的时间基。

2.5.1 PTS 与时间基的绑定关系

时间戳数值本身没有任何物理意义,必须搭配对应流的时间基(time_base)才能换算出真实的播放时间

核心换算公式

复制代码
真实播放时间(秒) = PTS数值 × (time_base.num / time_base.den)

举个最典型的例子:

  • 流时间基配置为 {1, 25}(1 秒切分为 25 份,每份 40ms)
  • 某帧 PTS = 10
  • 真实播放时间 = 10 × (1/25) = 0.4 秒,即第 400 毫秒播放该帧

硬性规则(所有场景必须遵守)

  1. 单调递增:后续帧的 PTS 必须大于前面的帧,不能回退、不能跳变、不能重复;
  2. 单位匹配 :PTS 的单位必须和 AVStream->time_base 完全对应,禁止跨时间基直接赋值;
  3. 帧率对齐:相邻两帧的 PTS 差值,必须等于真实的帧间隔,否则播放速度会变快或变慢。

2.5.2 实现方式一:帧计数累加式(本项目采用的方案)

对应代码

复制代码
video_packet->pts = ost->next_timestamp++;

这是嵌入式纯视频推流最常用的轻量化方案,标清、高清通道都采用了该实现。

计算原理

OutputStream 结构体中维护一个全局计数器 next_timestamp,初始值一般为 0;每成功处理一帧,计数器自增 1,直接作为当前帧的 PTS 值。 它的本质是用「帧序号」充当时间戳,配合与帧率一致的时间基,间接表示真实时间。

匹配的时间基配置

必须保证编码器 / 流的时间基和帧率严格对应:

  • 25 帧率 → 时间基 {1, 25},每帧 PTS+1 = 时间前进 40ms
  • 30 帧率 → 时间基 {1, 30},每帧 PTS+1 = 时间前进约 33.3ms

数值示例(25fps 场景)

帧序号 PTS 值 对应真实时间 说明
第 0 帧 0 0 秒 首帧,播放起点
第 1 帧 1 0.04 秒 间隔 40ms
第 25 帧 25 1 秒 刚好 1 秒 25 帧
第 125 帧 125 5 秒 累计播放 5 秒

优缺点与适用场景

  • 优点:实现极其简单,PTS 绝对单调递增,不会出现跳变、回退,稳定性极强;不依赖硬件时间戳,不会因为硬件计时异常导致播放错乱。
  • 缺点:必须和硬件实际编码帧率严格对齐,硬件丢帧、帧率波动时,会出现播放速度偏快 / 偏慢;纯视频场景无影响,搭配音频时容易出现音画不同步;无真实时间信息,不支持多设备时间同步。
  • 适用场景:纯视频监控推流、无音频、对绝对时间无要求、帧率稳定的嵌入式场景,和你当前的项目高度匹配。

2.5.3 实现方式二:硬件时间戳转换式

核心原理

RV1126 硬件 VENC 编码输出每帧时,会自带一个原生的硬件时间戳(通常为微秒级,对应时间基 {1, 1000000}),我们通过 FFmpeg 标准接口 av_rescale_q,将硬件时间戳转换为流时间基下的 PTS 值。

这是带音频、多码流同步、高精度场景的标准实现,也是工业级推流的通用方案。

标准计算步骤

  1. 从硬件帧结构体中取出原生时间戳 hw_pts(单位微秒);

  2. 定义源时间基(硬件时间基):AVRational hw_tb = {1, 1000000};

  3. 获取目标时间基(流时间基):AVRational stream_tb = ost->stream->time_base;

  4. 调用标准接口完成转换:

    复制代码
    video_packet->pts = av_rescale_q(hw_pts, hw_tb, stream_tb);

计算示例

  • 硬件输出某帧时间戳:40000 微秒(即 40ms)
  • 硬件时间基:{1, 1000000}
  • 流时间基:{1, 25}
  • 转换结果:40000 * (1/1000000) / (1/25) = 1
  • 和计数累加的结果完全一致,但是以硬件真实采集时间为基准。

优缺点与适用场景

  • 优点:以硬件采集时间为准,帧率波动、少量丢帧不影响整体播放速度;搭配音频时可共用同一时间基准,音画同步精度更高;支持多设备、多码流时间对齐。
  • 缺点:依赖硬件时间戳的稳定性,若硬件时间戳出现跳变、回退,需要额外做平滑修正。
  • 适用场景:带音频的推流、多码流同步、对播放精度要求高的工业级场景。

2.6. 把每一帧视频数据传输到流媒体服务器

时间基转换完成之后,就把视频数据写入到复合流文件里面,调用的API是av_interleaved_write_frame (注意:复合流文件可以是本地文件也可以是流媒体地址)。

复制代码
int write_ffmpeg_avpacket(AVFormatContext *fmt_ctx, const AVRational *time_base, AVStream *st, AVPacket *pkt)
{
    /*将输出数据包时间戳值从编解码器重新调整为流时基 */
    av_packet_rescale_ts(pkt, *time_base, st->time_base);
    pkt->stream_index = st->index;

    return av_interleaved_write_frame(fmt_ctx, pkt);
}

至此,推流部分内容就完成了。