FFmepg--20-合成H.264视频和AAC音频和时间基转化
文章目录
-
-
- 程序概述
- 核心功能模块
-
- 流创建与编码器设置
- 视频数据生成
- 音频数据生成
- 编码与混合流程
-
- 时间戳与同步管理
- 关键数据结构
- 文件输出流程
- 时间戳转换
-
- 时间基(Time Base)
- 完整的时间戳流转过程
- 关键时间戳转换过程
- 时间戳重缩放(Rescaling)
- 流时间基计算
- 容器格式与时间基对应表
- 详细流程解析
-
- 程序初始化与参数解析
- 创建输出格式上下文
- 添加音视频流
- 打开编码器与资源分配
- 文件写入准备
- 主编码循环
- 关键数据结构关系
- 完整代码
-
程序概述
这是一个完整的音视频媒体文件生成程序,使用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; // 下一帧PTSint 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.0226757 ≈ 23
意义:将"采样点计数"转换为"毫秒时间"
视频转换案例
// 编码器时间基: {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