“encode” 是指将原始或解码后的媒体数据(如视频帧或音频样本)转换为一种特定的压缩格式或编码格式。

编码过程将未压缩的媒体数据(如原始的 RGB 图像数据或 PCM 音频数据)转换为压缩格式(如 H.264 视频或 AAC 音频),
以便于存储或传输。

这段代码展示了如何使用 FFmpeg 库进行视频编码。

整个过程包括查找编码器、分配和配置编码器上下文、生成未压缩数据帧、编码数据帧、处理编码后的数据包,并将其写入文件。
通过这种方式,可以将未压缩的视频帧转换为压缩的视频格式(如 H.264 或 H.265),以便于存储和传输。

编码生成一段10秒的h.264视频

效果展示

output

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#include <iostream>
#include <fstream>

using namespace std;

extern "C" { // 指定函数是C语言函数,函数名不包含重载标注
// 引入ffmpeg头文件
#include <libavcodec/avcodec.h>
}

// 预处理指令导入库
#pragma comment(lib, "avcodec.lib")
#pragma comment(lib, "avutil.lib")

int main(int argc, char *argv[])
{
string filename = "400_300_25";
AVCodecID codec_id = AV_CODEC_ID_H264;

// 根据控制台输入的数据选择codec_id
if (argc > 1)
{
string codec = argv[1];
if (codec == "h265" || codec == "hevc")
{
codec_id = AV_CODEC_ID_HEVC;
}
}

if (codec_id == AV_CODEC_ID_H264)
{
filename += ".h264";
}
else if (codec_id == AV_CODEC_ID_HEVC)
{
filename += ".h265";
}

ofstream ofs;
ofs.open(filename, ios::binary);

// 1.找到编码器 AV_CODEC_ID_H265
// ========(一)========
auto codec = avcodec_find_encoder(codec_id);

// 2.编码上下文
// ========(二)========
auto c = avcodec_alloc_context3(codec);

// 3.设定上下文参数
c->width = 400; // 视频宽度
c->height = 300; // 视频高度
// 帧时间戳的时间单位 pts * time_base = 播放时间的位置

// ========(三)========
c->time_base = { 1, 25 }; // 分数代表 1/25

c->pix_fmt = AV_PIX_FMT_YUV420P; // 元数据像素格式,与编码算法相关
c->thread_count = 16; // 编码线程数,可以通过调用系统接口获取CPU核心数量

// 4.打开编码上下文
// ========(四)========
int re = avcodec_open2(c, codec, NULL);

// 创建好AVFrame空间 未压缩的数据
auto frame = av_frame_alloc();
frame->width = c->width;
frame->height = c->height;
frame->format = c->pix_fmt;

// 分配内存缓冲区,以存储未压缩的视频数据
// ========(五)========
re = av_frame_get_buffer(frame, 0);

// 分配并初始化一个 AVPacket 结构体
// 用于存储编码后的数据包(packet),即压缩后的音频或视频数据。
auto pkt = av_packet_alloc();

// 10秒视频 250帧 (因为时间基数为1 / 25)
for (int i = 0; i < 250; i++)
{
// ========(五)========
// 生成AVFrame 数据 每帧数据不同
// Y
for (int y = 0; y < c->height; y++)
{
for (int x = 0; x < c->width; x++)
{
// data[0]代表Y
frame->data[0][y * frame->linesize[0] + x] = x + y + i * 3;
}
}

// U、V
for (int y = 0; y < c->height / 2; y++)
{
for (int x = 0; x < c->width / 2; x++)
{
// data[1]代表U、data[2]代表V
frame->data[1][y * frame->linesize[1] + x] = 128 + y + i * 2;
frame->data[2][y * frame->linesize[2] + x] = 64 + y + i * 5;
}
}

/*
为什么要设置pts?

设置 frame->pts 是为了确保每一帧视频在正确的时间点显示,
以维持视频的流畅性和时间同步性。
通过 PTS,播放器可以准确控制视频帧的显示时机,
保证视频按预期的帧率播放,并与其他媒体流(如音频)同步。
*/
frame->pts = i; // 显示时间

// ========(六)========
// 发送未压缩帧到线程中压缩
re = avcodec_send_frame(c, frame);
if (re != 0)
{
break;
}

while (re >= 0) // >= 0表示有数据返回
{
// 接收通过avcodec_send_frame发送的帧数据
re = avcodec_receive_packet(c, pkt);

// AVERROR(EAGAIN) 表示当前没有可用的输出数据包
// AVERROR_EOF 表示编码器已经完成了所有的帧处理
if (re == AVERROR(EAGAIN) || re == AVERROR_EOF)
break;
if (re < 0)
{
char buf[1024] = { 0 };
av_strerror(re, buf, sizeof(buf) - 1);
cerr << "avcodec_receive_packet failed!" << buf << endl;
break;
}
cout << pkt->size << " " << flush;

// 将解码出来的一帧数据存储下来
ofs.write((char*)pkt->data, pkt->size);
av_packet_unref(pkt); // 释放内存 防止内存泄漏
}
}

ofs.close();
av_packet_free(&pkt);
av_frame_free(&frame);

// 释放编码器上下文
avcodec_free_context(&c);

return 0;
}

(一)

1
AVCodec *avcodec_find_encoder(enum AVCodecID id);

avcodec_find_encoder 是 FFmpeg 库中用于查找指定编码器的函数。编码器是将未压缩的媒体数据(如视频帧或音频样本)转换为压缩格式的组件。该函数返回一个指向 AVCodec 结构的指针,代表找到的编码器。

参数说明

  • id

    • 这是 AVCodecID枚举类型,表示要查找的编码器的标识符。FFmpeg 定义了许多编码器的标识符,例如:

      • AV_CODEC_ID_H264:表示 H.264 视频编码器。
  • AV_CODEC_ID_HEVC(或 AV_CODEC_ID_H265):表示 H.265/HEVC 视频编码器。

    • AV_CODEC_ID_AAC:表示 AAC 音频编码器。

    • AV_CODEC_ID_MP3:表示 MP3 音频编码器。

    • 你需要根据你要编码的媒体格式选择合适的编码器 ID。

返回值

  • 返回值

    • 成功时返回一个指向 AVCodec 结构的指针,该结构包含了编码器的相关信息。
  • 如果函数找不到与 id 对应的编码器,则返回 NULL

使用场景

  • 当你想要将未压缩的视频帧编码为 H.264、H.265 或其他格式时,首先需要通过 avcodec_find_encoder 查找对应的编码器,然后通过编码器生成编码上下文,进行编码操作。

(二)

1
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);

avcodec_alloc_context3 是 FFmpeg 库中的一个函数,用于分配并初始化一个编码器或解码器上下文 (AVCodecContext)。这个上下文是编码或解码过程中的核心结构,包含了所有相关的配置信息,如视频或音频的格式、分辨率、码率等。

参数说明

  • codec
    • 这是一个指向 AVCodec 结构的指针,表示你想要使用的编码器或解码器。通常这个指针是通过 avcodec_find_encoderavcodec_find_decoder 函数获取的。
    • 如果你传递 NULL,则会分配一个未初始化的 AVCodecContext,稍后你可以手动初始化它。

返回值

  • 返回值
    • 成功时,返回一个指向 AVCodecContext 结构的指针,这个结构已经分配了内存,并且部分字段已经初始化。
    • 如果分配失败,则返回 NULL

使用场景

  • avcodec_alloc_context3 分配并初始化一个 AVCodecContext 结构。这个上下文结构在编码或解码过程中存储所有的配置信息和状态。

  • 如果传入了有效的 AVCodec 指针,函数将根据编码器或解码器的默认设置初始化一些字段(如时间基、像素格式、采样率等)。

(三)

c->time_base = { 1, 25 };

设置AVCodecContext中的时间单位的长度

  • time_base 是一个分数形式的结构体,{ 1, 25 } 表示的就是分数 1/25。其中 1 是分子,25 是分母。

  • 这个设置的意思是:每个时间单位代表 1/25 秒,也就是说每秒钟有 25 个时间单位。

对于每一帧视频,都会有一个显示时间戳 pts(Presentation Time Stamp),用来标识该帧在视频播放过程中的显示时间pts * time_base 就代表该帧应该在视频播放的第几秒显示。

例子】:

通过c->time_base = { 1, 25 };定义时间单位的长度

  • 如果 pts 为 0,则这帧应该在视频播放的第 0 * (1/25) = 0 秒显示。

  • 如果 pts 为 24,则这帧应该在视频播放的第 24 * (1/25) = 0.96 秒显示。

  • 如果 pts 为 25,则这帧应该在视频播放的第 25 * (1/25) = 1 秒显示。

(四)

int re = avcodec_open2(c, codec, NULL);

打开指定的编码器,使其与编码上下文关联,并准备好编码操作。

函数作用

  • avcodec_open2() 是 FFmpeg 库中的一个函数,用于初始化编码器或解码器并将其与给定的编码上下文(AVCodecContext)关联。
  • 它会配置编码器,使其准备好处理数据(即编码或解码操作)。

参数说明

  • c: 这是一个指向 AVCodecContext 的指针,也就是编码上下文。它包含了编码器的配置信息,比如视频的宽度、高度、像素格式等。c 是通过之前的 avcodec_alloc_context3() 函数分配和初始化的。
  • codec: 这是一个指向 AVCodec 结构的指针,代表你要使用的编码器。在你的代码中,codec 是通过 avcodec_find_encoder(codec_id) 查找到的,表示具体的编码器(如 H.264 或 H.265 编码器)。
  • NULL: 这是一个 AVDictionary 类型的指针,可以用来传递额外的选项给编码器。在这里传入 NULL 表示没有额外的选项。

返回值

  • re: 这是 avcodec_open2() 函数的返回值,类型为 int
  • 如果返回值 re0,表示编码器成功打开,编码上下文已准备好进行编码操作。
  • 如果返回值为负数,表示出现错误,具体的错误代码可以通过 FFmpeg 的相关函数或宏定义来解析。

流程总结

  • 通过 avcodec_open2(),编码器 codec 被正式初始化,并与编码上下文 c 关联。这一步骤之后,就可以使用该上下文 c 进行帧的编码了。

(五)

分配内存,存储未压缩的数据,并将为每一帧画面填充数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int re = av_frame_get_buffer(frame, 0);

...

for (int y = 0; y < c->height; y++)
{
for (int x = 0; x < c->width; x++)
{
// data[0]代表Y
frame->data[0][y * frame->linesize[0] + x] = x + y + i * 3;
}
}

// U、V
for (int y = 0; y < c->height / 2; y++)
{
for (int x = 0; x < c->width / 2; x++)
{
// data[1]代表U、data[2]代表V
frame->data[1][y * frame->linesize[1] + x] = 128 + y + i * 2;
frame->data[2][y * frame->linesize[2] + x] = 64 + y + i * 5;
}
}
  • frame->data[0] 并不是自动拥有数据的,AVFrame 创建时它的初始值为 NULL

  • 需要通过 av_frame_get_buffer() 函数或手动分配内存来初始化 data[0],使其指向合适的缓冲区,以存储帧的像素数据。

  • 初始化后,你可以通过 data[0] 来访问或操作具体的像素数据,这是图像处理的基础步骤之一。

如何给每一帧填充画面?】

Y平面

  • 外层循环遍历每一 (y),内层循环遍历每一 (x)。

  • frame->data[0][y * frame->linesize[0] + x] 用于访问第 y 行第 x 列的像素。

U、V平面

  • U 和 V 平面的分辨率是 Y 平面的二分之一,因此这两个平面的循环分别只遍历一半的高度和宽度。

  • frame->data[1][y * frame->linesize[1] + x] 访问 U 平面的第 y 行第 x 列的像素

  • V平面同理

(六)

re = avcodec_send_frame(c, frame);

函数作用

  • avcodec_send_frame() 是 FFmpeg 库中的一个函数,用于将一帧视频数据发送给编码器。

  • 编码器会在接收到帧后开始处理,编码为压缩数据。

  • 这个函数是非阻塞的,它将帧推送到编码器的内部队列中,并立即返回。

由于for(int i = 0; i < 250; i ++ )

循环250次,250帧,也就是10秒,每一帧都会调用avcodec_send_frame()从而进行编码

参数说明

  • c:这是一个指向 AVCodecContext 的指针,表示编码上下文。编码上下文包含了编码器的配置信息和状态,是编码操作的核心对象。
  • frame:这是一个指向 AVFrame 的指针,表示要发送的一帧未压缩的原始视频数据。这个帧可能包含亮度(Y)和色度(U、V)的数据,具体格式取决于编码上下文的设置。
  • frame 可以是实际的图像数据,也可以是 NULL。如果传入 NULL,则表示告诉编码器已经没有更多的帧需要编码,这个操作通常用于结束编码并刷新编码器的缓冲区。

返回值

  • re:这是 avcodec_send_frame() 函数的返回值,类型为 int
  • 如果返回值为 0,表示帧已经成功发送给编码器。
  • 如果返回负数,表示发送帧失败。可能的错误包括:
    • AVERROR(EAGAIN):编码器的内部队列已满,需要调用 avcodec_receive_packet() 函数来提取已经编码好的数据包,以腾出空间。
    • AVERROR_EOF:编码器已经被标记为结束(例如在发送 NULL 帧之后),不再接受新的帧。
    • AVERROR(EINVAL):编码器的状态无效,可能是由于上下文配置错误。

编码流程

  • 在实际的编码过程中,

    avcodec_send_frame() 通常与 avcodec_receive_packet()配合使用:

    • 首先通过 avcodec_send_frame() 将帧发送给编码器。
  • 然后通过 avcodec_receive_packet() 提取编码后的数据包(AVPacket),该数据包包含了压缩后的视频数据。

  • 这个过程会反复进行,直到所有帧都被发送并编码完成。

所谓的编码就是通过avcodec_send_frame将帧数据传给编码器,在通过avcodec_receive_packet提取编码后的数据,然后将其存储下来

补充

设置AVCodecContext一些参数

1
2
3
4
5
6
7
8
auto c = avcodec_alloc_context3(codec);

// 3.设定上下文参数
c->width = 400; // 视频宽度
c->height = 300; // 视频高度

// 此处还可以设置视频编码器的不同参数
// 旨在控制编码质量、比特率、延迟等特性。
  1. c->max_b_frames = 0;

max_b_frames:控制 B 帧的数量。B 帧是双向预测帧,通常在提高编码效率时使用,但会增加延迟。将 max_b_frames 设置为 0 可以禁用 B 帧,从而减少延迟,虽然这样可能会增加文件大小或降低压缩效率。

  1. int opt_re = av_opt_set(c->priv_data, "preset", "ultrafast", 0);

preset:这是编码器的一种预设配置,用于控制编码速度和质量的平衡。ultrafast 是一个选项,表示最快的编码速度,但质量和压缩率可能会较低。其他常见的预设包括 fastmediumslow 等。

具体设置可以看XCodec中的SetOpt(),里面有介绍。