一、概述
本文记录如何使用 FFmpeg 实现一个H.264/H.265视频编码器,将生成的 YUV 原始数据编码为压缩视频文件。这是安防监控、视频存储等领域的核心功能。
二、编码器工作流程
YUV原始数据 ──→ 编码器 ──→ H.264/H.265压缩数据
(未压缩) (压缩后,体积小)
特点:
- 压缩率高,节省存储空间
- 保持画质的同时大幅减小文件体积
- 是视频存储和传输的核心技术
三、核心 API 清单
| 分类 | API | 作用 |
|---|---|---|
| 编码器 | avcodec_find_encoder_by_name() | 查找编码器(libx264/libx265) |
avcodec_alloc_context3() | 创建编码器上下文 | |
avcodec_open2() | 打开编码器 | |
| 参数设置 | av_opt_set() | 设置编码器预设(preset) |
| 帧管理 | av_frame_alloc() | 分配帧结构体 |
av_frame_get_buffer() | 分配帧缓冲区 | |
av_frame_make_writable() | 确保帧可写 | |
| 编码 | avcodec_send_frame() | 发送原始帧到编码器 |
avcodec_receive_packet() | 接收压缩数据包 | |
| 包管理 | av_packet_alloc() | 分配数据包 |
av_packet_unref() | 释放包内数据 | |
| 清理 | avcodec_free_context() | 释放编码器上下文 |
av_frame_free() | 释放帧 | |
av_packet_free() | 释放数据包 |
四、核心流程图
┌─────────────────────────────────────────────────────────────┐
│ 视频编码核心流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. avcodec_find_encoder_by_name() 查找编码器 │
│ ↓ │
│ 2. avcodec_alloc_context3() 创建编码器上下文 │
│ ↓ │
│ 3. 设置编码参数(宽/高/码率/帧率/GOP/B帧) │
│ ↓ │
│ 4. avcodec_open2() 打开编码器 │
│ ↓ │
│ 5. av_frame_alloc() 创建帧结构体 │
│ av_frame_get_buffer() 分配帧缓冲区 │
│ ↓ │
│ 6. 循环编码每一帧 │
│ ├── 填充 YUV 数据 │
│ ├── frame->pts = i 设置时间戳 │
│ ├── avcodec_send_frame() 发送原始帧 │
│ └── while 循环接收压缩包 │
│ ├── avcodec_receive_packet() 接收包 │
│ ├── fwrite() 写入文件 │
│ └── av_packet_unref() 释放包内数据 │
│ ↓ │
│ 7. 刷新编码器:avcodec_send_frame(ctx, NULL) │
│ ↓ │
│ 8. 释放所有资源 │
│ │
└─────────────────────────────────────────────────────────────┘
五、编码函数代码解析
static int encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *pkt, FILE *f) {
// 1. 发送帧到编码器
ret = avcodec_send_frame(ctx, frame);
if (ret < 0) return ret;
// 2. 循环接收编码后的数据包
while (1) {
ret = avcodec_receive_packet(ctx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break; // 需要更多帧或已结束
}
if (ret < 0) return ret;
// 3. 写入文件
fwrite(pkt->data, 1, pkt->size, f);
av_packet_unref(pkt); // 释放数据,重用包结构
}
return 0;
}
六、核心注意事项
| 要点 | 说明 |
|---|---|
| 帧参数设置 | av_frame_get_buffer() 前必须设置 width/height/format |
| 时间戳 | 只设置 PTS,DTS 编码器自动计算 |
| while 循环 | 一个 send 可能对应多个 receive,必须循环接收 |
| av_packet_unref | 释放 data 缓冲区,保留结构体供下次使用 |
| 刷新编码器 | 编码结束后 send(NULL) 取出剩余数据 |
| preset 设置 | 只有 H.264/H.265 编码器支持 |
七、源码
#include <libavutil/log.h>
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
static int encode(AVCodecContext *ctx,AVFrame *frame,AVPacket *pkt,FILE *f){
int ret = -1;
//把frame送入编码器上下文
ret = avcodec_send_frame(ctx,frame);
if (ret < 0)
{
av_log(NULL,AV_LOG_ERROR,"Failed to send frame to encoder\n");
return ret;
}
while (1)
{
//接收压缩数据 写入压缩数据到pkt
ret = avcodec_receive_packet(ctx,pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
{
break;
}else if (ret < 0)
{
av_log(NULL,AV_LOG_ERROR,"Failed to receive packet:%s\n",av_err2str(ret));
return ret; //严重错误
}
//将压缩数据写入f
fwrite(pkt->data,1,pkt->size,f);
//释放的是 pkt->data 的数据缓冲区
av_packet_unref(pkt);
}
return 0;
}
int main(int argc,char *argv[]){
av_log_set_level(AV_LOG_DEBUG);
// 1.处理输入参数
if (argc<3)
{
av_log(NULL,AV_LOG_ERROR,"arguments must be more than 3!\n");
goto __ERR;
}
int ret = -1;
char *dst; //目标编码后文件
const char *codeName;
AVCodec *codec; //编码器
AVCodecContext *codec_ctx; //编码器上下文
FILE *f;
AVFrame *frame;
AVPacket *pkt;
dst = argv[1];
codeName = argv[2];
//2.查找指定编码器
//codec = avcodec_find_encoder(AV_CODEC_ID_H264);
codec = avcodec_find_encoder_by_name(codeName);
if (!codec)
{
av_log(NULL,AV_LOG_ERROR,"Can't find codec:%s\n",codeName);
goto __ERR;
}
//3.创建编码器上下文
codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx)
{
av_log(NULL,AV_LOG_ERROR,"Can't alloc codec_ctx");
goto __ERR;
}
//4.设置编码器参数
codec_ctx->width = 640;
codec_ctx->height = 480;
codec_ctx->bit_rate = 500000;
codec_ctx->time_base = (AVRational){1,25};
codec_ctx->framerate = (AVRational){25,1};
codec_ctx->gop_size = 50;
codec_ctx->max_b_frames = 1;
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
if (codec->id == AV_CODEC_ID_H264)
{
av_opt_set(codec_ctx->priv_data,"preset","slow",0);
}
//5.编码器与编码器上下文绑定
ret = avcodec_open2(codec_ctx,codec,NULL);
if (ret < 0)
{
av_log(NULL,AV_LOG_ERROR,"Can't open codec:%s\n",av_err2str(ret));
goto __ERR;
}
//6.创建输出文件
f = fopen(dst,"wb");
if (!f)
{
av_log(NULL,AV_LOG_ERROR,"Can't open file:%s\n",dst);
goto __ERR;
}
//7.创建frame
frame = av_frame_alloc();
if (!frame)
{
av_log(NULL,AV_LOG_ERROR,"Can't alloc frame");
goto __ERR;
}
//8.分配缓冲区
//设置frame大小!!!
frame->width = codec_ctx->width;
frame->height = codec_ctx->height;
frame->format = codec_ctx->pix_fmt;
ret = av_frame_get_buffer(frame,0);
if (ret < 0)
{
av_log(NULL,AV_LOG_ERROR,"Can't get framebuffer");
goto __ERR;
}
//9.创建packet
pkt = av_packet_alloc();
if (!pkt)
{
av_log(NULL,AV_LOG_ERROR,"Can't alloc packet");
goto __ERR;
}
//10.读数据,编码
for (int i = 0; i < 25; i++)
{
ret = av_frame_make_writable(frame); //确保可用
if (ret < 0)
{
break;
}
for (int y = 0; y < codec_ctx->height; y++)
{
for (int x = 0; x < codec_ctx->width; x++)
{
//y*frame->linesize[0]+x:像素位置(跳过的行数y x 每行的数量 + x 即在这一行的偏移量)
frame->data[0][y*frame->linesize[0]+x] = (x + y + i * 10) % 256;
}
}
for (int y = 0; y < codec_ctx->height/2; y++)
{
for (int x = 0; x < codec_ctx->width/2; x++)
{
frame->data[1][y*frame->linesize[1]+x] = 128 + (y + i) % 128;
frame->data[2][y*frame->linesize[2]+x] = 64 + (x + i) % 192;
}
}
frame->pts = i; //dts自动计算
//编码 编码上下文 数据 编码后数据 写出文件
ret = encode(codec_ctx,frame,pkt,f);
if (ret == -1)
{
goto __ERR;
}
}
//11.清空缓冲区数据编码
ret = encode(codec_ctx,NULL,pkt,f);
if (ret == -1)
{
goto __ERR;
}
__ERR:
if (codec_ctx)
{
avcodec_free_context(&codec_ctx);
}
if (frame)
{
av_frame_free(&frame);
}
if (pkt)
{
av_packet_free(&pkt);
}
if (f)
{
fclose(f);
}
return 0;
}