FFMPEG推流器——重要结构体

一、FFMPEG介绍

FFmpeg 就是一套免费开源的音视频工具,被称作音视频领域的"瑞士军刀",普通人能直接敲命令用,程序员能把它嵌进自己软件里调用。

能干的事说白了就几类:

  1. 转视频格式,比如把 MKV 转 MP4、视频压缩;
  2. 直播推流、拉流,把画面发到直播服务器,或者从服务器拉视频;
  3. 解码播放视频、录制摄像头 / 屏幕画面;
  4. 做画面裁剪、加水印、调色这类画面处理。

市面上绝大多数播放器、直播软件、剪辑工具底层都靠它,不用自己从零写音视频处理代码,省事还兼容几乎所有音视频格式。

在这个项目里,FFMPEG主要的作用是进行视频推流的功能,就是把RV1126编码的视频码流利用FFMPEG框架推送到流媒体服务器。

二、FFMPEG重要结构体

FFMPEG中有六个比较重要的结构体,分别是AVFormatContext AVOutputFormat AVStream AVCodec AVCodecContext AVPacket AVFrame AVIOContext结构体,这几个结构体是贯穿着整个FFMPEG核心功能,并且也是我们这个项目中最重要的几个结构体。下面我们来重点介绍这几个结构体:

2.1 AVFormatContext结构体

它是 FFmpeg 的全局总管家,也是我们操作 FFmpeg 最核心的入口结构体,所有音视频流、输出格式、IO 上下文都挂载在它身上。

  • 核心作用:管理整个媒体文件 / 流的全局信息,包括有几路音视频流、输出封装格式、读写 IO 句柄等。
  • 项目中的角色:推流初始化时创建该结构体,用来绑定 RTMP 推流地址、添加视频流、写入视频文件头、循环发送码流、最后写入文件尾并释放资源,整个推流生命周期都围绕它运行。

2.1.1 核心成员拆解

我们只挑开发中最常用、最关键的成员展开说明,避开冷门冗余参数:

成员变量 对应类型 功能说明
iformat AVInputFormat * 输入格式描述符,仅拉流 / 读文件场景使用,定义了解封装的格式规则
oformat AVOutputFormat * 输出格式描述符,仅推流 / 写文件场景使用,定义了封装的格式规则(比如 RTMP 推流对应的 FLV 封装)
streams AVStream ** 音视频流指针数组,每一路音视频对应一个 AVStream 结构体,按下标索引访问
nb_streams unsigned int 当前上下文中的音视频流总数量,和 streams 数组长度对应
pb AVIOContext * 底层 IO 上下文句柄,所有实际的文件读写、网络数据收发都通过它完成
duration int64_t 媒体总时长,单位为微秒;直播流无固定时长,该值无实际意义
bit_rate int64_t 整个媒体流的总码率,单位为 bps
metadata AVDictionary * 媒体元数据字典,可存储标题、作者、时间戳等自定义信息

2.1.2 推流场景下的完整生命周期

在 RTMP 推流项目中,AVFormatContext 遵循严格的创建→配置→运行→销毁流程,对应推流从启动到结束的全阶段:

  1. 创建并初始化上下文 推流场景推荐使用 avformat_alloc_output_context2() 一步完成创建,它会自动匹配输出格式、分配内存、初始化基础成员。示例:

    复制代码
    AVFormatContext *fmt_ctx = NULL;
    // 根据RTMP地址自动匹配FLV输出格式
    int ret = avformat_alloc_output_context2(&fmt_ctx, NULL, "flv", rtmp_url);
  2. 添加音视频流 调用 avformat_new_stream() 在上下文中添加视频流、音频流;每新增一路流,streams 数组就会扩容,nb_streams 自动递增。

  3. 写入流头部信息 所有流参数配置完成后,调用 avformat_write_header() 写入封装格式的头部数据(比如 FLV 文件头、音视频编码参数),这是正式推流前必须执行的步骤。

  4. 循环写入编码数据包 主循环中,将填充好的 AVPacket 编码包通过 av_interleaved_write_frame() 写入上下文;FFmpeg 会自动完成数据封装、时间戳排序,最终通过 pb 指向的网络 IO 发送到流媒体服务器。

  5. 写入流尾部并释放资源 推流结束时,先调用 av_write_trailer() 完成流的规范收尾,再调用 avformat_free_context() 释放整个上下文及其挂载的全部资源。必须手动调用释放接口,否则会造成内存泄漏

2.1.3 开发注意事项

  1. 必须通过 FFmpeg 官方 API 分配和释放该结构体,禁止手动 malloc/free,其内部包含大量依赖资源需要统一管理。
  2. 同一个 AVFormatContext 只能作为输入或输出其中一种,不能同时既读又写。
  3. 多线程场景下,不建议跨线程直接修改结构体成员;写入数据包的操作建议收敛到单线程执行,避免并发导致数据错乱。

2.2 AVOutputFormat 输出格式

AVOutputFormat 是 FFmpeg 里所有输出封装格式的规则说明书。每一种可输出的容器格式(FLV、MP4、MKV)、每一种可输出的流媒体协议(RTMP、RTSP、UDP),在 FFmpeg 内部都对应一个 AVOutputFormat 实例,它定义了这种格式 / 协议的打包规则、支持的编码类型、文件特征等全部属性。

它和 AVFormatContext 的关系:如果说 AVFormatContext 是全局大管家,那么 AVFormatContext 里的 oformat 成员就指向 AVOutputFormat;管家会严格按照这份规则说明书,完成写文件头、打包码流、写文件尾的全部输出操作。

2.2.1 核心常用成员拆解

只讲开发中高频接触、和推流 / 写文件强相关的字段,冷门内部字段不展开:

成员变量 类型 功能说明
name const char * 格式短名称,比如 "flv""mp4""mpegts"。是程序里匹配格式的唯一关键字,avformat_alloc_output_context2 传的格式名、av_guess_format 查找格式,都是匹配这个字段。
long_name const char * 格式的全称描述,比如 "FLV (Flash Video)",仅用于日志打印、帮助信息,业务代码几乎不会直接使用。
extensions const char * 对应的文件后缀名,比如 "flv""mp4"。根据文件名后缀自动匹配输出格式时,FFmpeg 会靠这个字段做判断。
video_codec enum AVCodecID 该格式默认 / 支持的视频编码类型。比如 FLV 格式默认支持 H.264,MP4 兼容 H.264/H.265;如果往格式里塞不支持的编码,会直接封装失败。
audio_codec enum AVCodecID 该格式默认 / 支持的音频编码类型。比如 FLV 标准支持 AAC/MP3,RTMP 推流一般统一用 AAC。
flags int 格式特性标志位,用位或组合多个属性,是开发中最需要关注的字段。

flags 常用标志位说明

  • AVFMT_GLOBALHEADER:表示该格式需要把编码全局头(比如 H.264 的 SPS/PPS)放在文件 / 流头部,而不是每帧都携带。FLV、MP4 都带有这个标志,推流时必须把 SPS/PPS 写入编码参数的 extradata 中,否则播放器无法解码。
  • AVFMT_NOFILE:表示这是网络流格式,不需要本地文件句柄。RTMP、RTSP 这类网络输出协议都会带这个标志,告诉 FFmpeg 不用打开本地文件,直接走网络 IO 传输。
  • AVFMT_VARIABLE_FPS:表示支持可变帧率输出。

2.2.2 在输出 / 推流流程中的角色

AVOutputFormat 本身不做实际数据操作,只定义规则,真正执行的是 AVFormatContext。它贯穿整个输出生命周期的四个阶段:

  1. 格式匹配阶段 有两种方式获取对应格式的 AVOutputFormat:

    • 自动匹配:调用 avformat_alloc_output_context2 时,传入输出地址或格式名,FFmpeg 内部自动查找匹配的 AVOutputFormat,并赋值给 fmt_ctx->oformat
    • 手动查找:通过 av_guess_format("flv", NULL, NULL) 直接按名称获取对应格式的实例。
  2. 写头部阶段 调用 avformat_write_header 时,FFmpeg 会按照 AVOutputFormat 定义的规则,生成并写入格式头部数据(比如 FLV 文件头、音视频编码参数)。

  3. 写数据包阶段 每次调用 av_interleaved_write_frame,都会按照该格式的帧封装规则,把 AVPacket 里的裸码流打包成对应格式的数据包,再写入底层 IO。

  4. 写尾部阶段 调用 av_write_trailer 时,按照格式规则写入收尾数据,保证文件 / 流的结构完整性。

2.2.3 常见格式对应场景

  • FLV:RTMP 推流的标准搭配,支持 H.264 视频 + AAC 音频,头部体积小,适合网络直播传输。
  • MP4:本地文件存储最常用,兼容性好,但不适合直播推流(文件头信息在末尾,必须全部写完才能播放)。
  • MPEGTS:UDP / 组播推流常用,容错性高,网络丢包时不会导致整段无法解码,适合安防监控、广域网直播。

2.2.4 与 AVInputFormat 的区别

两者是一一对应的正反关系,分别负责 "拆包" 和 "打包":

  • AVInputFormat:对应输入场景 (读文件、拉流),定义解封装规则,挂载在 AVFormatContext 的 iformat 成员上。
  • AVOutputFormat:对应输出场景 (写文件、推流),定义封装规则,挂载在 AVFormatContext 的 oformat 成员上。

2.2.5 开发注意事项

  1. AVOutputFormat 是 FFmpeg 内部静态管理的结构体,开发者只需要获取指针使用,不需要手动分配、释放内存
  2. RTMP 推流必须匹配 FLV 格式,否则会出现封装错误、播放器无法拉流的问题。
  3. 如果格式带有 AVFMT_GLOBALHEADER 标志,必须提前把编码全局参数(如 H.264 的 SPS/PPS)写入编码上下文的 extradata,再调用写头部接口,否则会出现首帧花屏、无法解码的问题。

2.3 AVStream 流结构体

AVStream 是 FFmpeg 中描述一路独立音视频流 的核心结构体,相当于每一路视频、每一路音频的 "身份档案"。一个完整的媒体文件或直播流(比如带声音的监控画面)通常包含至少一路视频流 + 一路音频流,每一路流都对应一个独立的 AVStream 实例,全部由 AVFormatContext 的 streams 数组统一管理。

它是衔接「封装格式层」和「编解码层」的桥梁:向上归属于 AVFormatContext 管理,向下承载该路流的编码参数、时间基准、帧率码率等全部属性,音视频同步、码流封装、时间戳换算都依赖它的参数。

2.3.1 核心常用成员详解

我们只挑选开发中高频使用、理解成本高的核心字段展开,冷门内部字段不做展开:

成员变量 类型 功能说明
index int 流在 streams 数组中的下标索引,从 0 开始计数。是程序里区分不同流的最常用标识,AVPacket 里的 stream_index 就是和该字段一一对应。
id int 格式层定义的流 ID,由具体封装格式规定,和数组下标 index 无必然关联,开发中使用频率很低。
codecpar AVCodecParameters * 编码参数集合 ,存储该路流的所有编解码属性(编码格式、分辨率、采样率等),是当前 FFmpeg 官方推荐的编码参数载体,替代了老版本的 codec 指针。
time_base AVRational 时间基(时间刻度) ,该路流时间戳的最小单位,用分数形式表示(如 {1, 1000000} 代表微秒刻度)。所有 pts/dts 都是以它为单位的整数,是音视频时间同步的核心基础。
r_frame_rate AVRational 流的标称帧率,用分数表示(如 {25, 1} 代表 25 帧 / 秒),是该路流预设的标准帧率。
avg_frame_rate AVRational 流的实际平均帧率,根据帧间隔实时计算得出,通常和标称帧率接近。
start_time int64_t 流的起始时间戳,单位是 time_base 对应的刻度;直播流无固定起始点,该值通常无效。
duration int64_t 流的总时长,单位是 time_base 对应的刻度;直播流无固定时长,该值无实际意义。
bit_rate int64_t 该路流的平均码率,单位为 bps(比特每秒)。
metadata AVDictionary * 流的元数据字典,可存储语言、标题等附加信息。
disposition int 流属性标志位,用于标记是否为默认流、是否强制显示等属性。

2.3.2 重点概念:时间基 time_base

这是 AVStream 里最核心、也最容易踩坑的概念,单独做通俗解释:

  1. 通俗理解 :时间基就是这路流的 "时间尺子",它是一个分数(分子 num、分母 den),代表每一个时间刻度对应的真实秒数。 比如 time_base = {1, 25},意味着 1 个时间刻度 = 1/25 秒,刚好对应 25 帧率下每帧的时长。

  2. 核心作用 :所有时间戳(pts 显示时间戳、dts 解码时间戳)都是整数,单位就是这个 "刻度"。换算成真实秒数的公式为:

    复制代码
    真实时间(秒) = 时间戳数值 * time_base.num / time_base.den
  3. 存在的意义:不同封装格式、不同编码器的时间刻度标准不一样,通过统一的 time_base 可以完成跨格式换算,保证音视频在不同场景下时间同步准确。

  4. 开发注意:推流时必须给视频流设置正确的 time_base,且发送 AVPacket 时,pts/dts 必须基于该流的 time_base 计算,否则会出现播放快进、慢放、音画不同步等问题。

2.3.3 与其他核心结构体的关联

  1. 与 AVFormatContext:从属关系 AVFormatContext 的 streams 是 AVStream 指针数组,nb_streams 是流的总数;所有 AVStream 的生命周期都由 AVFormatContext 统一管理。

  2. 与 AVCodecParameters / AVCodecContext:参数承载关系 AVStream 的 codecpar 专门存储编码参数,创建编解码器上下文时,通过 avcodec_parameters_to_context() 即可把参数同步到 AVCodecContext 中。

  3. 与 AVPacket:对应关系 每个 AVPacket 都有 stream_index 字段,和 AVStream 的 index 一一对应,用来标记这个数据包属于哪一路流。

  4. 与 AVOutputFormat:规则匹配关系 AVStream 的编码参数必须符合输出格式的支持范围,比如 FLV 格式的视频流必须是 H.264 编码,否则会封装失败。

2.3.4 输出场景(推流 / 写文件)下的生命周期

  1. 创建流 调用 avformat_new_stream() 在 AVFormatContext 中创建新的 AVStream,函数会自动分配内存、初始化默认值,并自动加入 streams 数组。

    复制代码
    AVStream *video_stream = avformat_new_stream(fmt_ctx, NULL);
  2. 填充参数 给流设置编码格式、分辨率、帧率、time_base 等参数,写入 codecpar 中。

  3. 写入流头 调用 avformat_write_header() 时,FFmpeg 会根据所有 AVStream 的参数生成格式头部,写入输出上下文。

  4. 循环写入数据包 发送 AVPacket 时,通过 stream_index 匹配对应流,FFmpeg 自动按照该流的参数和时间基完成数据封装。

  5. 资源释放 调用 avformat_free_context() 释放 AVFormatContext 时,会自动释放内部所有 AVStream 及其关联资源,无需手动单独释放。

2.3.5 开发注意事项

  1. 必须通过 avformat_new_stream() 创建 AVStream,禁止手动 malloc/free,其内部包含大量 FFmpeg 统一管理的资源。
  2. 老版本 FFmpeg 用 codec 成员(AVCodecContext*)存储编码参数,新版本已废弃,推荐统一使用 codecpar 传递编码参数。
  3. 同一路媒体中,视频流和音频流的 time_base 可以不一样,计算时间戳时必须分别基于各自流的 time_base 换算。
  4. 区分不同流优先用 index 字段,不要依赖 id 字段,不同封装格式的 id 规则不统一。
  5. 推流场景建议将视频流的 time_base 设置为与帧率一致(如 1/25),或使用标准的 1/1000000 微秒刻度,减少时间戳换算出错的概率。

2.4 AVCodec 编解码器

AVCodec 是 FFmpeg 中编解码标准的静态规格说明书,对应每一种具体的音视频编码 / 解码实现。每一种编码格式(H.264、H.265、AAC)、每一种具体实现方案(软编码 libx264、瑞芯微硬件编码 h264_rkmpp),在 FFmpeg 内部都对应一个独立的 AVCodec 实例。

它只描述该编解码器的固有属性与能力边界,不存储运行时的配置参数、工作状态。它和 AVCodecContext 是「模板」与「运行实例」的关系:AVCodec 是空调的产品说明书,只标注支持的温度范围、功能特性;AVCodecContext 是实际运行的空调,可以调温度、设模式、记录运行状态。

2.4.1 核心常用成员拆解

成员变量 类型 功能说明
name const char * 编解码器短名称,比如 "h264""libx264""aac",是查找编解码器的核心唯一标识,按名称查找编解码器时匹配该字段。
long_name const char * 编解码器全称描述,比如 "H.264 / AVC / MPEG-4 AVC",仅用于日志打印、帮助信息,业务代码基本不会直接使用。
type enum AVMediaType 媒体类型,区分该编解码器属于视频(AVMEDIA_TYPE_VIDEO)、音频(AVMEDIA_TYPE_AUDIO)还是字幕。
id enum AVCodecID 编解码器全局唯一 ID,比如 AV_CODEC_ID_H264AV_CODEC_ID_AAC,是按编码格式查找编解码器的核心依据。
capabilities unsigned 能力标志位,通过位或组合多个特性,描述编解码器支持的功能,比如是否支持多线程、硬件加速、无损编码。
pix_fmts const enum AVPixelFormat * 支持的像素格式列表(仅视频编解码器有效),比如 NV12、YUV420P,为 NULL 表示支持所有通用格式。
sample_fmts const enum AVSampleFormat * 支持的音频采样格式列表(仅音频编解码器有效)。
supported_samplerates const int * 支持的音频采样率列表(仅音频编解码器有效)。

capabilities 常用标志位说明

  • AV_CODEC_CAP_HARDWARE:标记这是硬件编解码器,由硬件驱动完成编解码,不占用 CPU。
  • AV_CODEC_CAP_MULTITHREAD:支持多线程编解码,可以通过多线程提升性能。
  • AV_CODEC_CAP_VARIABLE_FRAME_SIZE:支持可变帧长度编码。

2.4.2 核心分类:编码器 vs 解码器

同一种编码格式,编码器和解码器是两个独立的 AVCodec 实例,功能完全不同:

  1. 解码器 :负责把压缩后的码流还原成原始音视频数据(比如把 H.264 码流还原成 YUV 画面),名称通常就是格式名,比如 h264 解码器。
  2. 编码器 :负责把原始音视频数据压缩成码流(比如把 YUV 画面压成 H.264 码流),名称通常带实现方案标识,比如 libx264 是 x264 软编码器,h264_rkmpp 是瑞芯微硬件编码器。

2.4.3 与其他核心结构体的关联

1. 与 AVCodecContext:模板与实例

AVCodec 是静态能力模板,AVCodecContext 是基于该模板创建的运行时实例

  • 创建编解码器上下文时,必须指定对应的 AVCodec:avcodec_alloc_context3(codec),相当于按照说明书生成一台可配置的编解码器。
  • 所有参数配置、状态数据都存在 AVCodecContext 中,AVCodec 全程只读,不会被修改。

2. 与 AVStream:格式匹配

  • 读取流时:AVStream 的 codecpar 中保存了 codec_id,通过 avcodec_find_decoder(stream->codecpar->codec_id) 就能匹配到对应的解码器。
  • 输出推流时:需要把编码格式 ID 同步到输出流的 codecpar->codec_id,保证封装格式与码流格式匹配,避免封装失败。

3. 两种标准查找方式

  • 按格式 ID 查找:avcodec_find_encoder(AV_CODEC_ID_H264) 查找默认 H.264 编码器;avcodec_find_decoder() 查找解码器。
  • 按名称精确查找:avcodec_find_encoder_by_name("libx264"),可以精准指定具体的编码实现。

2.4.4 在本项目中的角色

由于项目采用 RV1126 硬件 VENC 完成视频编码,FFmpeg 仅负责码流封装与 RTMP 推流,不执行实际编码,因此 AVCodec 的作用非常轻量: 仅用于匹配 H.264 编码格式,给输出视频流设置正确的 codec_id,保证 FLV 封装规则与硬件码流格式对齐,避免出现封装错误、播放器无法解码的问题。全程不会基于 AVCodec 创建编码器上下文,也不会调用 FFmpeg 软编码。

2.4.5 开发注意事项

  1. AVCodec 由 FFmpeg 内部静态注册、统一管理,开发者只需获取指针使用,不需要手动分配与释放内存
  2. 同一种编码格式可能存在多个 AVCodec 实现,按名称查找可以精确指定软硬编方案,按 ID 查找只会返回默认实现。
  3. 编码器与解码器不能混用,查找时必须区分 avcodec_find_encoderavcodec_find_decoder
  4. 初始化编解码器前,可以通过 pix_fmtssupported_samplerates 等字段校验参数是否在支持范围内,提前规避初始化失败。

2.5 AVIOContext IO 上下文

AVIOContext 是 FFmpeg 体系中的底层 IO 抽象层,也被称为 IO 上下文。它将所有类型的输入输出操作(本地文件读写、网络 Socket 收发、内存缓冲区读写、自定义设备交互)统一封装成一套标准接口,让上层的封装 / 解封装逻辑完全不用感知底层 IO 的具体类型,只需要通过统一的函数完成数据读写。

它是 AVFormatContext 的底层执行单元,挂载在 AVFormatContext->pb 成员上,所有文件 / 流的数据读写,最终都会通过 AVIOContext 落到实际的底层介质上。

2.5.1 核心作用

  1. 统一 IO 接口 :屏蔽本地文件、网络流、内存缓冲区等不同介质的差异,上层调用 avio_readavio_write 即可完成所有读写操作,不用区分是读文件还是发网络。
  2. 内部缓冲管理:自带数据缓冲区,减少底层系统调用次数(如文件 IO、网络发包),大幅提升读写性能,平衡上层逐帧操作与底层批量传输的效率差。
  3. 支持自定义 IO:允许开发者自行实现读写、定位、刷新回调,适配特殊场景(如加密传输、自定义网络库、内存码流处理)。
  4. 状态管理:维护读写位置、文件结束标记、读写模式等运行状态,保证 IO 操作的连续性。

2.5.2 核心常用成员拆解

按功能分为三类,仅列开发中高频接触的成员,内部冗余字段不展开:

分类 成员变量 类型 功能说明
缓冲区相关 buffer unsigned char * 内部缓冲区首地址,所有读写数据先经过该缓冲区缓存
buffer_size int 内部缓冲区总大小,默认通常为 32KB,可自定义调整
buf_ptr unsigned char * 当前读写位置指针,指向缓冲区中下一个要读写的字节
buf_end unsigned char * 缓冲区中有效数据的末尾指针,读模式下标记已读取到的有效数据边界
回调函数相关 read_packet int (*)(void *opaque, uint8_t *buf, int buf_size) 读数据回调:自定义 IO 时由开发者实现,从底层介质读取数据到 buf 中,返回实际读取字节数
write_packet int (*)(void *opaque, uint8_t *buf, int buf_size) 写数据回调:自定义 IO 时由开发者实现,将 buf 中的数据写入底层介质,返回实际写入字节数
seek int64_t (*)(void *opaque, int64_t offset, int whence) 定位回调:自定义 IO 时实现,调整读写指针位置,不支持定位的流可置空
opaque void * 用户自定义数据指针,回调函数中可通过该指针获取自定义上下文(如 Socket 句柄、文件描述符)
状态相关 eof_reached int 是否已到达数据末尾,读文件 / 拉流到结尾时置 1
write_flag int 读写模式标记,1 为写模式(推流 / 写文件),0 为读模式(拉流 / 读文件)
max_packet_size int 单次读写的最大数据包大小,网络流场景常用以限制单包长度

2.5.3 核心工作机制:内部缓冲的意义

AVIOContext 最核心的设计就是缓冲机制,解决 "上层逐帧调用" 和 "底层批量操作" 的效率矛盾:

  1. 写场景(推流 / 写文件) :上层每次写入一帧码流时,数据不会立刻发送到网络 / 写入文件,而是先写入内部缓冲区;当缓冲区写满、或主动调用 avio_flush 刷新时,才会一次性调用 write_packet 将整段缓冲数据写入底层。
  2. 读场景(拉流 / 读文件):上层请求读取少量数据时,IO 层会一次性读取一整个缓冲区的数据缓存起来,后续读取直接从缓存取,减少频繁的系统调用。

举个直观例子:25 帧 / 秒的 1080P 码流,上层每秒会调用 25 次写操作;如果没有缓冲,就要发 25 次网络包;有了 32KB 缓冲后,可能攒 3-5 帧才发一次网络包,大幅降低网络开销。

2.5.4 两种典型使用模式

1. 自动模式(最常用)

绝大多数常规场景都使用该模式,开发者完全不接触 AVIOContext 的细节:

  • 打开输入:调用 avformat_open_input() 打开文件或 RTSP/RTMP 地址时,FFmpeg 会自动根据 URL 类型(file://rtmp://)匹配对应的 IO 实现,自动创建并初始化 AVIOContext,挂载到 fmt_ctx->pb
  • 打开输出:调用 avformat_alloc_output_context2()avio_open() 时,同样自动创建输出 IO 上下文。
  • 特点:零配置、开箱即用,生命周期随 AVFormatContext 统一管理,无需手动分配和释放。

2. 自定义 IO 模式(特殊场景)

当默认 IO 无法满足需求时使用,比如:

  • 需要用自己的网络库发送数据,不用 FFmpeg 自带的 RTMP 实现;
  • 直接在内存中处理码流,不经过本地文件;
  • 需要对码流做加密、解密后再读写。

使用核心流程:

  1. 自行分配缓冲区内存;
  2. 实现 read_packet / write_packet / seek 回调函数;
  3. 调用 avio_alloc_context() 创建 AVIOContext,传入缓冲区、回调函数、自定义 opaque 指针;
  4. 将创建好的 AVIOContext 赋值给 fmt_ctx->pb,后续上层操作就会走自定义的 IO 逻辑。

2.5.5 与其他核心结构体的关联

  1. 与 AVFormatContext:执行与调度关系 AVFormatContext 是顶层调度者,负责管理流和封装逻辑;AVIOContext 是底层执行者,负责实际的数据读写,挂载在 AVFormatContext->pb 成员上。所有上层的读包、写包操作,最终都会通过 pb 指针落到 AVIOContext。

  2. 与 AVPacket:数据传输关系 写入 AVPacket 时,码流数据会先进入 AVIOContext 缓冲区,再批量写入底层;读取 AVPacket 时,数据先从底层读到缓冲区,再逐包解析给上层。

2.5.6 生命周期与常用操作

自动模式

  • 创建:随 avformat_open_input / avio_open 自动创建
  • 销毁:随 avformat_close_input / avformat_free_context 自动释放,无需手动操作

自定义模式

  1. 创建avio_alloc_context() 分配并初始化结构体
  2. 刷新 :写模式结束前必须调用 avio_flush(),将缓冲区残留数据全部刷到底层,避免丢尾帧
  3. 释放 :先释放自定义的缓冲区内存,再调用 av_free() 释放 AVIOContext 结构体本身

2.5.7 开发注意事项

  1. 禁止直接操作内部指针 :不要直接修改 buf_ptrbuf_end 等内部成员,所有读写操作通过 avio_readavio_writeavio_seek 等官方 API 完成,否则会破坏内部状态导致数据错乱。
  2. 写结束必须刷新缓冲 :推流 / 写文件结束时,调用 av_write_trailer() 会自动触发 flush;如果提前终止写入,必须手动调用 avio_flush(),否则缓冲区末尾的残留数据会丢失。
  3. 自定义回调规范
    • 读回调:成功返回实际读取字节数,到达末尾返回 AVERROR_EOF,出错返回负错误码;
    • 写回调:成功返回实际写入字节数,出错返回负错误码,不能返回 0(会被判定为写入失败)。
  4. opaque 是唯一自定义入口:多线程、多路流场景下,不要用全局变量传递自定义上下文,全部通过 opaque 指针传递,避免数据串扰。
  5. 延迟与性能平衡:缓冲区越大,读写效率越高,但延迟也越高;直播推流场景可适当调小缓冲区降低延迟,本地文件处理可保留默认大小保证性能。

2.6 AVPacket 编码数据包

AVPacket 是 FFmpeg 中压缩音视频数据的最小载体单元,一帧编码后的视频、一段编码后的音频,都会被封装成一个 AVPacket 进行传递。它只承载编码后的压缩数据,不包含原始画面 / 采样信息,是连接「解封装 / 封装层」与「编解码层」的核心数据结构,贯穿读文件、解码、编码、推流、写文件全流程。

2.6.1 核心常用成员拆解

成员变量 类型 功能说明
data uint8_t * 压缩码流数据指针,指向实际的 H.264、AAC 等编码数据内存
size int 压缩数据的有效字节长度,即当前这帧码流的真实大小
pts int64_t 显示时间戳,告诉播放器这一帧应该在什么时间点显示 / 播放,单位是所属流的 time_base
dts int64_t 解码时间戳,告诉解码器这一帧应该在什么时间点解码,单位是所属流的 time_base;无 B 帧时通常与 pts 相等
stream_index int 所属流的索引,对应 AVStreamindex 字段,用来区分视频流、音频流
flags int 数据包标志位,最常用 AV_PKT_FLAG_KEY 标记当前帧为关键帧
duration int64_t 该数据包播放的持续时长,单位是所属流的 time_base
pos int64_t 数据包在源文件中的字节偏移位置,网络直播流通常无效

2.6.2 重点概念深挖

1. pts 与 dts 的区别与联系

这是音视频开发最核心的基础概念之一,很多播放异常本质上都是时间戳配置错误。

  • dts(解码时间戳):规定解码器的工作顺序,必须按 dts 从小到大依次解码。
  • pts(显示时间戳):规定播放器的渲染顺序,必须按 pts 从小到大依次显示画面。

二者为什么会不一样? H.264 编码存在 B 帧(双向预测帧),B 帧需要同时参考前面和后面的帧才能解码,因此解码顺序和显示顺序不一致。比如显示顺序是 I → B → P,但解码顺序必须是 I → P → B------ 先解码 P 帧,才能用 P 帧做参考解码 B 帧,此时 dts 和 pts 数值不同。

如果编码时关闭 B 帧(监控、直播场景的通用做法),解码顺序和显示顺序完全一致,pts 和 dts 数值相等。

2. 引用计数机制

AVPacket 内部采用引用计数 管理数据内存:多个 AVPacket 可以指向同一份 data 数据,每复制一次引用计数 +1,每释放一次引用计数 -1,计数为 0 时自动释放数据内存。

  • 核心好处:避免大体积码流数据的内存拷贝,大幅降低 CPU 开销,高清码流场景下性能提升非常明显。
  • 对应操作 API:
    • av_packet_ref:增加一次数据引用
    • av_packet_unref:减少一次数据引用,计数归零时自动释放数据内存

2.6.3 与其他核心结构体的关联

  1. 与 AVStream:从属对应关系 通过 stream_index 匹配对应的 AVStream;ptsdtsduration 的时间单位,必须以对应 AVStream 的 time_base 为基准,不同流的时间基不能混用。

  2. 与 AVFrame:压缩与原始的对应关系

    • 编码流程:一帧原始数据(YUV 画面 / PCM 音频,即 AVFrame)经过编码后,生成一个压缩数据包 AVPacket
    • 解码流程:一个压缩数据包 AVPacket 经过解码后,生成一帧原始数据 AVFrame 常规场景下二者一一对应,特殊编码延迟、多帧参考场景除外。
  3. 与 AVFormatContext:数据出入口关系

    • 读文件 / 拉流:av_read_frame 从 AVFormatContext 中读取一个完整的 AVPacket
    • 写文件 / 推流:av_interleaved_write_frame 将一个 AVPacket 写入 AVFormatContext,完成封装后输出

2.6.4 典型场景下的生命周期

以推流 / 写文件场景为例,AVPacket 遵循标准的创建→填充→使用→释放流程:

  1. 分配结构体 调用 av_packet_alloc() 创建空的 AVPacket,此时 data 为 NULL、size 为 0,仅分配结构体本身的内存。

  2. 填充数据与属性 将码流数据指针、有效长度赋值给 datasize,同时设置 pts、dts、流索引、关键帧标记等附属属性;也可直接通过编码接口生成填充完整的 AVPacket。

  3. 业务处理 送入封装器完成格式封装,或送入解码器完成解码操作。

  4. 释放资源 调用 av_packet_unref() 释放数据引用;结构体不再使用时,调用 av_packet_free() 释放结构体本身。

2.6.5 开发注意事项

  1. 必须使用 FFmpeg 官方 API 分配和释放,禁止手动 malloc/free,内部包含引用计数、内存对齐等额外管理,手动操作会引发内存泄漏、程序崩溃。
  2. 时间戳必须与所属流的 time_base 严格对应,写入前做好时间基换算,否则会出现播放快进、慢放、音画不同步等问题。
  3. 每次使用完 AVPacket 后,必须调用 av_packet_unref 释放数据引用,否则会造成内存泄漏。
  4. 判断关键帧必须通过 flags & AV_PKT_FLAG_KEY,不要自行解析码流判断,保证跨编码格式的兼容性。
  5. AVPacket 仅承载压缩数据,无法直接用于画面显示或音频播放,必须经过解码得到 AVFrame 后才能渲染播放。

2.7 AVFrame 原始帧结构体

AVFrame 是 FFmpeg 中存储未压缩原始音视频数据的核心结构体,是原始画面、音频采样的标准载体。解码场景下,解码器将 AVPacket 压缩码流解码后,输出原始数据存入 AVFrame;编码场景下,原始的 YUV 画面、PCM 音频存入 AVFrame,送入编码器生成压缩的 AVPacket。

它和 AVPacket 是一一对应的 "正反组合":

  • AVPacket:压缩数据,体积小,用于存储、传输、封装
  • AVFrame:原始数据,体积大,用于渲染、处理、编解码输入输出

2.7.1 核心常用成员拆解

按功能分为通用成员、视频专属、音频专属三类,重点关注数据存储相关的核心字段。

1. 通用基础成员

成员变量 类型 功能说明
pts int64_t 显示时间戳,单位由对应编解码器上下文的 time_base 决定,标记该帧应该被渲染 / 播放的时间点
pkt_dts int64_t 对应输入数据包的解码时间戳,由解码过程自动填充
format int 数据格式标识:视频对应像素格式(如 AV_PIX_FMT_NV12AV_PIX_FMT_YUV420P);音频对应采样格式(如 AV_SAMPLE_FMT_S16
best_effort_timestamp int64_t 最佳估算时间戳,当输入码流时间戳异常时,FFmpeg 自动推算的可靠时间戳
flags int 帧标志位,可标记是否为关键帧、是否为损坏帧等

2. 视频专属成员

成员变量 类型 功能说明
width int 视频画面的宽度(像素)
height int 视频画面的高度(像素)
data[8] uint8_t * 数据指针数组,每个元素指向一个颜色平面的数据首地址,最多支持 8 个平面
linesize[8] int 行步长数组,对应每个平面每行数据的字节数,包含内存对齐填充的冗余字节
crop_top/crop_bottom/crop_left/crop_right int 画面裁剪边距,用于去掉边缘无效像素

3. 音频专属成员

成员变量 类型 功能说明
nb_samples int 单通道的采样点数量
channel_layout uint64_t 声道布局,如 AV_CH_LAYOUT_STEREO(立体声)
sample_rate int 采样率,单位 Hz(如 44100、48000)
channels int 声道总数

2.7.2 重点概念:data 与 linesize

这是 AVFrame 最核心、也最容易踩坑的两个字段,单独展开说明:

1. data 指针数组

AVFrame 按「平面」存储数据,不同格式的平面数量不同:

  • YUV420P(平面格式) :3 个平面,data[0] 存 Y 分量,data[1] 存 U 分量,data[2] 存 V 分量,三个平面各自连续存储。
  • NV12(半平面格式) :2 个平面,data[0] 存 Y 分量,data[1] 存 UV 交叉排列的分量,是瑞芯微、海思等嵌入式芯片的原生格式。
  • RGB24(打包格式) :1 个平面,data[0] 里按 RGBRGB 顺序连续存储所有像素。

音频同理,平面格式分声道独立存储,打包格式所有声道交错存储。

2. linesize 行步长

linesize 表示每个平面一行数据的实际字节数,注意:它不等于「宽度 × 单像素字节数」。 FFmpeg 为了内存对齐、提升读写性能,会在每行末尾填充冗余字节,因此 linesize 通常大于实际画面宽度对应的字节数。

  • 正确遍历画面的方式:每行起始地址 = data[i] + 行号 × linesize[i]
  • 常见错误:直接用 width 计算偏移量,导致画面花屏、颜色错乱。

2.7.3 核心机制:引用计数内存管理

AVFrame 承载的原始数据体积非常大(比如 1080P YUV420P 一帧约 3MB),如果每次传递都做内存拷贝,CPU 开销极高。因此 AVFrame 和 AVPacket 一样采用引用计数机制管理数据内存:

  • 多个 AVFrame 可以指向同一份原始数据,每复制一次引用计数 + 1,每释放一次引用计数 - 1;
  • 当引用计数归零时,自动释放数据内存。

对应核心 API:

  • av_frame_ref(dst, src):将 src 的数据引用赋值给 dst,引用计数 + 1,不拷贝实际数据
  • av_frame_unref(frame):释放该帧的数据引用,引用计数 - 1,计数归零则释放内存
  • av_frame_copy(dst, src):深拷贝,完整复制所有数据内容,开销大,非必要不使用

2.7.4 与其他核心结构体的关联

  1. 与 AVCodecContext:编解码的输入输出

    • 编码:avcodec_send_frame() 将 AVFrame 送入编码器,avcodec_receive_packet() 输出编码后的 AVPacket
    • 解码:avcodec_send_packet() 将 AVPacket 送入解码器,avcodec_receive_frame() 输出解码后的 AVFrame
  2. 与 AVPacket:压缩与原始的对应关系 正常编解码场景下,一帧 AVFrame 对应一个 AVPacket;存在 B 帧、编码延迟时,会出现一对多 / 多对一的情况。

  3. 与 sws_scale /swr_convert:格式转换的载体

    • 视频像素格式转换、画面缩放(libswscale 库):输入和输出都是 AVFrame
    • 音频重采样、声道格式转换(libswresample 库):输入和输出都是 AVFrame

2.7.5 典型生命周期

以编码输入场景为例:

  1. 分配结构体 :调用 av_frame_alloc() 创建空 AVFrame,仅分配结构体本身,data 指针为空。
  2. 分配数据缓冲区 :设置好 width、height、format 后,调用 av_frame_get_buffer() 按格式分配对应大小的数据内存。
  3. 填充原始数据:将摄像头采集的 YUV 数据、麦克风采集的 PCM 数据拷贝到 data 对应平面。
  4. 送入编码器 :调用 avcodec_send_frame() 将帧送入编码队列。
  5. 释放引用 :发送完成后调用 av_frame_unref() 释放数据引用,编码器内部会自行维护引用计数。
  6. 释放结构体 :流程结束后,调用 av_frame_free() 释放结构体本身。

解码场景则相反:解码器输出 AVFrame,使用完成后释放引用。

2.7.6 在本项目中的角色

由于本项目采用 RV1126 硬件 VENC 完成视频编码,FFmpeg 仅负责码流封装与 RTMP 推流,不涉及软编码、软解码与像素格式转换,因此 AVFrame 在当前推流主流程中基本不会被使用

只有后续需要基于 FFmpeg 做软处理时才会用到,比如:

  • 用 libswscale 做画面缩放、像素格式转换
  • 叠加文字 / 图片水印、画面滤镜
  • 解码回显、本地截图等功能

这八个结构体串起了 FFmpeg 推流的完整链路:从创建全局上下文、指定输出格式、配置视频流参数,到填充码流数据包、通过 IO 层发送到网络,是理解 FFmpeg 推流逻辑的核心基础。