代码

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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
/*
使用FFmpeg库打开一个媒体文件(如MP4视频),截取其中10秒到20秒的音频和视频,然后将截取的部分保存到一个新的文件中。
*/

#include <iostream>
#include <thread>

using namespace std;

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

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

// 用于打印错误信息
void PrintErr(int err)
{
char buf[1024] = { 0 };
av_strerror(err, buf, sizeof(buf) - 1);
cerr << buf << endl;
}
#define CERR(err) if(err != 0) {PrintErr(err);getchar();return -1;}


int main(int argc, char *argv[])
{
// 打开媒体文件
const char* url = "v1080.mp4";

// 解封装输入上下文
AVFormatContext* ic = nullptr;
// =======(一)=======
auto re = avformat_open_input(&ic, url,
NULL, // 封装器格式 null 自动探测 根据后缀名或者文件头探测
NULL // 参数设置, rtsp需要设置
);
CERR(re);

// =======(二)=======
// 获取媒体信息,无头部格式
avformat_find_stream_info(ic, NULL);
CERR(re);


// =======(三)=======
// 打印封装信息
av_dump_format(ic, 0, url,
0 // 0表示上下文输入 1表示上下文输出
);


// =======(四)=======
AVStream* as = nullptr; // 音频流
AVStream* vs = nullptr; // 视频流
for (int i = 0; i < ic->nb_streams; i++)
{
if (ic->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
{
as = ic->streams[i];
cout << "=====音频=====" << endl;
cout << "sample_rate:" << as->codecpar->sample_rate << endl;
}
else if (ic->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
vs = ic->streams[i];
cout << "=====视频=====" << endl;
cout << "width:" << vs->codecpar->width << endl;
cout << "height:" << vs->codecpar->height << endl;
}
}





// 解封装

const char *out_url = "test_mux.mp4";
// 编码器输出上下文
AVFormatContext *ec = nullptr;

// =======(五)=======
re = avformat_alloc_output_context2(&ec, NULL, NULL,
out_url // 根据文件名推测分装格式
);
CERR(re);

// 添加视频流
// =======(六)=======
auto mvs = avformat_new_stream(ec, NULL); // 视频流
auto mas = avformat_new_stream(ec, NULL); // 音频流

// 打开输出IO
// =======(七)=======
re = avio_open(&ec->pb, out_url, AVIO_FLAG_WRITE);
CERR(re);

// 设置解码音视频流参数

// =======(八)=======
if (vs)
{
mvs->time_base = vs->time_base; // 时间基数与原视频一致
// 从解封装复制参数
avcodec_parameters_copy(mvs->codecpar, vs->codecpar);
}
if (as)
{
mas->time_base = as->time_base;
// 从解封装复制参数
avcodec_parameters_copy(mas->codecpar, as->codecpar);
}


// =======(九)=======
// 写入文件头
re = avformat_write_header(ec, NULL);
CERR(re);

// 打印输出上下文
av_dump_format(ec, 0, out_url, 1);






// 截取 10 ~ 20秒的音频视频 取多不取少
// 假定9 11秒都有关键帧 取9秒的关键帧
double begin_sec = 10.0; // 截取开始时间
double end_sec = 20.0; // 截取结束时间
long long begin_pts = 0;
long long begin_audio_pts = 0; // 音频的开始时间
long long end_pts = 0;


// =======(十)=======
// 换算成pts 换算成输入ic的pts 以视频流为准
if (vs && vs->time_base.num > 0)
{
double t = (double)vs->time_base.den / (double)vs->time_base.num; // den分母 / num分子
begin_pts = begin_sec * t; // begin对应的pts
end_pts = end_sec * t; // end对应的pts
}
// 处理音频的pts
if (as && as->time_base.num > 0)
begin_audio_pts = begin_sec * ((double)as->time_base.den / (double)as->time_base.num);

// =======(十一)=======
// seek输入媒体文件 移动到地市秒的关键帧为止
if(vs)
re = av_seek_frame(ic, vs->index, begin_pts,
AVSEEK_FLAG_FRAME || AVSEEK_FLAG_BACKWARD);
CERR(re);




AVPacket pkt;
for (;;)
{
re = av_read_frame(ic, &pkt);
if (re != 0)
{
PrintErr(re);
break;
}

AVStream *in_stream = ic->streams[pkt.stream_index];
AVStream *out_stream = nullptr;
long long offset_pts = 0; // 偏移pts,用于截断的开头pts运算

// 判断当前处理的 AVPacket 是否属于视频流的
if (vs && pkt.stream_index == vs->index)
{
cout << "视频: ";

// 超过第20秒退出 只存10 ~ 20秒
if (pkt.pts > end_pts)
{
av_packet_unref(&pkt);
break;
}

out_stream = ec->streams[0];
offset_pts = begin_pts;
}
// 判断当前处理的 AVPacket 是否属于音频流的
else if (as && pkt.stream_index == as->index)
{
cout << "音频: ";
out_stream = ec->streams[1];
offset_pts = begin_audio_pts;
}
cout << pkt.pts << " : " << pkt.dts << " : " << pkt.size << endl;


// =======(十二)=======
// 重新计算pts dts duration
// a * bq(输入basetime) / cq(输出basetime)
pkt.pts = av_rescale_q_rnd(pkt.pts - offset_pts, in_stream->time_base,
out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)
);
pkt.dts = av_rescale_q_rnd(pkt.dts - offset_pts, in_stream->time_base,
out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)
);
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base,
out_stream->time_base);
pkt.pos = -1;



// =======(十三)=======
// 写入音视频帧 会清理pkt
re = av_interleaved_write_frame(ec, &pkt);
if (re != 0)
{
PrintErr(re);
}
}


// =======(十四)=======
// 写入结尾
re = av_write_trailer(ec);
if (re != 0) PrintErr(re);


avformat_close_input(&ic);

avio_closep(&ec->pb);
avformat_free_context(ec);
ec = nullptr;

return 0;
}

(一)

AVFormatContext

使用FFmpeg库中的avformat_open_input函数来打开一个媒体文件(如MP4视频)的操作。

这里的ic代表input context,即输入上下文

同时也是XDemux中的AVFormatContext,使用set_c()函数,也就是修改它。

1
2
3
4
5
6
7
8
9
10
11
12
13
const char* url = "v1080.mp4";
AVFormatContext* ic = nullptr;

auto re = avformat_open_input(&ic, url,
NULL, // 封装器格式 null 自动探测 根据后缀名或者文件头探测
NULL // 参数设置, rtsp需要设置
);

// 函数原型
int avformat_open_input(AVFormatContext **ps, const char *filename,
AVInputFormat *fmt,
AVDictionary **options
);

参数解释

  • AVFormatContext *ps:这是一个指向指针的指针,用于存储打开的媒体文件的上下文信息。在这里,ic是一个AVFormatContext的指针,函数会通过这个指针返回打开的文件的详细信息,如流信息等。
  • const char *filename:这是一个字符串,表示要打开的媒体文件的路径。在代码中,这个路径是通过url变量传递的,也就是文件名"v1080.mp4"
  • AVInputFormat *fmt:指定输入文件的格式。这通常用于强制指定文件格式,比如你明确知道要处理的是一个特定格式的流或者文件。在大多数情况下,可以传递NULL,FFmpeg会自动根据文件头或者扩展名来检测格式。
  • AVDictionary **options:这是一个用于传递额外参数的字典指针。一般用于像RTSP、HTTP等流媒体协议设置。对于本地文件操作,这里传递NULL即可。

代码中的解释

1
auto re = avformat_open_input(&ic, url, NULL, NULL);
  • &ic:传入一个AVFormatContext指针的地址,用于在函数内打开文件并填充这个上下文结构。
  • url:传入文件路径,即"v1080.mp4"
  • NULL(第三个参数):告诉FFmpeg自动探测文件的封装格式,不需要手动指定。
  • NULL(第四个参数):不需要额外的选项设置。

结果

  • avformat_open_input函数返回一个整数,表示操作结果。返回值为0表示成功打开文件,非零表示出错。这个返回值被存储在re变量中,可以通过CERR(re)来检查是否成功打开了文件。

总结

这行代码的目的是打开一个名为v1080.mp4的媒体文件,并使用FFmpeg自动检测文件的封装格式,同时不需要额外的参数设置。如果成功打开文件,ic指针将指向包含媒体文件信息的AVFormatContext结构,后续代码可以通过ic这个结构来操作文件的流信息

(二)

调用了FFmpeg库中的avformat_find_stream_info函数,分析打开的媒体文件,提取并填充其流信息。这一步是非常重要的,因为后续的音视频处理(如解码、截取等)都依赖于这些流的详细信息。同时ic中的streams数组将会被填充

1
2
3
4
avformat_find_stream_info(ic, NULL);

// 函数原型
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

参数解释

  • AVFormatContext \*ic:这是一个指向AVFormatContext结构的指针,该结构包含了媒体文件的上下文信息。ic是在之前通过avformat_open_input函数打开文件后获取的。
  • AVDictionary \**options:这是一个指向字典的指针,用于传递额外的选项,通常用于解码器的设置。如果不需要设置特殊选项,可以传递NULL

功能解释

avformat_find_stream_info函数的主要功能是从已打开的媒体文件中读取和分析流信息,填充AVFormatContext结构中的流信息数组(streams)。

具体来说,FFmpeg会分析文件头部的数据,尝试找到文件中的所有流(如视频流、音频流、字幕流等),并收集这些流的编码信息(如编码器类型、分辨率、采样率、比特率等)。这个过程可能涉及解码几帧数据,以便正确推断和填充这些信息。

这行代码调用了FFmpeg库中的avformat_find_stream_info函数,用于从打开的媒体文件中提取流(音频、视频、字幕等)的相关信息。让我们详细解析一下:

函数原型

1
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

参数解释

  • AVFormatContext \*ic:这是一个指向AVFormatContext结构的指针,该结构包含了媒体文件的上下文信息。ic是在之前通过avformat_open_input函数打开文件后获取的。
  • AVDictionary \**options:这是一个指向字典的指针,用于传递额外的选项,通常用于解码器的设置。如果不需要设置特殊选项,可以传递NULL

功能解释

avformat_find_stream_info函数的主要功能是从已打开的媒体文件中读取和分析流信息,填充AVFormatContext结构中的流信息数组(streams)。

具体来说,FFmpeg会分析文件头部的数据,尝试找到文件中的所有流(如视频流、音频流、字幕流等),并收集这些流的编码信息(如编码器类型、分辨率、采样率、比特率等)。这个过程可能涉及解码几帧数据,以便正确推断和填充这些信息。

代码中的解释

1
avformat_find_stream_info(ic, NULL);
  • ic:传入已经由avformat_open_input函数打开的媒体文件上下文。FFmpeg将在这个上下文中查找并填充流的信息。
  • NULL:这里不传递任何特殊选项,表示采用默认的设置。

返回值

  • 该函数返回一个整数。如果返回值为0,则表示成功找到了流信息并进行了正确填充;如果返回值为负数,则表示发生了错误,通常是因为文件格式不支持或文件损坏等原因。

结果

在成功调用avformat_find_stream_info之后,ic中的streams数组将会被填充,包含文件中每个流的详细信息。你可以通过遍历这个数组来获取每个流的具体参数(如视频的宽高、音频的采样率等)。

(三)

1
2
3
av_dump_format(ic, 0, url, 
0 // 0表示上下文输入 1表示上下文输出
);

调用了FFmpeg库中的av_dump_format函数,用于打印媒体文件的封装格式信息和流信息。具体来说,这个函数会将媒体文件的元数据和流信息输出到标准输出(通常是控制台),这对于调试和了解媒体文件结构非常有用。

函数原型

1
void av_dump_format(AVFormatContext *ic, int index, const char *url, int is_output);

参数解释

  • AVFormatContext *ic:这是一个指向AVFormatContext结构的指针,表示已打开的媒体文件的上下文信息。在前面的步骤中,通过avformat_open_inputavformat_find_stream_info已经获取并填充了这个结构。
  • int index:流索引号,这里通常设置为0,表示从第一个流开始打印。实际上,这个参数主要用于多路复用(muxing)或解复用(demuxing)情况。在这种情况下,你可以指定某个特定的流进行详细打印,但一般用0表示从第一个流开始。
  • const char *url:这是一个字符串,表示媒体文件的路径或URL。在这段代码中,它是之前定义的url变量,即文件的路径名"v1080.mp4"
  • int is_output:一个标志位,用于指示上下文是输入(0)还是输出(1)。如果是输入文件,则设置为0;如果是输出文件,则设置为1。在这个示例中,由于我们是在处理输入媒体文件,所以这个参数设置为0。

代码中的解释

1
av_dump_format(ic, 0, url, 0);
  • ic:传入之前打开的AVFormatContext,该上下文包含了媒体文件的所有信息。
  • 0(index):表示从第一个流开始打印信息。
  • url:媒体文件的路径,即"v1080.mp4"
  • 0(is_output):表示这是一个输入文件,因此打印的是输入媒体文件的信息。

功能解释

av_dump_format函数将会打印以下信息:

  • 文件的封装格式(如MP4、MKV等)。
  • 各个流的信息,如视频流的分辨率、帧率,音频流的采样率、声道数等。
  • 每个流使用的编码器类型(如H.264视频编码器,AAC音频编码器)。

这些信息将直接输出到控制台,非常有助于了解文件的结构和内容,尤其在调试和开发过程中,帮助确认文件是否正确打开、流信息是否正确读取。

(四)

AVStream、AVFormatContext结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
AVStream* as = nullptr;  // 音频流
AVStream* vs = nullptr; // 视频流
for (int i = 0; i < ic->nb_streams; i++)
{
if (ic->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
{
as = ic->streams[i];
cout << "=====音频=====" << endl;
cout << "sample_rate:" << as->codecpar->sample_rate << endl;
}
else if (ic->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
vs = ic->streams[i];
cout << "=====视频=====" << endl;
cout << "width:" << vs->codecpar->width << endl;
cout << "height:" << vs->codecpar->height << endl;
}
}

AVStream 结构

AVStream 是 FFmpeg 中用于表示媒体文件中**每个流(如视频流、音频流、字幕流等)**的数据结构。每个媒体文件通常包含一个或多个流,例如一个视频文件可能包含一个视频流和一个音频流。

用途

在代码中,这两个指针AVStream *as, *vs通常用于以下几种用途:

  1. 区分和处理不同类型的流:通过判断AVStream结构中的codecpar->codec_type,可以确定流的类型(视频、音频、字幕等)。然后,代码可以分别对音频流和视频流进行不同的处理。例如,音频流的采样率、声道数和音频编码器设置,视频流的分辨率、帧率和视频编码器设置等。
  2. 保存和使用流的参数:在找到音频和视频流后,代码可以使用这些指针来访问和操作流的参数(如分辨率、采样率、时间基等)。这些参数在后续处理(如解码、编码、截取片段等)中至关重要。
  3. 流遍历:代码通常会遍历媒体文件中的所有流,识别出音频流和视频流,并将它们分别存储在asvs中,供后续处理使用。
1
ic->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO

每一个 streams 数组中的元素(即每个 AVStream 结构体)都会有一个 codecpar 成员,而 codecpar 是一个指向 AVCodecParameters结构体的指针。

(五)

avformat_alloc_output_context2 函数,用于分配并初始化一个 AVFormatContext,该上下文用于输出操作(即写入一个新的媒体文件)。这个函数为即将创建的输出文件提供了一个封装格式的上下文环境,类似于打开一个现有的媒体文件时创建的输入上下文。

这里的ec代表output context,即输出上下文。

同时也是XMux中的AVFormatContext,使用set_c()函数,也就是修改它。

  • 在很多编程范式中,ec 被广泛使用来表示输出上下文。虽然 oc 似乎更符合逻辑,因为它直接与 “Output Context” 对应,但 ec 是从某种编程习惯或历史项目中沿袭下来的。
1
2
3
4
5
6
7
8
9
10
11
12
13
AVFormatContext *ec = nullptr;

re = avformat_alloc_output_context2(&ec, NULL, NULL,
out_url // 根据文件名推测分装格式
);

// 函数原型
int avformat_alloc_output_context2(
AVFormatContext **ctx,
AVOutputFormat *oformat,
const char *format_name,
const char *filename
);

参数解释

  • AVFormatContext \**ctx:这是一个指向 AVFormatContext 指针的指针,用于存储函数创建的输出上下文。函数会通过这个指针返回一个新的 AVFormatContext,表示输出文件的上下文。
  • AVOutputFormat *oformat:用于指定输出文件的封装格式。如果传递 NULL,FFmpeg 会根据文件名或文件扩展名自动检测适当的封装格式。
  • const char *format_name:用于指定输出格式的名称(如 "mp4""flv")。如果不指定(即传递 NULL),FFmpeg 将尝试根据文件扩展名或 filename 参数来推断封装格式。
  • const char *filename:输出文件的路径或名称。FFmpeg 会基于这个文件名的扩展名来推测合适的封装格式。

(六)

输出文件的上下文中创建新的音频流和视频流。

1
2
auto mvs =  avformat_new_stream(ec, NULL);  // 视频流
auto mas = avformat_new_stream(ec, NULL); // 音频流

函数原型

1
AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c);

参数解释

  • AVFormatContext *s:这是一个指向 AVFormatContext 结构体的指针,表示当前输出文件的上下文。s 是你在 avformat_alloc_output_context2 中创建并初始化的上下文。在这段代码中,ec 是这个上下文,用于描述输出文件。
  • const AVCodec *c:这是一个指向 AVCodec 结构体的指针,表示新流将要使用的编解码器。如果你传递 NULL,FFmpeg 将不会在创建流时设置编解码器。这通常用于稍后再设置具体的编解码器。

(七)

avio_open 函数,用于打开一个输出文件并为其分配和初始化 AVIOContextAVIOContext 是 FFmpeg 中用于管理输入/输出操作的结构体。通过这行代码,可以将输出文件与 AVFormatContext 关联起来,从而能够将编码后的数据写入该文件

1
2
3
4
5
6
const char *out_url = "test_mux.mp4";

re = avio_open(&ec->pb, out_url, AVIO_FLAG_WRITE);

// 函数原型
int avio_open(AVIOContext **s, const char *url, int flags);

参数解释

  • AVIOContext \**s:这是一个指向 AVIOContext 指针的指针,表示用于文件 I/O 操作的上下文。avio_open 函数会为指定的文件分配并初始化一个 AVIOContext,并通过这个指针返回。
  • const char *url:这是输出文件的路径或 URL。在这段代码中,out_url 是文件路径的字符串。例如,如果你想创建一个名为 "output.mp4" 的文件,则 out_url 可以是 "output.mp4"
  • int flags:这是一个标志位,用于指定文件的打开模式。在这种情况下,使用 AVIO_FLAG_WRITE,表示以写入模式打开文件

(八)

为新创建的输出视频流(mvs)设置时间基数(time_base)并复制输入视频流(vs)的编解码参数(codecpar),以确保输出的视频流与输入的视频流在时间轴和编码参数上保持一致。

1
2
3
4
5
6
if (vs)
{
mvs->time_base = vs->time_base; // 时间基数与原视频一致
// 从解封装复制参数
avcodec_parameters_copy(mvs->codecpar, vs->codecpar);
}

提问:为什么要进行复制,直接使用不行吗?

直接使用 vs 的参数而不进行赋值或复制是不行的,原因在于 vs 是一个指向输入文件中流的指针,而 mvs输出文件中流的指针。

这两个指针分别属于不同的 AVFormatContext,它们代表的是两个不同的媒体文件的上下文和状态。

  1. 关于vs输入文件流代码片段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
AVFormatContext* ic = nullptr;
auto re = avformat_open_input(&ic, url, NULL, NULL); // 打开输入文件

// 获取媒体信息和流信息
avformat_find_stream_info(ic, NULL);

// 查找视频流
AVStream* vs = nullptr;
for (int i = 0; i < ic->nb_streams; i++) {
if (ic->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
vs = ic->streams[i];
break;
}
}

里面通过avformat_find_stream_info(ic, NULL);AVFormatContext *ic进行初始化,并通过ic初始化vs

  1. 关于mvs输出文件流代码片段:
1
2
3
4
5
AVFormatContext* ec = nullptr;
re = avformat_alloc_output_context2(&ec, NULL, NULL, out_url); // 创建输出文件的上下文

// 添加视频流到输出文件中
AVStream* mvs = avformat_new_stream(ec, NULL);

里面通过avformat_alloc_output_context2(&ec, NULL, NULL, out_url);AVFormatContext *ec进行初始化,并通过ec初始化mvs

(九)

用于将媒体文件的头部信息(header)写入到输出文件中。这个头部信息包含了文件的全局元数据和每个流(如音频流、视频流)的编解码参数。写入文件头是准备输出文件的关键步骤之一,确保文件格式正确,并为后续的音视频数据写入做好准备。

1
2
3
4
avformat_write_header(ec, NULL);

// 函数原型
int avformat_write_header(AVFormatContext *s, AVDictionary **options);

参数解释

  • AVFormatContext *s:这是一个指向 AVFormatContext 结构体的指针,表示输出文件的上下文。在这段代码中,ec 是之前使用 avformat_alloc_output_context2 创建并初始化的 AVFormatContext,用于管理输出文件的信息。
  • AVDictionary \**options:这是一个指向 AVDictionary 指针的指针,用于设置额外的选项或元数据。通常在不需要设置额外选项时,这里传递 NULL

(十)

在视频和音频处理过程中,时间戳 (PTS,Presentation Timestamp) 是一个非常重要的概念,它表示解码后的帧在播放时应该展示的时间点。

1
2
3
4
5
6
7
8
9
if (vs && vs->time_base.num > 0)
{
double t = (double)vs->time_base.den / (double)vs->time_base.num; // den分母 / num分子
begin_pts = begin_sec * t; // begin对应的pts
end_pts = end_sec * t; // end对应的pts
}
// 处理音频的pts
if (as && as->time_base.num > 0)
begin_audio_pts = begin_sec * ((double)as->time_base.den / (double)as->time_base.num);

时间基数 (time_base)

  • time_base 是一个 AVRational 结构体,表示时间戳的单位。它通常是一个分数,表示一秒被分成多少个单位。例如,如果 time_base = {1, 1000},表示时间戳的单位是毫秒(1 秒 = 1000 毫秒)。

计算转换因子

这里计算了一个转换因子 t,表示每秒钟对应的 PTS 值。time_base 是一个分数,表示每 num 个单位内经过 den 个时间单位。因此,每秒对应的 PTS 值是 den / num。这个因子 t 用于将秒转换为 PTS。

  • 假设 time_base = {1, 1000},这意味着时间基数是 1 毫秒(1 秒 = 1000 毫秒)。t 的计算结果为 1000 / 1 = 1000表示每秒钟有 1000 个 PTS 单位

计算 begin_ptsend_pts

1
2
begin_pts = begin_sec * t;  // begin 对应的 PTS
end_pts = end_sec * t; // end 对应的 PTS
  • begin_sec * t:计算给定的开始时间 begin_sec(如 10 秒)对应的 PTS 值。假设 begin_sec = 10t = 1000,则 begin_pts = 10 * 1000 = 10000。这意味着在时间轴上 10 秒对应的 PTS 值是 10000
  • end_sec * t:计算给定的结束时间 end_sec(如 20 秒)对应的 PTS 值。同样地,如果 end_sec = 20t = 1000,则 end_pts = 20 * 1000 = 20000。这意味着在时间轴上 20 秒对应的 PTS 值是 20000

为什么要这样计算?

  1. 精确定位:PTS 是流中每一帧的时间戳,通过计算 begin_ptsend_pts,你可以精确地定位到流中的哪一帧对应于你希望截取的时间段(如 10 秒到 20 秒)。
  2. 用于剪辑操作:在进行视频剪辑或提取操作时,你需要知道在给定的时间范围内应该提取哪些帧。通过计算 PTS,你可以告诉 FFmpeg 从哪里开始读取帧,以及何时停止。
  3. 与时间基数一致:因为不同的流可能有不同的时间基数(time_base),所以直接用秒数进行操作是不准确的。通过将秒数转换为 PTS,可以确保操作与流的时间基数一致,从而保证了时间精度。

(十一)

av_seek_frame 函数,用于在媒体文件中查找并定位到指定的帧位置。具体来说,这行代码尝试在输入文件的指定流(通常是视频流)中,按照给定的时间戳 begin_pts 查找最接近的关键帧位置,以便从这个位置开始读取或处理数据。

这个函数也可以用作进度条的拖动

1
2
3
4
5
re = av_seek_frame(ic, vs->index, begin_pts, 
AVSEEK_FLAG_FRAME || AVSEEK_FLAG_BACKWARD);

// 函数原型
int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags);

参数解释

  • AVFormatContext *s:这是指向 AVFormatContext 的指针,表示输入媒体文件的上下文。在这行代码中,ic 是输入文件的上下文,包含了文件的流信息、解封装器等。

  • int stream_index:表示要在其中进行查找的流的索引。在这行代码中,vs->index 是视频流的索引,表示希望在视频流中执行查找操作。

  • int64_t timestamp:这是一个时间戳,表示要查找的目标位置。在这行代码中,begin_pts 是之前计算的 PTS 值,表示希望查找到的时间点(对应于实际时间,比如10秒)。

  • int flags:这是一个标志位,用于控制查找行为。常用的标志包括:

    • AVSEEK_FLAG_BACKWARD:向后查找,即查找小于等于指定时间戳的最接近的关键帧。
    • AVSEEK_FLAG_FRAME:按照帧进行查找,通常与其他标志结合使用。
    • AVSEEK_FLAG_ANY:查找可以是关键帧,也可以是非关键帧(一般用于特定情况)。

    在这段代码中,使用了 AVSEEK_FLAG_FRAME || AVSEEK_FLAG_BACKWARD,表示希望找到小于或等于 begin_pts 的最接近的关键帧,并且以帧为单位进行查找。

功能和作用

  1. 定位到指定时间段
    • 这行代码的目的是在视频流中找到最接近 begin_pts 的关键帧位置,并将文件读取位置移动到这个位置。这对于在媒体文件中准确定位到某个时间段非常重要,尤其是在进行剪辑或播放操作时。
  2. 确保从关键帧开始
    • 使用 AVSEEK_FLAG_BACKWARD 标志确保查找操作找到的帧是一个关键帧,因为解码通常必须从关键帧开始。如果不是从关键帧开始,后续的帧可能无法正确解码。
  3. 优化数据处理
    • 查找到合适的关键帧后,可以更高效地从这个位置开始读取和处理数据,避免了不必要的帧解码操作。

(十二)

  • 使用 av_rescale_q_rnd 函数将 PTS 和 DTS 从输入流的时间基转换到输出流的时间基。

  • 减去 offset_pts,调整时间戳,使其在输出流中从正确的时间点开始。

  • AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX 是用于控制舍入方式的标志,确保时间戳的舍入行为在最小和最大值范围内正确执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pkt.pts = av_rescale_q_rnd(pkt.pts - offset_pts, in_stream->time_base,
out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)
);
pkt.dts = av_rescale_q_rnd(pkt.dts - offset_pts, in_stream->time_base,
out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)
);
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base,
out_stream->time_base);
pkt.pos = -1;

// 函数原型
int64_t av_rescale_q_rnd(int64_t a,
AVRational bq, AVRational cq,
enum AVRounding round
);

参数解释

  • int64_t a:要转换的时间戳或其他值。通常,这个值表示一个时间戳,如 PTS(显示时间戳)或 DTS(解码时间戳)。
  • AVRational bq:输入时间戳的时间基(源时间基),是一个 AVRational 结构体,表示一个分数,通常用来描述时间单位。例如 {1, 1000} 表示时间单位是毫秒。
  • AVRational cq:输出时间戳的时间基(目标时间基),也是一个 AVRational 结构体。这个参数定义了目标时间基的时间单位。
  • enum AVRounding round:用于指定舍入模式的枚举值。舍入模式定义了在缩放过程中如何处理非整数结果,常用的舍入模式包括:
    • AV_ROUND_ZERO:向零方向舍入(截断小数)。
    • AV_ROUND_INF:向最近的整数舍入。
    • AV_ROUND_DOWN:向下舍入(向负无穷方向)。
    • AV_ROUND_UP:向上舍入(向正无穷方向)。
    • AV_ROUND_NEAR_INF:向最近的整数舍入;如果刚好在两个整数中间,则选择绝对值较大的整数。
    • AV_ROUND_PASS_MINMAX:确保最小值和最大值不会溢出。

计算过程

时间戳转换的公式如下:

1
result = a * (cq.num * bq.den) / (cq.den * bq.num)

其中 cq 是目标时间基,bq 是源时间基。转换结果会应用舍入模式来决定最终的整数值。

代码示例

假设你有一个时间戳 a,它的时间基是 {1, 1000}(表示毫秒),你想要将其转换为另一种时间基 {1, 90000}(表示单位是 1/90000 秒,即常见的视频时间基),你可以使用如下代码:

1
2
3
4
5
int64_t timestamp_ms = 5000;  // 5 秒,单位为毫秒
AVRational src_time_base = {1, 1000}; // 源时间基:毫秒 bq
AVRational dst_time_base = {1, 90000}; // 目标时间基:1/90000 秒 cq

int64_t rescaled_pts = av_rescale_q_rnd(timestamp_ms, src_time_base, dst_time_base, AV_ROUND_NEAR_INF);

在这个例子中:

  • 你将 5000 毫秒转换为以 1/90000 秒为单位的时间戳。
  • 使用 AV_ROUND_NEAR_INF 确保舍入到最近的整数。

(十三)

av_interleaved_write_frame() 是 FFmpeg 库中的一个函数,用于将数据包写入输出媒体文件中。它是处理多路复用器的关键函数,通常用于将编码后的音频、视频或其他媒体数据写入输出文件。它通常用于需要将多个流(如音频和视频)写入同一个文件的场景,确保数据的同步性。

1
2
3
4
re = av_interleaved_write_frame(ec, &pkt);

// 函数原型
int av_interleaved_write_frame(AVFormatContext *s, AVPacket *pkt);

AVFormatContext *s: 这是一个指向 AVFormatContext 结构的指针,它代表了输出格式的上下文。这个结构包含了输出文件的格式信息、文件名、IO 上下文等。

AVPacket *pkt: 这是一个指向 AVPacket 结构的指针,AVPacket 代表了一个解码前或编码后的数据包。它包含了要写入的实际数据和相关的元数据,比如时间戳、数据大小等。

(十四)

av_write_trailer 是 FFmpeg 库中的一个函数,用于在结束多媒体文件的写入操作时写入文件尾部的相关信息。这个函数通常在你完成所有音视频数据的写入后调用,以确保文件格式的完整性和正确性。

1
2
3
re = av_write_trailer(ec);

int av_write_trailer(AVFormatContext *s);

参数解释

  • AVFormatContext \*s:这是一个指向 AVFormatContext 结构体的指针,表示输出文件的上下文。在这段代码中,ec 是输出文件的上下文,包含了所有与文件格式、流信息和 I/O 操作相关的数据。

功能和作用

  1. 写入文件尾部信息
    • av_write_trailer 函数会在输出文件的末尾写入必要的尾部信息,这些信息对于某些文件格式(如 MP4、MKV 等)是必需的。尾部信息通常包括索引数据、元数据和其他与流相关的信息,这些信息有助于播放器在播放文件时快速定位和访问数据。
  2. 刷新缓冲区
    • 在写入文件尾部信息的过程中,FFmpeg 会确保所有缓存的数据被写入到输出文件中。这包括还没有写入的帧数据、流信息和其他需要在文件结束时写入的内容。
  3. 关闭输出文件
    • 在写入完尾部信息后,av_write_trailer 会准备关闭输出文件。虽然它不会直接关闭文件,但它标志着写入操作的结束。通常在调用这个函数后,你会关闭文件 I/O 操作(例如通过 avio_closeavio_closep 函数)。