C 库 FFmpeg 入门指南
简介
FFmpeg 是一个开源的跨平台 C 语言音视频编解码和多媒体解封装库,包含 3 个命令行工具:
ffmpeg转码器ffprobe分析器ffplay播放器
还有多个函数库,其中最主要的两个是:
libavformat解封装模块,原生支持 MP4 等文件封装格式和 RTMP 等流媒体传输协议。libavcodec编解码模块,原生支持 MPEG-4、M-JPEG 等视频流的编解码和 MP3、AAC 和 AC3 等音频流的编解码,可以集成第三方编解码器以支持更多的编解码,例如第三方 H.264 (AVC) 编解码器 OpenH264 和第三方 H.265 (HEVC) 编解码器 X265。
获取
FFmpeg 的各个稳定版的源码可以从 FFmpeg 官网的下载。 官网还罗列了一些预编译的 FFmpeg 的下载链接:
- Windows builds from gyan.dev
ffmpeg-5.1.2-essentials_build.7z只包含命令行工具ffmpeg-5.1.2-full_build-shared.7z包含命令行工具、库和头文件
- BtbN
ffmpeg-n5.1-latest-win64-gpl-5.1.zip只包含命令行工具ffmpeg-n5.1-latest-win64-gpl-shared-5.1.zip包含命令行工具、库和头文件
FFmpeg 没有提供 CMakeLists.txt 文件,也没有提供 *.vcxproj 文件,只提供一个 Makefile 文件,这意味着无法直接用 Visual Studio 来构建 FFmpeg。为了减少在 Windows 上构建 FFmpeg 的麻烦,有人发起了 Shift Media Project 项目,专门为 FFmpeg 制作 *.vcxproj 文件(见 SMP 子文件夹),同时也提供预编译的 FFmpeg 下载(见发布页)。
文档
由于 FFmpeg 分成很多模块,每个模块都有很多功能,因此,FFmpeg 的 文档 也分成很多部分。其中比较常用的有下列几个:
- Command Line Tools Documentation,介绍各个命令行工具的用法
- Components Documentation,各个功能部件的参数的解释
- General Documentation
- Frequently Asked Questions 常见问题
- Supported External Libraries, Formats, Codecs or Features,介绍 FFmpeg 集成第三方库的方法
- Platform Specific Information,FFmpeg 在不同平台的构建方法
- Community Contributed Documentation
- Official FFmpeg Wiki
- Generic Compilation Guide,通用编译指南
- FFmpeg Compilation Guides,各个平台的编译指南
- Hardware acceleration introduction with FFmpeg,介绍 FFmpeg 对各种硬件加速平台的支持
- Official FFmpeg Wiki
构建
FFmpeg 的编译选项可以用命令 ./configure -h 查看。常用选项如下:
1 | # 工具链 |
可以用下列命令查看 FFmpeg 支持的所有解封装器、编解码器。
1 | ./configure --list-encoders |
FFmpeg 已经实现 H.264 解码器,但没有实现 H.264 编码器。
命令行
要查看编译好的 FFmpeg 支持的解封装器、编解码器,可以使用命令行工具 ffmpeg 的下列选项:
1 | ./ffmpeg -muxers # 查看 FFmpeg 支持封装的格式 |
可以用命令行工具 ffmpeg 进行一次转码,以验证刚编译好的 FFmpeg 能否工作。
1 | ./ffmpeg -i input.mp4 -vcodec h264 -acodec aac -r 30 -b 1000k output.mp4 |
-vcodec 和 -acodec 分别用于指定视频编码格式和音频编码格式,没有指定则使用封装格式默认的编码格式。
某种解封装器、编解码器的详细信息可以用 -h 选项查看。
1 | ./ffmpeg -h muxer=mp4 # 查看封装器 MP4 |
API
可以通过 API 获取 FFmpeg 的编译选项:
1 |
|
解封装
打开媒体
AVFormatContext
打开媒体要用 avformat_open_input() 函数。在那之前,要先准备一个 AVFormatContext 结构,用于包含解封装操作的上下文。avformat_open_input() 函数负责分配内存并填充其中几个字段。
1 | AVFormatContext* ifmt_ctx = nullptr; |
AVFormatContext 结构的内存也可以用 avformat_alloc_context() 函数事先分配,但最后都要用 avformat_close_input() 函数释放。
媒体不一定来源于文件,也有可能来源于网络,也就是流媒体。因此,avformat_open_input() 函数的第二个参数有可能是流媒体的 URL,也有可能是媒体文件的路径。
1 | std::cout << "Input url: " << ifmt_ctx->url << std::endl; |
媒体文件的解封装操作包含文件 I/O 操作。文件 I/O 操作的上下文由 AVFormatContext 结构的 pb 成员指示。打开媒体文件时,avformat_open_input() 函数自动调用 avio_open() 函数打开文件;关闭媒体文件时,avformat_close_input() 函数自动调用 avio_closep() 函数释放资源。
AVInputFormat
AVFormatContext 结构的 iformat 成员是一个 AVInputFormat 结构,它表示输入媒体的封装格式,其 name 字段是封装格式的名称。
1 | const AVInputFormat* ifmt = ifmt_ctx->iformat; |
实际上,avformat_open_input() 函数就可以接受一个 AVFormatContext 结构作为第三个参数,用于强制指定媒体文件的封装格式,设为 nullptr 则由 FFmpeg 自动识别(根据扩展名和文件头部)。
检索流
AVStream
在打开媒体文件之后,要用 avformat_find_stream_info() 函数获取视频流和音频流的列表。
1 | ret = avformat_find_stream_info(ifmt_ctx, nullptr); |
AVFormatContext 结构的 nb_streams 成员指示流的数量。
1 | std::cout << "Number of streams: " << ifmt_ctx->nb_streams; |
nb_streams 成员同时也是 streams 成员的长度。streams 成员是 AVStream 结构的列表。每个 AVStream 结构表示一个流,其中包含流的信息,如时间基(单位秒)、时长(以时间基为单位)和帧数等。
1 | AVStream* stream = ifmt_ctx->streams[0]; |
时间基是一个分数。分数都用一个 AVRational 结构表示。
1 | typedef struct AVRational{ |
有了以上三项信息,就可以计算流的平均帧率:
1 | double time_base = 1.0 * stream->time_base.num / stream->time_base.den; |
可以用 av_find_best_stream() 函数获取指定类型的流的索引。
1 | int video_stream_index = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_VIDEO, |
AVMediaType
第 2 个参数是枚举类型 AVMediaType,用于指示流的类型。
1 | enum AVMediaType { |
可以用 av_get_media_type_string() 函数获取类型的名称。
1 | std::cout << av_get_media_type_string(AVMEDIA_TYPE_VIDEO); // "video" |
解码
检索解码器
AVCodecParameters
AVStream 结构的 codecpar 成员是一个 AVCodecParameters 结构,包含流的编解码参数。
1 | AVCodecParameters* decode_para = video_stream->codecpar; |
AVCodecParameters 结构的 codec_type 成员是枚举类型 AVMediaType。
流的比特率由 bit_rate 成员指示;视频的宽高分别由 width 和 height 两个成员指示;音频的采样率由 sample_rate 成员指示。
1 | if (decode_para->codec_type == AVMEDIA_TYPE_VIDEO || |
视频的像素格式或音频的采样格式由 format 成员指示,对应的枚举类型如下:
1 | enum AVSampleFormat { |
AVCodec
codec_id 成员是枚举类型 AVCodecID,它是编解码器的标识。用 avcodec_find_decoder() 函数获取编解码器时,要用它作为参数。编解码器用 AVCodec 结构表示,该结构的 name 成员是编解码器的名称。
1 | if (decode_para->codec_type == AVMEDIA_TYPE_VIDEO || |
打开解码器
AVCodecContext
在编解码前,要准备一个 AVCodecContext 结构,用于保存编解码操作的上下文。AVCodecContext 结构的内存要用 avcodec_alloc_context3() 函数分配,用函数 avcodec_free_context() 释放,它的内容要用 avcodec_parameters_to_context() 函数从 AVCodecParameters 结构复制。
1 | AVCodecContext* decode_ctx = avcodec_alloc_context3(decoder); |
准备好 AVCodecContext 结构后,就可以用 avcodec_open2() 函数来打开编解码器。
1 | ret = avcodec_open2(decode_ctx, decoder, nullptr); |
解码流
流是由数据包组成的。数据包是经过编码压缩的数据帧。数据包和数据帧分别用 AVPacket 和 AVFrame 结构表示。在编解码时,要准备好这两个结构。
1 | AVPacket* packet = av_packet_alloc(); |
数据包可以用 av_read_frame() 函数从媒体文件中获取。数据包来自哪个流要通过检查 AVPacket 结构的 stream_index 成员才能确定。
1 | while (av_read_frame(ifmt_ctx, packet) >= 0) { |
在获取下一个数据包之前,都要用 av_packet_unref() 函数重置 AVPacket 结构。
解码时需要反复调用 av_read_frame() 函数从媒体文件读取数据包。解码的过程是先调用 avcodec_send_packet() 函数将数据包发送给解码器,再调用 avcodec_receive_frame() 函数从解码器接收数据帧的过程。注意,并非每发送一个数据包就能接收一个数据帧,也不是每次都只能接收一个数据帧。
1 | while (av_read_frame(ifmt_ctx, packet) >= 0) { |
AVPacket
每个数据包都有下列成员:
time_base时间基,时间戳的单位。未来,在解封装或编码时可能会设置该字段,但是在封装或解码时总是忽略该字段。dts解码时间戳,即解码该帧的时机,以AVStream::time_base为单位,下同。pts显示时间戳,即显示该帧的时机。duration显示时长,即显示该帧的时长,它等于下一帧的pts与该帧的pts的差。
AVFrame
每个数据帧都有下列成员:
time_base时间基,时间戳的单位。未来,在解码时可能会设置该字段,但是在编码时总是忽略该字段。pts显示时间戳,即显示该帧的时机,以AVCodecContext::time_base为单位,下同。pkt_dts解码时间戳,从AVPacket拷贝而来。key_frames是否为关键帧。pict_type帧的类型,0、1、2分别代表 I 帧、P 帧和 B 帧。format像素格式,最常用的是AV_PIX_FMT_YUV420P。
AVFrame 结构的 data 成员是 8 个图像数据的指针,因此它最多可以保存 8 个图像分量。当图像数据以打包格式存储时,只有 data[0] 有效;以平面格式存储时,只有 data[0] data[1] data[2] 有效,分别对应 Y U V 分量。
定位帧
av_read_frame() 函数总是按照数据帧在文件中的存储顺序读取下一个数据帧。如果要打破这种顺序,就要借助 av_seek_frame() 函数。
1 | int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags); |
flags 决定跳转的方向和方式,有下列取值可选:
1 |
比如回到视频的开头:
1 | av_seek_frame(ifmt_ctx, 0, 0, AVSEEK_FLAG_BACKWARD); |
清空解码器
读取到媒体文件的末尾时,av_read_frame() 函数将返回 AVERROR_EOF。
1 | while ((ret = av_read_frame(ifmt_ctx, packet)) >= 0) { |
此时要继续向解码器发送一个空的数据包,以便解码并取回解码器中剩余的所有数据帧。
转封装
新建媒体
要将流封装成新的媒体,需要准备一个 AVFormatContext 结构来保存封装操作的上下文。
1 | AVFormatContext* ofmt_ctx = nullptr; |
AVOutputFormat
AVFormatContext 结构的 oformat 成员是一个 AVOutputFormat 结构,它表示输出媒体的封装格式,其 name 字段是封装格式的名称。
1 | const AVOutputFormat* ofmt = ofmt_ctx->oformat; |
对于需要进行文件 I/O 操作的封装格式来说,还需要用 avio_open() 函数准备好文件 I/O 操作的上下文。封装格式是否需要文件 I/O 操作可以通过检查 AVOutputFormat 结构的 flags 成员确定。表示无需文件 I/O 操作的标志是 AVFMT_NOFILE 宏。
1 | if (!(ofmt->flags & AVFMT_NOFILE)) { |
新建输出流
新建流用 avformat_new_stream() 函数。如果没有对流进行转码,输出流的编码参数可以直接用 avcodec_parameters_copy() 函数从输入流复制。
1 | AVStream* ostream = avformat_new_stream(ofmt_ctx, nullptr); |
改变封装格式时,有些编码参数可能不通用,需要重置。
1 | ostream->codecpar->codec_tag = 0; |
如果不想把输入媒体的每个流都封装到新的媒体中,还需要建立一个流的映射表。映射表把输入流的索引为输出流的索引,可以用一个整型数组实现。比如,只保留视频流和音频流:
1 | int *stream_mapping = (int *)av_calloc(ifmt_ctx->nb_streams, sizeof(int)); |
av_calloc() 函数调用 av_mallocz() 函数来完成实际的内存分配工作。av_mallocz() 函数会将每个字节初始化为 0。
封装为媒体文件
媒体文件的开头和结尾要用专门的函数负责处理。
1 | avformat_write_header(ofmt_ctx, nullptr); |
封装媒体文件的过程就是从输入的媒体文件读数据包,再向输出的媒体文件写数据包的过程,中间还可以插入转码的过程。如果输入流和输出流的时间基不同,还要先用 av_packet_rescale_ts() 函数重新计算并设置数据包的时间戳。将数据包写入媒体文件要用 av_interleaved_write_frame() 函数。
1 | AVPacket* pkt = av_packet_alloc(); |
工具
av_dump_format()
关于媒体的封装格式的信息,可以用 av_dump_format() 函数打印。它需要一个 AVFormatContext 结构作为第一个参数,其余三个参数用于格式化输出的第一行。
1 | void av_dump_format(AVFormatContext *ic, int index, const char *url, int is_output); |
当 is_output 为 0 时,第一行为 Input ... from ... 的形式;当 is_output 为 1 时,第一行为 Output ... to ... 的形式。index 是紧随在井号 # 后的数字。url 是引号中的文件名。
1 | av_dump_format(ifmt_ctx, 0, ifmt_ctx->url, 0); |
硬件加速
依赖硬件的 H.264 编解码器如下:
| Name | Codec | Description | Platform |
|---|---|---|---|
| h264_amf | E | Advanced Media Framework | AMD GPU, Windows |
| h264_nvenc | E | NVIDIA NVENC | NVIDIA GPU |
| h264_cuvid | D | NVIDIA NVDNC/CUVID | NVIDIA GPU |
| h264_qsv | ED | Intel Media SDK (Quick Sync Video) | Intel CPU, Windows |
| h264_crystalhd | D | Crystal HD | Broadcom Crystal HD Decoder BCM70015 |
| h264_mmal | D | Multi-Media Abstraction Layer | Broadcom VideoCore CPU, Raspberry Pi |
| h264_rkmpp | D | Rockchip Media Process Platform | RK32xx |
| h264_mf | E | Microsoft Media Foundation | Windows API |
| h264_v4l2m2m | ED | Video for Linux | Linux API |
| h264_vaapi | E | Video Acceleration API | Linux API |
| h264_videotoolbox | E | Video Toolbox | macOS |
| h264_mediacodec | D | MediaCodec | Android |
| h264_omx | E | OpenMAX Integration Layer | Embedded device |
AMD AMF
编译 FFmpeg 时需要提供 AMF SDK 的头文件才能启用基于 AMD GPU 的 H.264 硬编码器。
- 将 AMF SDK 中的文件夹
amf/public/include复制到系统包含目录并重命名为AMF。 - 添加编译选项
--enable-amf。 - 添加编码器
h264_amf。
Intel QSV
FFmpeg 需要链接到库 libmfx 才能启用基于 Intel 集成显卡的 H.264 硬编解码器。
- 编译库
libmfx。 - 添加
--enable-libmfx和--enable-filter=scale编译选项。 - 添加编解码器
h264_qsv。
Intel Media SDK 是 Intel 显卡驱动的一部分。
NVIDIA CUDA
编译 FFmpeg 时需要提供 NVIDIA Codec SDK 的头文件。
- 下载 NVIDIA Codec SDK,用
make install命令安装。 - 添加编译选项
--enable-cuvid、--enable-nvenc和--enable-nvdec。 - 添加编码器
h264_nvenc和解码器h264_cuvid。