截取一段视频
代码
1 | /* |
(一)
使用FFmpeg库中的avformat_open_input
函数来打开一个媒体文件(如MP4视频)的操作。
这里的ic
代表input context
,即输入上下文
同时也是XDemux中的AVFormatContext
,使用set_c()
函数,也就是修改它。
1 | const char* url = "v1080.mp4"; |
参数解释
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 | avformat_find_stream_info(ic, NULL); |
参数解释
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 | av_dump_format(ic, 0, url, |
调用了FFmpeg库中的av_dump_format
函数,用于打印媒体文件的封装格式信息和流信息。具体来说,这个函数会将媒体文件的元数据和流信息输出到标准输出(通常是控制台),这对于调试和了解媒体文件结构非常有用。
函数原型
1 | void av_dump_format(AVFormatContext *ic, int index, const char *url, int is_output); |
参数解释
AVFormatContext *ic
:这是一个指向AVFormatContext
结构的指针,表示已打开的媒体文件的上下文信息。在前面的步骤中,通过avformat_open_input
和avformat_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音频编码器)。
这些信息将直接输出到控制台,非常有助于了解文件的结构和内容,尤其在调试和开发过程中,帮助确认文件是否正确打开、流信息是否正确读取。
(四)
1 | AVStream* as = nullptr; // 音频流 |
AVStream
结构
AVStream
是 FFmpeg 中用于表示媒体文件中**每个流(如视频流、音频流、字幕流等)**的数据结构。每个媒体文件通常包含一个或多个流,例如一个视频文件可能包含一个视频流和一个音频流。
用途
在代码中,这两个指针AVStream *as, *vs
通常用于以下几种用途:
- 区分和处理不同类型的流:通过判断
AVStream
结构中的codecpar->codec_type
,可以确定流的类型(视频、音频、字幕等)。然后,代码可以分别对音频流和视频流进行不同的处理。例如,音频流的采样率、声道数和音频编码器设置,视频流的分辨率、帧率和视频编码器设置等。 - 保存和使用流的参数:在找到音频和视频流后,代码可以使用这些指针来访问和操作流的参数(如分辨率、采样率、时间基等)。这些参数在后续处理(如解码、编码、截取片段等)中至关重要。
- 流遍历:代码通常会遍历媒体文件中的所有流,识别出音频流和视频流,并将它们分别存储在
as
和vs
中,供后续处理使用。
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 | AVFormatContext *ec = nullptr; |
参数解释
AVFormatContext \**ctx
:这是一个指向AVFormatContext
指针的指针,用于存储函数创建的输出上下文。函数会通过这个指针返回一个新的AVFormatContext
,表示输出文件的上下文。AVOutputFormat *oformat
:用于指定输出文件的封装格式。如果传递NULL
,FFmpeg 会根据文件名或文件扩展名自动检测适当的封装格式。const char *format_name
:用于指定输出格式的名称(如"mp4"
或"flv"
)。如果不指定(即传递NULL
),FFmpeg 将尝试根据文件扩展名或filename
参数来推断封装格式。const char *filename
:输出文件的路径或名称。FFmpeg 会基于这个文件名的扩展名来推测合适的封装格式。
(六)
输出文件的上下文中创建新的音频流和视频流。
1 | auto mvs = 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
函数,用于打开一个输出文件并为其分配和初始化 AVIOContext
。AVIOContext
是 FFmpeg 中用于管理输入/输出操作的结构体。通过这行代码,可以将输出文件与 AVFormatContext
关联起来,从而能够将编码后的数据写入该文件。
1 | const char *out_url = "test_mux.mp4"; |
参数解释
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 | if (vs) |
【提问:为什么要进行复制,直接使用不行吗?】
直接使用 vs
的参数而不进行赋值或复制是不行的,原因在于 vs
是一个指向输入文件中流的指针,而 mvs
是输出文件中流的指针。
这两个指针分别属于不同的 AVFormatContext
,它们代表的是两个不同的媒体文件的上下文和状态。
- 关于
vs
的输入文件流代码片段:
1 | AVFormatContext* ic = nullptr; |
里面通过avformat_find_stream_info(ic, NULL);
对AVFormatContext *ic
进行初始化,并通过ic
初始化vs
。
- 关于
mvs
的输出文件流代码片段:
1 | AVFormatContext* ec = nullptr; |
里面通过avformat_alloc_output_context2(&ec, NULL, NULL, out_url);
对AVFormatContext *ec
进行初始化,并通过ec
初始化mvs
。
(九)
用于将媒体文件的头部信息(header)写入到输出文件中。这个头部信息包含了文件的全局元数据和每个流(如音频流、视频流)的编解码参数。写入文件头是准备输出文件的关键步骤之一,确保文件格式正确,并为后续的音视频数据写入做好准备。
1 | avformat_write_header(ec, NULL); |
参数解释
AVFormatContext *s
:这是一个指向AVFormatContext
结构体的指针,表示输出文件的上下文。在这段代码中,ec
是之前使用avformat_alloc_output_context2
创建并初始化的AVFormatContext
,用于管理输出文件的信息。AVDictionary \**options
:这是一个指向AVDictionary
指针的指针,用于设置额外的选项或元数据。通常在不需要设置额外选项时,这里传递NULL
。
(十)
在视频和音频处理过程中,时间戳 (PTS,Presentation Timestamp) 是一个非常重要的概念,它表示解码后的帧在播放时应该展示的时间点。
1 | if (vs && vs->time_base.num > 0) |
时间基数 (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_pts
和 end_pts
:
1 | begin_pts = begin_sec * t; // begin 对应的 PTS |
begin_sec * t
:计算给定的开始时间begin_sec
(如 10 秒)对应的 PTS 值。假设begin_sec = 10
且t = 1000
,则begin_pts = 10 * 1000 = 10000
。这意味着在时间轴上 10 秒对应的 PTS 值是 10000。end_sec * t
:计算给定的结束时间end_sec
(如 20 秒)对应的 PTS 值。同样地,如果end_sec = 20
且t = 1000
,则end_pts = 20 * 1000 = 20000
。这意味着在时间轴上 20 秒对应的 PTS 值是 20000。
为什么要这样计算?
- 精确定位:PTS 是流中每一帧的时间戳,通过计算
begin_pts
和end_pts
,你可以精确地定位到流中的哪一帧对应于你希望截取的时间段(如 10 秒到 20 秒)。 - 用于剪辑操作:在进行视频剪辑或提取操作时,你需要知道在给定的时间范围内应该提取哪些帧。通过计算 PTS,你可以告诉 FFmpeg 从哪里开始读取帧,以及何时停止。
- 与时间基数一致:因为不同的流可能有不同的时间基数(
time_base
),所以直接用秒数进行操作是不准确的。通过将秒数转换为 PTS,可以确保操作与流的时间基数一致,从而保证了时间精度。
(十一)
av_seek_frame
函数,用于在媒体文件中查找并定位到指定的帧位置。具体来说,这行代码尝试在输入文件的指定流(通常是视频流)中,按照给定的时间戳 begin_pts
查找最接近的关键帧位置,以便从这个位置开始读取或处理数据。
这个函数也可以用作进度条的拖动
1 | re = av_seek_frame(ic, vs->index, begin_pts, |
参数解释
-
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
的最接近的关键帧,并且以帧为单位进行查找。
功能和作用
- 定位到指定时间段:
- 这行代码的目的是在视频流中找到最接近
begin_pts
的关键帧位置,并将文件读取位置移动到这个位置。这对于在媒体文件中准确定位到某个时间段非常重要,尤其是在进行剪辑或播放操作时。
- 这行代码的目的是在视频流中找到最接近
- 确保从关键帧开始:
- 使用
AVSEEK_FLAG_BACKWARD
标志确保查找操作找到的帧是一个关键帧,因为解码通常必须从关键帧开始。如果不是从关键帧开始,后续的帧可能无法正确解码。
- 使用
- 优化数据处理:
- 查找到合适的关键帧后,可以更高效地从这个位置开始读取和处理数据,避免了不必要的帧解码操作。
(十二)
-
使用
av_rescale_q_rnd
函数将 PTS 和 DTS 从输入流的时间基转换到输出流的时间基。 -
减去
offset_pts
,调整时间戳,使其在输出流中从正确的时间点开始。 -
AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX
是用于控制舍入方式的标志,确保时间戳的舍入行为在最小和最大值范围内正确执行。
1 | pkt.pts = av_rescale_q_rnd(pkt.pts - offset_pts, in_stream->time_base, |
参数解释
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 | int64_t timestamp_ms = 5000; // 5 秒,单位为毫秒 |
在这个例子中:
- 你将 5000 毫秒转换为以 1/90000 秒为单位的时间戳。
- 使用
AV_ROUND_NEAR_INF
确保舍入到最近的整数。
(十三)
av_interleaved_write_frame()
是 FFmpeg 库中的一个函数,用于将数据包写入输出媒体文件中。它是处理多路复用器的关键函数,通常用于将编码后的音频、视频或其他媒体数据写入输出文件。它通常用于需要将多个流(如音频和视频)写入同一个文件的场景,确保数据的同步性。
1 | re = av_interleaved_write_frame(ec, &pkt); |
AVFormatContext *s
: 这是一个指向 AVFormatContext
结构的指针,它代表了输出格式的上下文。这个结构包含了输出文件的格式信息、文件名、IO 上下文等。
AVPacket *pkt
: 这是一个指向 AVPacket
结构的指针,AVPacket
代表了一个解码前或编码后的数据包。它包含了要写入的实际数据和相关的元数据,比如时间戳、数据大小等。
(十四)
av_write_trailer
是 FFmpeg 库中的一个函数,用于在结束多媒体文件的写入操作时写入文件尾部的相关信息。这个函数通常在你完成所有音视频数据的写入后调用,以确保文件格式的完整性和正确性。
1 | re = av_write_trailer(ec); |
参数解释
AVFormatContext \*s
:这是一个指向AVFormatContext
结构体的指针,表示输出文件的上下文。在这段代码中,ec
是输出文件的上下文,包含了所有与文件格式、流信息和 I/O 操作相关的数据。
功能和作用
- 写入文件尾部信息:
av_write_trailer
函数会在输出文件的末尾写入必要的尾部信息,这些信息对于某些文件格式(如 MP4、MKV 等)是必需的。尾部信息通常包括索引数据、元数据和其他与流相关的信息,这些信息有助于播放器在播放文件时快速定位和访问数据。
- 刷新缓冲区:
- 在写入文件尾部信息的过程中,FFmpeg 会确保所有缓存的数据被写入到输出文件中。这包括还没有写入的帧数据、流信息和其他需要在文件结束时写入的内容。
- 关闭输出文件:
- 在写入完尾部信息后,
av_write_trailer
会准备关闭输出文件。虽然它不会直接关闭文件,但它标志着写入操作的结束。通常在调用这个函数后,你会关闭文件 I/O 操作(例如通过avio_close
或avio_closep
函数)。
- 在写入完尾部信息后,