FFMPEG利用H264+AAC合成TS文件
本次的DEMO是利用FFMPEG框架把H264文件和AAC文件合并成一个TS文件。这个DEMO很重要,因为在后面的推流项目中用到了这方面的技术。所以,大家最好把这个项目好好了解。
下面这个是流程图
从这个图我们能看出来,在main函数中我们主要做了这几步:
1.创建read_video_file_thread(读取视频文件线程)。
2. 创建read_audio_file_thread(读取音频文件线程)。
3.初始化音视频队列,初始化ffmpeg的 H264,aac合成模块的容器。
4.创建muxer_ts_thread线程进行H264文件和aac文件的合成ts文件。
每一个代码模块的讲解:
初始化视频队列、音频队列、FFMPEG合成容器。
在代码第一步就需要把所有东西给初始化,包括队列、FFMPEG输出容器。
int main
{
// 为音频和视频队列对象分配内存
video_queue = new VIDEO_QUEUE();
audio_queue = new AUDIO_QUEUE();
// 为ffmpeg_group结构体分配内存
FFMPEG_CONFIG *ffmpeg_config = (FFMPEG_CONFIG *)malloc(sizeof(FFMPEG_CONFIG));
// 判断是否分配成功,如果不成功就退出
if (ffmpeg_config == NULL)
{
printf("malloc ffmpeg_group failed\n");
return -1;
}
// 初始化ffmpeg_group结构体相关成员
ffmpeg_config->config_id = 0; // 流媒体id
ffmpeg_config->network_type = TS_FILE;
ffmpeg_config->video_codec = AV_CODEC_ID_H264; // 视频编码格式为h264
ffmpeg_config->audio_codec = AV_CODEC_ID_AAC; // 音频编码格式为AAC
memcpy(ffmpeg_config->network_addr, "test.ts", strlen("test.ts")); // 进行流媒体地址字符赋值,这是写到本地文件
// 初始化流媒体格式上下文
init_ffmpeg_config_params(ffmpeg_config);
}
FFMPEG合成容器,这部分非常重要,只有做好这个一步,接下来的工作才有意义,因为里面配置了生产什么的复合流是ts还是flv等,要往哪里推流......。
init_ffmpeg_config_params
初始化推流器
int init_ffmpeg_config_params(FFMPEG_CONFIG *ffmpeg_config)
{
AVOutputFormat *fmt = NULL; // 音视频输出格式
AVCodec *audio_codec = NULL; // 音频编码器
AVCodec *video_codec = NULL; // 视频编码器
int ret = 0;
//第二步:创建一个用于输出的容器对象
/**
* 为什么?
* 推流需要将音视频数据封装成某种格式(比如 FLV 是 RTMP 的常用容器格式),
* 这个上下文对象管理流的元数据、格式信息和输出目标。
* 如果没有这个对象,FFmpeg 无法知道数据要以什么形式输出到哪里。
*
* 原理:
* 输出格式上下文是 FFmpeg 的核心数据结构,负责协调编码、封装和写入操作。它相当于推流的“总指挥”。
*/
// 流媒体类型判断并分配输出上下文
if (ffmpeg_config->network_type == FLV_FILE)
{
// 分配FLV格式的AVFormatContext
ret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "flv", ffmpeg_config->network_addr);
if (ret < 0)
{
return -1;
}
}
else if (ffmpeg_config->network_type == TS_FILE)
{
// 分配TS格式的AVFormatContext
ret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "mpegts", ffmpeg_config->network_addr);
if (ret < 0)
{
return -1;
}
}
// 音视频输出格式上下文句柄
fmt = ffmpeg_config->oc->oformat;
/* 指定编码器 */
fmt->video_codec = ffmpeg_config->video_codec;
fmt->audio_codec = ffmpeg_config->audio_codec;
//第三步:创建输出流
/**
* 作用:
* 在输出格式上下文中添加一个流(如视频流或音频流),并为其分配编码参数。
* 为什么需要?
* 推流通常包含视频和音频两种数据,每种数据需要独立的流来承载。
* 创建流是为了定义这些数据的结构(如分辨率、帧率、采样率等),否则 FFmpeg 无法组织数据。
* 原理:
* 流(AVStream)是容器中的独立轨道,推流时服务器和客户端会根据流信息解码数据。
*/
// 初始化视频流
if (fmt->video_codec != AV_CODEC_ID_NONE)
{
// 创建视频输出流
ret = add_stream(&ffmpeg_config->video_stream, ffmpeg_config->oc, &video_codec, fmt->video_codec);
if (ret < 0)
{
avcodec_free_context(&ffmpeg_config->video_stream.enc);
close_stream(ffmpeg_config->oc, &ffmpeg_config->video_stream);
avformat_free_context(ffmpeg_config->oc);
return -1;
}
// 打开视频编码器
ret = open_video(ffmpeg_config->oc, video_codec, &ffmpeg_config->video_stream, NULL);
if (ret < 0)
{
avformat_free_context(ffmpeg_config->oc);
return -1;
}
}
// 初始化音频流
if (fmt->audio_codec != AV_CODEC_ID_NONE)
{
// 创建音频输出流
ret = add_stream(&ffmpeg_config->audio_stream, ffmpeg_config->oc, &audio_codec, fmt->audio_codec);
if (ret < 0)
{
avcodec_free_context(&ffmpeg_config->audio_stream.enc);
close_stream(ffmpeg_config->oc, &ffmpeg_config->audio_stream);
avformat_free_context(ffmpeg_config->oc);
return -1;
}
// 打开音频编码器
ret = open_audio(ffmpeg_config->oc, audio_codec, &ffmpeg_config->audio_stream, NULL);
if (ret < 0)
{
avformat_free_context(ffmpeg_config->oc);
return -1;
}
}
// 打印输出格式信息
av_dump_format(ffmpeg_config->oc, 0, ffmpeg_config->network_addr, 1);
// 打开输出文件
if (!(fmt->flags & AVFMT_NOFILE))
{
// 打开输出文件用于建立与rtmp服务器的连接
ret = avio_open(&ffmpeg_config->oc->pb, ffmpeg_config->network_addr, AVIO_FLAG_WRITE);
if (ret < 0)
{
close_stream(ffmpeg_config->oc, &ffmpeg_config->video_stream);
close_stream(ffmpeg_config->oc, &ffmpeg_config->audio_stream);
avformat_free_context(ffmpeg_config->oc);
return -1;
}
}
// 写入文件头信息
//这行代码会根据ffmpeg_config->oc中指定的输出格式(如FLV或TS),
//生成并写入相应的头部信息到输出文件或流中。
avformat_write_header(ffmpeg_config->oc, NULL);
return 0;
}
ts流合成的流程,是一样ffmpeg的初始化很重要,在合成工作都是根据这个ffmpeg的配置来做的,是和成ts流还是flv,是推动远端还是保存到本地, FFmpeg 的核心数据结构,负责协调编码、封装和写入操作。它相当于推流的“总指挥”。
创建线程
初始化所有容器和队列之后,紧接着就需要创建三个线程:分别是read_video_file_thread线程、read_audio_file_thread线程、muxer_ts_thread线程。
int main
{
//读取视频文件线程
pthread_t pid;
int ret = pthread_create(&pid, NULL, read_video_file_thread, NULL);
if (ret != 0)
{
printf("Create read_video_file_thread failed...\n");
}
//读取音频文件线程
ret = pthread_create(&pid, NULL, read_audio_file_thread, NULL);
if (ret != 0)
{
printf("Create read_video_file_thread failed...\n");
}
//TS复用线程函数
ret = pthread_create(&pid, NULL, muxer_ts_thread, (void *)ffmpeg_config);
if (ret != 0)
{
printf("Create muxer_ts_thread failed...\n");
}
}
read_video_file_threa
// 读取视频线程
void *read_video_file_thread(void *args)
{
const char *input_video_file_name = "test_output.h264";
AVFormatContext *ifmt_ctx_v = NULL;
int in_stream_index_v = -1;
int ret;
// 打开输入test_output.h264文件
if ((ret = avformat_open_input(&ifmt_ctx_v, input_video_file_name, NULL, NULL)) < 0)
{
av_log(NULL, AV_LOG_ERROR, "Cannot open input file\n");
}
// 这个函数的作用不仅会检索视频的一些信息(宽、高、帧率等),而且会持续的读取和解码一些视频帧和音频帧,读取到的帧会放到缓存中;如果你知道这个媒体流的信息,可以不用这个函数去查找
if ((ret = avformat_find_stream_info(ifmt_ctx_v, NULL)) < 0)
{
av_log(NULL, AV_LOG_ERROR, "Cannot find stream information\n");
}
ret = av_find_best_stream(ifmt_ctx_v, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "Cannot find video stream\n");
}
in_stream_index_v = ret;
av_dump_format(ifmt_ctx_v, 0, input_video_file_name, 0);
AVPacket *packet = av_packet_alloc();
while (av_read_frame(ifmt_ctx_v, packet) >= 0)
{
if (packet->stream_index == in_stream_index_v)
{
printf("detect video index.....\n");
// 自己封装的视频数据包结构体video_data_packet_t分配内存
video_data_packet_t *video_data_packet = (video_data_packet_t *)malloc(sizeof(video_data_packet_t));
// 把读取到的视频数据拷贝到结构体成员里面的buffer缓冲区里面区
memcpy(video_data_packet->buffer, packet->data, packet->size);
// 数据帧的大小赋值
video_data_packet->video_frame_size = packet->size;
// 往视频队列里面写入视频流数据
video_queue->putVideoPacketQueue(video_data_packet);
}
}
return NULL;
}
read_video_file_thread线程主要的作用是,利用ffmpeg的api avformat_open_input打开我们需要的H264文件,然后用av_read_frame读取每一帧的视频数据。当成功读取一帧数据的时候,把这个数据存放到VIDEO_QUEUE里面(这里封装的函数是putVideoPacketQueue)。流程如下
read_audio_file_thread线程
//读取音频文件
void *read_audio_file_thread(void *args)
{
AVFormatContext *pFormatCtx = NULL;
// 给pFormatCtx进行内存分配
pFormatCtx = avformat_alloc_context();
char *aac_ptr = "test_mic.aac";
// 打开输入test_mic.aac文件
if (avformat_open_input(&pFormatCtx, aac_ptr, NULL, NULL) != 0)
{
printf("无法打开信息流");
}
// 这个函数的作用不仅会检索视频的一些信息(宽、高、帧率等),而且会持续的读取和解码一些视频帧和音频帧,读取到的帧会放到缓存中;如果你知道这个媒体流的信息,可以不用这个函数去查找
if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
{
printf("无法查找到流信息");
}
int audioindex = -1;
audioindex = -1;
//遍历找到音频流,有可能多语言音轨,立体声/环绕声备用音轨
for (int i = 0; i < pFormatCtx->nb_streams; i++)
{
//找到第一个音频流,就可以了
if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
{
//保存音频流索引
audioindex = i;
break;
}
}
// 没有找到音频流
if (audioindex < 0)
{
printf("No Audio Stream...\n");
}
// 给AVPacket结构体分配堆空间,释放堆空间使用av_packet_free
AVPacket *packet = av_packet_alloc();
while (av_read_frame(pFormatCtx, packet) >= 0)
{
if (packet->stream_index == audioindex)
{
printf("detect audio index.....\n");
// 自己封装的视频数据包结构体audio_data_packet_t分配内存
audio_data_packet_t *audio_data_packet = (audio_data_packet_t *)malloc(sizeof(audio_data_packet_t));
// 把读取到的视频数据拷贝到结构体成员里面的buffer缓冲区里面区
memcpy(audio_data_packet->buffer, packet->data, packet->size);
// 数据帧的大小赋值
audio_data_packet->audio_frame_size = packet->size;
// 往音频队列里面写入视频流数据
audio_queue->putAudioPacketQueue(audio_data_packet);
}
}
return NULL;
}
read_audio_file_thread线程主要的作用是利用ffmpeg的api avformat_open_input打开我们需要的音频aac文件,然后用av_read_frame读取每一帧的音频数据。当成功读取一帧数据的时候,把这个数据存放到AUDIO_QUEUE里面(这里封装的函数是putAudioPacketQueue)。处理流程大概和视频一样。
muxer_ts_thread线程
/**
* TS复用线程函数
* 该函数负责根据ffmpeg配置,将音视频数据复用到一个TS流中
* @param args 传递给线程的参数,这里是ffmpeg配置的指针
* @return 线程的返回值,本函数中始终返回NULL
*/
void *muxer_ts_thread(void *args)
{
// 解析传入的ffmpeg配置,并释放相应的内存
FFMPEG_CONFIG ffmpeg_config = *(FFMPEG_CONFIG *)args;
free(args);
// 初始化输出格式指针
AVOutputFormat *fmt = NULL;
// 初始化返回值变量
int ret;
// 无限循环,直到遇到错误或完成处理
while (1)
{
/*
我们以转换到同一时基下的时间戳为例,假设上一时刻音、视频帧的保存时间戳都是0。
当前任意保存一种视频帧,例如保存视频的时间戳为video_t1。接着比较时间戳,发现音频时间戳为0 < video_t1,保存一帧音频,时间戳为audio_t1。
继续比较时间戳,发现audio_t1 < video_t1,选择保存一帧音频,时间戳为audio_t2。
再一次比较时间戳video_t1 < audio_t2,选择保存一帧视频,时间戳为video_t2。
//同过比较时间基处理时间戳,如果视频时间戳小于音频时间戳,则保存视频,否则保存音频。写进复合流里面
*/
// 比较视频和音频的时间戳,决定下一步处理哪个流
ret = av_compare_ts(ffmpeg_config.video_stream.next_pts,
ffmpeg_config.video_stream.enc->time_base,
ffmpeg_config.audio_stream.next_pts,
ffmpeg_config.audio_stream.enc->time_base);
if (ret <= 0) //== 0 一样快,<0视频快,处理视频
{
// 处理视频包
ret = deal_video_packet(ffmpeg_config.oc, &ffmpeg_config.video_stream);
// 如果处理失败,打印错误信息并退出循环
if (ret == -1)
{
printf("deal video packet error break");
break;
}
}
else //>1音频快
{
// 处理音频包
ret = deal_audio_packet(ffmpeg_config.oc, &ffmpeg_config.audio_stream);
// 如果处理失败,打印错误信息并退出循环
if (ret == -1)
{
printf("deal audio packet error break");
break;
}
}
}
// 线程结束,返回NULL
return NULL;
}
muxer_ts_thread线程的作用是通过av_compare_ts(这个函数是重中之重)的api进行音视频PTS时间戳的对比。若判断此PTS是视频的pts,则调用deal_video_packet从视频队列取出每一个视频包然后发送到ts文件;若判断此PTS是视频的pts,则调用deal_audio_packet从音频队列取出每一个音频包然后发送到ts文件。
为什么需要进行时间基比较?我们要搞明白一个问题就是一个是时间基,一个是时间戳,时间基是代表单位,时间戳是代表,一个时间里面的格子,比如钱,1,2,3,4,5,这样是没有任何意义的,但是配合起时间基就有意义了,时间基就是美元或者rmb。所以这两个东西是决定了我们音频和视频在什么时候写进去复合流里面。

错误的写入。
deal_video_packet 视频快,处理视频
int deal_video_packet(AVFormatContext *oc, OutputStream *ost)
{
int ret;
// 从队列中获取视频数据包
AVPacket *video_pkt = get_video_ffmpeg_packet_from_queue(ost->packet);
if (video_pkt != NULL)
{
// 更新数据包的呈现时间戳(PTS)和下一个预期的时间戳
ost->packet->pts = ost->next_pts++;
}
// 将AV数据包写入到输出文件中
ret = write_avffmpeg_packet(oc, &(ost->enc->time_base), ost->stream, ost->packet);
if (ret != 0)
{
// 写入操作失败,返回错误代码
return -1;
}
// 成功处理视频数据包,返回0
return 0;
}
我们来看看deal_video_packet做了什么功能,首先调用get_video_packet_from_queue取出每一个视频数据包,若video_queue不为空,则让视频的pts自增1(视频的pts每次都是自增1)。然后再利用write_avffmpeg_packet发送到ts文件。
write_avffmpeg_packet
int write_avffmpeg_packet(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;
// 将编码后的音视频数据(AVPacket)写入输出流,发送到服务器/或本地文件中。
/**
*为什么需要: 这是推流的核心步骤,所有的音视频内容都通过这一步传输到目标。
*如果没有这一步,之前的准备工作就毫无意义,因为数据没有实际发送。
*原理: FFmpeg 使用“交错写入”(interleaved)方式处理多路流(如视频和音频),
*确保时间戳对齐,符合实时传输的要求。数据会被封装为 FLV 标签(Tag)并通过网络发送。
*/
return av_interleaved_write_frame(fmt_ctx, pkt);
}
第一步就是时间转换,视频时间基转换复合流时间基 ,假设视频时间基是{1,30},ts 封装格式的 time_base 为 {1,90000},这个api自动转换 av_packet_rescale_ts但是我们要知道
视频h264时间基转换成MPEGTS时间基:
DST_VIDEO_PTS = VIDEO_PTS * VIDEO_TIME_BASE / DST_TIME_BASE
音频AAC时间基转换成MPEGTS时间基:
**DST_AUDIO_PTS = AUDIO_PTS * AUDIO_TIME_BASE / DST_TIME_BASE
最后后面调用这个 av_interleaved_write_frame(fmt_ctx, pkt);交错写入
deal_audio_packet:
int deal_audio_packet(AVFormatContext *oc, OutputStream *ost)
{
int ret;
AVCodecContext *c = ost->enc;
AVPacket *pkt = get_audio_ffmpeg_packet_from_queue(ost->packet);
if (pkt != NULL)
{
pkt->pts = av_rescale_q(ost->samples_count, (AVRational){1, c->sample_rate}, c->time_base);
ost->samples_count += 1024;
ost->next_pts = ost->samples_count;
}
ret = write_avffmpeg_packet(oc, &(ost->enc->time_base), ost->stream, pkt);
if (ret != 0)
{
printf("[FFMPEG] write video frame error");
return -1;
}
return 0;
}
我们来看看deal_audio_packet做了什么功能,首先调用get_audio_packet_from_queue取出每一个视频数据包,若audio_queue不为空,则让音频pts自增1024(音频aac的pts每一帧都是1024个采样点)。然后再利用write_avffmpeg_packet发送到ts文件。 处理过程是和音频一样的。
其实把aac文件和h264文件,改成直接从摄像头,麦克风采集,就变成了一个录制ts流。