音视频开发之ffplay渲染线程分析

视频渲染线程

视频渲染线程实际就是main线程。

初始化

初始化SDL
1
2
3
4
5
6
7
8
// SDL初始化
flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER;
if (SDL_Init(flags)) {
av_log(NULL, AV_LOG_FATAL, "Could not initialize SDL - %s\n",
SDL_GetError());
av_log(NULL, AV_LOG_FATAL, "(Did you set the DISPLAY variable?)\n");
exit(1);
}
创建Window和Renderer
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
if (borderless)
flags |= SDL_WINDOW_BORDERLESS; // 去掉窗口状态栏
else
flags |= SDL_WINDOW_RESIZABLE; // 窗口是否可缩放
// 创建SDL窗口
window = SDL_CreateWindow(program_name, SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED, default_width,
default_height, flags);
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
if (window) {
// 创建SDL渲染器,SDL_RENDERER_ACCELERATED 使用硬件加速
renderer = SDL_CreateRenderer(
window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!renderer) {
av_log(NULL, AV_LOG_WARNING,
"Failed to initialize a hardware accelerated renderer: %s\n",
SDL_GetError());
// 如果创建SDL渲染器失败,则去掉标记再重试,可能是当前设备不支持该标记
renderer = SDL_CreateRenderer(window, -1, 0);
}
if (renderer) {
// 输出渲染器信息
if (!SDL_GetRendererInfo(renderer, &renderer_info))
av_log(NULL, AV_LOG_VERBOSE, "Initialized %s renderer.\n",
renderer_info.name);
}
}
if (!window || !renderer || !renderer_info.num_texture_formats) {
// 如果窗口或者渲染器创建失败,或者渲染器中可用的纹理格式为0,则退出
av_log(NULL, AV_LOG_FATAL, "Failed to create window or renderer: %s",
SDL_GetError());
do_exit(NULL);
}

轮询

主线程在开启解复用线程后,就会开始轮询处理SDL事件

1
2
3
4
5
6
7
8
9
10
11
static void event_loop(VideoState *cur_stream) {
SDL_Event event;
double incr, pos, frac;
// 开始轮询SDL消息
for (;;) {
double x;
// 获取事件,如果有,则执行下面的switch,如果没有,则会尝试刷新视频渲染
refresh_loop_wait_event(cur_stream, &event);
...
}
}

音视频开发之ffplay解码线程分析

视频解码线程

轮询

不断从Packet队列中取出一帧数据进行解码,然后将解码数据放入到Frame队列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for (;;) {
// 获取一帧解码数据
ret = get_video_frame(is, frame);
if (ret < 0) goto the_end;
if (!ret) continue;
...

// 根据帧率计算每帧的显示时长
duration = (frame_rate.num && frame_rate.den
? av_q2d((AVRational){frame_rate.den, frame_rate.num})
: 0);
// 将帧的PTS转换为秒
pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
// 将解码后的帧数据放入到视频原始数据队列中
ret = queue_picture(is, frame, pts, duration, frame->pkt_pos,
is->viddec.pkt_serial);
av_frame_unref(frame);
...

if (ret < 0) goto the_end;
}

音视频开发之ffplay解复用线程分析

由主线程创建,负责媒体文件的解复用和读取,读取的数据根据流类型放入到对应的编码数据队列中

查询流信息

创建解复用上下文
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

// 创建解复用上下文
ic = avformat_alloc_context();
if (!ic) {
...
}
// 设置中断回调
ic->interrupt_callback.callback = decode_interrupt_cb;
// 回调函数的参数
ic->interrupt_callback.opaque = is;
...
// 打开输入
err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
...

is->ic = ic;

if (genpts) ic->flags |= AVFMT_FLAG_GENPTS;

av_format_inject_global_side_data(ic);

if (find_stream_info) {
AVDictionary **opts = setup_find_stream_info_opts(ic, codec_opts);
int orig_nb_streams = ic->nb_streams;
// 读取文件头,获取文件的流详细信息
err = avformat_find_stream_info(ic, opts);

...
}
查找流索引

查找音频和视频流的索引

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
for (i = 0; i < ic->nb_streams; i++) {
AVStream *st = ic->streams[i];
enum AVMediaType type = st->codecpar->codec_type;
st->discard = AVDISCARD_ALL;
if (type >= 0 && wanted_stream_spec[type] && st_index[type] == -1)
if (avformat_match_stream_specifier(ic, st, wanted_stream_spec[type]) > 0)
st_index[type] = i;
}
for (i = 0; i < AVMEDIA_TYPE_NB; i++) {
if (wanted_stream_spec[i] && st_index[i] == -1) {
av_log(NULL, AV_LOG_ERROR,
"Stream specifier %s does not match any %s stream\n",
wanted_stream_spec[i], av_get_media_type_string(i));
st_index[i] = INT_MAX;
}
}
// 查找流索引
if (!video_disable)
st_index[AVMEDIA_TYPE_VIDEO] = av_find_best_stream(
ic, AVMEDIA_TYPE_VIDEO, st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);
if (!audio_disable)
st_index[AVMEDIA_TYPE_AUDIO] = av_find_best_stream(
ic, AVMEDIA_TYPE_AUDIO, st_index[AVMEDIA_TYPE_AUDIO],
st_index[AVMEDIA_TYPE_VIDEO], NULL, 0);
if (!video_disable && !subtitle_disable)
st_index[AVMEDIA_TYPE_SUBTITLE] = av_find_best_stream(
ic, AVMEDIA_TYPE_SUBTITLE, st_index[AVMEDIA_TYPE_SUBTITLE],
(st_index[AVMEDIA_TYPE_AUDIO] >= 0 ? st_index[AVMEDIA_TYPE_AUDIO]
: st_index[AVMEDIA_TYPE_VIDEO]),
NULL, 0);

音视频开发之ffplay队列分析

ffplay的整体结构是由5个线程和4个队列组成、运转的(不分析字幕)。

整体结构

音视频开发之ffplay队列分析

5个线程

  • 解复用线程 : read_thread,由主线程创建,负责媒体文件的解复用和读取,读取的数据根据流类型放入到对应的编码数据队列中。
  • 音频解码线程 : audio_thread,由read_thread创建,负责将编码数据队列中的数据解码,放入到原始数据队列中。
  • 视频解码线程 : video_thread,由read_thread创建,负责将编码数据队列中的数据解码,放入到原始数据队列中。
  • 音频渲染线程 : 由SDL创建,负责将音频原始数据队列中的数据发送给音频播放设备。
  • 视频渲染线程 : main线程,负责用视频原始数据队列中的数据不断的更新纹理内容,并刷新显示器进行显示。

4个队列

  • FrameQueue pictq : 视频原始数据队列
  • FrameQueue sampq : 音频原始数据队列
  • PacketQueue audioq : 音频编码数据队列
  • PacketQueue videoq : 视频编码数据队列
PacketQueue

Packet队列是基于链表实现的普通队列,由于编码数据帧比较小,所以这是个无限队列。

1
2
3
4
5
6
7
8
9
10
11
typedef struct PacketQueue {
// Packet队列采用的是链表结构
MyAVPacketList *first_pkt, *last_pkt; // 第一个节点和最后一个节点
int nb_packets; // 队列中节点个数
int size; // 队列中所有节点的字节总数
int64_t duration; // 队列中所有节点的总时长
int abort_request; // 是否退出标记 1 退出,0 不退出
int serial; // 序号
SDL_mutex *mutex; // 锁
SDL_cond *cond; // 互斥量
} PacketQueue;

音视频开发之FFmpeg录制视频

视频录制流程

前面讲解录制音频时讲过,FFmpeg对各大平台的输入和输出API进行了统一的封装,这里就不在阐述。

视频的录制相比音频的录制流程基本差不多,主要是去掉了FIFO队列的逻辑。另外对于视频帧来说,FFmpeg提供的SwsContext API处理较慢,一般不太使用,可以用libyuv库,或者使用OpenGL API用GPU来提速。

0c9c218a544d7c258f06aeded22f3197

打开视频输入

注册视频设备

所有的输入和输出设备默认都是没有注册的,如果需要使用,需要手动调用下面的注册函数。

1
avdevice_register_all();
创建输入上下文

FFmpeg将对视频文件的解封装和封装等操作都抽象到AVFormatContext中,所以视频文件的打开和输出都是要通道AVFormatContext来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Mac平台上依旧使用avfoundation API
AVInputFormat* format = av_find_input_format("avfoundation");
// 视频的采集必须设置采集的分辨率和帧率以及像素格式,如果设置的组合在设备上不支持,会出错
AVDictionary* options = nullptr;
av_dict_set(&options, "video_size", "1280x720", 0);
av_dict_set(&options, "framerate", "30", 0);
av_dict_set(&options, "pixel_format", "nv12", 0);
// 开启输入上下文
if ((error = avformat_open_input(&fmt_ctx_in_, "0", format, &options)) < 0) {
std::cout << "Could not open input device " << av_err2str(error)
<< std::endl;
fmt_ctx_in_ = nullptr;
return error;
}

音视频开发之FFmpeg录制音频

音频录制流程

FFmpeg对各大平台的音频输入和输出的API进行了统一的封装,将输入API封装成AVInputFormat,将输出API封装成AVOutputFormat,并且将音频采集设备封装成一个音频文件进行处理,所以我们在采集音频数据的时候,可以将音频设备当做一个无限大的音频文件,不断的从音频文件中读取数据。

FFmpeg录制音频

打开音频输入

注册音频设备

所有的输入和输出设备默认都是没有注册的,如果需要使用,需要手动调用下面的注册函数。

1
avdevice_register_all();
创建输入上下文

FFmpeg将对音频文件的解封装和封装等操作都抽象到AVFormatContext中,所以音频文件的打开和输出都是要通道AVFormatContext来完成。

1
2
3
4
5
6
7
8
9
10
11
// 设置音频设备API Mac上为avfoundation
AVInputFormat* format = av_find_input_format("avfoundation");
// :0 为音频第一个设备,开启音频设备,FFmpeg中将音频设备封装为一个音频文件
// video:audio 如果视频设备为空,则写成 :音频设备
// 获取到输入上下文
if ((error = avformat_open_input(&fmt_ctx_in_, ":0", format, nullptr)) < 0) {
std::cout << "Could not open input device " << av_err2str(error)
<< std::endl;
fmt_ctx_in_ = nullptr;
return error;
}

FFmpeg深入之ffmpeg_parse_options

简述

1
ffmpeg [global_options] {[input_file_options] -i input_url} ... {[output_file_options] output_url} ...

根据FFmpeg官方文档定义,FFmpeg命令由 全局参数+{输入参数+输入源}+{输出参数+输出流} 组成。其中全局参数可放在任何地方,输入参数必须放在输入源之前。

例:

1
ffmpeg -i video.mp4 -i 1.png -i 1.mp3 -filter_complex "[0:v]scale=750:1334,pad=750:1334:0:0:black[source];[source][1:v]overlay=x=0:y=0[result]" -map "[result]" -map 2:a -s 540x960 -t 7 -shortest -movflags faststart -y output.mp4

那么当FFmpeg接收到上面这样一串字符命令时,是如何解析和处理的?

1
2
3
4
5
6
int main(int argc, char **argv)
{
...
/* parse options and open all input/output files */
ret = ffmpeg_parse_options(argc, argv);
}

在FFmpeg的main方法中可以看到,所有的解析逻辑都在ffmpeg_parse_options函数中,下面我们来一起分析下该函数

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
int ffmpeg_parse_options(int argc, char **argv)
{
OptionParseContext octx;
uint8_t error[128];
int ret;

memset(&octx, 0, sizeof(octx));

/* 解析输入的字符命令 */
ret = split_commandline(&octx, argc, argv, options, groups,
FF_ARRAY_ELEMS(groups));
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error splitting the argument list: ");
goto fail;
}

/* 应用全局参数 */
ret = parse_optgroup(NULL, &octx.global_opts);
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error parsing global options: ");
goto fail;
}

/* configure terminal and setup signal handlers */
term_init();

/* 打开输入文件流 */
ret = open_files(&octx.groups[GROUP_INFILE], "input", open_input_file);
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error opening input files: ");
goto fail;
}

/* 初始化滤镜图 */
ret = init_complex_filters();
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error initializing complex filters.\n");
goto fail;
}

/* 打开输出文件流 */
ret = open_files(&octx.groups[GROUP_OUTFILE], "output", open_output_file);
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error opening output files: ");
goto fail;
}

check_filter_outputs();

fail:
uninit_parse_context(&octx);
if (ret < 0) {
av_strerror(ret, error, sizeof(error));
av_log(NULL, AV_LOG_FATAL, "%s\n", error);
}
return ret;
}

FFmpeg深入之avformat_open_input

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# libavformat/avformat.h

/**
* 读取多媒体文件头信息
* 此时并未开始解码,开启的流通过avformat_close_input()关闭。
*
* @param ps AVFormatContext (通过avformat_alloc_context创建),可以传NULL。
* @param url 多媒体URL
* @param fmt 指定解封装的格式,传NULL时将自动检测.
* @param options 解封装参数自定义,可以传NULL
*
* @return 0 成功
*/
int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);

方法调用流程图

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×