源码:
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 的关键点
- 常见踩坑
二、整体流程
整个流程可以拆成四步:
-
读取 RGB 裸数据
-
转换为 YUV420P(编码器需要)
-
编码为 H264(AVPacket)
-
封装为 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)
- 封装流程