FFmpeg 基础
FFmpeg 基础概念和功能介绍
播放器框架
一般的播放器框架主要包含以下几个组件:
- 解复用器(Demuxer) :解复用器负责解析媒体文件的封装格式,将其中的音视频数据分离开来。在 FFmpeg 中,解复用器的功能由 libavformat 库提供。
- 解码器(Decoder) :解码器将从解复用器获取的压缩音视频数据解码成原始的、可以直接渲染的帧。在 FFmpeg 中,解码器的功能由 libavcodec 库提供。
- 渲染器(Renderer) :渲染器是负责将解码后的音视频帧呈现给用户的部分。对于视频,这通常涉及到一些图形库,如 SDL、OpenGL 或 DirectX;对于音频,这可能需要一个音频接口,如 ALSA 或 PulseAudio。在 FFmpeg 中,虽然没有直接提供渲染功能,但是它可以提供解码后的原始帧数据,然后可以将这些数据传递给其他库进行渲染。
下面是框架图:
下面是一个简单的示例,说明如何在 FFmpeg 中实现播放器框架:
#include <libavformat/avformat.h>
int main() {
// 注册所有的复用器和解复用器
av_register_all(); //4.0已经弃用
//用于存放打开对象的句柄
AVFormatContext *formatContext = NULL;
// 打开输入文件,并将其格式信息读入 AVFormatContext
if (avformat_open_input(&formatContext, "input.mp4", NULL, NULL) != 0) {
// 处理错误
return -1;
}
// 查找流信息
if (avformat_find_stream_info(formatContext, NULL) < 0) {
// 处理错误
return -1;
}
// 找到视频流,并获取相应的解码器
AVCodec *codec;
int videoStreamIndex = av_find_best_stream(formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
// 打开解码器
AVCodecContext *codecContext = formatContext->streams[videoStreamIndex]->codec;
if (avcodec_open2(codecContext, codec, NULL) < 0) {
// 处理错误
return -1;
}
// 从解复用器读取包,送入解码器,解码后的帧可用于渲染
AVPacket packet; //存放压缩后的音视频数据
while (av_read_frame(formatContext, &packet) >= 0) {
if (packet.stream_index == videoStreamIndex) {
AVFrame *frame = av_frame_alloc();
int gotFrame;
if (avcodec_decode_video2(codecContext, frame, &gotFrame, &packet) < 0) {
// 处理错误
return -1;
}
if (gotFrame) {
// 在这里渲染帧(需要额外的渲染库,如 SDL)
// ...
av_frame_free(&frame);
}
}
av_packet_unref(&packet);
}
// 清理并退出
avcodec_close(codecContext);
avformat_close_input(&formatContext);
return 0;
常用音视频术语
- 容器/文件(Container/File) :音视频文件通常包含一个或多个音频、视频或字幕流。这些流都被存储在一个容器文件中。容器文件负责存储和组织这些流的数据,使它们能够同时播放。例如,MP4、AVI、MKV、FLV等都是常见的容器格式。
- 媒体流(Media Stream) :媒体流是一系列编码的音频或视频数据。例如,一个视频文件可能有一个视频流、一个音频流和一个字幕流。
- 数据帧(Frame) :在视频编码中,一帧是一张静止的图像,连续播放多帧图像就形成了动态视频。在音频编码中,一帧是一段时间内的音频数据。
- GOP(Group of Pictures) :GOP是一个连续的帧序列,其中第一帧是I帧(关键帧),接下来是一些P帧(预测帧)和B帧(双向预测帧)。I帧可以独立解码,而P帧和B帧需要依赖其他帧才能解码。
- 编解码器(Codec) :编解码器是用于编码或解码音频或视频数据的软件或硬件。编码是将原始数据(如音频或视频)转换为压缩的格式,以便存储或传输。解码则是将压缩的数据转换回原始格式,以便播放。
- 封装格式(Container Format) :封装格式定义了如何存储多个音频、视频、字幕等流的数据,并如何同步这些流的播放。封装格式并不关心流的内容如何编码,只关心如何存储和组织这些流的数据。
复用器
复用器(Muxer)在音视频处理中是一个重要的概念。复用器的主要作用是将编码后的音频流和视频流打包(或封装)到一个容器格式中。一个媒体文件通常包括多个音频、视频和字幕流,复用器负责将这些流正确地组织到同一个文件中,同时保证这些流的同步播放。
在 FFmpeg 中,复用的过程大致可以分为以下几步:
-
初始化一个封装格式上下文(AVFormatContext):这个上下文结构包含了音视频文件的所有信息。
AVFormatContext *formatContext = avformat_alloc_context(); -
设置输出的封装格式:FFmpeg 支持多种封装格式,如 MP4、FLV、MKV、AVI 等。
formatContext->oformat = av_guess_format(NULL, "output.mp4", NULL); -
打开输出文件并写入文件头:这个过程中,FFmpeg 会写入一些必要的文件元数据。
avio_open(&formatContext->pb, "output.mp4", AVIO_FLAG_WRITE); avformat_write_header(formatContext, NULL); -
循环写入编码后的音视频数据:对每一个编码后的音视频帧(AVPacket),调用 av_write_frame() 函数将它写入到输出文件中。
AVPacket packet; while (encode()) { av_write_frame(formatContext, &packet); } -
写入文件尾并关闭文件:所有的音视频数据写入完成后,调用 av_write_trailer() 函数写入文件尾,然后关闭文件。
av_write_trailer(formatContext); avio_close(formatContext->pb);
以上就是在 FFmpeg 中进行复用的基本流程。具体的复用过程可能会因为不同的封装格式和编解码器而有所不同。
编解码器
FFmpeg 的编解码器是用于转换音频和视频数据的重要组件。编码器将原始音频或视频数据(如 PCM 音频或 YUV 视频)转换为压缩格式(如 MP3 或 H.264),以减少数据的大小并方便存储或传输。相反,解码器则将压缩格式的数据转换回原始格式,以便播放。
以下是在 FFmpeg 中使用编解码器的基本步骤:
-
查找编解码器:首先,使用
avcodec_find_encoder()或avcodec_find_decoder()函数来查找要使用的编解码器。AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264); -
创建编解码器上下文:找到编解码器后,创建一个编解码器上下文(AVCodecContext)。这个上下文保存了编解码过程中的所有信息。
AVCodecContext *codecContext = avcodec_alloc_context3(codec); -
打开编解码器:使用
avcodec_open2()函数打开编解码器。avcodec_open2(codecContext, codec, NULL); -
编解码数据:将原始数据填充到一个 AVFrame 结构,然后调用
avcodec_send_frame()函数发送到编解码器。然后调用avcodec_receive_packet()函数来获取编码后的数据。对于解码,过程类似,只是需要先发送压缩数据(AVPacket),然后接收解码后的原始数据(AVFrame)。AVFrame *frame = ...; avcodec_send_frame(codecContext, frame); AVPacket packet; av_init_packet(&packet); avcodec_receive_packet(codecContext, &packet); -
关闭编解码器:最后,使用
avcodec_close()函数关闭编解码器,并释放编解码器上下文。avcodec_close(codecContext); avcodec_free_context(&codecContext);
以上就是在 FFmpeg 中使用编解码器的基本步骤。你需要根据实际的音视频格式和编解码需求来选择合适的编解码器。
FFmpeg 库简介
- libavcodec:这是 FFmpeg 的核心库之一,提供了编码和解码的功能。这个库支持多种音频和视频编解码器,包括一些流行的编码标准如 H.264、HEVC、VP9、AAC 等。
- libavformat:这个库负责处理多种音频/视频封装格式。它可以根据输入或输出的文件名或 URL 自动检测封装格式,也支持手动指定。libavformat 能读取包含多个音频、视频和字幕流的媒体文件,并能提取每个流的元数据信息。
- libavfilter:这个库提供了处理音视频数据的滤镜框架。它包含了多种内置的滤镜,如裁剪、缩放、旋转、颜色转换、水印添加等。用户也可以通过简单的表达式语言将多个滤镜组合起来,实现复杂的音视频处理效果。
- libavdevice:这个库提供了获取系统音视频设备输入输出的功能。例如,你可以使用这个库来捕获屏幕,录制麦克风的声音,或者将音视频数据输出到指定的播放设备。
- libavutil:这是一个实用工具库,提供了一些 FFmpeg 其他库通用的工具函数,如数学运算、字符串处理、日志打印、时间处理等。这个库也定义了一些 FFmpeg 中常用的数据结构,如 AVFrame、AVPacket 等。
- libswresample 和 libswscale:这两个库提供了音视频数据的转换功能,如音频采样率转换、音频格式转换、视频像素格式转换、视频尺寸缩放等。
FFmpeg 函数简介及封装格式
在FFmpeg中,有一些关键的函数和概念,其中包括封装格式。这里为你介绍一些主要的函数和关于封装格式的概念。下面是一些关键函数解释。
av_register_all():注册所有编解码器、封装格式以及协议等,现在的FFmpeg版本(4.0 及以上)不再需要显示调用此函数。avformat_alloc_context():负责申请一个AVFormatContext结构的内存,并进行简单初始化。avformat_free_context():负责释放一个AVFormatContext结构的内存。avformat_open_input():打开媒体文件或者URL,获取AVFormatContext。avformat_close_input():关闭复用器,关闭后不用再使用avformat_free_context()进行释放。avformat_find_stream_info():获取媒体文件中的流信息。avcodec_find_decoder():查找适合的解码器。avcodec_open2():打开解码器。av_read_frame():读取音视频帧。avcodec_send_packet()和avcodec_receive_frame():发送数据包到解码器并接收解码后的帧。av_packet_unref()和av_frame_unref():释放AVPacket和AVFrame的引用,避免内存泄漏。av_seek_frame():定位文件。
封装格式(Container format)或者说文件格式(File format),是用于包装音频、视频、字幕流的格式。这些流被封装在一个容器文件里,容器会定义怎么存储这些流,以及怎样标记音轨和字幕等元数据。FFmpeg支持大量的封装格式,如 MP4、MKV、FLV、AVI、MOV等。
注意,封装格式和编解码格式是两个不同的概念。封装格式决定了文件的结构,而编解码格式决定了音视频数据如何被编码和解码。在FFmpeg中,封装格式由AVFormatContext表示,编解码格式由AVCodecContext表示。
FFmpeg 解码函数简介-解码器相关
在FFmpeg中,解码音视频流的主要过程通常包括以下步骤:
- 查找解码器
- 打开解码器
- 取数据帧并进行解码
- 关闭解码器
下面是这些步骤中使用的主要函数:
1. 查找解码器
AVCodec* avcodec_find_decoder(enum AVCodecID id); 这个函数的参数是一个编解码器ID,这个ID定义在AVCodecID枚举类型中。函数返回一个对应的解码器。如果找不到匹配的解码器,函数将返回NULL。
2. 打开解码器
int avcodec_open2(AVCodecContext* avctx, const AVCodec* codec, AVDictionary** options); 这个函数打开一个解码器。avctx是一个解码器上下文,codec是需要打开的解码器,options是一个可选的指向AVDictionary的指针,用于传递解码器选项。如果打开解码器成功,函数将返回零。否则,函数将返回一个负的错误码。
3. 读取数据帧并进行解码
这通常需要两个函数来完成,首先使用av_read_frame函数读取数据包,然后使用avcodec_send_packet和avcodec_receive_frame进行解码。
int av_read_frame(AVFormatContext* s, AVPacket* pkt);
int avcodec_send_packet(AVCodecContext* avctx, const AVPacket* avpkt);
int avcodec_receive_frame(AVCodecContext* avctx, AVFrame* frame);
av_read_frame函数从输入文件中读取下一个帧到AVPacket结构体。avcodec_send_packet函数将数据包发送到解码器,avcodec_receive_frame函数从解码器接收解码后的帧。
4. 关闭解码器
int avcodec_close(AVCodecContext* avctx); 这个函数关闭解码器,并释放所有使用的资源。在你完成解码操作并不再需要解码器的时候,应该调用这个函数。
FFmpeg 组件注册方式
FFmpeg的一个重要特性是其高度模块化和可扩展性。在FFmpeg中,几乎所有的功能,如编解码器、滤镜、复用器/解复用器、协议等,都被设计为单独的组件。这些组件在使用前需要被注册到FFmpeg的全局组件列表中。
在FFmpeg 3.x版本之前,你需要手动调用av_register_all()函数来注册所有可用的组件。但在FFmpeg 4.0及以后的版本中,这个函数被废弃,所有的组件在第一次使用时都会被自动注册。
尽管不需要手动注册组件,但了解注册机制依然很重要。下面是一些可能会接触到的函数:
avcodec_register(): 注册一个编解码器。在实际使用中,你通常不需要直接调用这个函数,因为所有的编解码器都已经通过调用avcodec_register_all()函数被自动注册了。avfilter_register(): 注册一个滤镜。同样,所有内置的滤镜都已经被自动注册,你不需要手动调用这个函数。
这些函数在注册组件时,会将组件的信息存储在一个全局的链表中。当需要查找一个特定的组件时,例如通过调用avcodec_find_decoder()或avfilter_get_by_name(),FFmpeg就会遍历这个链表来找到对应的组件。
如果需要开发一个新的组件,例如一个新的编解码器或滤镜,就需要将这个组件注册到FFmpeg中。这通常需要实现一个注册函数,然后在的初始化代码中调用这个函数。
FFmpeg 数据结构简介及数据结构之间的关系
FFmpeg的主要数据结构包括以下几种:
AVFormatContext:这是一个用于处理媒体数据封装格式的结构体。它包含了与媒体文件或媒体流有关的信息,例如媒体流的数量、媒体流的类型(视频、音频、字幕等)、元数据等。每个媒体文件或媒体流都对应一个AVFormatContext实例。AVStream:这个结构体表示一个媒体流,例如一个视频流或音频流。一个AVFormatContext实例包含一个或多个AVStream实例,每个实例对应文件中的一个媒体流。AVCodecContext:这个结构体包含了编解码器的相关信息,例如编解码器的类型、编解码参数等。每个AVStream实例都有一个与之关联的AVCodecContext实例。AVFrame:这个结构体表示一个解码后的帧,可以是一个视频帧或音频帧。编解码器从AVPacket解码得到AVFrame。AVPacket:这个结构体表示一个封装后的数据包,它包含一帧或多帧的压缩数据。AVPacket是从AVFormatContext读取,然后发送给编解码器进行解码。
它们之间的关系可以简单描述为:AVFormatContext包含AVStream,AVStream包含AVCodecContext,而AVCodecContext用于解码AVPacket得到AVFrame。
这些结构体中包含大量的字段,用于描述媒体数据的各种属性。熟悉这些数据结构以及它们的字段是理解和使用FFmpeg的关键。
FFmpeg 数据结构分析
理解FFmpeg数据结构对于深入了解FFmpeg的工作原理至关重要。这里我们更深入地了解一下FFmpeg的一些关键数据结构:
-
AVFormatContext:这个数据结构是FFmpeg工作的核心,可以视为一个媒体文件的抽象表示。其主要成员包括:
AVIOContext *pb:用于文件读写的IO上下文。unsigned int nb_streams:媒体流的数量。AVStream **streams:媒体流数组,每个元素都是一个指向AVStream的指针。
-
AVStream:每个媒体文件都包含一个或多个媒体流,如视频流、音频流等。AVStream代表了其中的一个媒体流。其主要成员包括:
AVCodecParameters *codecpar:包含了流的编码参数,如编解码器类型、视频的宽高、音频的采样率等。AVRational time_base:这个流中的时间基。
-
AVCodecContext:每个媒体流都有一个与之对应的编解码器上下文,包含了编解码器的所有信息。其主要成员包括:
enum AVCodecID codec_id:编解码器的ID,用于标识使用的是哪个编解码器。AVCodec *codec:指向实际编解码器的指针。int bit_rate:比特率,影响编码质量和文件大小。
-
AVPacket:这是一个压缩的数据包,包含了一帧(音频可能包含多帧)的数据。其主要成员包括:
int64_t pts:显示时间戳,表示此帧应该何时被显示。int64_t dts:解码时间戳,表示此帧何时可以被解码。uint8_t *data:指向数据包中实际数据的指针。int size:数据包中数据的大小。
-
AVFrame:这是一个未压缩的数据帧,包含了一帧的图像或声音数据。其主要成员包括:
uint8_t *data[AV_NUM_DATA_POINTERS]:指向数据帧中实际数据的指针数组。对于视频,data[0]、data[1]和data[2]分别指向Y、U和V分量(在YUV格式中)。int linesize[AV_NUM_DATA_POINTERS]:每个数据平面的大小。
-
AVCodecParameters:存储媒体流的编解码参数 通过检查
codec_type字段,可以区分音频流(AVMEDIA_TYPE_AUDIO)和视频流(AVMEDIA_TYPE_VIDEO)。
AVFormatContext* pFormatCtx = ... // 媒体文件的格式上下文
for (int i = 0; i < pFormatCtx->nb_streams; i++) {
AVStream* stream = pFormatCtx->streams[i];
if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
// 这是一个视频流
} else if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
// 这是一个音频流
}
}
这段代码通过循环遍历媒体文件中的每个媒体流,并通过codec_type字段来判断每个流是音频流还是视频流。
FFmpeg 内存模型和常用 API
FFmpeg 内存模型引用计数
FFmpeg在设计上使用了引用计数的内存模型,该模型主要在数据结构如AVFrame和AVPacket中有所体现。这个设计的优点在于它可以有效地管理内存,避免了频繁的内存分配和释放,从而提高了性能。
引用计数内存模型的基本原理是,当一个对象被创建时,它的引用计数设置为1。每次该对象被另一个对象引用时,其引用计数增加1。每次该对象的引用被释放时,其引用计数减少1。当引用计数达到0时,该对象的内存将被释放。
以下是如何在FFmpeg中使用这种内存模型的一个例子:
// 创建一个新的AVFrame
AVFrame* frame = av_frame_alloc();
// 假设有一段代码引用了这个frame
AVFrame* ref = frame;
av_frame_ref(ref, frame);
// 在引用结束后,调用av_frame_unref()来减少引用计数
av_frame_unref(ref);
// 当不再使用frame时,需要调用av_frame_free()来释放其内存
av_frame_free(&frame);
在以上的代码中,当调用av_frame_alloc()函数时,会创建一个新的AVFrame,并将其引用计数设置为1。然后,当调用av_frame_ref()函数来增加一个引用时,AVFrame的引用计数将增加1。之后,当引用结束并调用av_frame_unref()函数时,引用计数将减少1。最后,当调用av_frame_free()函数时,如果引用计数为0,那么AVFrame的内存将被释放。
在处理AVFrame和AVPacket时,要确保在所有的引用都结束后正确地减少引用计数,否则可能会导致内存泄漏。
AVPacket 常用 API
AVPacket是FFmpeg中一个重要的数据结构,它主要用于存储封装格式中的数据包。在FFmpeg中,音频、视频等数据在经过编码后会被打包成数据包(packet)进行传输和存储,这些数据包就是通过AVPacket来表示的。
下面是AVPacket的基本成员:
data:包数据的开始位置,包含了原始的、尚未解码的媒体数据。size:包数据的大小,以字节为单位。stream_index:此数据包在流中的索引位置,它与AVFormatContext中的streams数组相关联。flags:数据包标志,如关键帧等。dts:解码时间戳,表示此包应该何时被解码。pts:显示时间戳,表示此包应该何时被显示。
以下是一些与AVPacket相关的常用API:
av_packet_alloc():用于分配一个AVPacket并初始化为默认值。av_packet_free():释放给定的AVPacket以及其内部的数据,free内部包括了ref。av_packet_ref():将一个引用添加到一个AVPacket。av_packet_unref():取消对AVPacket的引用并将其重置为默认未初始化状态。av_packet_move_ref():转移引用计数。av_packet_clone(): 创建给定AVPacket的副本。av_packet_rescale_ts(): 更改AVPacket的时间基准。
例子:
// 假设我们已经打开了一个AVFormatContext
AVFormatContext *formatContext = ...;
// 创建一个新的AVPacket
AVPacket *packet = av_packet_alloc();
// 从流中读取一个数据包
if (av_read_frame(formatContext, packet) >= 0) {
// 读取成功,我们可以对数据包进行处理
// 创建一个AVPacket的副本
AVPacket *clone = av_packet_clone(packet);
// 执行某些处理,例如发送数据包到其他线程进行解码
// 处理完成后,释放副本
av_packet_unref(clone);
// 更改时间戳
int64_t new_tb_num = 1;
int64_t new_tb_den = 1000; // 新的时间基准为1/1000秒
av_packet_rescale_ts(packet, formatContext->streams[packet->stream_index]->time_base, (AVRational){new_tb_num, new_tb_den});
}
// 释放数据包
av_packet_unref(packet);
av_packet_free(&packet);
这个例子展示了如何创建一个AVPacket的副本,如何对一个数据包进行引用计数操作,以及如何更改数据包的时间基准。所有分配出来的AVPacket都需要用av_packet_free()来释放,并且在释放之前要先使用av_packet_unref()来取消对其的引用。
注意:AVPacket是线程不安全的,同一时间只能有一个线程操作同一个AVPacket。如果你需要在多个线程之间共享AVPacket,你需要自己实现同步机制,如互斥锁等。
AVFrame 常用 API
AVFrame 是 FFmpeg 中的一个重要的数据结构,主要用于存储解码后的音/视频帧数据。
typedef struct AVFrame {
uint8_t *data[AV_NUM_DATA_POINTERS]; // 指向图片/音频数据的指针
int linesize[AV_NUM_DATA_POINTERS]; // 每个图片/音频行的大小
...
} AVFrame;
以下是一些常用的 AVFrame 相关的 API:
-
av_frame_alloc(): 分配一个AVFrame结构体。它只是分配了AVFrame结构体本身的内存,并未分配 data 数据的内存。AVFrame *frame = av_frame_alloc(); -
av_frame_free(): 释放一个AVFrame结构体及其内部的所有字段,并将指针设为 NULL。AVFrame *frame = ...; av_frame_free(&frame); -
av_frame_clone(): 克隆一个AVFrame结构体。AVFrame *src = ...; AVFrame *dst = av_frame_clone(src); -
av_frame_ref(): 将一个AVFrame结构体的引用赋给另一个AVFrame结构体。AVFrame *src = ...; AVFrame *dst = av_frame_alloc(); av_frame_ref(dst, src); -
av_frame_move_ref(): 将一个AVFrame结构体的引用移动给另一个AVFrame结构体。AVFrame *src = ...; AVFrame *dst = av_frame_alloc(); av_frame_move_ref(dst, src); -
av_frame_unref(): 减少AVFrame结构体的引用计数。AVFrame *frame = ...; av_frame_unref(frame);
在实际的编解码操作中,音/视频帧数据存储在 AVFrame 中,通过以上的 API 可以对音/视频帧数据进行分配、释放、引用和取消引用等操作。