.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;
}
视频效果截图
