程序概述

这是一个完整的音视频媒体文件生成程序,使用FFmpeg库合成H.264视频和AAC音频,并将它们混合封装到指定的容器格式中。

核心功能模块

流创建与编码器设置

// 视频流配置:

  • 编码器:H.264
  • 分辨率:352×288
  • 帧率:25fps
  • 像素格式:YUV420P
  • 码率:400kbps

// 音频流配置:

  • 编码器:AAC
  • 采样率:44100Hz
  • 声道:立体声
  • 采样格式:FLTP
  • 码率:64kbps
视频数据生成

// 合成YUV图像数据

  • 使用算法生成动态的YUV测试图像
  • Y分量:x + y + 帧序号×3 的渐变
  • U/V分量:基于位置和帧序号的渐变
  • 支持格式转换(如需要转换为非YUV420P格式)
音频数据生成

// 合成PCM音频数据

  • 生成110Hz的正弦波信号
  • 频率随时间线性增加(每秒增加110Hz)
  • 采样格式:16位有符号整数(S16)
  • 通过重采样转换为编码器所需的格式(FLTP)

编码与混合流程

// 主要处理循环:  
while (encode_video || encode_audio) {

if (应该编码视频帧) {

// 1. 生成YUV视频帧  
// 2. 格式转换(如果需要)  
// 3. H.264编码  
// 4. 时间戳转换  
// 5. 写入容器  
} else {

// 1. 生成PCM音频帧    
// 2. 重采样到目标格式  
// 3. AAC编码  
// 4. 时间戳转换  
// 5. 写入容器  
}
}
时间戳与同步管理

// 音视频同步策略:

  • 视频PTS:基于帧序号,每帧递增1
  • 音频PTS:基于采样点数,每帧递增nb_samples
  • 同步决策:使用av_compare_ts比较下一帧的显示时间
  • 确保时间戳较小的媒体先被处理写入

关键数据结构

OutputStream 结构体

typedef struct OutputStream {

AVStream *st;           // 媒体流
AVCodecContext *enc;    // 编码器上下文
// 时间戳相关
int64_t next_pts;       // 下一帧PTS
int samples_count;      // 音频采样计数
// 帧管理
AVFrame *frame;         // 编码用帧(处理后)
AVFrame *tmp_frame;     // 原始数据帧
// 音频生成参数
float t, tincr, tincr2; // 正弦波生成参数
// 处理上下文
struct SwsContext *sws_ctx; // 图像缩放/转换
struct SwrContext *swr_ctx; // 音频重采样
} OutputStream;

文件输出流程

// 格式探测与上下文创建
avformat_alloc_output_context2(&oc, NULL, NULL, filename);
// 如果不能推测格式,默认使用FLV格式
// 流创建与参数设置
add_stream() → 创建流并初始化编码器参数
// 编码器打开与资源分配
open_video() / open_audio()- 打开编码器
- 分配帧缓冲区
- 初始化格式转换器
// 文件写入准备
avio_open()    // 打开输出文件
avformat_write_header()  // 写入文件头
// 数据生成与编码循环
// 持续生成并编码数据,直到达到5秒时长
// 资源清理
av_write_trailer()  // 写入文件尾
close_stream()      // 关闭各流
avio_closep()       // 关闭文件
avformat_free_context() // 释放上下文

时间戳转换

时间基(Time Base)

时间基是FFmpeg中表示时间的基本单位,定义为分数形式 {分子, 分母}:

音频时间基: {1, 采样率} (如 {1, 44100})
视频时间基: {1, 帧率} (如 {1, 25})

流时间基(Stream Time Base):
流时间基是容器格式(如MP4、FLV、AVI等)内部使用的时间基准单位,用于:统一管理不同媒体流(音频、视频)的时间戳;确保播放器能够正确解析和同步各媒体流

完整的时间戳流转过程

音频时间戳流转

信号生成 → PCM帧 → 重采样 → 编码 → 混合写入
    ↓         ↓        ↓       ↓        ↓
 采样计数   采样PTS   转换PTS  包PTS   流PTS
 (0,1024..) (0,1024) (重计算) (保持)  (重缩放)

视频时间戳流转

图像生成 → YUV帧 → 格式转换 → 编码 → 混合写入
    ↓        ↓         ↓       ↓        ↓
 帧计数    帧PTS     保持PTS  包PTS   流PTS
 (0,1,2..) (0,1,2)   (保持)  (保持)  (重缩放)
关键时间戳转换过程

音频时间戳处理

// 音频PTS计算
frame->pts = ost->next_pts;                    // 使用采样计数作为PTS
ost->next_pts += frame->nb_samples;           // 累计采样数
// 重采样后时间戳转换
frame->pts = av_rescale_q(ost->samples_count,
(AVRational){
1, codec_ctx->sample_rate},
codec_ctx->time_base);

音频PTS计算流程:

  • 初始PTS = 0
  • 每帧增加 nb_samples 个采样点
  • 实际时间 = PTS × 时间基 = PTS × (1/44100) 秒

视频时间戳处理

// 视频PTS计算
ost->frame->pts = ost->next_pts++;  // 每帧PTS递增1
// 视频时间基设置
ost->st->time_base = (AVRational){
1, STREAM_FRAME_RATE};  // {1, 25}
codec_ctx->time_base = ost->st->time_base;                // 编码器使用相同时间基

视频PTS计算流程:

  • 初始PTS = 0
  • 每帧PTS递增1
  • 实际时间 = PTS × 时间基 = PTS × (1/25) 秒
时间戳重缩放(Rescaling)
// 写入前的关键时间戳转换
av_packet_rescale_ts(pkt, *time_base, st->time_base);

转换公式:

新的时间戳 = 原时间戳 × (原时间基 / 新时间基)

具体示例

  • 音频转换:

    • 编码器时间基:{1, 44100}
    • 流时间基:{1, 1000} (常见值)
    • PTS转换:新PTS = 原PTS × 1000/44100
  • 视频转换:

    • 编码器时间基:{1, 25}
    • 流时间基:{1, 1000}
    • PTS转换:新PTS = 原PTS × 40
static int write_frame(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);
}

转换时机:
1 编码完成后:获得编码器时间基的packet
2 写入容器前:转换为流时间基
3 写入文件:使用统一的流时间基存储

时间戳同步机制 :

// 音视频帧写入顺序决策
if (encode_video &&
(!encode_audio ||
av_compare_ts(video_st.next_pts, video_st.enc->time_base,
audio_st.next_pts, audio_st.enc->time_base) <= 0)) {

// 先写入视频帧
} else {

// 先写入音频帧
}

av_compare_ts 比较两个时间戳,确保:

  • 时间戳较小的帧先写入
  • 维持音视频同步
流时间基计算

音频转换案例

// 编码器时间基: {1, 44100}
// 流时间基: {1, 1000} 
// 原始PTS: 1024 (表示第1024个采样点)
新PTS = 1024 × (1/44100) ÷ (1/1000)
= 1024 × (1000/44100)
= 1024 × 0.022675723
意义:将"采样点计数"转换为"毫秒时间"

视频转换案例

// 编码器时间基: {1, 25}  
// 流时间基: {1, 1000}
// 原始PTS: 2 (表示第2帧)
新PTS = 2 × (1/25) ÷ (1/1000)
= 2 × 40 = 80
意义:将"帧序号"转换为"毫秒时间"
容器格式与时间基对应表
容器格式常用时间基原因
MP4{1, 90000}MPEG标准,高精度
FLV{1, 1000}基于毫秒,简单
AVI{1, 帧率}保持与视频帧率一致
TS{1, 90000}MPEG传输流标准

详细流程解析

程序初始化与参数解析

代码段:

int main(int argc, char **argv)
{

// 参数检查
if (argc < 2) {

printf("usage: %s output_file\n", argv[0]);
return 1;
}
filename = argv[1];
// 解析可选参数
for (i = 2; i+1 < argc; i+=2) {

if (!strcmp(argv[i], "-flags") || !strcmp(argv[i], "-fflags"))
av_dict_set(&opt, argv[i]+1, argv[i+1], 0);
}
}

功能说明:

检查命令行参数,确保指定了输出文件名
解析可选的FFmpeg标志参数
初始化全局变量和数据结构

创建输出格式上下文

代码段:

/* 分配AVFormatContext并根据filename绑定合适的AVOutputFormat */
avformat_alloc_output_context2(&oc, NULL, NULL, filename);
if (!oc) {

// 如果不能根据文件后缀名找到合适的格式,缺省使用flv格式
avformat_alloc_output_context2(&oc, NULL, "flv", filename);
}
fmt = oc->oformat;

功能说明:

根据文件扩展名自动推测容器格式(如.mp4、.avi等)
如果无法推测,默认使用FLV格式
获取输出格式对象用于后续配置

添加音视频流

代码段:

// 指定编码格式
fmt->video_codec = AV_CODEC_ID_H264;
fmt->audio_codec = AV_CODEC_ID_AAC;
// 添加视频流
if (fmt->video_codec != AV_CODEC_ID_NONE) {

add_stream(&video_st, oc, &video_codec, fmt->video_codec);
have_video = 1;
encode_video = 1;
}
// 添加音频流
if (fmt->audio_codec != AV_CODEC_ID_NONE) {

add_stream(&audio_st, oc, &audio_codec, fmt->audio_codec);
have_audio = 1;
encode_audio = 1;
}

add_stream函数详解:

static void add_stream(OutputStream *ost, AVFormatContext *oc,
AVCodec **codec, enum AVCodecID codec_id)
{

// 查找编码器
*codec = avcodec_find_encoder(codec_id);
// 创建新流
ost->st = avformat_new_stream(oc, NULL);
ost->st->id = oc->nb_streams - 1;
// 分配编码器上下文
codec_ctx = avcodec_alloc_context3(*codec);
ost->enc = codec_ctx;
// 配置编码器参数
switch ((*codec)->type) {

case AVMEDIA_TYPE_AUDIO:
// 音频参数配置:采样率、声道、格式等
codec_ctx->sample_fmt = (*codec)->sample_fmts[0];
codec_ctx->bit_rate = 64000;
codec_ctx->sample_rate = 44100;
codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
ost->st->time_base = (AVRational){
 1, codec_ctx->sample_rate };
break;
case AVMEDIA_TYPE_VIDEO:
// 视频参数配置:分辨率、帧率、格式等
codec_ctx->bit_rate = 400000;
codec_ctx->width = 352;
codec_ctx->height = 288;
ost->st->time_base = (AVRational){
 1, STREAM_FRAME_RATE };
codec_ctx->time_base = ost->st->time_base;
codec_ctx->pix_fmt = STREAM_PIX_FMT;
break;
}
}
打开编码器与资源分配

视频编码器打开:

static void open_video(AVFormatContext *oc, AVCodec *codec, OutputS