1. FFMPEG输出模块初始化概述
在音视频处理领域,FFMPEG作为一套功能强大的多媒体处理工具库,其输出模块的初始化是整个推流流程的关键起点。特别是在RV1126这类嵌入式平台上,合理的FFMPEG输出模块配置直接决定了推流的稳定性和效率。
输出模块的核心任务是为音视频推流建立完整的处理管道,这个管道需要包含从原始数据接收、编码、封装到最终输出的全链路能力。在RV1126平台上,我们通常需要处理多路码流的并行推流场景,这就对FFMPEG输出模块的初始化提出了更高要求。
典型的FFMPEG输出模块初始化包含以下关键步骤:
- 分配AVFormatContext结构体 - 这是整个输出流程的"大脑"
- 初始化AVStream结构体 - 管理音视频流的基本信息
- 查找并配置合适的编码器
- 设置AVCodecContext参数 - 控制编码过程的各种细节
- 建立输入输出通道
- 写入头部信息 - 完成整个输出管道的搭建
在RV1126的实际项目中,这些初始化工作通常集中在rkmedia_ffmpeg_config.cpp文件的init_rkmedia_ffmpeg_context函数中实现。这个函数就像是一个精密的装配车间,把FFMPEG的各个组件按照特定顺序组装起来,最终形成一个可以正常工作的推流管道。
2. FFMPEG输出配置的核心组件
2.1 AVFormatContext的分配与初始化
AVFormatContext是FFMPEG输出模块的核心数据结构,它承载了整个输出流程的上下文信息。我们可以通过avformat_alloc_output_context2函数来创建这个关键结构体:
c复制int avformat_alloc_output_context2(AVFormatContext **ctx,
AVOutputFormat *oformat,
const char *format_name,
const char *filename);
这个函数的四个参数各有其重要作用:
- ctx:输出参数,用于接收新创建的AVFormatContext指针
- oformat:指定输出格式,通常设为NULL让FFMPEG自动判断
- format_name:明确指定封装格式,如"flv"、"mpegts"等
- filename:输出目标地址,可以是本地文件或网络地址
在RV1126项目中,我们通常根据推流协议类型来选择不同的封装格式:
c复制// TS协议类型
avformat_alloc_output_context2(&group->oc, NULL, "mpegts", group->url_addr);
// FLV协议类型
avformat_alloc_output_context2(&group->oc, NULL, "flv", group->url_addr);
值得注意的是,TS格式具有更广泛的适应性,可以支持SRT、UDP、本地TS文件等多种场景;而FLV格式则主要用于RTMP推流和本地FLV文件录制。
当avformat_alloc_output_context2调用成功后,新创建的AVFormatContext结构体会被初始化为以下状态:
| 字段 | 初始值 | 说明 |
|---|---|---|
| oformat | FLV/TS对象 | 包含格式名称、MIME类型等信息 |
| pb | NULL | IO上下文尚未初始化 |
| nb_streams | 0 | 还没有添加任何流 |
| streams | 空数组 | 流指针数组已分配但内容为空 |
| start_time | AV_NOPTS_VALUE | 起始时间未确定 |
| duration | AV_NOPTS_VALUE | 持续时间未确定 |
| bit_rate | 0 | 比特率待计算 |
| metadata | NULL | 元数据字典为空 |
2.2 AVStream的创建与配置
AVStream结构体负责管理单个音视频流的所有信息。在FFMPEG中,我们使用avformat_new_stream函数来创建新的流:
c复制AVStream *avformat_new_stream(AVFormatContext *s, AVDictionary **options);
这个函数执行后会产生以下关键变化:
- 在内存中创建新的AVStream结构体
- 将新流添加到AVFormatContext的streams数组中
- 更新nb_streams计数
- 初始化流的基本字段:
- index:设置为当前流索引
- id:临时ID,通常为0
- time_base:初始化为
- start_time/duration:设置为AV_NOPTS_VALUE
在RV1126项目中,我们通常将options参数设为NULL,这意味着:
- 不自动关联编码器
- 需要手动设置编码器参数
- 后续需要通过avcodec_alloc_context3显式分配编码器上下文
2.3 编码器的查找与配置
编码器是推流管道中的核心组件,FFMPEG提供了avcodec_find_encoder函数来查找合适的编码器:
c复制AVCodec *avcodec_find_encoder(enum AVCodecID id);
在RV1126平台上,我们主要使用两种视频编码器:
- H.264编码器:AV_CODEC_ID_H264
- H.265编码器:AV_CODEC_ID_H265
找到编码器后,我们需要创建并配置AVCodecContext:
c复制AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
这个函数会:
- 在堆上分配AVCodecContext内存
- 根据编码器类型设置合理的默认值
- 将上下文与编码器关联
在RV1126项目中,编码器参数的配置必须与硬件编码器的设置完全一致,否则可能导致推流失败或质量下降。关键参数包括:
| 参数 | 说明 | 示例值 |
|---|---|---|
| width/height | 视频分辨率 | 1920x1080/1280x720 |
| time_base | 时间基 | 1/25(25fps) |
| framerate | 帧率 | 25/1 |
| gop_size | 关键帧间隔 | 25 |
| pix_fmt | 像素格式 | NV12 |
| bit_rate | 目标码率 | 4000000(4Mbps) |
特别需要注意的是,time_base的值必须与视频帧率严格对应。例如25fps的视频,time_base应设置为1/25。
2.4 SPS/PPS头部信息的处理策略
H.264流中的SPS(序列参数集)和PPS(图像参数集)是解码器正常工作的关键信息。FFMPEG提供了两种处理这些头部信息的方式:
-
每个关键帧前带SPS/PPS(默认模式)
- 每个关键帧前都重复发送SPS/PPS
- 优点:新加入的播放器可以随时开始解码
- 缺点:增加了少量带宽开销
- 适用场景:RTMP等流媒体协议
-
全局头部模式(启用AV_CODEC_FLAG_GLOBAL_HEADER)
- SPS/PPS只在文件头部写一次
- 优点:节省空间
- 缺点:播放器必须从头开始才能解码
- 适用场景:MP4/MKV等文件格式
在代码中,我们可以这样设置全局头部模式:
c复制if (oc->oformat->flags & AVFMT_GLOBALHEADER) {
c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
需要注意的是,不是所有封装格式都支持全局头部。例如:
- 支持:MP4、MKV
- 不支持:FLV、MPEG-TS
3. 参数传递与IO初始化
3.1 编码器参数的传递过程
在配置好编码器参数后,我们需要将这些参数传递到AVStream中。这个过程分为三个关键步骤:
- 打开编码器:
c复制avcodec_open2(c, codec, NULL);
这个调用会使编码器进入可工作状态,准备好接收帧数据并输出编码后的包。
- 分配AVPacket:
c复制ost->packet = av_packet_alloc();
AVPacket用于存储编码后的数据,分配后其内部状态为:
- data:NULL
- size:0
- buf:已分配但为空
- 参数拷贝:
c复制avcodec_parameters_from_context(ost->stream->codecpar, c);
这一步将编码器工作参数复制到流的参数集中,确保封装器能正确写入文件头信息。
3.2 IO通道的建立
FFMPEG使用AVIOContext来抽象各种IO操作,我们可以通过avio_open函数建立输出通道:
c复制int avio_open(AVIOContext **s, const char *url, int flags);
参数说明:
- s:输出参数,接收创建的AVIOContext
- url:输出目标地址,可以是:
- 本地文件:"/path/to/file.mp4"
- 网络流:"rtmp://example.com/live/stream"
- flags:打开模式:
- AVIO_FLAG_READ:只读
- AVIO_FLAG_WRITE:只写
- AVIO_FLAG_READ_WRITE:读写
在RV1126项目中,我们主要使用写模式(AVIO_FLAG_WRITE)来建立推流通道。根据不同的协议前缀,FFMPEG会自动选择相应的处理方式:
| 协议类型 | 处理方式 |
|---|---|
| rtmp:// | 建立RTMP连接并完成握手 |
| udp:// | 创建UDP socket |
| file://或本地路径 | 打开本地文件 |
3.3 头部信息的写入
最后一步是调用avformat_write_header写入封装格式的头部信息:
c复制int avformat_write_header(AVFormatContext *s, AVDictionary **options);
这个函数会根据不同的封装格式写入特定的头部信息:
-
FLV格式:
- 写入"FLV"签名
- 写入版本号
- 写入初始metadata tag
-
MP4格式:
- 写入ftyp box
- 写入moov box的初始部分
-
MPEG-TS格式:
- 写入PAT(节目关联表)
- 写入PMT(节目映射表)
- 写入PCR(节目时钟参考)
同时,这个函数还会初始化各流的时间戳状态,为后续的数据写入做好准备。
4. 初始化完成后的状态检查
当init_rkmedia_ffmpeg_context函数执行完成后,整个FFMPEG输出模块应该处于以下就绪状态:
4.1 内存数据结构状态
-
AVFormatContext:
- oformat:已设置为FLV或TS格式对象
- pb:已初始化的AVIOContext
- nb_streams:1(假设只有视频流)
- streams[0]:配置完成的视频流
-
AVStream:
- codecpar:已填充编码参数
- time_base:已设置为正确值(如1/25)
- r_frame_rate:已设置为目标帧率(如25/1)
-
OutputStream(自定义结构):
- stream:指向关联的AVStream
- enc:已打开并配置好的编码器上下文
- packet:已分配的AVPacket
4.2 网络/文件状态
对于网络推流:
- RTMP连接已建立并完成握手
- 流通道已创建
- FLV/TS头部已发送
对于文件输出:
- 文件已创建并打开
- 文件头部已写入
- 准备接收编码数据
4.3 编码器状态
- H.264/H.265编码器已打开
- 编码参数已配置
- SPS/PPS已生成(根据全局头部设置决定位置)
- 编码器处于等待接收原始帧状态
5. 后续推流流程
初始化完成后,推流流程通常遵循以下模式:
c复制while(running) {
// 1. 从摄像头获取原始帧
AVFrame *frame = get_frame_from_camera();
// 2. 发送到编码器
avcodec_send_frame(video_stream.enc, frame);
// 3. 接收编码后的包
while(avcodec_receive_packet(video_stream.enc, video_stream.packet) == 0) {
// 4. 写入封装格式
av_write_frame(oc, video_stream.packet);
// 5. 释放包资源
av_packet_unref(video_stream.packet);
}
}
这个循环会持续运行,直到所有数据都处理完毕或收到停止信号。在整个过程中,初始化阶段建立的各个组件会协同工作,确保视频数据能够高效、稳定地推送到目标地址。