
音视频流合并
音视频流合并
笔者目前有两个流分别是音频流和视频流,现在需要合并成复合流用于保存文件和网络推流
笔者的ffmpeg接口移植的RV1126,可前去借鉴RV1126的SDK
具体线程如下:
生产者线程 消费者线程
──────────────────────────────────────────────────────────
get_video → high_video_queue → save_file (MP4 视频)
→ network_stream (RTMP 视频)
get_audio → high_audio_queue → save_audio (MP4 音频)
→ stream_audio (RTMP 音频)
有两个生产者线程,分别去获取队列中的音频和视频,对于该框架如果不了解的朋友可以前往我的另一篇博客:单生产者多消费者框架 – kidwjb的小站
由于合并的复合流都需要音频和视频的extradata(SPS,PPS,ATDS等),而视频的extradata需要等待获取到第一帧才能确认,所以让视频流作为主导,音频流等待通知
三个必须满足的前提条件
前提 1:必须等 avformat_write_header 执行完毕
save_file(视频线程)负责在第一个 IDR 关键帧时调用 avformat_write_header,音频线程必须等这个动作完成后才能开始写。
用原子标志位通知(定义在 AV_dev.h):
std::atomic<bool> mp4_header_written{false}; // save_file 写完 header 后置 true
std::atomic<bool> rtmp_header_written{false}; // network_stream 写完 header 后置 true
音频线程开头的等待循环(同时检测队列关闭,防止死等):
void AV_DEV::save_audio() {
while (!mp4_header_written.load()) {
if (high_audio_queue->isShutdown()) {
printf("save_audio: 队列已关闭且 header 未就绪,退出。\n");
return;
}
usleep(5000); // 每 5ms 检查一次
}
// 然后开始写音频...
}
视频线程写完 header 后置标志:
if (mp4_ffmpeg_config_ptr->ffmpeg_inited == 1 && !mp4_header_written.load()) {
mp4_header_written.store(true);
}
前提 2:两个线程写同一个 AVFormatContext 必须加锁
AVFormatContext 内部不是线程安全的,save_file 和 save_audio 同时调用 av_interleaved_write_frame 会发生数据竞争。
互斥锁(定义在 AV_dev.h):
pthread_mutex_t mp4_mutex; // save_file 和 save_audio 共用
pthread_mutex_t rtmp_mutex; // network_stream 和 stream_audio 共用
所有写入操作都需加锁:
pthread_mutex_lock(&mp4_mutex);
write_audio_frame(mp4_ffmpeg_config_ptr, raw_aac, raw_aac_size, timestamp);
pthread_mutex_unlock(&mp4_mutex);
前提 3:av_write_trailer 必须在两个线程都写完后才能调用
av_write_trailer 写的是 MP4 的索引表(moov atom),必须等音频和视频全部写完才能调用,否则索引不完整(拖动进度条失效)。
用原子计数器,最后退出的线程负责写 trailer:
std::atomic<int> mp4_active_threads{0}; // 定义在 AV_dev.h
std::atomic<int> rtmp_active_threads{0};
每个线程启动时 fetch_add(1),退出时:
if (mp4_active_threads.fetch_sub(1) == 1) {
// 我是最后一个退出的,负责写 trailer
pthread_mutex_lock(&mp4_mutex);
write_trailer(mp4_ffmpeg_config_ptr);
pthread_mutex_unlock(&mp4_mutex);
}
整体流程图
mp4_active_threads = 2 (两个线程启动时各 fetch_add(1))
save_file 线程 save_audio 线程
│ │
│ getVideoPacketQueue(FILE_VIDEO) │ while(!mp4_header_written)
│ 阻塞等待... │ usleep(5000) + isShutdown() 检查
│ │
│ 取到 IDR 关键帧 │
│ 提取 SPS/PPS,设置 extradata │
│ avformat_write_header() │
│ mp4_header_written = true ────────────► 等待结束!
│ │ getAudioPacketQueue(FILE_AUDIO)
│ lock(mp4_mutex) │ 阻塞等待...
│ av_interleaved_write_frame(视频帧) │
│ unlock(mp4_mutex) │ 取到音频帧
│ │ 去掉 ADTS 头(7 字节)
│ ...持续写视频... │ lock(mp4_mutex)
│ │ write_audio_frame(...)
│ │ unlock(mp4_mutex)
│ │ ...持续写音频...
│ │
│ 视频队列 shutdown,退出循环 │
│ fetch_sub(1) → 还剩 1 个线程 │
│ 不写 trailer,退出 │ 音频队列 shutdown,退出循环
│ │ fetch_sub(1) → 还剩 0 个线程
│ │ 我是最后一个!
│ │ lock(mp4_mutex)
│ │ write_trailer() ← 写 MP4 尾部索引
│ │ unlock(mp4_mutex)
│ │ 退出
具体代码解析
初始化FFMPEG全局指针
首先初始化两个复合流的全局指针
// OutputStream 结构体,用于管理每个媒体流
typedef struct
{
AVStream* stream;
AVPacket* pkt; // 用于在推流循环中复用 AVPacket 对象
int64_t next_pts; // 下一个帧的 PTS (Presentation Timestamp)
// 新增 extradata 相关成员
uint8_t* h264_extradata;
int h264_extradata_size;
int h264_extradata_set; // 标志位:是否已设置过 extradata
} OutputStream;
// RKMEDIA_FFMPEG_CONFIG 结构体,保存 FFmpeg 相关的配置和上下文
typedef struct
{
int width;
int height;
unsigned int config_id;
int protocol_type; // 流媒体协议类型 (FLV_PROTOCOL 或 TS_PROTOCOL)
char network_addr[NETWORK_ADDR_LENGTH]; // 流媒体地址
enum AVCodecID video_codec; // 视频编码器ID (如 AV_CODEC_ID_H264)
enum AVCodecID audio_codec; // 音频编码器ID (如 AV_CODEC_ID_AAC)
OutputStream video_stream; // 视频流配置
OutputStream audio_stream; // 音频流配置
AVFormatContext* oc; // FFmpeg 封装格式上下文
int ffmpeg_inited; // 新增标志,指示 FFmpeg
// 推流器是否已完全初始化并准备好推送
} RKMEDIA_FFMPEG_CONFIG;
// 推流配置(RTMP)
RKMEDIA_FFMPEG_CONFIG* stream_ffmpeg_config_ptr = NULL;
// 录像配置(MP4)
RKMEDIA_FFMPEG_CONFIG* mp4_ffmpeg_config_ptr = NULL;
// 初始化 MP4 封装配置(带音频流)
ret = init_ffmpeg_config(mp4_path, MP4_PROTOCOL, AV_CODEC_ID_AAC,
&mp4_ffmpeg_config_ptr);
if (ret != 0) {
fprintf(stderr, "save_file: MP4 FFmpeg 配置初始化失败!\n");
eturn;
}
printf("save_file: init_ffmpeg_config 成功。\n");
// 初始化 FFmpeg 推流配置
ret = init_ffmpeg_config(network_address, FLV_PROTOCOL, AV_CODEC_ID_AAC,
&stream_ffmpeg_config_ptr);
if (ret != 0) {
fprintf(stderr, "network_stream: FFmpeg 配置初始化失败!\n");
return;
}
printf("network_stream: init_ffmpeg_config 成功。\n");
ffmpeg_inited 标志:init_rkmedia_ffmpeg_context 只创建流和上下文,avformat_write_header 在视频线程收到首个 IDR 帧时才调用,成功后 ffmpeg_inited 置 1,此后音频线程才能安全写入。
init_ffmpeg_config
init_ffmpeg_config是具体去实现初始化
// --- init_rkmedia_ffmpeg_context 函数实现 ---
// 这个函数只负责初始化 AVFormatContext 和添加流,不进行网络连接或写入头
int init_rkmedia_ffmpeg_context(RKMEDIA_FFMPEG_CONFIG* ffmpeg_config)
{
AVOutputFormat* fmt = NULL;
int ret = 0;
printf(
"init_rkmedia_ffmpeg_context: before avformat_alloc_output_context2\n");
if (ffmpeg_config->protocol_type == FLV_PROTOCOL) {
// 协议类型为 FLV_PROTOCOL,则使用 "flv" 封装格式
ret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "flv",
ffmpeg_config->network_addr);
}
else if (ffmpeg_config->protocol_type == TS_PROTOCOL) {
// 协议类型为 TS_PROTOCOL,则使用 "mpegts" 封装格式
ret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "mpegts",
ffmpeg_config->network_addr);
}
else if (ffmpeg_config->protocol_type == MP4_PROTOCOL) {
// 协议类型为 MP4_PROTOCOL,则使用 "mp4" 封装格式
ret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "mp4",
ffmpeg_config->network_addr);
}
else {
fprintf(stderr, "init_rkmedia_ffmpeg_context: 不支持的协议类型: %d\n",
ffmpeg_config->protocol_type);
return -1;
}
if (ret < 0) {
fprintf(stderr, "init_rkmedia_ffmpeg_context: 无法分配输出上下文: %s\n",
av_err2str_custom(ret));
return -1;
}
if (ffmpeg_config->oc == NULL) {
fprintf(
stderr,
"init_rkmedia_ffmpeg_context: 错误: avformat_alloc_output_context2 "
"返回成功但 ffmpeg_config->oc 仍然为 NULL!\n");
return -1;
}
printf("init_rkmedia_ffmpeg_context: after avformat_alloc_output_context2, "
"ffmpeg_config->oc = %p\n",
(void*)ffmpeg_config->oc);
fmt = ffmpeg_config->oc->oformat;
/* 指定容器支持的编解码器 ID */
fmt->video_codec = ffmpeg_config->video_codec;
fmt->audio_codec = ffmpeg_config->audio_codec;
// 添加视频流
if (fmt->video_codec != AV_CODEC_ID_NONE) {
printf("init_rkmedia_ffmpeg_context: 尝试添加视频流...\n");
ret = add_stream(&ffmpeg_config->video_stream, ffmpeg_config->oc,
fmt->video_codec, ffmpeg_config->width,
ffmpeg_config->height, AV_PIX_FMT_YUV420P,
(AVRational){1, 30});
if (ret < 0) {
fprintf(stderr, "init_rkmedia_ffmpeg_context: 添加视频流失败: %d\n",
ret);
avformat_free_context(ffmpeg_config->oc);
return -1;
}
printf("init_rkmedia_ffmpeg_context: 成功添加视频流\n");
}
// 添加音频流 (如果需要)
if (fmt->audio_codec != AV_CODEC_ID_NONE) {
printf("init_rkmedia_ffmpeg_context: 尝试添加音频流...\n");
ret = add_stream(&ffmpeg_config->audio_stream, ffmpeg_config->oc,
fmt->audio_codec, 0, 0, AV_PIX_FMT_NONE,
(AVRational){1, 48000});
if (ret < 0) {
fprintf(stderr, "init_rkmedia_ffmpeg_context: 添加音频流失败: %d\n",
ret);
free_stream(
&ffmpeg_config
->video_stream); // 如果视频流已创建但音频流失败,则需释放视频流资源
avformat_free_context(ffmpeg_config->oc);
return -1;
}
printf("init_rkmedia_ffmpeg_context: 成功添加音频流\n");
}
printf("init_rkmedia_ffmpeg_context: after add_stream(s)\n");
// 打印格式信息 (详细的调试输出)
av_dump_format(ffmpeg_config->oc, 0, ffmpeg_config->network_addr, 1);
printf("init_rkmedia_ffmpeg_context: after av_dump_format\n");
// *** 关键修改:移除这里的 avio_open 和 avformat_write_header ***
// 这些操作将延迟到 deal_video_avpacket 中处理第一帧时执行
ffmpeg_config->ffmpeg_inited = 0; // 初始为未发送 RTMP 请求
return 0; // 成功
}
// --- init_ffmpeg_config 函数实现 (高层封装) ---
int init_ffmpeg_config(char* network_address,
int protocol_type,
enum AVCodecID audio_codec,
RKMEDIA_FFMPEG_CONFIG** config_out)
{
// 为 RKMEDIA_FFMPEG_CONFIG 结构体分配内存
RKMEDIA_FFMPEG_CONFIG* ffmpeg_config =
(RKMEDIA_FFMPEG_CONFIG*)malloc(sizeof(RKMEDIA_FFMPEG_CONFIG));
if (ffmpeg_config == NULL) {
fprintf(stderr, "init_ffmpeg_config: malloc ffmpeg_config failed.\n");
*config_out = NULL;
return -1; // 返回错误码
}
// 关键:在使用前将整个结构体内存清零,确保所有指针和成员都是初始化状态
memset(ffmpeg_config, 0, sizeof(RKMEDIA_FFMPEG_CONFIG));
// 设置基本配置参数
ffmpeg_config->width = WIDTH;
ffmpeg_config->height = HEIGHT;
ffmpeg_config->config_id = 0;
ffmpeg_config->protocol_type = protocol_type;
ffmpeg_config->video_codec = AV_CODEC_ID_H264;
ffmpeg_config->audio_codec = audio_codec;
// 复制地址
strncpy(ffmpeg_config->network_addr, network_address,
NETWORK_ADDR_LENGTH - 1);
ffmpeg_config->network_addr[NETWORK_ADDR_LENGTH - 1] =
'\0'; // 确保字符串以 null 结尾
printf("init_ffmpeg_config: before calling init_rkmedia_ffmpeg_context.\n");
// 调用核心的 FFmpeg 上下文初始化函数
int ret = init_rkmedia_ffmpeg_context(ffmpeg_config);
if (ret != 0) {
fprintf(stderr,
"init_ffmpeg_config: init_rkmedia_ffmpeg_context failed with "
"error code: %d\n",
ret);
// init_rkmedia_ffmpeg_context 内部已处理了大部分失败清理
free(ffmpeg_config); // 释放主结构体内存
*config_out = NULL;
return -1; // 返回错误码
}
printf("init_ffmpeg_config: after init_rkmedia_ffmpeg_context success.\n");
// ffmpeg_config->ffmpeg_inited 仍为 0,等待 deal_video_avpacket
// 触发实际的推流启动
*config_out = ffmpeg_config; // 将分配的配置指针返回给调用者
return 0; // 成功
}
在开始为RKMEDIA_FFMPEG_CONFIG动态分配内存,然后设置一些基本参数,如分辨率,音频视频流类型
然后调用核心的 FFmpeg 上下文初始化函数init_rkmedia_ffmpeg_context
在函数里面实现责初始化 AVFormatContext和添加音视频流
添加音视频流
添加音视频流单独封装了一个函数
// --- add_stream 函数实现 ---
int add_stream(OutputStream* ost,
AVFormatContext* oc,
enum AVCodecID codec_id,
int width,
int height,
enum AVPixelFormat pix_fmt,
AVRational time_base)
{
printf("add_stream: 进入函数.\n");
printf("add_stream: ost 地址 = %p, oc 地址 = %p\n", (void*)ost, (void*)oc);
if (!oc) {
printf("add_stream: 错误:AVFormatContext (oc) 为 NULL。\n");
return -1;
}
printf("add_stream: oc->nb_streams (在 avformat_new_stream 之前) = %d\n",
oc->nb_streams);
// 创建输出码流的 AVStream
ost->stream = avformat_new_stream(oc, NULL);
if (!ost->stream) {
fprintf(stderr, "add_stream: 无法创建 AVStream。\n");
return -1;
}
printf("add_stream: 成功创建 AVStream。\n");
printf("调试: ost->stream 地址 (在 avformat_new_stream 之后) = %p\n",
(void*)ost->stream);
printf("调试: oc->nb_streams (在 avformat_new_stream 之后) = %d\n",
oc->nb_streams);
// 设置流的ID
ost->stream->id = oc->nb_streams - 1;
printf("调试: 成功设置 ost->stream->id = %d\n", ost->stream->id);
// 获取并设置 AVCodecParameters (编解码器参数)
AVCodecParameters* codecpar = ost->stream->codecpar;
if (!codecpar) {
fprintf(stderr,
"add_stream: 无法获取 AVStream 的 AVCodecParameters。\n");
return -1;
}
// 初始化 extradata 相关字段,确保它们是空的或未设置的
ost->h264_extradata = NULL;
ost->h264_extradata_size = 0;
ost->h264_extradata_set = 0; // 初始为未设置
switch (codec_id) {
case AV_CODEC_ID_H264: // 视频流
codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
codecpar->codec_id = codec_id;
codecpar->width = width;
codecpar->height = height;
codecpar->format = pix_fmt; // 像素格式,通常是 AV_PIX_FMT_YUV420P
// 或 AV_PIX_FMT_NV12
// 对于裸流复用,通常不需要设置码率、gop_size等,这些由原始数据决定
// 但是对于 RTMP/FLV,必须确保在 avformat_write_header 之前设置
// extradata (SPS/PPS) extradata 的设置将在第一次关键帧处理时进行
ost->stream->time_base =
time_base; // 设置流的时间基,例如 {1, 30} (25 fps)
printf("add_stream: 视频流时间基设置为 %d/%d\n", time_base.num,
time_base.den);
// 如果输出格式需要全局头部 (如 FLV),设置此标志
// 但注意这个标志是针对 AVCodecContext 的,这里是 AVCodecParameters
// FFmpeg 在 avformat_write_header 时会根据格式和 extradata 自动处理
if (oc->oformat->flags & AVFMT_GLOBALHEADER) {
// 在 avcodecpar_from_context 中会将 codec->flags 拷贝过来,
// 但这里我们只设置 codecpar,这个标志通常在编码器 context
// 中设置
// 对于裸流封装,通常不需要手动设置这个,FFmpeg会根据extradata的存在性自动处理
// codecpar->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // 这是
// AVCodecContext 的标志
}
break;
case AV_CODEC_ID_AAC: // 音频流 (如果需要)
codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
codecpar->codec_id = codec_id;
codecpar->sample_rate = 48000; // 实际采样率
codecpar->channel_layout = AV_CH_LAYOUT_STEREO;
codecpar->channels = 2;
codecpar->frame_size = 1024; // AAC 每帧固定 1024 个采样
codecpar->format = AV_SAMPLE_FMT_FLTP; // 实际采样格式
ost->stream->time_base =
(AVRational){1, codecpar->sample_rate}; // 音频时间基
set_aac_extradata(codecpar,48000,2);//提前设置好音频的extradata,保证部分播放器可以播放
// 同样,对于音频 extradata(如 AAC ADTS
// header),也可能需要类似视频的延迟设置
// 这里只是示例,具体取决于你的 AAC 裸流格式
if (oc->oformat->flags & AVFMT_GLOBALHEADER) {
// codecpar->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
break;
default:
fprintf(stderr, "add_stream: 不支持或未处理的编码器 ID: %d\n",
codec_id);
return -1;
}
// 为流分配一个 AVPacket,用于后续循环中复用
ost->pkt = av_packet_alloc();
if (!ost->pkt) {
fprintf(stderr, "add_stream: 无法分配 AVPacket。\n");
return -1;
}
ost->next_pts = 0; // 初始化 PTS 计数器
return 0; // 成功
}
在
a在这里面进行流创建,编码器参数配置,以及为流分配一个AVPAcket
打开文件和写入头信息
在初始化完成之后,各自线程就开始获取队列中的音视频数据
以保存MP4文件为例:
// 通知其他写入线程(save_audio)此路 config 已就绪,可以开始计数
// header 尚未写,mp4_header_written 仍为 false,save_audio 会等待
mp4_active_threads.fetch_add(1); // save_file 自己算一个写入线程
while (true) {
std::shared_ptr<video_data_packet_t> video_data_packet =
high_video_queue->getVideoPacketQueue(FILE_VIDEO);
if (video_data_packet == nullptr) {
// shutdown 后队列已排空,安全退出
break;
}
if (video_data_packet->buffer.empty()) {
fprintf(stderr, "save_file: 收到空帧,跳过\n");
continue;
}
// 调用 deal_video_avpacket:内部会在首个 IDR 帧时提取 SPS/PPS、
// 调用 avformat_write_header,并将 ffmpeg_inited 置为 1
pthread_mutex_lock(&mp4_mutex);
ret = deal_video_avpacket(mp4_ffmpeg_config_ptr->oc,
&mp4_ffmpeg_config_ptr->video_stream,
video_data_packet, mp4_ffmpeg_config_ptr);
// header 写完后(ffmpeg_inited 从 0 变 1),通知音频线程
if (mp4_ffmpeg_config_ptr->ffmpeg_inited == 1 &&
!mp4_header_written.load()) {
mp4_header_written.store(true);
printf("save_file: MP4 header 写入完成,通知音频线程。\n");
}
pthread_mutex_unlock(&mp4_mutex);
if (ret == -1) {
usleep(1000 * 10);
continue;
}
}
由于AVFormatContext 内部有缓冲区和状态机,不是线程安全的。save_file 和 save_audio 同时调用 av_interleaved_write_frame 会数据竞争。
视频帧第一次获取到I帧后调用deal_videoo_packet后会去配置extradata
写入数据
// --- write_ffmpeg_avpacket 函数 (将 AVPacket 写入到复合流) ---
// fmt_ctx: AVFormatContext
// st: 对应的 AVStream
// pkt: 待写入的 AVPacket
int write_ffmpeg_avpacket(AVFormatContext* fmt_ctx, AVStream* st, AVPacket* pkt)
{
if (!fmt_ctx || !st || !pkt) {
fprintf(stderr, "write_ffmpeg_avpacket: 输入参数为 NULL。\n");
return -1;
}
// 将 AVPacket 的时间戳从其内部时间基(如果已设置)调整到目标流的时间基
// 如果 pkt->pts/dts 已经是在 ost->stream->time_base
// 下计算的,这一步可以省略 但为了健壮性,通常保留。这里将 pkt
// 的时间基调整为流的时间基,不会改变其值。
av_packet_rescale_ts(pkt, st->time_base, st->time_base);
pkt->stream_index = st->index; // 设置数据包所属的流索引
// printf("调试: 准备写入视频帧,Stream Index: %d, PTS: %lld, DTS: %lld,
// Size: %d, KeyFrame: %d\n",
// pkt->stream_index, pkt->pts, pkt->dts, pkt->size, (pkt->flags &
// AV_PKT_FLAG_KEY) ? 1 : 0);
// 写入数据包到复合流
int ret = av_interleaved_write_frame(fmt_ctx, pkt);
if (ret < 0) {
fprintf(stderr, "write_ffmpeg_avpacket: 写入数据包到复合流失败: %s\n",
av_err2str_custom(ret));
}
return ret;
}
// --- deal_video_avpacket 函数 (处理视频 AVPacket 并推流) ---
int deal_video_avpacket(AVFormatContext* oc,
OutputStream* ost,
std::shared_ptr<video_data_packet_t> video_data_packet,
RKMEDIA_FFMPEG_CONFIG* ffmpeg_config)
{
AVPacket* pkt_out = ost->pkt;
int ret = 0;
if (video_data_packet == nullptr) {
// printf("deal_video_avpacket: 视频队列为空,等待数据...\n");
return -1; // 表示当前没有数据
}
if (video_data_packet->buffer.empty()) {
std::cerr
<< "deal_video_avpacket: 警告: 收到 0 字节的视频帧。丢弃此帧。\n";
return 0;
}
// *** 新增:调用封装后的网络初始化函数 ***
int init_status =
init_ffmpeg_network_and_header(ffmpeg_config, video_data_packet);
if (init_status == 1) { // 尚未初始化且当前帧不满足初始化条件
// (例如,不是关键帧或无 SPS/PPS)
return 0; // 丢弃当前帧,等待下一帧
}
else if (init_status == -1) { // 初始化过程中发生致命错误
std::cerr << "deal_video_avpacket: FFmpeg 网络初始化失败,停止处理。\n";
return -1; // 返回错误,可能导致线程退出
}
// 如果 init_status == 0,表示已成功初始化或之前已初始化
// 清空 AVPacket,准备填充新数据
av_packet_unref(pkt_out);
int frame_size = video_data_packet->buffer.size();
ret = av_grow_packet(pkt_out, frame_size);
if (ret < 0) {
std::cerr << "deal_video_avpacket: 无法扩展 AVPacket 大小: "
<< av_err2str_custom(ret) << std::endl;
return ret;
}
memcpy(pkt_out->data, video_data_packet->buffer.data(), frame_size);
pkt_out->size = frame_size;
//AVPacket默认没有设置关键帧,需要手动设置,如果没有关键帧会黑屏
if (video_data_packet->is_key_frame) {
pkt_out->flags |= AV_PKT_FLAG_KEY;
}
else {
pkt_out->flags &= ~AV_PKT_FLAG_KEY;
}
// 计算并设置 PTS 和 DTS
pkt_out->pts = av_rescale_q(video_data_packet->timestamp, AV_TIME_BASE_Q,
ost->stream->time_base);
pkt_out->dts = pkt_out->pts;
pkt_out->stream_index = ost->stream->index;
// 实际的推流代码
ret = write_ffmpeg_avpacket(oc, ost->stream, pkt_out);
if (ret < 0) {
std::cerr << "Error writing video frame: " << av_err2str_custom(ret)
<< std::endl;
}
return ret;
}
在deal_video_packet这种将视频帧数据写入AVPacket,然后转换时间基,并进行av_interleaved_write_frame复合流写入
“交替写入”不需要代码手动保证,av_interleaved_write_frame 内部自动完成。
只需保证每帧的 pts 时间戳准确,FFmpeg 会自动按时间顺序把音视频帧交错写入文件。加锁只是为了线程安全,不是为了控制写入顺序。
退出写文件尾
// 退出:递减计数,若是最后一个线程则写 trailer 并释放资源
if (mp4_active_threads.fetch_sub(1) == 1) {
printf("save_file: 最后一个 MP4 写入线程,写入 trailer。\n");
pthread_mutex_lock(&mp4_mutex);
write_trailer(mp4_ffmpeg_config_ptr);
pthread_mutex_unlock(&mp4_mutex);
}
mp4_active_threads最主要的作用就是类似与引用计数,使用起来和信号量类似。因为复合流需要写视频尾,而音视频两个线程都在写入数据,都退出时无法确认谁先退出谁后退出,所以采取引用计数方式,当mp4_active_threads为0时的线程是最后退出线程,写入文件尾
在视频文件头成功写入后就可以通知音频流写入数据了
音频数据写入
音频数据写入和视频数据是一样的,主要区别是音频需要等待视频头写入后才能开始写入
void AV_DEV::save_audio(hi_void)
{
// 等待 save_file 线程写完 MP4 header 后再开始(同时检查队列是否已关闭)
while (!mp4_header_written.load()) {
if (high_audio_queue->isShutdown()) {
printf("save_audio: 队列已关闭且 header 未就绪,退出。\n");
return;
}
usleep(5000);
}
// 将自己计入活跃写入线程
mp4_active_threads.fetch_add(1);
if (!mp4_ffmpeg_config_ptr->audio_stream.stream ||
!mp4_ffmpeg_config_ptr->audio_stream.pkt) {
fprintf(stderr, "save_audio: 音频流未初始化,退出。\n");
if (mp4_active_threads.fetch_sub(1) == 1) {
pthread_mutex_lock(&mp4_mutex);
write_trailer(mp4_ffmpeg_config_ptr);
pthread_mutex_unlock(&mp4_mutex);
}
return;
}
while (true) {
std::shared_ptr<audio_data_packet_t> audio_data_packet =
high_audio_queue->getAudioPacketQueue(FILE_AUDIO);
if (audio_data_packet == nullptr) {
break;
}
if (audio_data_packet->buffer.empty()) {
fprintf(stderr, "save_audio: 收到空帧,跳过\n");
continue;
}
const int ADTS_HEADER_SIZE = 7;
if ((int)audio_data_packet->buffer.size() <= ADTS_HEADER_SIZE) {
fprintf(stderr, "save_audio: 帧太短(%zu 字节),跳过\n",
audio_data_packet->buffer.size());
continue;
}
const uint8_t* raw_aac = audio_data_packet->buffer.data() + ADTS_HEADER_SIZE;
int raw_aac_size = (int)audio_data_packet->buffer.size() - ADTS_HEADER_SIZE;
pthread_mutex_lock(&mp4_mutex);
write_audio_frame(mp4_ffmpeg_config_ptr, raw_aac, raw_aac_size,
audio_data_packet->timestamp);
pthread_mutex_unlock(&mp4_mutex);
}
// 退出:递减计数,若是最后一个线程则写 trailer
if (mp4_active_threads.fetch_sub(1) == 1) {
printf("save_audio: 最后一个 MP4 写入线程,写入 trailer。\n");
pthread_mutex_lock(&mp4_mutex);
write_trailer(mp4_ffmpeg_config_ptr);
pthread_mutex_unlock(&mp4_mutex);
}
printf("save_audio: 音频写入线程退出。\n");
return;
}
海思 AENC 编码输出的 AAC 数据带 7 字节 ADTS 头,MP4 和 FLV 封装均使用 raw AAC(AudioSpecificConfig 写在 extradata 里),写入前必须跳过:
const int ADTS_HEADER_SIZE = 7;
const uint8_t* raw_aac = audio_data_packet->buffer.data() + ADTS_HEADER_SIZE;
int raw_aac_size = (int)audio_data_packet->buffer.size() - ADTS_HEADER_SIZE;
额外注意
MP4 封装的 AAC 需要在 avformat_write_header 前设置 AudioSpecificConfig(2 字节),否则部分播放器无法识别音轨
MP4 / FLV 封装格式
使用 raw AAC,帧数据里没有头,采样率和声道数信息必须放在容器的元数据里,这个元数据就叫 AudioSpecificConfig(ASC),只有 2 字节:
[AVStream.codecpar.extradata] = AudioSpecificConfig (2 bytes)
avformat_write_header 写 MP4/FLV 文件头时,会把这 2 字节写进 stsd box(MP4)或 AudioTag(FLV)。播放器打开文件时先读这里,拿到采样率和声道数,才能初始化解码器。
ASC 的 2 字节内容
bits [15:11] = Audio Object Type (AAC-LC = 2)
bits [10:7] = Sampling Frequency Index
(0=96000, 3=48000, 4=44100, 6=22050, 7=16000...)
bits [6:3] = Channel Configuration (1=单声道, 2=立体声)
bits [2:0] = 其他标志,通常为 0
在创建音频流之后,填写 extradata
在 avformat_new_stream 之后、avformat_write_header 之前,加入以下代码:
#include <stdint.h>
// AAC-LC = 2, 采样率索引, 声道数 —— 根据你的实际参数修改
static int get_aac_sample_rate_index(int sample_rate) {
static const int aac_freq_table[] = {
96000, 88200, 64000, 48000, 44100, 32000,
24000, 22050, 16000, 12000, 11025, 8000, 7350
};
for (int i = 0; i < 13; i++) {
if (aac_freq_table[i] == sample_rate)
return i;
}
return -1; // 不支持的采样率
}
// 在 audio_stream.stream->codecpar 设置完后调用
void set_aac_extradata(AVCodecParameters *par, int sample_rate, int channels) {
int sri = get_aac_sample_rate_index(sample_rate);
if (sri < 0) return;
// AudioSpecificConfig = 2 bytes
// [15:11] AudioObjectType=2 (AAC-LC)
// [10:7] SamplingFrequencyIndex
// [6:3] ChannelConfiguration
// [2:0] 0
uint16_t asc = (2 << 11) | (sri << 7) | (channels << 3);
par->extradata_size = 2;
par->extradata = (uint8_t *)av_malloc(2 + AV_INPUT_BUFFER_PADDING_SIZE);
par->extradata[0] = (asc >> 8) & 0xFF;
par->extradata[1] = (asc) & 0xFF;
memset(par->extradata + 2, 0, AV_INPUT_BUFFER_PADDING_SIZE);
}
然后在add_stream函数音频流创建中修改代码如下:
case AV_CODEC_ID_AAC: // 音频流 (如果需要)
codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
codecpar->codec_id = codec_id;
codecpar->sample_rate = 48000; // 实际采样率
codecpar->channel_layout = AV_CH_LAYOUT_STEREO;
codecpar->channels = 2;
codecpar->frame_size = 1024; // AAC 每帧固定 1024 个采样
codecpar->format = AV_SAMPLE_FMT_FLTP; // 实际采样格式
ost->stream->time_base =
(AVRational){1, codecpar->sample_rate}; // 音频时间基
set_aac_extradata(codecpar,48000,2);//提前设置好音频的extradata,保证部分播放器可以播放
// 同样,对于音频 extradata(如 AAC ADTS
// header),也可能需要类似视频的延迟设置
// 这里只是示例,具体取决于你的 AAC 裸流格式
if (oc->oformat->flags & AVFMT_GLOBALHEADER) {
// codecpar->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
break;
通过上面步骤即可成功合并音视频流
往期相关博客连接:
