FFMPEG音视频流合并源码分析

音视频流合并

音视频流合并

笔者目前有两个流分别是音频流和视频流,现在需要合并成复合流用于保存文件和网络推流

笔者的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_filesave_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;

通过上面步骤即可成功合并音视频流

往期相关博客连接:

海思mpp音频编码 – kidwjb的小站

单生产者多消费者框架 – kidwjb的小站

FFMPEG&网络推流 – kidwjb的小站

海思mpp视频编码 – kidwjb的小站

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇