基于FFmpeg将H.264视频流封装为MP4格式

一、项目背景

  服务器的核心目标是:响应客户端的 “检测” 指令,检测声学故障场景中,一旦触发声学告警(比如分贝过高),按指定的 MP4 文件名(时间戳命名,如2026-01-10-15-46-58.mp4)持续保存摄像头采集的 H264 视频帧到文件,直到客户端发出 “结束检测” 指令。

  H.264是一种裸视频流文件, windows自带的播放器没有办法直接打开它,它就像是一份纯文字稿件,没有索引,没有标题,没有页码,虽然内容都在,但是读起来不方便。那么怎么才能播放H.264视频流,这就需要对H.264做一次“封装”,这中“封装”就是我们所熟知的MP4、AVI等格式。这种格式它定义了文件的整体结构,记录了视频和音频流是如何交织存放的,更重要的是,它保存了时间戳(PTS/DTS) 信息。没有正确的时间戳,播放器就不知道每一帧应该在什么时间点显示。

二、FFmpeg封装MP4

  FFmpeg 是行业标准级的音视频处理工具,FFmpeg 来封装 H264 为 MP4 格式很可靠。

  以下是一个例子,先准备一个H.264流。

ffmpeg -f lavfi -i testsrc=duration=5:size=960x408:rate=25 -c:v libx264 -f h264 test.h264
#include <stdio.h>
#include <stdlib.h>

extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
}

int main(int argc, char *argv[]) {
    AVFormatContext *in_fmt_ctx = NULL;
    const char *input_filename = "test.h264";  // H264文件
    int video_stream_idx = -1;

    // 1. 打开输入文件
    if (avformat_open_input(&in_fmt_ctx, input_filename, NULL, NULL) < 0) {
        fprintf(stderr, "无法打开输入文件: %s\n", input_filename);
        return -1;
    }

    // 2. 探测流信息 解析出视频宽高、帧率、编码格式、SPS/PPS
    if (avformat_find_stream_info(in_fmt_ctx, NULL) < 0) {
        fprintf(stderr, "无法从输入文件中提取流信息\n");
        avformat_close_input(&in_fmt_ctx);
        return -1;
    }

    // 3. 找到视频流
    for (int i = 0; i < in_fmt_ctx->nb_streams; i++) {
        if (in_fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_stream_idx = i;
            break;
        }
    }
    if (video_stream_idx == -1) {
        fprintf(stderr, "未找到视频流\n");
        avformat_close_input(&in_fmt_ctx);
        return -1;
    }

    av_dump_format(in_fmt_ctx, 0, input_filename, 0);

    // ==================== 输出 MP4 ====================
    AVFormatContext *out_fmt_ctx = NULL;
    const char *output_filename = "output.mp4";

    avformat_alloc_output_context2(&out_fmt_ctx, NULL, NULL, output_filename);
    if (!out_fmt_ctx) {
        fprintf(stderr, "无法分配输出上下文\n");
        avformat_close_input(&in_fmt_ctx);
        return -1;
    }

    AVStream *out_stream = avformat_new_stream(out_fmt_ctx, NULL);
    if (!out_stream) {
        fprintf(stderr, "无法创建输出流\n");
        avformat_close_input(&in_fmt_ctx);
        avformat_free_context(out_fmt_ctx);
        return -1;
    }

    // 复制编码参数
    AVCodecParameters *in_codecpar = in_fmt_ctx->streams[video_stream_idx]->codecpar;
    //把输入流的所有解码信息复制给输出流
    avcodec_parameters_copy(out_stream->codecpar, in_codecpar);

    // 时间基
    out_stream->time_base = (AVRational){1, 25};

    // 打开输出文件
    if (!(out_fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
        if (avio_open(&out_fmt_ctx->pb, output_filename, AVIO_FLAG_WRITE) < 0) {
            fprintf(stderr, "无法打开输出文件\n");
            avformat_close_input(&in_fmt_ctx);
            avformat_free_context(out_fmt_ctx);
            return -1;
        }
    }

    // 写入文件头
    avformat_write_header(out_fmt_ctx, NULL);

    AVPacket *pkt = av_packet_alloc();
    AVStream *in_stream = in_fmt_ctx->streams[video_stream_idx];
    int frame_count = 0;

    // 读取 + 封装
    //读取一帧 H.264,装进 AVPacket, AVPacket 包含:帧数据 时间戳 关键帧标记
    while (av_read_frame(in_fmt_ctx, pkt) >= 0) {
        if (pkt->stream_index == video_stream_idx) {

            // 自动生成PTS(裸流必备),H.264 裸流没有时间戳,必须手动生成
            if (pkt->pts == AV_NOPTS_VALUE) {
                AVRational frame_rate = in_stream->r_frame_rate;
                int64_t duration = AV_TIME_BASE / av_q2d(frame_rate);
                pkt->pts = frame_count * duration / (av_q2d(in_stream->time_base) * AV_TIME_BASE);
                pkt->dts = pkt->pts;
                pkt->duration = duration / (av_q2d(in_stream->time_base) * AV_TIME_BASE);
                frame_count++;
            }

            // 时间戳转换
            pkt->pts = av_rescale_q_rnd(pkt->pts, in_stream->time_base, out_stream->time_base,
                (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
            pkt->dts = av_rescale_q_rnd(pkt->dts, in_stream->time_base, out_stream->time_base,
                (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
            pkt->duration = av_rescale_q(pkt->duration, in_stream->time_base, out_stream->time_base);

            pkt->stream_index = out_stream->index;
            pkt->pos = -1;
            //把帧写入MP4
            av_interleaved_write_frame(out_fmt_ctx, pkt);
        }
        //写入 MP4 文件结束标记
        av_packet_unref(pkt);
    }

    // 写入尾
    av_write_trailer(out_fmt_ctx);

    // 释放资源
    av_packet_free(&pkt);
    if (out_fmt_ctx && !(out_fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
        avio_closep(&out_fmt_ctx->pb);
    }
    avformat_close_input(&in_fmt_ctx);
    avformat_free_context(out_fmt_ctx);

    printf("MP4 封装成功!output.mp4\n");
    return 0;
}

  流程图

1. 打开 H264
2. 读取流信息(拿 SPS/PPS)
3. 创建 MP4
4. 复制 codecpar(复制 SPS/PPS) 
5. 写 MP4 头
6. 循环读帧 → AVPacket
7. 生成时间戳
8. 写入帧
9. 写 MP4 尾

三、转换失败的原因

  1. 没有 SPS / PPS,MP4 必须要有 SPS/PPS 才能解码
  2. 原始 H.264 是没有时间戳(PTS/DTS)的,必须手动生成
posted @ 2026-03-24 16:56  看时光飞驰我  阅读(145)  评论(0)    收藏  举报