一.本章节内容:

本章节主要介绍如何通过FFMPEG推流器将低分辨率队列的每帧数据传输至低码流分辨率流媒体服务器,相关代码位于rkmedia_assignment_manage.cpp和rkmedia_data_process.cpp文件中。低分辨率编码码流的推送流程与高分辨率基本一致,唯一区别在于分辨率参数的设置。

二.低分辨率编码码流推流的流程

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

2.1. 初始化RKMEDIA_FFMPEG_CONFIG结构体

typedef struct
{
    AVStream *stream; 
    AVCodecContext *enc;
    int64_t next_timestamp;
    int samples_count;
    AVPacket *packet;
} OutputStream;

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的成员变量

2.2.1. width:推流器的width,width和rv1126编码器的width一致

2.2.2. height:推流器的height,height和rv1126编码器的height一致

2.2.3. config_id:config_id,暂时没用到

2.2.4. protocol_type:流媒体的类型

2.2.5.network_addr:流媒体地址

2.2.6.video_codec:视频编码器ID
2.2.7.audio_codec:音频编码器ID

2.2.8.video_stream:自定义VIDEO的STREAM结构体配置

2.2.9.audio_stream:自定义AUDIO的STREAM结构体配置

上面是低分辨率rkmedia_ffmpeg_config的设置

2.2. 调用init_rkmedia_ffmpeg_context来初始化1280* 720推流器

 init_rkmedia_ffmpeg_context(ffmpeg_config);

init_rkmedia_ffmpeg_context是初始化rkmedia_ffmpeg_config的设置,关于这个函数的内容在之前的课程已经说了。这里不做过多的介绍。

2.3. 创建low_video_push_thread线程

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;
}

Low_video_push_thread的核心功能是从LOW_VIDEO_QUEUE队列获取1280×720分辨率的H264编码视频帧,将其封装为AVPacket结构体后,通过FFmpeg接口推送至流媒体服务器。

2.4. 从LOW_VIDEO_QUEUE获取每一帧H264数据码流并且赋值到AVPacket

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 每个AVPacket中都进行关键帧设置,这个标识符必须要加,否则播放器则无法正常解码出视频。
        if (video_data_packet != NULL)
        {
            free(video_data_packet);
            video_data_packet = NULL;
        }

        return pkt;
    }
    else
    {
        return NULL;
    }
}
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;
}

该代码从LOW_VIDEO_QUEUE队列中提取1280×720分辨率的H264帧数据,并将其封装到AVPacket中,这一过程由deal_low_video_packet函数实现。deal_high_video_packet函数的核心功能是从队列获取帧数据并填充到AVPacket,具体实现如下:

关键处理环节包括:

  1. 将video_data_packet的视频数据赋给AVPacket
  2. 该赋值操作分为两部分:
    • AVPacket缓冲区数据的填充
    • AVPacket长度的设置

2.4.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。

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

2.4.2. AVPacket缓冲区长度的赋值:

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

2.4.3. AVPacket关键帧标识符的赋值:

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

pkt->flags |= AV_PKT_FLAG_KEY;                                                          // 默认flags是AV_PKT_FLAG_KEY 每个AVPacket中都进行关键帧设置,这个标识符必须要加,否则播放器则无法正常解码出视频。

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

根据AVPacket的数据去计算视频的PTS,若AVPacket的数据不为空。则让视频pts = ost->next_timestamp++(关于video的PTS计算,前几章节已经讲了)

 if (video_packet != NULL)
    {
        video_packet->pts = ost->next_timestamp++; // VIDEO_PTS按照帧率进行累加
    }

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

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

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

int write_ffmpeg_avpacket(AVFormatContext *fmt_ctx, const AVRational *time_base, AVStream *st, AVPacket *pkt)
{
    /*返回值类型 int:FFmpeg标准错误码(0成功,负值失败)。
    参数 AVFormatContext *fmt_ctx:FFmpeg格式上下文,管理输出文件格式(如MP4/TS)、IO操作及全局元数据。
    参数 const AVRational *time_base:编码器时间基(如{1, 90000}),用于时间戳单位转换。
    参数 AVStream *st:目标输出流(视频流),包含流索引、时间基、编码参数等。
    参数 AVPacket *pkt:待写入的视频数据包,需包含有效数据、时间戳及流索引。 */

    /*将数据包的时间戳(PTS/DTS)从编码器时间基转换为流时间基*/
    av_packet_rescale_ts(pkt, *time_base, st->time_base);
    pkt->stream_index = st->index;//将数据包写入输出文件,并自动处理数据包交织(确保音视频帧按时间顺序排列)

    //将数据包写入输出文件,并自动处理数据包交织(确保音视频帧按时间顺序排列)
    return av_interleaved_write_frame(fmt_ctx, pkt);
}

Logo

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

更多推荐