FFmpeg 视频编码器实现讲解

5 阅读5分钟

一、概述

本文记录如何使用 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;
}