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);
...
}
}

在refresh_loop_wait_event中,会尝试获取SDL事件,如果获取成功,则返回,如果获取失败,则会执行while中的视频刷新操作。SDL事件会优先处理,在没有SDL事件的时候,就会轮询刷新视频

remaining_time的时间默认是0.01,当video_refresh刷新时,如果继续显示当前帧,则remaining_time的值等于当前帧结束的时间差,如果是显示下一帧,则不改变remaining_time的值,依旧是0.01。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
double remaining_time = 0.0;
SDL_PumpEvents();
// 如果获取到事件,则会直接返回,不会进入while
while (
!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)) {
// 如果没有获取到事件,则会尝试更新视频渲染

if (!cursor_hidden &&
av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) {
SDL_ShowCursor(0);
cursor_hidden = 1;
}
// 第二次之后刷新时,就会先睡个一定时间,这个时间由音视频同步机制计算得出
if (remaining_time > 0.0) av_usleep((int64_t)(remaining_time * 1000000.0));
// 默认刷新间隔是0.01s
remaining_time = REFRESH_RATE;
// 如果当前是显示视频模式,并且 当前没有暂停或者当前强制刷新
if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
video_refresh(is, &remaining_time); // 刷新视频
SDL_PumpEvents();
}
}

刷新视频

主要逻辑是计算当前显示的帧是否显示结束,如果显示结束,那么下一帧是否应该显示,根据音视频同步机制,进行丢帧或者对当前帧的结束时间进行缩短和增长。

音视频同步机制就是通过两个时钟的差值,来调整当前显示帧的结束时间,也就是frame_timer的值,进而调整remaining_time的值,也就是线程等待的时间。当视频时钟慢,则减小当前帧显示的时间,当视频时钟快,则加大当前帧显示的时间。

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
static void video_refresh(void *opaque, double *remaining_time) {
VideoState *is = opaque;
double time;

Frame *sp, *sp2;
...

if (is->video_st) {
retry:
if (frame_queue_nb_remaining(&is->pictq) == 0) {
// 如果当前Frame队列中没有数据,则什么也不干
// nothing to do, no picture to display in the queue
} else {
double last_duration, duration, delay;
Frame *vp, *lastvp;

/* dequeue the picture */
// 获取当前已经在显示的帧,可以认为是上一帧
lastvp = frame_queue_peek_last(&is->pictq);
// 获取当前帧
vp = frame_queue_peek(&is->pictq);
// 如果当前帧的序号不符合,则丢弃
if (vp->serial != is->videoq.serial) {
// 读取移动到下一位,并重试
frame_queue_next(&is->pictq);
goto retry;
}
// 如果上一帧与当前帧的序号不符合,重新更新播放时间frame_timer为当前系统时间
if (lastvp->serial != vp->serial)
is->frame_timer = av_gettime_relative() / 1000000.0;
// 如果暂停了,则继续显示当前显示的帧
if (is->paused) goto display;

/* compute nominal last_duration */
// 获取上一帧要显示的时长
last_duration = vp_duration(is, lastvp, vp);
// 计算音视频同步的延迟时差
delay = compute_target_delay(last_duration, is);

time = av_gettime_relative() / 1000000.0;
// frame_timer 当前显示的帧要结束的时间
// frame_timer + delay = 当前显示的帧真正要结束的时间
// 如果上一帧还没有结束,则继续显示上一帧
if (time < is->frame_timer + delay) {
// frame_timer + delay - time 就是当前显示的帧结束的时间与当前时间的差值,也就是线程要wait的时间
*remaining_time =
FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display;
}
// 如果上一帧帧的显示时间已经过去了

// 更新frame_timer,指向的是当前显示的帧结束的时间
is->frame_timer += delay;
if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
is->frame_timer = time; // 与系统时间偏离太大,则更新为系统时间

SDL_LockMutex(is->pictq.mutex);
if (!isnan(vp->pts)) update_video_pts(is, vp->pts, vp->pos, vp->serial);
SDL_UnlockMutex(is->pictq.mutex);
// 如果队列中还有剩余的帧
if (frame_queue_nb_remaining(&is->pictq) > 1) {
// 获取下一个帧
Frame *nextvp = frame_queue_peek_next(&is->pictq);
// 计算当前帧的显示时长
duration = vp_duration(is, vp, nextvp);
// time > is->frame_timer + duration
// 如果下一帧的时间还没有过去,则丢弃当前帧,队列读索引向后移动一位,重试
if (!is->step &&
(framedrop > 0 ||
(framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) &&
time > is->frame_timer + duration) {
is->frame_drops_late++;
frame_queue_next(&is->pictq);
goto retry;
}
}

...

// 队列向后移动一位
frame_queue_next(&is->pictq);
// 强制刷新
is->force_refresh = 1;

if (is->step && !is->paused) stream_toggle_pause(is);
}
display:
/* display picture */
// 显示当前帧
if (!display_disable && is->force_refresh &&
is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
video_display(is);
}
is->force_refresh = 0;
...
}

显示视频

更新纹理数据,刷新屏幕

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void video_display(VideoState *is) {
if (!is->width) video_open(is);
// 设置颜色 黑色
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
// 使用黑色清空缓冲区
SDL_RenderClear(renderer);

if (is->audio_st && is->show_mode != SHOW_MODE_VIDEO)
video_audio_display(is); // 如果是音频播放模式,显示音频声波图
else if (is->video_st)
video_image_display(is);// 更新纹理数据
// 将渲染的内容刷新到屏幕上
SDL_RenderPresent(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
static void video_image_display(VideoState *is) {
Frame *vp;
Frame *sp = NULL;
SDL_Rect rect;
// 从视频原始数据队列中取出一帧,这里没有调用frame_queue_peek
// 是因为更新纹理有两种情况:一种是继续显示当前帧,这种使用frame_queue_peek_last正好,另一种是显示下一帧,在这种情况下,会提前调用frame_queue_next移动,所以这里直接调用frame_queue_peek_last,不管哪种情况都是合适的。
vp = frame_queue_peek_last(&is->pictq);

...

// 计算显示的矩阵
calculate_display_rect(&rect, is->xleft, is->ytop, is->width, is->height,
vp->width, vp->height, vp->sar);
// 重复显示当前在显示的帧时,没必要重复上传数据到纹理中
if (!vp->uploaded) {
// 如果视频帧还有没更新到纹理上
if (upload_texture(&is->vid_texture, vp->frame, &is->img_convert_ctx) < 0)
return;
// 标记已经更新
vp->uploaded = 1;
// TODO
vp->flip_v = vp->frame->linesize[0] < 0;
}
// 设置YUV转换RGB的转换公式
set_sdl_yuv_conversion_mode(vp->frame);
// 将纹理上的内容拷贝到渲染器的默认渲染目标上
SDL_RenderCopyEx(renderer, is->vid_texture, NULL, &rect, 0, NULL,
vp->flip_v ? SDL_FLIP_VERTICAL : 0);
set_sdl_yuv_conversion_mode(NULL);

...
}

音频渲染线程

音频渲染线程是由SDL创建的,会在该线程中执行我们设置的回调函数。

回调函数

在回调函数中主要处理两件事,一件就是从FrameQueue中取出len长度的数据放入到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
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
/**
* 音频渲染线程
*/
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len) {
VideoState *is = opaque;
int audio_size, len1;

audio_callback_time = av_gettime_relative();

while (len > 0) {
// 如果audio_buf中数据已经全部读完,则说明需要加入更多的数据
if (is->audio_buf_index >= is->audio_buf_size) {
// 获取buffer到 is->audio_buf 中,返回获取的buffer大小
audio_size = audio_decode_frame(is);
if (audio_size < 0) {
/* if error, just output silence */
is->audio_buf = NULL;
is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE /
is->audio_tgt.frame_size *
is->audio_tgt.frame_size;
} else {
if (is->show_mode != SHOW_MODE_VIDEO)
update_sample_display(is, (int16_t *)is->audio_buf, audio_size);
// 读取的buffer大小
is->audio_buf_size = audio_size;
}
// 已经读取的buffer位置重置
is->audio_buf_index = 0;
}
// 计算还未读取的字节大小
len1 = is->audio_buf_size - is->audio_buf_index;
// 如果为读取的字节大小大于音频设备想要的大小,则使用音频设备想要的大小
if (len1 > len) len1 = len;
// 将audio_buf中的数据copy到stream中
if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)
memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
else {
memset(stream, 0, len1);
if (!is->muted && is->audio_buf)
SDL_MixAudioFormat(stream,
(uint8_t *)is->audio_buf + is->audio_buf_index,
AUDIO_S16SYS, len1, is->audio_volume);
}
// 减去已经传递给音频设备的buffer大小,如果剩下的还有,则再次读取传递
len -= len1;
// stream加上偏移量
stream += len1;
// 更新audio_buf的偏移量
is->audio_buf_index += len1;
}

// 记录audio_buf中还剩余的大小
is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;
/* Let's assume the audio driver that is used by SDL has two periods. */
// 更新音频时钟
if (!isnan(is->audio_clock)) {
// 音频时钟中保存的是当前audio_buf开始的时间+时长,也就是结束的时间
// 真正播放的准确时间应该要减去缓冲区中未使用的数据的时长,才是真正当前播放的时长
// 缓冲区中未使用的字节大小 = 2 * is->audio_hw_buf_size 音频设备的缓冲区大小 + is->audio_write_buf_size audio_buf中还剩余的大小
// 缓冲区中未使用的时长 = 缓冲区中未使用的字节大小 / (音频设备)1s数据的字节大小
// 音频时钟准确时间 = 当前音频包结束时长 - 音频缓冲中剩余的数据时长
set_clock_at(&is->audclk,
is->audio_clock - (double)(2 * is->audio_hw_buf_size +
is->audio_write_buf_size) /
is->audio_tgt.bytes_per_sec,
is->audio_clock_serial, audio_callback_time / 1000000.0);
sync_clock_to_slave(&is->extclk, &is->audclk);
}
}

重采样

如果音频数据参数与开启音频设备的参数不一致,那么是无法播放的,需要进行重采样

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
static int audio_decode_frame(VideoState *is) {
int data_size, resampled_data_size;
int64_t dec_channel_layout;
av_unused double audio_clock0;
int wanted_nb_samples;
Frame *af;

if (is->paused) return -1;
// 循环从音频原始数据列表中获取一个可读帧,如果帧的序号与音频编码数据队列中的序号不一样,则丢帧
do {
...
// 从音频原始数据列表中获取一个可读帧
if (!(af = frame_queue_peek_readable(&is->sampq))) return -1;
// 队列移动到下一个可读帧
frame_queue_next(&is->sampq);
} while (af->serial != is->audioq.serial);
// 根据音频参数计算一帧的字节大小
data_size = av_samples_get_buffer_size(
NULL, af->frame->channels, af->frame->nb_samples, af->frame->format, 1);

dec_channel_layout =
(af->frame->channel_layout &&
af->frame->channels ==
av_get_channel_layout_nb_channels(af->frame->channel_layout))
? af->frame->channel_layout
: av_get_default_channel_layout(af->frame->channels);
wanted_nb_samples = synchronize_audio(is, af->frame->nb_samples);
// 如果音频数据的参数发生了变化, audio_src 默认是音频设备开启时的参数
// 只有当音频数据参数发生了变化才会重新创建重采样上下文
if (af->frame->format != is->audio_src.fmt ||
dec_channel_layout != is->audio_src.channel_layout ||
af->frame->sample_rate != is->audio_src.freq ||
(wanted_nb_samples != af->frame->nb_samples && !is->swr_ctx)) {
swr_free(&is->swr_ctx);
// 创建重采样上下文,将音频重采样成音频设备开启的参数
is->swr_ctx = swr_alloc_set_opts(NULL, is->audio_tgt.channel_layout,
is->audio_tgt.fmt, is->audio_tgt.freq,
dec_channel_layout, af->frame->format,
af->frame->sample_rate, 0, NULL);

...

// 记录当前帧的参数作为音频源的参数,如果后面的帧的参数发生了变化,则会重新创建重采样上下文
is->audio_src.channel_layout = dec_channel_layout;
is->audio_src.channels = af->frame->channels;
is->audio_src.freq = af->frame->sample_rate;
is->audio_src.fmt = af->frame->format;
}
// 重采样
if (is->swr_ctx) {
// 输入
const uint8_t **in = (const uint8_t **)af->frame->extended_data;
// 输出
uint8_t **out = &is->audio_buf1;
// 计算重采样后AVFrame中具体有几帧数据
int out_count = (int64_t)wanted_nb_samples * is->audio_tgt.freq /
af->frame->sample_rate +
256;
// 计算重采样后的数据大小
int out_size = av_samples_get_buffer_size(NULL, is->audio_tgt.channels,
out_count, is->audio_tgt.fmt, 0);
int len2;
if (out_size < 0) {
av_log(NULL, AV_LOG_ERROR, "av_samples_get_buffer_size() failed\n");
return -1;
}
// 如果想要的帧数和AVFrame中的帧数不符合
if (wanted_nb_samples != af->frame->nb_samples) {
if (swr_set_compensation(is->swr_ctx,
(wanted_nb_samples - af->frame->nb_samples) *
is->audio_tgt.freq / af->frame->sample_rate,
wanted_nb_samples * is->audio_tgt.freq /
af->frame->sample_rate) < 0) {
av_log(NULL, AV_LOG_ERROR, "swr_set_compensation() failed\n");
return -1;
}
}
// 分配输出buffer的内存
av_fast_malloc(&is->audio_buf1, &is->audio_buf1_size, out_size);
if (!is->audio_buf1) return AVERROR(ENOMEM);
// 重采样
len2 = swr_convert(is->swr_ctx, out, out_count, in, af->frame->nb_samples);
if (len2 < 0) {
av_log(NULL, AV_LOG_ERROR, "swr_convert() failed\n");
return -1;
}
if (len2 == out_count) {
av_log(NULL, AV_LOG_WARNING, "audio buffer is probably too small\n");
if (swr_init(is->swr_ctx) < 0) swr_free(&is->swr_ctx);
}
// 获取buffer
is->audio_buf = is->audio_buf1;
// 计算重采样后数据大小
resampled_data_size = len2 * is->audio_tgt.channels *
av_get_bytes_per_sample(is->audio_tgt.fmt);
} else {
// 不需要重采样,则直接使用AVFrame中的data
// 获取buffer
is->audio_buf = af->frame->data[0];
// 获取buffer大小
resampled_data_size = data_size;
}

audio_clock0 = is->audio_clock;
/* update the audio clock with the pts */
// 更新音频时钟,音频时钟 = 当前帧的PTS + (样本数量 / 每秒采样率) 也就是这些数据时长是多少秒
if (!isnan(af->pts))
is->audio_clock =
af->pts + (double)af->frame->nb_samples / af->frame->sample_rate;
else
is->audio_clock = NAN;
// 更新音频时钟序号
is->audio_clock_serial = af->serial;

return resampled_data_size;
}