FFMPEG推流器——输出模块初始化

上一章了解了FFMPEG的重要结构体,这一章是对输出模块的初始化,输出模块的最大作用是对音视频推流模块进行初始化让其能够正常工作起来,RV1126的码流通过FFMPEG进行推流,输出模块一般由几个步骤。分别由avformat_alloc_output_context2 分配 AVFormatContextavformat_new_stream 初始化 AVStream 结构体 、avcodec_find_encoder找出对应的codec编码器、利用avcodec_alloc_context3分配AVCodecCotext、设置AVCodecContext结构体参数、利用avcodec_parameters_from_context把codec参数传输到AVStream里面的参数、avio_open初始化FFMPEG的IO结构体、avformat_write_header初始化AVFormatContext。在RV1126+FFMPEG多路码流推流的项目中,FFMPEG输出模块的初始化在rkmedia_ffmpeg_config.cpp的init_rkmedia_ffmpeg_context 里面**。**

一、输出配置框图

这是 FFmpeg 标准「软编码 + 封装输出」的完整初始化流程,涵盖了从创建输出上下文、配置编码器、同步流参数、打开 IO 到写入文件头的全链路步骤,是本地文件录制、网络推流的通用标准流程。

二、具体代码实现

2.1 分配 AVFormatContext 输出的上下文结构体指针

在rkmedia_ffmpeg_config.cpp中配置

复制代码
// 初始化RKMedia对接FFmpeg的输出格式上下文
// 入参 ffmpeg_config:FFmpeg推流全局配置结构体指针,包含协议类型、推流地址、输出上下文句柄等核心配置
int init_rkmedia_ffmpeg_context(RKMEDIA_FFMPEG_CONFIG *ffmpeg_config)
{
    int ret = 0;

    //FLV_PROTOCOL is RTMP TCP
    // FLV封装格式分支:对应RTMP协议、TCP传输的常规直播推流场景
    if (ffmpeg_config->protocol_type == FLV_PROTOCOL)
    {
        //初始化一个FLV的AVFormatContext
        // 分配并初始化FLV格式的输出格式上下文,将创建好的句柄存入配置结构体的oc成员
        ret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "flv", ffmpeg_config->network_addr); 
        if (ret < 0)
        {
            // 上下文分配失败,直接返回错误
            return -1;
        }
    }
    //TS_PROTOCOL is SRT UDP RTSP
    // TS封装格式分支:对应SRT、UDP、RTSP等低延迟/高容错流媒体场景
    else if (ffmpeg_config->protocol_type == TS_PROTOCOL)
    {
        //初始化一个TS的AVFormatContext
        // 分配并初始化MPEG-TS格式的输出格式上下文,将创建好的句柄存入配置结构体的oc成员
        ret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "mpegts", ffmpeg_config->network_addr);
        if (ret < 0)
        {
            // 上下文分配失败,直接返回错误
            return -1;
        }
    }

这是 FFmpeg 输出模块初始化的第一步入口函数 ,核心职责是根据配置的协议类型,创建对应封装格式的 AVFormatContext 全局输出上下文,为后续添加音视频流、打开 IO、写文件头打下基础。

int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat, const char *format_name, const char *filename)

第一个传输参数: AVFormatContext结构体指针的指针,是存储音视频封装格式中包含的信息的结构体,所有对文件的封装、编码都是从这个结构体开始。

第二个传输参数: AVOutputFormat的结构体指针,它主要存储复合流信息的常规配置,默认为设置NULL。

第三个传输参数: format_name指的是复合流的格式,比方说:flv、ts、mp4等等

第四个传输参数: filename是输出地址,输出地址可以是本地文件(如:xxx.mp4、xxx.ts等等)。也可以是网络流地址(如:rtmp://xxx.xxx.xxx.xxx:1935/live/01)

上面这个API是根据我们流媒体类型去分配AVFormatContext 结构体。我们传进来的类型会分为FLV_PROTOCOLTS_PROTOCOL,具体如何配置如下面:

若TS_PROTOCOL类型 :avformat_alloc_output_context2(&group->oc, NULL, "mpegts", group->url_addr);

若FLV_PROTOCOL类型 :avformat_alloc_output_context2(&group->oc, NULL, "flv", group->url_addr);

注意:TS格式分别可以适配以下流媒体复合流,包括:SRT、UDP、TS本地文件等。flv格式包括:RTMP、FLV本地文件等等。

2.2 配置推流器编码参数喝AVStream结构体

AVStream 主要是存储流信息结构体,这个流信息包含音频流和视频流。创建的API是**avformat_new_stream,**在rkmedia_ffmpeg_config.cpp中配置

复制代码
// 创建输出码流对应的AVStream结构体,AVStream用于承载单路音视频流的全部属性与参数
// ost是自定义的输出流管理结构体,stream成员用于存放FFmpeg原生AVStream指针
ost->stream = avformat_new_stream(oc, NULL);
// 判断流创建是否成功
if (!ost->stream)
{
    // 创建失败打印错误日志
    printf("Can't not avformat_new_stream\n");
    // 创建失败返回0,终止初始化流程
    return 0;
}
else
{
    // 创建成功打印日志
    printf("Success avformat_new_stream\n");
}

这是 FFmpeg 输出模块初始化的第二步核心操作 ,紧随 avformat_alloc_output_context2 之后。 上一步创建了全局格式上下文 AVFormatContext,这一步就在这个全局容器里,创建一路独立的音视频流载体 AVStream,为后续填充编码参数、写入码流数据做准备。

AVStream * avformat_new_stream(AVFormatContext *s, AVDictionary **options);

第一个传输参数: AVFormatContext的结构体指针

第二个传输参数: AVDictionary结构体指针的指针

返回值: AVStream结构体指针

2.3 设置对应的推流器编码器参数

复制代码
// 通过编码ID查找对应的编码器实例
// codec 是二级指针,指向外部传入的 AVCodec* 指针的地址,用于把找到的编码器指针带回调用方
*codec = avcodec_find_encoder(codec_id);

// 判断是否成功找到编码器
if (!(*codec))
{
    // 查找失败,打印错误日志
    printf("Can't not find any encoder");
    // 查找失败,返回0终止初始化流程
    return 0;
}
else
{
    // 查找成功,打印日志
    printf("Success find encoder");
}

这是 FFmpeg软编码初始化流程的标准步骤 ,作用是:根据传入的编码格式 ID(比如 H.264、AAC),在 FFmpeg 内部注册的所有编码器中,查找匹配的编码器模板(AVCodec 结构体),并把找到的编码器指针回传给调用方。 找到编码器后,后续才能基于它创建编码器上下文、配置编码参数、开启编码器执行编码工作。

AVCodec *avcodec_find_encoder(enum AVCodecID id); //

第一个传输参数: 传递参数AVCodecID

2.4 根据编码器ID分配 AVCodecContext 结构体

复制代码
// 基于已找到的编码器模板,分配并初始化编码器运行上下文 AVCodecContext
// c 为 AVCodecContext* 类型指针,用于接收分配好的编码器上下文句柄
c = avcodec_alloc_context3(*codec);

// 判断上下文内存分配是否成功
if (!c)
{
    // 分配失败,打印错误日志
    printf("Can't not allocate context3\n");
    // 分配失败,直接终止初始化流程并返回
    return 0;
}
else
{
    // 分配成功,打印日志
    printf("Success allocate context3");
}

这是 FFmpeg软编码初始化流程的第三步 ,紧接在 avcodec_find_encoder 之后。 上一步我们找到了编码器的静态能力模板(AVCodec),这一步的作用就是基于这个模板,创建一个可配置、可运行的编码器实例(AVCodecContext)。后续所有编码参数设置、编码状态管理、实际编码调用,都围绕这个上下文展开。

AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);

**第一个参数:**传递AVCodec结构体指针

avcodec_find_encoder 的主要作用是通过codec_id (编码器id )找到对应的AVCodec 结构体。在RV1126推流项目中codec_id 我们使用两种,分别是AV_CODEC_ID_H264AV_CODEC_ID_H265 并利用avcodec_alloc_context3 去创建AVCodecContext上下文。

初始化完AVStream和编码上下文结构体之后,我们就需要对这些参数进行配置。重点:推流编码器参数和RV1126编码器的参数要完全一样,否则可能会出问题,具体的如下图:

1920 * 1080编码器和FFMPEG推流器的配置

1280* 720编码器和FFMPEG推流器的配置

FFMPEG的视频编码参数如:分辨率(WIDTHHEIGHT )、时间基(time_base )、 帧率(r_frame_rate )、GOP_SIZE等都需要和右边VENC的参数要一一对应起来。其中time_base的值要和视频帧率必须要一致。如RV1126高编码器分辨率是1920 * 1080,则FFMPEG推流器的WIDTH = 1920,HEIGHT = 1080;若RV1126编码器的分辨率是1280 * 720,则FFMPEG推流器的WIDTH = 1280,HEIGHT = 720;若RV1126的GOP的值是25,那右边FFMPEG的gop_size 也等于25;time_base的数值和帧率保持一致

AV_CODEC_FLAG_GLOBAL_HEADER **:**发送视频数据的时候都会在关键帧前面添加SPS/PPS,这个标识符在FFMPEG初始化的时候都需要添加。

2.5 拷贝参数到 AVStream 编解码器

拷贝参数到AVStream,我们封装到open_video自定义函数里面,要先调用avcodec_open2打开编码器,然后再调用avcodec_parameters_from_context把编码器参数传输到AVStream里面

复制代码
// 打开并初始化视频编码器,完成编码器启动、数据包分配、编码参数同步到封装层
// 入参 oc: 输出格式上下文指针
// 入参 codec: 已找到的视频编码器模板指针
// 入参 ost: 自定义输出流管理结构体指针
// 入参 opt_arg: 编码器选项字典,用于传递高级编码参数
int open_video(AVFormatContext *oc, AVCodec *codec, OutputStream *ost, AVDictionary *opt_arg)
{
    // 从输出流结构体中取出提前分配好的编码器上下文指针
    AVCodecContext *c = ost->enc;

    // 打开编码器:将配置好参数的编码器上下文与编码器绑定,正式初始化编码资源
    avcodec_open2(c, codec, NULL);

    // 分配AVPacket结构体,用于接收编码器输出的压缩码流数据包
    ost->packet = av_packet_alloc();

    /* 将AVCodecContext中的编码参数,同步复制到AVStream的编码参数集合中
       供封装层(复用器)写入文件头、生成流信息使用 */
    avcodec_parameters_from_context(ost->stream->codecpar, c);
    return 0;
}

2.5.1.int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);

这个函数的具体作用是,打开编解码器

**第一个参数:**AVCodecContext结构体指针

**第二个参数:**AVCodec结构体指针

**第三个参数:**AVDictionary二级指针

2.5.2. int avcodec_parameters_from_context(AVCodecParameters *par, const AVCodecContext *codec);

这个函数的具体作用是,把 AVCodecContext 的参数拷贝到 AVCodecParameters 里面。

**第一个参数:**AVCodecParameters结构体指针

**第二个参数:**AVCodecContext结构体指针

2.6 打开 IO 文件操作

这段代码是 FFmpeg 输出初始化流程中打开底层 IO 通道的核心环节,位于「流参数配置完成」之后、「写入媒体文件头」之前。它的核心作用是:根据输出格式的特性,按需建立实际的数据输出通道(本地文件 / 网络连接),同时包含了完善的错误回滚机制,确保打开失败时不会造成内存泄漏。

复制代码
// 判断当前输出格式是否需要打开底层IO文件/网络句柄
// AVFMT_NOFILE 标志表示该格式为无文件型输出(如纯自定义IO、内存流),无需执行avio_open
if (!(fmt->flags & AVFMT_NOFILE))
{
    //打开输出文件/网络地址,初始化底层IO上下文
    ret = avio_open(&ffmpeg_config->oc->pb, ffmpeg_config->network_addr, AVIO_FLAG_WRITE);
    // 判断IO打开是否失败
    if (ret < 0)
    {
        // 打开失败,执行资源回滚:依次释放视频流、音频流的相关资源
        free_stream(ffmpeg_config->oc, &ffmpeg_config->video_stream);
        free_stream(ffmpeg_config->oc, &ffmpeg_config->audio_stream);
        // 释放整个输出格式上下文,回收已分配的全部内存
        avformat_free_context(ffmpeg_config->oc);
        // 返回错误码,终止初始化流程
        return -1;
    }
}

使用avio_open 打开对应的文件,注意这里的文件不仅是指本地的文件也指的是网络流媒体文件,下面是avio_open的定义。

int avio_open(AVIOContext **s, const char *url, int flags);

**第一个参数:**AVIOContext的结构体指针,它主要是管理数据输入输出的结构体

第二个参数 **:**url地址,这个URL地址既包括本地文件如(xxx.ts、xxx.mp4),也可以是网络流媒体地址,如(rtmp://192.168.22.22:1935/live/01)等

**第三个参数:**flags标识符

复制代码
#define AVIO_FLAG_READ  1                                      /**< read-only */

#define AVIO_FLAG_WRITE 2                                      /**< write-only */

#define AVIO_FLAG_READ_WRITE (AVIO_FLAG_READ|AVIO_FLAG_WRITE)  /**< read-write pseudo flag */

2.7 avformat_write_header 对头部进行初始化,

输出模块头部进行初始化

int avformat_write_header(AVFormatContext *s, AVDictionary **options);

第一个参数: 传递AVFormatContext结构体指针

第二个参数: 传递AVDictionary结构体指针的指针