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 的 文档 也分成很多部分。其中比较常用的有下列几个:

构建

FFmpeg 的编译选项可以用命令 ./configure -h 查看。常用选项如下:

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
# 工具链
--toolchain=msvc # 工具链
--host-os=win64 # or win32


# 标准
--prefix=install # 安装路径


# 授权
--enable-gpl # 启用 GPL 协议
--enable-version3 # 启用 LGPL3 协议
--enable-nonfree # 启用付费代码


# 构建配置
--disable-static # 不要静态库
--enable-shared # 要动态库
--enable-small # 针对体积优化
--disable-runtime-cpudetect # 不要在运行时检测 CPU 性能


# 命令行工具
--disable-programs # 禁用所有命令行工具
--disable-ffmpeg # 禁用 ffmpeg
--disable-ffplay # 禁用 ffplay
--disable-ffprobe # 禁用 ffprobe


# 文档
--disable-doc # 禁用所有文档


# 模块
--disable-avcodec
--disable-avformat
--disable-avfilter
--disable-swresample
--disable-swscale
--disable-avdevice
--disable-postproc # 启用 postproc,需同时启用 GPL 协议,见 --enable-gpl 选项
--disable-network # 禁用网络


# 独立模块
--disable-everything # 禁用所有独立模块

--enable-encoder=aac,ac3 # 启用 AAC、AC3 编码器
--enable-encoder=mpeg4,mjpeg # 启用 MPEG-4、M-JPEG 编码器
--enable-encoder=libopenh264 # 启用 OpenH264 作为 H.264 编码器

--enable-decoder=aac,ac3,eac3,mp3 # 启用 AAC、AC3、MP3 解码器
--enable-decoder=mpeg4,mjpeg # 启用 MPEG-4、M-JPEG 解码器
--enable-decoder=h264 # 启用内置的 H.264 解码器

--enable-muxer=mp4,avi # 启用 AVI、MP4 封装器
--enable-demuxer=mov,avi # 启用 AVI、MP4 解封器
# mov 和 mp4 是同一个解封器,都是 mov,mp4,m4a,3gp,3g2,mj2

--enable-protocol=file # 启用媒体文件


# 第三方库
--enable-libopenh264 # 集成 OpenH264 以支持 H.264 编码


# 开发
--disable-debug # 禁用调试符号

可以用下列命令查看 FFmpeg 支持的所有解封装器、编解码器。

1
2
3
4
5
./configure --list-encoders
./configure --list-decoders

./configure --list-muxers
./configure --list-demuxers

FFmpeg 已经实现 H.264 解码器,但没有实现 H.264 编码器。

命令行

要查看编译好的 FFmpeg 支持的解封装器、编解码器,可以使用命令行工具 ffmpeg 的下列选项:

1
2
3
4
./ffmpeg -muxers   # 查看 FFmpeg 支持封装的格式
./ffmpeg -demuxers # 查看 FFmpeg 支持解封的格式
./ffmpeg -decoders # 查看 FFmpeg 支持的解码器
./ffmpeg -encoders # 查看 FFmpeg 支持的编码器

可以用命令行工具 ffmpeg 进行一次转码,以验证刚编译好的 FFmpeg 能否工作。

1
./ffmpeg -i input.mp4 -vcodec h264 -acodec aac -r 30 -b 1000k output.mp4

-vcodec-acodec 分别用于指定视频编码格式和音频编码格式,没有指定则使用封装格式默认的编码格式。

某种解封装器、编解码器的详细信息可以用 -h 选项查看。

1
2
3
4
./ffmpeg -h muxer=mp4    # 查看封装器 MP4
./ffmpeg -h demuxer=mov # 查看解封器 MOV
./ffmpeg -h decoder=aac # 查看解码器 AAC
./ffmpeg -h encoder=h264 # 查看编码器 H264

API

可以通过 API 获取 FFmpeg 的编译选项:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
extern "C" {
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
}

int main()
{
std::cout << avcodec_configuration();
return 0;
}

解封装

打开媒体

AVFormatContext

打开媒体要用 avformat_open_input() 函数。在那之前,要先准备一个 AVFormatContext 结构,用于包含解封装操作的上下文。avformat_open_input() 函数负责分配内存并填充其中几个字段。

1
2
3
4
5
6
7
8
9
10
AVFormatContext* ifmt_ctx = nullptr;
int ret = avformat_open_input(&ifmt_ctx, "video.mp4", nullptr, nullptr);
if (ret < 0) {
std::cerr << "Cannot open input file\n";
return ret;
}

// ...

avformat_close_input(&ifmt_ctx);

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
2
const AVInputFormat* ifmt = ifmt_ctx->iformat;
std::cout << "Input format: " << ifmt->name; // "mov,mp4,m4a,3gp,3g2,mj2"

实际上,avformat_open_input() 函数就可以接受一个 AVFormatContext 结构作为第三个参数,用于强制指定媒体文件的封装格式,设为 nullptr 则由 FFmpeg 自动识别(根据扩展名和文件头部)。

检索流

AVStream

在打开媒体文件之后,要用 avformat_find_stream_info() 函数获取视频流和音频流的列表。

1
2
3
4
5
ret = avformat_find_stream_info(ifmt_ctx, nullptr);
if (ret < 0) {
std::cerr << "Cannot find stream information\n";
return ret;
}

AVFormatContext 结构的 nb_streams 成员指示流的数量。

1
std::cout << "Number of streams: " << ifmt_ctx->nb_streams;

nb_streams 成员同时也是 streams 成员的长度。streams 成员是 AVStream 结构的列表。每个 AVStream 结构表示一个流,其中包含流的信息,如时间基(单位秒)、时长(以时间基为单位)和帧数等。

1
2
3
4
AVStream* stream = ifmt_ctx->streams[0];
std::cout << "Time base: " << stream->time_base.num << "/" << stream->time_base.den;
std::cout << "Duration: " << stream->duration;
std::cout << "Number of frames: " << stream->nb_frames;

时间基是一个分数。分数都用一个 AVRational 结构表示。

1
2
3
4
typedef struct AVRational{
int num; ///< Numerator
int den; ///< Denominator
} AVRational;

有了以上三项信息,就可以计算流的平均帧率:

1
2
3
double time_base = 1.0 * stream->time_base.num / stream->time_base.den;
double duration = stream->duration * time_base;
std::cout << "Frame rate: " << stream->nb_frames / duration;

可以用 av_find_best_stream() 函数获取指定类型的流的索引。

1
2
3
4
5
6
int video_stream_index = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_VIDEO, 
-1, -1, nullptr, 0);
if (video_stream_index < 0) {
std::cerr << "av_find_best_stream() failed\n";
return -1;
}
AVMediaType

第 2 个参数是枚举类型 AVMediaType,用于指示流的类型。

1
2
3
4
5
6
7
8
9
enum AVMediaType {
AVMEDIA_TYPE_UNKNOWN = -1, ///< Usually treated as AVMEDIA_TYPE_DATA
AVMEDIA_TYPE_VIDEO,
AVMEDIA_TYPE_AUDIO,
AVMEDIA_TYPE_DATA, ///< Opaque data information usually continuous
AVMEDIA_TYPE_SUBTITLE,
AVMEDIA_TYPE_ATTACHMENT, ///< Opaque data information usually sparse
AVMEDIA_TYPE_NB
};

可以用 av_get_media_type_string() 函数获取类型的名称。

1
2
std::cout << av_get_media_type_string(AVMEDIA_TYPE_VIDEO);    // "video"
std::cout << av_get_media_type_string(AVMEDIA_TYPE_SUBTITLE); // "subtitle"

解码

检索解码器

AVCodecParameters

AVStream 结构的 codecpar 成员是一个 AVCodecParameters 结构,包含流的编解码参数。

1
AVCodecParameters* decode_para = video_stream->codecpar;

AVCodecParameters 结构的 codec_type 成员是枚举类型 AVMediaType

流的比特率由 bit_rate 成员指示;视频的宽高分别由 widthheight 两个成员指示;音频的采样率由 sample_rate 成员指示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (decode_para->codec_type == AVMEDIA_TYPE_VIDEO || 
decode_para->codec_type == AVMEDIA_TYPE_AUDIO)
{
std::cout << "Bit rate: " << decode_para->bit_rate;
}

if (decode_para->codec_type == AVMEDIA_TYPE_VIDEO) {
std::cout << "Width: " << decode_para->width;
std::cout << "Height: " << decode_para->height;
}

if (decode_para->codec_type == AVMEDIA_TYPE_AUDIO) {
std::cout << "Sampling rate: " << decode_para->sample_rate;
}

视频的像素格式或音频的采样格式由 format 成员指示,对应的枚举类型如下:

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
enum AVSampleFormat {
AV_SAMPLE_FMT_NONE = -1,
AV_SAMPLE_FMT_U8, ///< unsigned 8 bits
AV_SAMPLE_FMT_S16, ///< signed 16 bits
AV_SAMPLE_FMT_S32, ///< signed 32 bits
AV_SAMPLE_FMT_FLT, ///< float
AV_SAMPLE_FMT_DBL, ///< double

AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar
AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar
AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar
AV_SAMPLE_FMT_FLTP, ///< float, planar
AV_SAMPLE_FMT_DBLP, ///< double, planar
AV_SAMPLE_FMT_S64, ///< signed 64 bits
AV_SAMPLE_FMT_S64P, ///< signed 64 bits, planar

AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking dynamically
};

enum AVPixelFormat {
AV_PIX_FMT_NONE = -1,
AV_PIX_FMT_YUV420P,
AV_PIX_FMT_YUYV422,
AV_PIX_FMT_RGB24,
AV_PIX_FMT_BGR24,
// ...
};
AVCodec

codec_id 成员是枚举类型 AVCodecID,它是编解码器的标识。用 avcodec_find_decoder() 函数获取编解码器时,要用它作为参数。编解码器用 AVCodec 结构表示,该结构的 name 成员是编解码器的名称。

1
2
3
4
5
6
7
8
9
10
if (decode_para->codec_type == AVMEDIA_TYPE_VIDEO || 
decode_para->codec_type == AVMEDIA_TYPE_AUDIO)
{
const AVCodec* decoder = avcodec_find_decoder(decode_para->codec_id);
if (decoder == nullptr) {
std::cerr << "avcodec_find_decoder() failed\n";
return -1;
}
std::cout << "Decoder: " << decoder->name;
}

打开解码器

AVCodecContext

在编解码前,要准备一个 AVCodecContext 结构,用于保存编解码操作的上下文。AVCodecContext 结构的内存要用 avcodec_alloc_context3() 函数分配,用函数 avcodec_free_context() 释放,它的内容要用 avcodec_parameters_to_context() 函数从 AVCodecParameters 结构复制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
AVCodecContext* decode_ctx = avcodec_alloc_context3(decoder);
if (decode_ctx == nullptr) {
std::cerr << "avcodec_alloc_context3() failed\n";
return -1;
}
ret = avcodec_parameters_to_context(decode_ctx, decode_para);
if (ret < 0) {
std::cerr << "avcodec_parameters_to_context() failed\n";
return ret;
}

// ...

avcodec_free_context(&decode_ctx);

准备好 AVCodecContext 结构后,就可以用 avcodec_open2() 函数来打开编解码器。

1
2
3
4
5
ret = avcodec_open2(decode_ctx, decoder, nullptr);
if (ret < 0) {
std::cerr << "avcodec_open2() failed\n";
return ret;
}

解码流

流是由数据包组成的。数据包是经过编码压缩的数据帧。数据包和数据帧分别用 AVPacketAVFrame 结构表示。在编解码时,要准备好这两个结构。

1
2
3
4
5
6
7
8
9
10
11
AVPacket* packet = av_packet_alloc();
AVFrame* frame = av_frame_alloc();
if (packet == nullptr || frame == nullptr) {
std::cerr << "av_packet_alloc() or av_frame_alloc() failed\n";
return -1;
}

// ...

av_packet_free(&packet);
av_frame_free(&frame);

数据包可以用 av_read_frame() 函数从媒体文件中获取。数据包来自哪个流要通过检查 AVPacket 结构的 stream_index 成员才能确定。

1
2
3
4
5
6
7
while (av_read_frame(ifmt_ctx, packet) >= 0) {
if (packet->stream_index == video_stream_index) {
// ...
}

av_packet_unref(packet);
}

在获取下一个数据包之前,都要用 av_packet_unref() 函数重置 AVPacket 结构。

解码时需要反复调用 av_read_frame() 函数从媒体文件读取数据包。解码的过程是先调用 avcodec_send_packet() 函数将数据包发送给解码器,再调用 avcodec_receive_frame() 函数从解码器接收数据帧的过程。注意,并非每发送一个数据包就能接收一个数据帧,也不是每次都只能接收一个数据帧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
while (av_read_frame(ifmt_ctx, packet) >= 0) {
if (packet->stream_index == video_stream_index) {
ret = avcodec_send_packet(decode_ctx, packet);
if (ret < 0) {
std::cerr << "avcodec_send_packet() failed\n";
return ret;
}

while (ret >= 0) {
ret = avcodec_receive_frame(decode_ctx, frame);
if (ret < 0) {
if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN)) {
break;
}
std::cerr << "avcodec_receive_frame() failed\n";
return ret;
}
// ...
av_frame_unref(frame);
}
}
av_packet_unref(packet);
}
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 帧的类型,012 分别代表 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
2
3
4
#define AVSEEK_FLAG_BACKWARD 1 ///< seek backward
#define AVSEEK_FLAG_BYTE 2 ///< seeking based on position in bytes
#define AVSEEK_FLAG_ANY 4 ///< seek to any frame, even non-keyframes
#define AVSEEK_FLAG_FRAME 8 ///< seeking based on frame number

比如回到视频的开头:

1
av_seek_frame(ifmt_ctx, 0, 0, AVSEEK_FLAG_BACKWARD);

清空解码器

读取到媒体文件的末尾时,av_read_frame() 函数将返回 AVERROR_EOF

1
2
3
4
5
6
7
while ((ret = av_read_frame(ifmt_ctx, packet)) >= 0) {
// ...
}
if (ret != AVERROR_EOF) {
std::cerr << "av_read_frame() failed\n";
return ret;
}

此时要继续向解码器发送一个空的数据包,以便解码并取回解码器中剩余的所有数据帧。

转封装

新建媒体

要将流封装成新的媒体,需要准备一个 AVFormatContext 结构来保存封装操作的上下文。

1
2
3
4
5
6
7
8
9
10
AVFormatContext* ofmt_ctx = nullptr;
ret = avformat_alloc_output_context2(&ofmt_ctx, nullptr, nullptr, "output.mkv");
if (ret < 0) {
std::cerr << "avformat_alloc_output_context2() failed\n";
return ret;
}

// ...

avformat_free_context(ofmt_ctx);
AVOutputFormat

AVFormatContext 结构的 oformat 成员是一个 AVOutputFormat 结构,它表示输出媒体的封装格式,其 name 字段是封装格式的名称。

1
2
const AVOutputFormat* ofmt = ofmt_ctx->oformat;
std::cout << "Output format: " << ofmt->name;

对于需要进行文件 I/O 操作的封装格式来说,还需要用 avio_open() 函数准备好文件 I/O 操作的上下文。封装格式是否需要文件 I/O 操作可以通过检查 AVOutputFormat 结构的 flags 成员确定。表示无需文件 I/O 操作的标志是 AVFMT_NOFILE 宏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (!(ofmt->flags & AVFMT_NOFILE)) {
// Format wants file
ret = avio_open(&ofmt_ctx->pb, filename, AVIO_FLAG_WRITE);
if (ret < 0) {{
std::cerr << "avio_open() failed\n";
return ret;
}
}

// ...

if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE)) {
avio_closep(&ofmt_ctx->pb);
}

新建输出流

新建流用 avformat_new_stream() 函数。如果没有对流进行转码,输出流的编码参数可以直接用 avcodec_parameters_copy() 函数从输入流复制。

1
2
3
4
5
6
7
8
9
10
11
AVStream* ostream = avformat_new_stream(ofmt_ctx, nullptr);
if (ostream == nullptr) {
std::cerr << "avformat_new_stream() failed\n";
return -1;
}

ret = avcodec_parameters_copy(ostream->codecpar, istream->codecpar);
if (ret < 0) {
std::cerr << "avcodec_parameters_copy() failed\n";
return ret;
}

改变封装格式时,有些编码参数可能不通用,需要重置。

1
ostream->codecpar->codec_tag = 0;

如果不想把输入媒体的每个流都封装到新的媒体中,还需要建立一个流的映射表。映射表把输入流的索引为输出流的索引,可以用一个整型数组实现。比如,只保留视频流和音频流:

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
int *stream_mapping = (int *)av_calloc(ifmt_ctx->nb_streams, sizeof(int));
if (stream_mapping == nullptr) {
std::cerr << "av_calloc() failed\n";
return -1;
}

for (int i = 0; i < ifmt_ctx->nb_streams; i++) {
AVStream* istream = ifmt_ctx->streams[i];
AVCodecParameters* decode_para = istream->codecpar;
if (
decode_para->codec_type != AVMEDIA_TYPE_VIDEO &&
decode_para->codec_type != AVMEDIA_TYPE_AUDIO
) {
stream_mapping[i] = -1;
continue;
}

AVStream* ostream = avformat_new_stream(ofmt_ctx, nullptr);
avcodec_parameters_copy(ostream->codecpar, istream->codecpar);
ostream->codecpar->codec_tag = 0;

stream_mapping[i] = ostream->index;
}

// ...

av_freep(&stream_mapping);

av_calloc() 函数调用 av_mallocz() 函数来完成实际的内存分配工作。av_mallocz() 函数会将每个字节初始化为 0

封装为媒体文件

媒体文件的开头和结尾要用专门的函数负责处理。

1
2
3
4
5
avformat_write_header(ofmt_ctx, nullptr);

// ...

av_write_trailer(ofmt_ctx);

封装媒体文件的过程就是从输入的媒体文件读数据包,再向输出的媒体文件写数据包的过程,中间还可以插入转码的过程。如果输入流和输出流的时间基不同,还要先用 av_packet_rescale_ts() 函数重新计算并设置数据包的时间戳。将数据包写入媒体文件要用 av_interleaved_write_frame() 函数。

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
AVPacket* pkt = av_packet_alloc();
while (av_read_frame(ifmt_ctx, pkt) >= 0) {

const int istream_index = pkt->stream_index;
if (istream_index < ifmt_ctx->nb_streams) {

const int ostream_index = stream_mapping[pkt->stream_index];
if (ostream_index != -1) {

AVStream* istream = ifmt_ctx->streams[istream_index];
AVStream* ostream = ofmt_ctx->streams[ostream_index];

av_packet_rescale_ts(pkt, istream->time_base, ostream->time_base);
pkt->stream_index = ostream_index;
pkt->pos = -1;

ret = av_interleaved_write_frame(ofmt_ctx, pkt);
if (ret < 0) {
std::cerr << "av_interleaved_write_frame() failed\n";
return -1;
}
}
}
av_packet_unref(pkt);
}
av_packet_free(&pkt);

工具

av_dump_format()

关于媒体的封装格式的信息,可以用 av_dump_format() 函数打印。它需要一个 AVFormatContext 结构作为第一个参数,其余三个参数用于格式化输出的第一行。

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

is_output0 时,第一行为 Input ... from ... 的形式;当 is_output1 时,第一行为 Output ... to ... 的形式。index 是紧随在井号 # 后的数字。url 是引号中的文件名。

1
2
3
4
5
av_dump_format(ifmt_ctx, 0, ifmt_ctx->url, 0);
av_dump_format(ofmt_ctx, 1, ofmt_ctx->url, 1);

// Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'video.mp4':
// Output #1, matroska, to 'output.mkv':

硬件加速

依赖硬件的 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 硬编码器。

  1. AMF SDK 中的文件夹 amf/public/include 复制到系统包含目录并重命名为 AMF
  2. 添加编译选项 --enable-amf
  3. 添加编码器 h264_amf

Intel QSV

FFmpeg 需要链接到库 libmfx 才能启用基于 Intel 集成显卡的 H.264 硬编解码器。

  1. 编译库 libmfx
  2. 添加 --enable-libmfx--enable-filter=scale 编译选项。
  3. 添加编解码器 h264_qsv

Intel Media SDK 是 Intel 显卡驱动的一部分。

NVIDIA CUDA

编译 FFmpeg 时需要提供 NVIDIA Codec SDK 的头文件。

  1. 下载 NVIDIA Codec SDK,用 make install 命令安装。
  2. 添加编译选项 --enable-cuvid--enable-nvenc--enable-nvdec
  3. 添加编码器 h264_nvenc 和解码器 h264_cuvid