FFmpeg 笔记

基础概念

https://www.ruanyifeng.com/blog/2020/01/ffmpeg.html

码率

比特率,bps,每秒位数。

muxer

封装,把视频流、音频流、字幕流等封装到一个容器格式中。

demuxer

从容器格式中分离出流。

容器

视频文件本身其实是一个容器(container),里面包括了视频和音频,也可能有字幕等其他内容。

查看 ffmpeg 支持的容器:

1
ffmpeg -formats

编码格式

视频和音频都需要经过编码,才能保存成文件。不同的编码格式(CODEC),有不同的压缩率,会导致文件大小和清晰度的差异。比如H.264就是一种视频编码格式。

查看 ffmpeg 支持的编码格式:

1
ffmpeg -codecs

编码器

编码器(encoders)是实现某种编码格式的库文件。只有安装了某种格式的编码器,才能实现该格式视频/音频的编码和解码。比如libx264是一种H.264编码器。

查看 ffmpeg 已安装的编码器:

1
ffmpeg -encoders

查看文件的元信息

1
ffmpeg -i input.mp4 -hide_banner

一个容器包含多个流(stream),视频流、音频流、字幕流等。

以下示例中包含一个视频流、一个音频流、一个字幕流和三个附加文件流(字体文件)。

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
Input #0, matroska,webm, from 'F:\Why.Poverty.1of8.Poor.Us.-.An.Animated.History.of.Poverty.1080p.WEB-DL.AVC.AAC-Conan06.mkv':
Metadata:
title : Poor Us: An Animated History of Poverty
encoder : libebml v1.3.0 + libmatroska v1.4.0
creation_time : 2013-01-26T09:49:37.000000Z
Duration: 00:58:05.04, start: 0.000000, bitrate: 2719 kb/s
Stream #0:0(eng): Video: h264 (High), yuv420p(progressive), 1920x1080, SAR 1:1 DAR 16:9, 25 fps, 25 tbr, 1k tbn, 50 tbc (default)
Metadata:
title : Episode 1 of 8
Stream #0:1(eng): Audio: aac (LC), 44100 Hz, stereo, fltp (default)
Metadata:
title : Conan06
Stream #0:2(chi): Subtitle: ass (default)
Metadata:
title : by Conan06
Stream #0:3: Attachment: ttf
Metadata:
filename : 方正准园.ttf
mimetype : application/x-truetype-font
Stream #0:4: Attachment: ttf
Metadata:
filename : 方正兰亭特黑长.TTF
mimetype : application/x-truetype-font
Stream #0:5: Attachment: ttf
Metadata:
filename : 张海山锐线体简.ttf
mimetype : application/x-truetype-font

下面这个示例包含一个视频流、两个音频流(即两个音轨):

1
2
3
4
5
6
7
8
9
10
11
12
Input #0, matroska,webm, from 'D:\IronMan.mkv':
Metadata:
encoder : libebml v1.3.0 + libmatroska v1.4.0
creation_time : 2014-08-26T16:56:08.000000Z
Duration: 02:06:01.81, start: 0.000000, bitrate: 2048 kb/s
Stream #0:0: Video: h264 (High), yuv420p(progressive), 1280x720 [SAR 1:1 DAR 16:9], 23.98 fps, 23.98 tbr, 1k tbn, 47.95 tbc (default)
Stream #0:1(eng): Audio: aac (HE-AAC), 48000 Hz, 5.1, fltp (default)
Metadata:
title : 英语
Stream #0:2(chi): Audio: aac (HE-AAC), 48000 Hz, 5.1, fltp
Metadata:
title : 国语

教程

http://dranger.com/ffmpeg/ffmpeg.html

教程中的某些函数在 FFmpeg 新版本中已被废弃,所以代码有一些改动。

源代码:https://gitee.com/yjwx0017/ffmpeg_tutorial

编译环境:vscode + cmake + vcpkg

截图

实现功能:加载视频文件,将开头几帧图像保存为.ppm文件(ppm文件可用photoshop查看)

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
#include <fstream>
#include <iostream>
#include <sstream>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}

void saveFrame(AVFrame *frame, int width, int height, int frameIndex) {
std::stringstream filename;
filename << "frame" << frameIndex << ".ppm";
std::ofstream file(filename.str(), std::ios_base::binary);

// header
file << "P6\n" << width << " " << height << "\n255\n";

// pixel data
for (int i = 0; i < height; ++i) {
file.write((const char *)(frame->data[0] + i * frame->linesize[0]),
width * 3);
}
}

// Making Screencaps
int main(int argc, char *argv[]) {
int ret(0);
AVFormatContext *formatCtx(nullptr);
AVCodecContext *codecCtx(nullptr);
AVFrame *frame(nullptr);
AVFrame *frameRGB(nullptr);
uint8_t *buffer(nullptr);

// 取命令行参数获取文件名
std::string filename;
if (argc > 1) {
filename = argv[1];
} else {
filename = "C:/Users/zhou/Videos/bigbuckbunny.mp4";
}

// 打开文件获取上下文
ret = avformat_open_input(&formatCtx, filename.c_str(), nullptr, nullptr);
if (ret < 0) {
std::cout << "error: avformat_open_input() failed" << std::endl;
goto fail;
}

// 初始化流信息
ret = avformat_find_stream_info(formatCtx, nullptr);
if (ret < 0) {
std::cout << "error: avformat_find_stream_info() failed" << std::endl;
goto fail;
}

// 打印媒体文件信息
av_dump_format(formatCtx, 0, filename.c_str(), 0);

// 查找视频流
int videoStream(-1);
for (uint32_t i = 0; i < formatCtx->nb_streams; ++i) {
if (formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStream = i;
break;
}
}

if (videoStream == -1) {
std::cout << "error: Didn't find a video stream";
goto fail;
}

// 获取解码器
const AVCodec *codec =
avcodec_find_decoder(formatCtx->streams[videoStream]->codecpar->codec_id);
if (!codec) {
std::cout << "error: Unsupported codec" << std::endl;
goto fail;
}

// 创建解码器上下文
codecCtx = avcodec_alloc_context3(codec);
if (!codecCtx) {
std::cout << "error: avcodec_alloc_context3() failed" << std::endl;
goto fail;
}

// 使用视频流的编码参数设置解码器上下文
ret = avcodec_parameters_to_context(
codecCtx, formatCtx->streams[videoStream]->codecpar);
if (ret < 0) {
std::cout << "error: avcodec_parameters_to_context() failed" << std::endl;
goto fail;
}

// 初始化解码器上下文
ret = avcodec_open2(codecCtx, codec, nullptr);
if (ret < 0) {
std::cout << "error: avcodec_open2() failed" << std::endl;
goto fail;
}

// 创建帧结构,RGB格式截图帧,用于保存视频帧转换后的结果
frameRGB = av_frame_alloc();
if (!frameRGB) {
std::cout << "error: av_frame_alloc() failed" << std::endl;
goto fail;
}

// 计算截图帧所需的内存大小
int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, codecCtx->width,
codecCtx->height, 1);
if (numBytes < 0) {
std::cout << "error: av_image_get_buffer_size() failed" << std::endl;
goto fail;
}

// 为截图帧分配内存
buffer = (uint8_t *)av_malloc(numBytes);
if (!buffer) {
std::cout << "error: av_malloc() failed" << std::endl;
goto fail;
}

// 将内存关联到截图帧frameRGB
ret = av_image_fill_arrays(frameRGB->data, frameRGB->linesize, buffer,
AV_PIX_FMT_RGB24, codecCtx->width,
codecCtx->height, 1);
if (ret < 0) {
std::cout << "error: av_image_fill_arrays() failed" << std::endl;
goto fail;
}

// 创建格式转换上下文
// 用于执行视频帧到截图帧之间的像素格式转换,视频帧的像素格式一般为YUV
SwsContext *swsCtx =
sws_getContext(codecCtx->width, codecCtx->height, codecCtx->pix_fmt,
codecCtx->width, codecCtx->height, AV_PIX_FMT_RGB24,
SWS_BILINEAR, nullptr, nullptr, nullptr);

// 创建帧结构,用于接收视频帧
frame = av_frame_alloc();
if (!frame) {
std::cout << "error: av_frame_alloc() failed" << std::endl;
goto fail;
}

// 循环读取 packet,媒体流被划分为多个 packet,将 packet 喂给解码器,
// 然后从解码器获取解码后的帧,媒体流的一个 packet 包含一个或多个帧
AVPacket packet;
int frameFinished(0);
int i(0);
while (av_read_frame(formatCtx, &packet) >= 0) {
// 判断是否是所需的媒体流
if (packet.stream_index == videoStream) {
// 将 packet 发送给解码器上下文
ret = avcodec_send_packet(codecCtx, &packet);
if (ret < 0) {
std::cout << "error: avcodec_send_packet() failed" << std::endl;
goto fail;
}

// 从解码器上下文中获取解码后的帧
ret = avcodec_receive_frame(codecCtx, frame);
if (ret == 0) {
// 获取到一帧,执行像素格式转换
sws_scale(swsCtx, frame->data, frame->linesize, 0, codecCtx->height,
frameRGB->data, frameRGB->linesize);

// 将截图帧保存到文件,前5帧
if (i < 5) {
saveFrame(frameRGB, codecCtx->width, codecCtx->height, i);
++i;
} else {
break;
}
}
}

// 根据 av_read_frame() 函数的文档可知,函数成功时,
// 需要调用 av_packet_unref() 释放内存
av_packet_unref(&packet);
}

fail:
av_free(buffer);
av_frame_free(&frameRGB);
av_frame_free(&frame);
avcodec_free_context(&codecCtx);
avformat_close_input(&formatCtx);

return 0;
}

输出到屏幕

实现功能:加载视频文件,通过 SDL 显示到窗口上。

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
#include <SDL/SDL.h>
#include <SDL/SDL_thread.h>
#include <fstream>
#include <iostream>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}

// Outputting to the Screen
int main(int argc, char *argv[]) {
// 初始化SDL
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER) < 0) {
std::cout << "error: Could not initialize SDL - " << SDL_GetError()
<< std::endl;
return 0;
}

int ret(0);
AVFormatContext *formatCtx(nullptr);
AVCodecContext *codecCtx(nullptr);
AVFrame *frame(nullptr);
SDL_Overlay *sdlOverlay(nullptr);

// 取命令行参数获取文件名
std::string filename;
if (argc > 1) {
filename = argv[1];
} else {
filename = "C:/Users/zhou/Videos/bigbuckbunny.mp4";
}

// 打开文件
ret = avformat_open_input(&formatCtx, filename.c_str(), nullptr, nullptr);
if (ret < 0) {
std::cout << "error: avformat_open_input() failed" << std::endl;
goto fail;
}

// 初始化流信息
ret = avformat_find_stream_info(formatCtx, nullptr);
if (ret < 0) {
std::cout << "error: avformat_find_stream_info() failed" << std::endl;
goto fail;
}

// 查找视频流
int videoStream(-1);
for (uint32_t i = 0; i < formatCtx->nb_streams; ++i) {
if (formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStream = i;
break;
}
}

if (videoStream == -1) {
std::cout << "error: Didn't find a video stream";
goto fail;
}

// 获取解码器
const AVCodec *codec =
avcodec_find_decoder(formatCtx->streams[videoStream]->codecpar->codec_id);
if (!codec) {
std::cout << "error: Unsupported codec" << std::endl;
goto fail;
}

// 创建解码器上下文
codecCtx = avcodec_alloc_context3(codec);
if (!codecCtx) {
std::cout << "error: avcodec_alloc_context3() failed" << std::endl;
goto fail;
}

// 使用视频流的编码参数设置解码器上下文
ret = avcodec_parameters_to_context(
codecCtx, formatCtx->streams[videoStream]->codecpar);
if (ret < 0) {
std::cout << "error: avcodec_parameters_to_context() failed" << std::endl;
goto fail;
}

// 初始化解码器上下文
ret = avcodec_open2(codecCtx, codec, nullptr);
if (ret < 0) {
std::cout << "error: avcodec_open2() failed" << std::endl;
goto fail;
}

// 创建 SDL_Surface
SDL_Surface *screen =
SDL_SetVideoMode(codecCtx->width, codecCtx->height, 0, 0);
if (!screen) {
std::cout << "error: SDL: could not set video mode" << std::endl;
goto fail;
}

// YV12 SDL_Overlay
sdlOverlay = SDL_CreateYUVOverlay(codecCtx->width, codecCtx->height,
SDL_YV12_OVERLAY, screen);

// 创建格式转换上下文
SwsContext *swsCtx =
sws_getContext(codecCtx->width, codecCtx->height, codecCtx->pix_fmt,
codecCtx->width, codecCtx->height, AV_PIX_FMT_YUV420P,
SWS_BILINEAR, nullptr, nullptr, nullptr);

// 创建帧结构,用于接收视频帧
frame = av_frame_alloc();
if (!frame) {
std::cout << "error: av_frame_alloc() failed" << std::endl;
goto fail;
}

// 循环读取 packet
AVPacket packet;
int frameFinished(0);
int i(0);
while (av_read_frame(formatCtx, &packet) >= 0) {
if (packet.stream_index == videoStream) {
// 将 packet 发送给解码器上下文
ret = avcodec_send_packet(codecCtx, &packet);
if (ret < 0) {
std::cout << "error: avcodec_send_packet() failed" << std::endl;
goto fail;
}

// 从解码器上下文中获取解码后的帧
ret = avcodec_receive_frame(codecCtx, frame);
if (ret == 0) {
SDL_LockYUVOverlay(sdlOverlay);

// 将 SDL_Overlay 的内存指定到 AVFrame 结构
// YV12 和 YUV420P 格式区别,UV分量顺序不同
// https://blog.csdn.net/dss875914213/article/details/120836765
AVFrame frameDst;
frameDst.data[0] = sdlOverlay->pixels[0];
frameDst.data[1] = sdlOverlay->pixels[2];
frameDst.data[2] = sdlOverlay->pixels[1];
frameDst.linesize[0] = sdlOverlay->pitches[0];
frameDst.linesize[1] = sdlOverlay->pitches[2];
frameDst.linesize[2] = sdlOverlay->pitches[1];

// 执行像素格式转换
sws_scale(swsCtx, frame->data, frame->linesize, 0, codecCtx->height,
frameDst.data, frameDst.linesize);

SDL_UnlockYUVOverlay(sdlOverlay);

SDL_Rect rect = {0, 0, (Uint16)codecCtx->width,
(Uint16)codecCtx->height};
SDL_DisplayYUVOverlay(sdlOverlay, &rect);
}
}

av_packet_unref(&packet);

bool quit(false);

SDL_Event event;
SDL_PollEvent(&event);

switch (event.type) {
case SDL_QUIT:
SDL_Quit();
quit = true;
break;

default:
break;
}

if (quit) {
break;
}
}

fail:
SDL_FreeYUVOverlay(sdlOverlay);
av_frame_free(&frame);
sws_freeContext(swsCtx);
avcodec_free_context(&codecCtx);
avformat_close_input(&formatCtx);

return 0;
}

播放音频

实现功能:在上一个程序的基础上播放音频。

创建音频重采样上下文,用于将音频数据转换为所需的格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
AVChannelLayout outChLayout;
AVChannelLayout inChLayout;
av_channel_layout_default(&outChLayout, 2);
av_channel_layout_default(&inChLayout, audioCodecCtx->ch_layout.nb_channels);

// 创建音频重采样上下文
ret = swr_alloc_set_opts2(&swrContext, &outChLayout, AV_SAMPLE_FMT_S16,
audioCodecCtx->sample_rate, &inChLayout,
audioCodecCtx->sample_fmt,
audioCodecCtx->sample_rate, 0, nullptr);
if (ret < 0) {
return -1;
}

swr_init(swrContext);

初始化 SDL Audio:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 初始化 SDL Audio
SDL_AudioSpec audioSpec;
audioSpec.freq = audioCodecCtx->sample_rate;
audioSpec.format = AUDIO_S16SYS;
audioSpec.channels = 2;
audioSpec.silence = 0;
audioSpec.samples = 1024;
audioSpec.callback = audioCallback;
audioSpec.userdata = audioCodecCtx;
if (SDL_OpenAudio(&audioSpec, nullptr) != 0) {
std::cout << "error: SDL_OpenAudio() failed, " << SDL_GetError()
<< std::endl;
goto fail;
}

SDL_PauseAudio(0);

处理音频 packet,放到一个队列中:

1
2
3
4
5
6
7
8
9
if (packet.stream_index == videoStream) {
// ...
} else if (packet.stream_index == audioStream) {
AVPacket *copyPacket = av_packet_clone(&packet);
std::unique_lock<std::mutex> lock(mutex);
packets.push(copyPacket);
lock.unlock();
waitCondition.notify_all();
}

SDL 音频回调函数:

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
// 该回调函数负责往 stream 缓冲区存放 len 字节的数据
void audioCallback(void *userdata, Uint8 *stream, int len) {
// 存放解码出音频数据
static uint8_t audioBuf[MAX_AUDIO_FRAME_SIZE];
static uint32_t audioBufSize = 0;
static uint32_t audioBufIndex = 0;

AVCodecContext *codecCtx = (AVCodecContext *)userdata;

// 需要存放的字节数剩余
int lenRemain = len;
while (lenRemain > 0) {
if (audioBufIndex >= audioBufSize) {
// 解码出的音频数据为空(或已消耗完毕),则执行音频解码
int audioSize = audioDecodeFrame(codecCtx, audioBuf, sizeof(audioBuf));
if (audioSize < 0) {
// 如果出错,填充数据 0,即无声音
audioBufSize = 1024;
std::memset(audioBuf, 0, audioBufSize);
} else {
audioBufSize = audioSize;
}

audioBufIndex = 0;
}

// 需要拷贝的音频数据大小
int cpSize = audioBufSize - audioBufIndex;
if (lenRemain < cpSize) {
cpSize = lenRemain;
}

// 拷贝数据,并更新缓冲区相关信息
std::memcpy(stream, (uint8_t *)audioBuf + audioBufIndex, cpSize);
audioBufIndex += cpSize;
stream += cpSize;
lenRemain -= cpSize;
}
}

音频解码:

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
int audioDecodeFrame(AVCodecContext *codecCtx, uint8_t *audioBuf, int bufSize) {
int dataSize(-1);

std::unique_lock<std::mutex> lock(mutex);
if (packets.empty()) {
waitCondition.wait(lock);
}

// 队列中取 packet
AVPacket *packet = packets.front();
packets.pop();
lock.unlock();

// packet 发送到解码器
avcodec_send_packet(codecCtx, packet);

// 获取解码后的帧,一个音频帧包含多个音频采样点
AVFrame *frame = av_frame_alloc();
while (avcodec_receive_frame(codecCtx, frame) == 0) {
// 音频重采样,将音频数据转换为所需的格式(采样频率、声道数、样本位数)
int out_nb_samples =
swr_convert(swrContext, &audioBuf, frame->nb_samples,
(const uint8_t **)frame->data, frame->nb_samples);

dataSize = av_samples_get_buffer_size(nullptr, 2, out_nb_samples,
AV_SAMPLE_FMT_S16, 1);
}

av_frame_free(&frame);
av_packet_free(&packet);
return dataSize;
}

重构代码,使用线程

https://gitee.com/yjwx0017/ffmpeg_tutorial/blob/master/tutorial_4/main.cpp

同步:视频同步到音频

https://gitee.com/yjwx0017/ffmpeg_tutorial/blob/master/tutorial_5/main.cpp

维护一个音频时钟,显示视频画面时计算下一帧距当前音频时钟的时间间隔以便决定 delay 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
double delay(10.);
VideoPicture vp_next = is->picture_queue.peek();
if (vp_next.bmp) {
double diff = vp_next.pts - get_audio_clock(is);
if (diff > 0) {
delay = diff * 1000.;
} else {
delay = 5;
}
}

if (delay < 5) {
delay = 5;
}

schedule_refresh(is, (int)delay);