FFmpeg 实战:RGB 裸流编码成 MP4,全流程详解(含源码

源码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
extern "C"
{
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
}

int main()
{
    //输入rgb裸流文件与编码封装输出文件
    char infile[] = "E:/videos/output.rgb";
    char outfile[] = "E:/videos/rgb.mp4";

    //以读的方式,二进制模式打开infile
    FILE* fp = fopen(infile, "rb");
    if (!fp)
    {
        std::cout << infile << " open failed" << std::endl;
        return -1;
    }

    //视频分辨率,帧率参数
    int width = 1920;
    int height = 1080;
    int fps = 24;

    //查找一个支持 H264 编码的编码器
    const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!codec)
    {
        std::cout << "av codec_find_encoder failed!" << std::endl;
        return -1;
    }

    // 为该编码器分配上下文,用来保存编码参数和运行状态
    AVCodecContext* c = avcodec_alloc_context3(codec);
    if (!c)
    {
        std::cout << " av_codec_alloc_context3" << std::endl;
        return -1;
    }
    //编码信息
    c->bit_rate = 4000000;
    c->width = width;
    c->height = height;
    c->time_base = { 1,fps };
    c->framerate = { fps,1 };
    c->gop_size = 50;           //画面组大小,两个关键帧之间的最大距离
    c->max_b_frames = 0;        //不需要b帧
    c->pix_fmt = AV_PIX_FMT_YUV420P;        //输入像素格式(给编码器吃的)
    c->codec_id = AV_CODEC_ID_H264;
    c->thread_count = 8;

    // 使用全局头,MP4 等封装格式通常需要
    c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

    int ret = avcodec_open2(c, codec, NULL);    //真正初始化编码器并且启动
    if (ret < 0)
    {
        std::cout << " avcodec_open2  failed!" << std::endl;
        return -1;
    }


    //创建输出的mp4文件的上下文
    AVFormatContext* oc = nullptr;
    avformat_alloc_output_context2(&oc, 0, 0, outfile);

    //添加视频流
    AVStream* st = avformat_new_stream(oc, NULL);
    st->time_base = { 1, fps };
    st->codecpar->codec_tag = 0;    //将编码标签置为0,让 FFmpeg 自己决定最合适的封装标识
    avcodec_parameters_from_context(st->codecpar, c);   //将编码器的参数信息拷贝到流的codecpar中(从编码器上下文中拷贝)

    //输出文件信息
    std::cout << "===============================================" << std::endl;
    av_dump_format(oc, 0, outfile, 1);
    std::cout << "===============================================" << std::endl;

    //将RGB原始数据转成YUV(mp4封装格式需要)
    SwsContext* ctx = NULL;                     //创建转换器上下文
    ctx = sws_getCachedContext(ctx,
        width, height, AV_PIX_FMT_RGB24,
        width, height, AV_PIX_FMT_YUV420P,
        SWS_BICUBIC,        //插值算法
        NULL, NULL, NULL
    );
    //输入缓存
    unsigned char* rgb = new unsigned char[width * height * 3];

    //输出帧缓存
    AVFrame* frame = av_frame_alloc();      //创建帧
    frame->format = AV_PIX_FMT_YUV420P;
    frame->width = width;
    frame->height = height; 
    ret = av_frame_get_buffer(frame, 32);   //为帧分配缓存
    if (ret < 0)
    {
        std::cout << "av_frame_get_buffer failed!" << std::endl;
        return -1;
    }

    //写出文件头
    ret = avio_open(&oc->pb, outfile, AVIO_FLAG_WRITE); //打开输出文件
    if (ret < 0)
    {
        std::cout << "avio_open failed!" << std::endl;
        return -1;
    }
    ret = avformat_write_header(oc, NULL);  //写出文件头
    if (ret < 0)
    {
        std::cout << "avformat_write_header failed!" << std::endl;
        return -1;
    }

    int current_frame = 1;
    int p = 0;
    AVPacket* pkt = av_packet_alloc();   //编码后的数据
    //循环读取rgb数据
    for (;;)
    {
        //从文件中读取rgb数据
        int len = fread(rgb, 1, width * height * 3, fp);
        if (len <= 0) break;
        uint8_t* indata[AV_NUM_DATA_POINTERS] = { 0 };
        indata[0] = rgb;
        int inlinesize[AV_NUM_DATA_POINTERS] = { 0 };
        inlinesize[0] = width * 3;

        int h = sws_scale(ctx, indata, inlinesize, 0, height, frame->data, frame->linesize);    //RGB24 转 YUV420P
        if (h <= 0)break;
        std::cout << "frame:" << current_frame << std::endl;

        //编码一帧数据
        frame->pts = p;
        p ++;
        ret = avcodec_send_frame(c, frame); //发送一帧数据给编码器
        if (ret < 0)
        {
            std::cout << "avcodec_send_frame failed!" << std::endl;
            continue;
        }
        
        while (true)    //循环读取编码后的数据
        {
            ret = avcodec_receive_packet(c, pkt);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
                break;
            if (ret < 0)
            {
                // 真失败
                break;
            }
            //先转换时间基数,流索引,再写出文件(编码器输出的 packet,默认是用 c->time_base,封装器要求的是 st->time_base)
            av_packet_rescale_ts(pkt, c->time_base, st->time_base);
            pkt->stream_index = st->index;

            av_interleaved_write_frame(oc, pkt);   //写出编码后的数据到文件
            av_packet_unref(pkt);
        }

        current_frame++;
    }
    avcodec_send_frame(c, nullptr);     //发送结束帧
    while (true)    //循环读取编码后的数据
    {
        ret = avcodec_receive_packet(c, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            break;
        if (ret < 0)
            break;

        av_interleaved_write_frame(oc, pkt);
        av_packet_unref(pkt);
    }

    av_write_trailer(oc);       //写出文件尾,视频索引
    avio_close(oc->pb);         //关闭输出文件
    avformat_free_context(oc);  //释放oc
    avcodec_free_context(&c);   //释放编码器和上下文
    sws_freeContext(ctx);       //释放转换器
    av_frame_free(&frame);      //释放帧

    fclose(fp);                 //关闭输入文件

    delete[] rgb;
    return 0;
}

一、项目背景

在音视频开发中,经常会遇到这样一个问题:

如何把原始的 RGB 裸数据编码成视频文件(如 MP4)?

本文将通过一个完整的 demo,实现如下流程:

RGB裸流 → YUV420P → H264编码 → MP4封装

并重点讲解:

  • sws_scale 的作用
  • 编码器 send/receive 模型
  • pts / time_base 处理
  • 封装 mp4 的关键点
  • 常见踩坑

二、整体流程

整个流程可以拆成四步:

复制代码
  1. 读取 RGB 裸数据

  2. 转换为 YUV420P(编码器需要)

  3. 编码为 H264(AVPacket)

  4. 封装为 MP4

三、核心代码

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
extern "C"
{
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
}

int main()
{
    //输入rgb裸流文件与编码封装输出文件
    char infile[] = "E:/videos/output.rgb";
    char outfile[] = "E:/videos/rgb.mp4";

    //以读的方式,二进制模式打开infile
    FILE* fp = fopen(infile, "rb");
    if (!fp)
    {
        std::cout << infile << " open failed" << std::endl;
        return -1;
    }

    //视频分辨率,帧率参数
    int width = 1920;
    int height = 1080;
    int fps = 24;

    //查找一个支持 H264 编码的编码器
    const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!codec)
    {
        std::cout << "av codec_find_encoder failed!" << std::endl;
        return -1;
    }

    // 为该编码器分配上下文,用来保存编码参数和运行状态
    AVCodecContext* c = avcodec_alloc_context3(codec);
    if (!c)
    {
        std::cout << " av_codec_alloc_context3" << std::endl;
        return -1;
    }
    //编码信息
    c->bit_rate = 4000000;
    c->width = width;
    c->height = height;
    c->time_base = { 1,fps };
    c->framerate = { fps,1 };
    c->gop_size = 50;           //画面组大小,两个关键帧之间的最大距离
    c->max_b_frames = 0;        //不需要b帧
    c->pix_fmt = AV_PIX_FMT_YUV420P;        //输入像素格式(给编码器吃的)
    c->codec_id = AV_CODEC_ID_H264;
    c->thread_count = 8;

    // 使用全局头,MP4 等封装格式通常需要
    c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

    int ret = avcodec_open2(c, codec, NULL);    //真正初始化编码器并且启动
    if (ret < 0)
    {
        std::cout << " avcodec_open2  failed!" << std::endl;
        return -1;
    }


    //创建输出的mp4文件的上下文
    AVFormatContext* oc = nullptr;
    avformat_alloc_output_context2(&oc, 0, 0, outfile);

    //添加视频流
    AVStream* st = avformat_new_stream(oc, NULL);
    st->time_base = { 1, fps };
    st->codecpar->codec_tag = 0;    //将编码标签置为0,让 FFmpeg 自己决定最合适的封装标识
    avcodec_parameters_from_context(st->codecpar, c);   //将编码器的参数信息拷贝到流的codecpar中(从编码器上下文中拷贝)

    //输出文件信息
    std::cout << "===============================================" << std::endl;
    av_dump_format(oc, 0, outfile, 1);
    std::cout << "===============================================" << std::endl;

    //将RGB原始数据转成YUV(mp4封装格式需要)
    SwsContext* ctx = NULL;                     //创建转换器上下文
    ctx = sws_getCachedContext(ctx,
        width, height, AV_PIX_FMT_RGB24,
        width, height, AV_PIX_FMT_YUV420P,
        SWS_BICUBIC,        //插值算法
        NULL, NULL, NULL
    );
    //输入缓存
    unsigned char* rgb = new unsigned char[width * height * 3];

    //输出帧缓存
    AVFrame* frame = av_frame_alloc();      //创建帧
    frame->format = AV_PIX_FMT_YUV420P;
    frame->width = width;
    frame->height = height; 
    ret = av_frame_get_buffer(frame, 32);   //为帧分配缓存
    if (ret < 0)
    {
        std::cout << "av_frame_get_buffer failed!" << std::endl;
        return -1;
    }

    //写出文件头
    ret = avio_open(&oc->pb, outfile, AVIO_FLAG_WRITE); //打开输出文件
    if (ret < 0)
    {
        std::cout << "avio_open failed!" << std::endl;
        return -1;
    }
    ret = avformat_write_header(oc, NULL);  //写出文件头
    if (ret < 0)
    {
        std::cout << "avformat_write_header failed!" << std::endl;
        return -1;
    }

    int current_frame = 1;
    int p = 0;
    AVPacket* pkt = av_packet_alloc();   //编码后的数据
    //循环读取rgb数据
    for (;;)
    {
        //从文件中读取rgb数据
        int len = fread(rgb, 1, width * height * 3, fp);
        if (len <= 0) break;
        uint8_t* indata[AV_NUM_DATA_POINTERS] = { 0 };
        indata[0] = rgb;
        int inlinesize[AV_NUM_DATA_POINTERS] = { 0 };
        inlinesize[0] = width * 3;

        int h = sws_scale(ctx, indata, inlinesize, 0, height, frame->data, frame->linesize);    //RGB24 转 YUV420P
        if (h <= 0)break;
        std::cout << "frame:" << current_frame << std::endl;

        //编码一帧数据
        frame->pts = p;
        p ++;
        ret = avcodec_send_frame(c, frame); //发送一帧数据给编码器
        if (ret < 0)
        {
            std::cout << "avcodec_send_frame failed!" << std::endl;
            continue;
        }
        
        while (true)    //循环读取编码后的数据
        {
            ret = avcodec_receive_packet(c, pkt);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
                break;
            if (ret < 0)
            {
                // 真失败
                break;
            }
            //先转换时间基数,流索引,再写出文件(编码器输出的 packet,默认是用 c->time_base,封装器要求的是 st->time_base)
            av_packet_rescale_ts(pkt, c->time_base, st->time_base);
            pkt->stream_index = st->index;

            av_interleaved_write_frame(oc, pkt);   //写出编码后的数据到文件
            av_packet_unref(pkt);
        }

        current_frame++;
    }
    avcodec_send_frame(c, nullptr);     //发送结束帧
    while (true)    //循环读取编码后的数据
    {
        ret = avcodec_receive_packet(c, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            break;
        if (ret < 0)
            break;

        av_interleaved_write_frame(oc, pkt);
        av_packet_unref(pkt);
    }

    av_write_trailer(oc);       //写出文件尾,视频索引
    avio_close(oc->pb);         //关闭输出文件
    avformat_free_context(oc);  //释放oc
    avcodec_free_context(&c);   //释放编码器和上下文
    sws_freeContext(ctx);       //释放转换器
    av_frame_free(&frame);      //释放帧

    fclose(fp);                 //关闭输入文件

    delete[] rgb;
    return 0;
}

四、关键步骤解析

RGB → YUV420P(sws_scale)

cpp 复制代码
ctx = sws_getCachedContext(
ctx,
width, height, AV_PIX_FMT_RGB24,
width, height, AV_PIX_FMT_YUV420P,
SWS_BICUBIC,
NULL, NULL, NULL
);

为什么要转?

因为 H264 编码器只接受 YUV 格式(通常是 YUV420P)

编码器初始化

cpp 复制代码
c->time_base = {1, fps};
c->framerate = {fps, 1};
c->pix_fmt = AV_PIX_FMT_YUV420P;

关键点:

time_base 决定 pts 单位

一帧一单位 → frame->pts = 0,1,2...

编码流程

cpp 复制代码
avcodec_send_frame(c, frame);

while (true)
{
    ret = avcodec_receive_packet(c, pkt);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
        break;

    ...
}

为什么要 while?

编码器不是"一帧输入 → 一帧输出",编码器内部有一个缓冲区,调用avcodec_send_frame发送到缓冲区进行编码,由于b帧的存在,需要等待下一帧编码成功,该b帧才能进行编码,所以一次avcodec_send_frame可能会输出不止一帧的AVPacket。

时间戳转换(最容易踩坑)

cpp 复制代码
av_packet_rescale_ts(pkt, c->time_base, st->time_base);
pkt->stream_index = st->index;

为什么必须做?

编码器输出 packet 使用:

cpp 复制代码
c->time_base

而封装器要求:

cpp 复制代码
st->time_base

不转换就会出现:

cpp 复制代码
Timestamps are unset
mp4 文件损坏

写入 MP4

cpp 复制代码
av_interleaved_write_frame(oc, pkt);

flush 编码器(非常重要)

cpp 复制代码
avcodec_send_frame(c, nullptr);

while (true)
{
ret = avcodec_receive_packet(c, pkt);
...
}

为什么?

编码器内部有缓存,不 flush 会丢最后几帧,发送一个空帧给编码器,让编码器输出最后的几帧,并且我们进行循环接收

五、常见踩坑总结(重点)

坑1:只 receive 一次 packet

cpp 复制代码
send_frame
receive_packet(只调用一次)

会丢数据 → 文件损坏

坑2:忘记 flush

cpp 复制代码
avcodec_send_frame(c, nullptr);

不调用 → 最后几帧丢失

坑3:不做时间戳转换

cpp 复制代码
av_packet_rescale_ts(...)

不写 → mp4 播放失败

坑4:先 unref 再写 packet

cpp 复制代码
av_packet_unref(pkt); 
write_frame(pkt); 

packet 已被清空 → 时间戳丢失

坑5:RGB 格式搞错

cpp 复制代码
AV_PIX_FMT_RGB24 vs AV_PIX_FMT_BGR24

不会报错,但画面异常

会丢数据 → 文件损坏

六、调试思路

如果输出 MP4 播放不了:

按顺序排查:

cpp 复制代码
1. 输入数据对不对(分辨率/格式)
2. YUV 转换对不对
3. 编码器是否正常输出 packet
4. pts/dts 是否正确
5. 是否做了 rescale
6. 是否 flush
7. trailer 是否写成功

七、总结

通过本 demo,我们实现了:

  • RGB 裸流读取
  • YUV 转换
  • H264 编码
  • MP4 封装

同时理解了:

  • FFmpeg 编码模型(send/receive)
  • 时间戳(pts/dts/time_base)
  • 封装流程
相关推荐
Yupureki2 小时前
《Linux系统编程》20.常见程序设计模式
linux·服务器·c语言·c++·单例模式·建造者模式·责任链模式
誰能久伴不乏2 小时前
给开发板装上嘴巴与耳朵:i.MX6ULL 裸机串口 (UART) 驱动终极指南
arm开发·c++·单片机·嵌入式硬件·arm
biter down2 小时前
深入浅出 C++ string 类:从原理到实战
开发语言·c++
Lhan.zzZ3 小时前
Qt多线程数据库操作:安全分离连接,彻底解决段错误
数据库·c++·qt·安全
小樱花的樱花3 小时前
C++引用:高效编程的技巧
开发语言·数据结构·c++·算法
Yupureki3 小时前
《算法竞赛从入门到国奖》算法基础:动态规划-最长子序列
c语言·c++·算法·动态规划
南境十里·墨染春水3 小时前
C++笔记 继承中重载规则 公有私有继承的区别(面向对象)
开发语言·c++·笔记
沉鱼.443 小时前
进制转换题
开发语言·c++·算法
liulilittle3 小时前
SQLITE3 KG-CC
数据库·c++·sqlite