FFmpeg H264视频编码全流程解析

.h264视频文件,是被H.264压缩后形成,因此需要使用到ffmpeg的编码函数; 如下是整理流程:

bash 复制代码
				开始(begin)
					↓
				1.打开输出文件(std::ofstream::open)
					↓
				2.查找编码器(avcodec_find_encoder)
					↓
				3.创建编码器上下文(avcodec_alloc_context3)
					↓
				4.打开编码器上下文(avcodec_open2)
					↓
				5.手动"绘制"每一帧的 Y、U、V 图像数据(for循环)
					↓
				6.发送编码(avcodec_send_frame)
					↓
				7.接收编码(avcodec_receive_packet)
					↓
				8.写入文件(std::ofstream::write)
					↓
				9.冲刷编码器
					↓
				结束(end)

包含头文件:

cpp 复制代码
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavutil/avutil.h"
}

#include <iostream>
#include <string>
#include <fstream>

#include <cerrno>
#include <cstring>

1. 打开输出文件

使用了C++的std::ofstream文件处理

cpp 复制代码
// 1.打开输出文件
std::string fileName = "800_600_25.h264";
std::ofstream ofd;
ofd.open(fileName, std::ios::binary);   // 一定要以二进制方式打开
if (!ofd.is_open()) {
    std::cout << fileName << " open failed!" << std::endl;
    std::cout << "Error code: " << errno << std::endl;
    std::cout << "Error message: " << strerror(errno) << std::endl;
    return -1;
}

注意,一定要以二进制方式打开文件,否则视频会出现乱码!(本人在此踩过坑)

2. 查找编码器 avcodec_find_encoder

avcodec_find_encoder 用于根据指定的编码器 ID 查找并返回对应的编码器。

①函数原型

cpp 复制代码
AVCodec* avcodec_find_encoder(enum AVCodecID id);

②参数说明

参数 类型 说明
id enum AVCodecID 请求的编码器 ID,如 AV_CODEC_ID_H264、AV_CODEC_ID_AAC 等

③返回值

  • 成功:返回一个指向 AVCodec 结构体的指针,表示找到的编码器
  • 失败:返回 NULL,表示没有找到匹配的编码器

④使用示例

cpp 复制代码
// 2.查找编码器
const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
    std::cout << "查找编码器失败!" << std::endl;
    return 0;
}

3. 创建编码器上下文 avcodec_alloc_context3

avcodec_alloc_context3 是 FFmpeg 库中用于分配并初始化编解码器上下文 AVCodecContext 结构体的核心 API。

①函数原型

cpp 复制代码
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);

②参数说明

参数 类型 说明
codec const AVCodec* 指向编解码器结构体的指针,通常通过 avcodec_find_encoder 或 avcodec_find_decoder 获得

③返回值

  • 成功:非NULL,返回指向已分配的 AVCodecContext 结构体的指针,各字段已设置为默认值
  • 失败:返回 NULL,分配失败

④使用示例

cpp 复制代码
AVCodecContext *context = avcodec_alloc_context3(codec);
if (!context) {
    std::cout << "创建编码器上下文 失败!" << std::endl;
    return 0;
}

成功获取后,需要给其设置基础信息,例如:

cpp 复制代码
context->width = 800;
context->height = 600;
context->time_base = { 1, 25 };
context->pix_fmt = AV_PIX_FMT_YUV420P;

4. 打开编码器上下文 avcodec_open2

avcodec_open2 是 FFmpeg 库中用于初始化编解码器上下文(AVCodecContext)并将其与指定的编解码器(AVCodec)进行关联的核心 API。 在调用此函数前,所有必要的编解码参数(如分辨率、比特率、像素格式等)需设置完成。

①函数原型

cpp 复制代码
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);

②参数说明

参数 类型 说明
avctx AVCodecContext* 需要初始化的编解码器上下文,必须已通过 avcodec_alloc_context3() 分配
codec const AVCodec* 要打开的编解码器。如果上下文中已经通过 avcodec_alloc_context3() 传入了非 NULL 的编解码器,则此参数必须为 NULL 或与之前传入的编解码器相同
options AVDictionary** 指向 AVDictionary 的指针,用于传递编解码器上下文选项和编解码器私有选项,一般传NULL即可

③返回值

  • 成功:0,编解码器已成功打开并初始化
  • 失败:< 0,返回负的错误码(AVERROR)

④使用示例

cpp 复制代码
int re = avcodec_open2(context, codec, NULL);
if (0 != re) {
    std::cout << "打开编码器失败!" << std::endl;
    return 0;
}

5. 手动"绘制"每一帧的 Y、U、V 图像数据

上面查找的编码器是YUV420P,所以是每四个Y对应一组UV,它是‌未经压缩的原始视频数据‌。

YUV420P 是一种‌平面(Planar)布局‌的色彩表示方式:

  • ‌Y‌:亮度分量(每个像素都有,W×H) ‌
  • U、V‌:色度分量(水平和垂直方向都减半采样,各占 W/2 × H/2)

内存布局(以 ffmpeg 为例):

  • data → Y 分量(完整分辨率)
  • data → U 分量(半分辨率)
  • data → V 分量(半分辨率)

①新建一个AVFrame

cpp 复制代码
AVFrame *frame = av_frame_alloc();
// 设置基础信息
frame->width = context->width;
frame->height = context->height;
frame->format = context->pix_fmt;
// 给frame分配内存
re = av_frame_get_buffer(frame, 32);
if (0 != re) {
    return -1;
}

②在for循环内部,给frame的data设置值即可 frame->data0 对应的是Y,只需要给frame->data0n设置值即可; frame->data1 对应的是U,只需要给frame->data1n设置值即可; frame->data2 对应的是V,只需要给frame->data2n设置值即可;

cpp 复制代码
// 生成两千帧
for (int i = 0; i < 2000; ++i) {

    // y
    for (int y = 0; y < frame->height; ++y) {
        for (int x = 0; x < frame->width; ++x) {
            frame->data[0][y*frame->linesize[0]+x] = i * 3 + x + y;     // 使用linesize,而不是height,因为内存对齐原因会有填充字符
        }
    }

    // uv
    for (int y = 0; y < frame->height / 2; ++y) {
        for (int x = 0; x < frame->width / 2; ++x) {
            frame->data[1][y*frame->linesize[1]+x] = i * 2 + x + y;
            frame->data[2][y*frame->linesize[2]+x] = i * 1 + x + y;
        }
    }

    frame->pts = i;     // 当前显示

	// other code ...
}

遍历Y的时候,使用的是完整的height和width,先遍历height再遍历width,是因为要一行一行的进行处理写入数据; frame->data0y\*frame-\>linesize\[0+x]:使用linesize,而不是height,因为内存对齐原因会有填充字符,在av_frame_get_buffer(frame, 32);时,指定了32位对齐方式,默认也是32位对齐; 而遍历UV时,只用了height和widht的一半,因为是YUV420P的原因。

注意需要设置pts,否则显示会乱的的。

6. 发送编码 avcodec_send_frame

avcodec_send_frame 用于向编码器提供原始的未压缩音视频帧(AVFrame)进行编码 ;与avcodec_receive_packet配对使用;

①函数原型

cpp 复制代码
int avcodec_send_frame(AVCodecContext *avctx, const AVFrame *frame);

②参数说明

参数 类型 说明
avctx AVCodecContext* 已通过 avcodec_open2() 打开的编码器上下文
frame const AVFrame* 包含原始音视频数据的帧指针,可以为 NULL,代表冲刷缓冲区

参数 frame 的详细说明:

  • 正常输入:传入一个包含原始音视频数据的 AVFrame,编码器将对其进行编码
  • 刷新编码器(Flush):传入NULL,表示没有更多的输入帧,通知编码器结束编码,并输出所有缓存的剩余数据

⚠️ 所有权说明:frame 的所有权始终属于调用者,编码器不会修改帧内容。

③返回值

  • 成功:0,帧已成功送入编码器
  • 失败:> 0,失败了

④使用示例

cpp 复制代码
int re = avcodec_send_frame(context, frame);
if (re < 0) {
    std::cout << "avcodec_send_frame failed!" << std::endl;
    break;
}

7. 接收编码 avcodec_receive_packet

avcodec_receive_packet 用于从编码器中读取编码后的压缩数据包(AVPacket ;与avcodec_send_frame配对使用。

①函数原型

cpp 复制代码
int avcodec_receive_packet(AVCodecContext *avctx, AVPacket *avpkt);

②参数说明

参数 类型 说明
avctx AVCodecContext* 已通过 avcodec_open2() 打开的编码器上下文
avpkt AVPacket* 输出参数,函数执行成功后会填充为编码器输出的压缩数据包

参数 avpkt 的重要说明:

  • 引用计数:输出的 AVPacket 由编码器分配,是引用计数的(refcounted)
  • 自动解引用:函数在执行业务逻辑前,总是会先调用av_packet_unref(avpkt) 清理 avpkt 中之前可能残留的数据
  • 内存管理:调用者负责在使用完数据包后调用av_packet_unref() 释放引用

⚠️ 所有权说明:输出的 AVPacket 由编码器内部管理,调用者获得的是引用,不应手动释放其内部数据(av_free 等),而应使用 av_packet_unref()。

③返回值

  • 成功:0,avpkt 中已填充编码后的压缩数据包
  • 失败:> 0,失败了,或已经读取完毕等

④使用示例

cpp 复制代码
// 新建一个AVPacket
AVPacket *pkt = av_packet_alloc();

while (re >= 0) {
    // 接收编码后的数据包
    int re = avcodec_receive_packet(context, pkt);
    if (re == AVERROR(EAGAIN) || re == AVERROR_EOF) {
        // 目前还没有数据或已经到结尾
        av_packet_unref(pkt);
        break;
    } else if (re < 0) {
        std::cout << "avcodec_receive_packet failed!" << std::endl;
        av_packet_unref(pkt);
        return -2;
    }

	// other code ...
    
    // 释放
    av_packet_unref(pkt);
}

8. 写入文件

将读取到的pkt写入文件即可

cpp 复制代码
ofd.write((char *)pkt->data, pkt->size);

9. 冲刷编码器

将缓冲区中未被接收的packet全部读取出来;与正常发送编码,接收编码流程一致;

为什么需要冲刷(Flush)? 编码器(特别是视频编码器)并不是接收到一帧就立即输出一个数据包的。为了提高压缩效率,编码器内部存在延迟队列(Delay / Latency)。如果不冲刷,这些停留在编码器内部缓冲区(Internal Buffer)中的帧会被直接丢弃,导致输出的视频文件末尾缺失几帧(音视频不同步或画面截断)。

cpp 复制代码
re = avcodec_send_frame(context, NULL);
if (re < 0) {
    std::cout <<  "冲刷编码器失败!" << std::endl;
    return -3;
}
if (re >= 0 ) {
    while (1) {
        re = avcodec_receive_packet(context, pkt);
        if (re == AVERROR(EAGAIN)) {
            av_packet_unref(pkt);
            continue;
        } else if (re == AVERROR_EOF) {
            av_packet_unref(pkt);
            break;
        } else if (re < 0) {
            av_packet_unref(pkt);
            break;
        }

        std::cout << "- ";
        ofd.write((char *)pkt->data, pkt->size);
        av_packet_unref(pkt);
    }
}

最后,将内存释放掉即可;

cpp 复制代码
ofd.close();
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&context);

代码合集

cpp 复制代码
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavutil/avutil.h"
}

#include <iostream>
#include <string>
#include <fstream>

#include <cerrno>
#include <cstring>

int main(int argc, char *argv[])
{

    std::cout << av_version_info() << std::endl;

    // 1.打开输出文件
    std::string fileName = "800_600_25.h264";
    std::ofstream ofd;
    ofd.open(fileName, std::ios::binary);   // 一定要以二进制方式打开
    if (!ofd.is_open()) {
        std::cout << fileName << " open failed!" << std::endl;
        std::cout << "Error code: " << errno << std::endl;
        std::cout << "Error message: " << strerror(errno) << std::endl;
        return -1;
    }

    // 2.查找编码器
    const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!codec) {
        std::cout << "查找编码器失败!" << std::endl;
        return 0;
    }

    // 3.创建编码器上下文
    AVCodecContext *context = avcodec_alloc_context3(codec);
    if (!context) {
        std::cout << "创建编码器上下文 失败!" << std::endl;
        return 0;
    }

    // 设置基础信息
    context->width = 800;
    context->height = 600;
    context->time_base = { 1, 25 };
    context->pix_fmt = AV_PIX_FMT_YUV420P;

    // 4.打开编码器上下文
    int re = avcodec_open2(context, codec, NULL);
    if (0 != re) {
        std::cout << "打开编码器失败!" << std::endl;
        return 0;
    }

    // 新建一个AVFrame
    AVFrame *frame = av_frame_alloc();
    // 设置基础信息
    frame->width = context->width;
    frame->height = context->height;
    frame->format = context->pix_fmt;
    // 给frame分配内存
    re = av_frame_get_buffer(frame, 32);
    if (0 != re) {
        return -1;
    }

    // 新建一个AVPacket
    AVPacket *pkt = av_packet_alloc();

    // 每循环一次,生成一帧画面(手动"绘制"每一帧的 Y、U、V 图像数据)
    for (int i = 0; i < 2000; ++i) {

        // y
        for (int y = 0; y < frame->height; ++y) {
            for (int x = 0; x < frame->width; ++x) {
                frame->data[0][y*frame->linesize[0]+x] = i * 3 + x + y;     // 使用linesize,而不是height,因为内存对齐原因会有填充字符
            }
        }

        // uv
        for (int y = 0; y < frame->height / 2; ++y) {
            for (int x = 0; x < frame->width / 2; ++x) {
                frame->data[1][y*frame->linesize[1]+x] = i * 2 + x + y;
                frame->data[2][y*frame->linesize[2]+x] = i * 1 + x + y;
            }
        }

        frame->pts = i;     // 当前显示


        // 发送编码
        int re = avcodec_send_frame(context, frame);
        if (re < 0) {
            std::cout << "avcodec_send_frame failed!" << std::endl;
            break;
        }


        while (re >= 0) {
            // 接收编码后的数据包
            int re = avcodec_receive_packet(context, pkt);
            if (re == AVERROR(EAGAIN) || re == AVERROR_EOF) {
                // 目前还没有数据或已经到结尾
                av_packet_unref(pkt);
                break;
            } else if (re < 0) {
                std::cout << "avcodec_receive_packet failed!" << std::endl;
                av_packet_unref(pkt);
                return -2;
            }


            std::cout << "* ";
            ofd.write((char *)pkt->data, pkt->size);

            // 释放
            av_packet_unref(pkt);
        }
    }

    std::cout << std::endl;

    // 冲刷编码器
    re = avcodec_send_frame(context, NULL);
    if (re < 0) {
        std::cout <<  "冲刷编码器失败!" << std::endl;
        return -3;
    }
    if (re >= 0 ) {
        while (1) {
            re = avcodec_receive_packet(context, pkt);
            if (re == AVERROR(EAGAIN)) {
                av_packet_unref(pkt);
                continue;
            } else if (re == AVERROR_EOF) {
                av_packet_unref(pkt);
                break;
            } else if (re < 0) {
                av_packet_unref(pkt);
                break;
            }

            std::cout << "- ";
            ofd.write((char *)pkt->data, pkt->size);
            av_packet_unref(pkt);
        }
    }


    std::cout << std::endl;
    std::cout << "编码完成!" << std::endl;

    ofd.close();

    av_frame_free(&frame);
    av_packet_free(&pkt);

    avcodec_free_context(&context);

    return 0;
}

视频效果截图

相关推荐
4 小时前
使用ffmpeg将mp4视频转为m3u8格式
android·ffmpeg·音视频
xcLeigh1 天前
KingbaseES 的卢智能运维体架构深度拆解
运维·数据库·人工智能·ai·架构·ffmpeg·智能体
程序员老陆1 天前
FFmpeg6 在 Windows 打开麦克风并录成 PCM:不走 Qt Multimedia,也不碰 WASAPI
windows·ffmpeg·音视频·pcm
程序员老陆2 天前
从零写一个 FFmpeg 播放器:架构设计远比“解码显示”复杂
ffmpeg·音视频·播放器
程序员老陆2 天前
FFmpeg 硬解在 Linux 上:从“能跑”到“跑满 GPU”的实战笔记
linux·笔记·ffmpeg
≮傷£≯√3 天前
移植ffmpeg 到 GEC6818
ffmpeg
海带紫菜菠萝汤4 天前
FFmpeg.wasm 实践:在浏览器中运行 FFmpeg 的能力边界与性能瓶颈
前端·javascript·ffmpeg·音视频·wasm
程序员老陆5 天前
FFmpeg6的滤镜函数解析
ffmpeg·音视频·avfilter
看浪的路人5 天前
第10讲:Python 调用 FFmpeg 批量处理——打造你的视频处理工具箱
python·ffmpeg·音视频