热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

ffplay源码分析4音视频同步

ffplay是FFmpeg工程自带的简单播放器,使用FFmpeg提供的解码器和SDL库进行视频播放。本文基于FFmpeg工程4.1版本进行分析,其中ff

ffplay是FFmpeg工程自带的简单播放器,使用FFmpeg提供的解码器和SDL库进行视频播放。本文基于FFmpeg工程4.1版本进行分析,其中ffplay源码清单如下:
https://github.com/FFmpeg/FFmpeg/blob/n4.1/fftools/ffplay.c

在尝试分析源码前,可先阅读如下参考文章作为铺垫:
[1]. 雷霄骅,视音频编解码技术零基础学习方法
[2]. 视频编解码基础概念
[3]. 色彩空间与像素格式
[4]. 音频参数解析
[5]. FFmpeg基础概念

“ffplay源码分析”系列文章如下:
[1]. ffplay源码分析1-概述
[2]. ffplay源码分析2-数据结构
[3]. ffplay源码分析3-代码框架
[4]. ffplay源码分析4-音视频同步
[5]. ffplay源码分析5-图像格式转换
[6]. ffplay源码分析6-音频重采样
[7]. ffplay源码分析7-播放控制

4. 音视频同步

音视频同步的目的是为了使播放的声音和显示的画面保持一致。视频按帧播放,图像显示设备每次显示一帧画面,视频播放速度由帧率确定,帧率指示每秒显示多少帧;音频按采样点播放,声音播放设备每次播放一个采样点,声音播放速度由采样率确定,采样率指示每秒播放多少个采样点。如果仅仅是视频按帧率播放,音频按采样率播放,二者没有同步机制,即使最初音视频是基本同步的,随着时间的流逝,音视频会逐渐失去同步,并且不同步的现象会越来越严重。这是因为:一、播放时间难以精确控制,二、异常及误差会随时间累积。所以,必须要采用一定的同步策略,不断对音视频的时间差作校正,使图像显示与声音播放总体保持一致。

我们以一个44.1KHz的AAC音频流和25FPS的H264视频流为例,来看一下理想情况下音视频的同步过程:
一个AAC音频frame每个声道包含1024个采样点(也可能是2048,参“FFmpeg关于nb_smples,frame_size以及profile的解释”),则一个frame的播放时长(duration)为:(1024/44100)×1000ms = 23.22ms;一个H264视频frame播放时长(duration)为:1000ms/25 = 40ms。声卡虽然是以音频采样点为播放单位,但通常我们每次往声卡缓冲区送一个音频frame,每送一个音频frame更新一下音频的播放时刻,即每隔一个音频frame时长更新一下音频时钟,实际上ffplay就是这么做的。我们暂且把一个音频时钟更新点记作其播放点,理想情况下,音视频完全同步,音视频播放过程如下图所示:

 

音视频同步的方式基本是确定一个时钟(音频时钟、视频时钟、外部时钟)作为主时钟,非主时钟的音频或视频时钟为从时钟。在播放过程中,主时钟作为同步基准,不断判断从时钟与主时钟的差异,调节从时钟,使从时钟追赶(落后时)或等待(超前时)主时钟。按照主时钟的不同种类,可以将音视频同步模式分为如下三种:
音频同步到视频,视频时钟作为主时钟。
视频同步到音频,音频时钟作为主时钟。
音视频同步到外部时钟,外部时钟作为主时钟。
ffplay中同步模式的定义如下:

1
2
3
4
5
enum {AV_SYNC_AUDIO_MASTER, /* default choice */AV_SYNC_VIDEO_MASTER,AV_SYNC_EXTERNAL_CLOCK, /* synchronize to an external clock */
};

4.1 time_base

time_base是PTS和DTS的时间单位,也称时间基。不同的封装格式time_base不一样,转码过程中的不同阶段time_base也不一样。以mpegts封装格式为例,假设视频帧率为25FPS。编码数据包packet(数据结构AVPacket)的time_base为AVRational{1,90000},这个是容器层的time_base,定义在AVStream结构体中。原始数据帧frame(数据结构AVFrame)的time_base为AVRational{1,25},这个是视频层的time_base,是帧率的倒数,定义在AVCodecContext结构体中。time_base的类型是AVRational,表示一个分数,例如AVRational{1,25}表示值为1/25(单位是秒)。

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
typedef struct AVStream {....../*** This is the fundamental unit of time (in seconds) in terms* of which frame timestamps are represented.** decoding: set by libavformat* encoding: May be set by the caller before avformat_write_header() to* provide a hint to the muxer about the desired timebase. In* avformat_write_header(), the muxer will overwrite this field* with the timebase that will actually be used for the timestamps* written into the file (which may or may not be related to the* user-provided one, depending on the format).*/AVRational time_base;......
}typedef struct AVCodecContext {....../*** This is the fundamental unit of time (in seconds) in terms* of which frame timestamps are represented. For fixed-fps content,* timebase should be 1/framerate and timestamp increments should be* identically 1.* This often, but not always is the inverse of the frame rate or field rate* for video. 1/time_base is not the average frame rate if the frame rate is not* constant.** Like containers, elementary streams also can store timestamps, 1/time_base* is the unit in which these timestamps are specified.* As example of such codec time base see ISO/IEC 14496-2:2001(E)* vop_time_increment_resolution and fixed_vop_rate* (fixed_vop_rate == 0 implies that it is different from the framerate)** - encoding: MUST be set by user.* - decoding: the use of this field for decoding is deprecated.* Use framerate instead.*/AVRational time_base;......
}/*** Rational number (pair of numerator and denominator).*/
typedef struct AVRational{int num; ///} AVRational;

time_base是一个分数,av_q2d(time_base)则可将分数转换为对应的double类型数。因此有如下计算:

1
2
3
AVStream *st;
double duration_of_stream = st->duration * av_q2d(st->time_base); // 视频流播放时长
double pts_of_frame = frame->pts * av_q2d(st->time_base); // 视频帧显示时间戳

4.2 PTS/DTS/解码过程

DTS(Decoding Time Stamp, 解码时间戳),表示压缩帧的解码时间。
PTS(Presentation Time Stamp, 显示时间戳),表示将压缩帧解码后得到的原始帧的显示时间。
音频中DTS和PTS是相同的。视频中由于B帧需要双向预测,B帧依赖于其前和其后的帧,因此含B帧的视频解码顺序与显示顺序不同,即DTS与PTS不同。当然,不含B帧的视频,其DTS和PTS是相同的。下图以一个开放式GOP示意图为例,说明视频流的解码顺序和显示顺序

采集顺序指图像传感器采集原始信号得到图像帧的顺序。
编码顺序指编码器编码后图像帧的顺序。存储到磁盘的本地视频文件中图像帧的顺序与编码顺序相同。
传输顺序指编码后的流在网络中传输过程中图像帧的顺序。
解码顺序指解码器解码图像帧的顺序。
显示顺序指图像帧在显示器上显示的顺序。
采集顺序与显示顺序相同。编码顺序、传输顺序和解码顺序相同。
以图中“B[1]”帧为例进行说明,“B[1]”帧解码时需要参考“I[0]”帧和“P[3]”帧,因此“P[3]”帧必须比“B[1]”帧先解码。这就导致了解码顺序和显示顺序的不一致,后显示的帧需要先解码。

上述内容可参考“视频编解码基础概念”。

理解了含B帧视频流解码顺序与显示顺序的不同,才容易理解解码函数decoder_decode_frame()中对视频解码的处理:
avcodec_send_packet()按解码顺序发送packet。
avcodec_receive_frame()按显示顺序输出frame。
这个过程由解码器处理,不需要用户程序费心。
decoder_decode_frame()是非常核心的一个函数,代码本身并不难理解。decoder_decode_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
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
// 从packet_queue中取一个packet,解码生成frame
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {int ret &#61; AVERROR(EAGAIN);for (;;) {AVPacket pkt;// 本函数被各解码线程(音频、视频、字幕)首次调用时&#xff0c;d->pkt_serial等于-1&#xff0c;d->queue->serial等于1if (d->queue->serial &#61;&#61; d->pkt_serial) {do {if (d->queue->abort_request)return -1;// 3. 从解码器接收frameswitch (d->avctx->codec_type) {case AVMEDIA_TYPE_VIDEO:// 3.1 一个视频packet含一个视频frame// 解码器缓存一定数量的packet后&#xff0c;才有解码后的frame输出// frame输出顺序是按pts的顺序&#xff0c;如IBBPBBP// frame->pkt_pos变量是此frame对应的packet在视频文件中的偏移地址&#xff0c;值同pkt.posret &#61; avcodec_receive_frame(d->avctx, frame);if (ret >&#61; 0) {if (decoder_reorder_pts &#61;&#61; -1) {frame->pts &#61; frame->best_effort_timestamp;} else if (!decoder_reorder_pts) {frame->pts &#61; frame->pkt_dts;}}break;case AVMEDIA_TYPE_AUDIO:// 3.2 一个音频packet含多个音频frame&#xff0c;每次avcodec_receive_frame()返回一个frame&#xff0c;此函数返回。// 下次进来此函数&#xff0c;继续获取一个frame&#xff0c;直到avcodec_receive_frame()返回AVERROR(EAGAIN)&#xff0c;// 表示解码器需要填入新的音频packetret &#61; avcodec_receive_frame(d->avctx, frame);if (ret >&#61; 0) {AVRational tb &#61; (AVRational){1, frame->sample_rate};if (frame->pts !&#61; AV_NOPTS_VALUE)frame->pts &#61; av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb);else if (d->next_pts !&#61; AV_NOPTS_VALUE)frame->pts &#61; av_rescale_q(d->next_pts, d->next_pts_tb, tb);if (frame->pts !&#61; AV_NOPTS_VALUE) {d->next_pts &#61; frame->pts &#43; frame->nb_samples;d->next_pts_tb &#61; tb;}}break;}if (ret &#61;&#61; AVERROR_EOF) {d->finished &#61; d->pkt_serial;avcodec_flush_buffers(d->avctx);return 0;}if (ret >&#61; 0)return 1; // 成功解码得到一个视频帧或一个音频帧&#xff0c;则返回} while (ret !&#61; AVERROR(EAGAIN));}do {if (d->queue->nb_packets &#61;&#61; 0) // packet_queue为空则等待SDL_CondSignal(d->empty_queue_cond);if (d->packet_pending) { // 有未处理的packet则先处理av_packet_move_ref(&pkt, &d->pkt);d->packet_pending &#61; 0;} else {// 1. 取出一个packet。使用pkt对应的serial赋值给d->pkt_serialif (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) <0)return -1;}} while (d->queue->serial !&#61; d->pkt_serial);// packet_queue中第一个总是flush_pkt。每次seek操作会插入flush_pkt&#xff0c;更新serial&#xff0c;开启新的播放序列if (pkt.data &#61;&#61; flush_pkt.data) {// 复位解码器内部状态/刷新内部缓冲区。当seek操作或切换流时应调用此函数。avcodec_flush_buffers(d->avctx);d->finished &#61; 0;d->next_pts &#61; d->start_pts;d->next_pts_tb &#61; d->start_pts_tb;} else {if (d->avctx->codec_type &#61;&#61; AVMEDIA_TYPE_SUBTITLE) {int got_frame &#61; 0;ret &#61; avcodec_decode_subtitle2(d->avctx, sub, &got_frame, &pkt);if (ret <0) {ret &#61; AVERROR(EAGAIN);} else {if (got_frame && !pkt.data) {d->packet_pending &#61; 1;av_packet_move_ref(&d->pkt, &pkt);}ret &#61; got_frame ? 0 : (pkt.data ? AVERROR(EAGAIN) : AVERROR_EOF);}} else {// 2. 将packet发送给解码器// 发送packet的顺序是按dts递增的顺序&#xff0c;如IPBBPBB// pkt.pos变量可以标识当前packet在视频文件中的地址偏移if (avcodec_send_packet(d->avctx, &pkt) &#61;&#61; AVERROR(EAGAIN)) {av_log(d->avctx, AV_LOG_ERROR, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");d->packet_pending &#61; 1;av_packet_move_ref(&d->pkt, &pkt);}}av_packet_unref(&pkt);}}
}

本函数实现如下功能&#xff1a;
[1]. 从视频packet队列中取一个packet
[2]. 将取得的packet发送给解码器
[3]. 从解码器接收解码后的frame&#xff0c;此frame作为函数的输出参数供上级函数处理

注意如下几点&#xff1a;
[1]. 含B帧的视频文件&#xff0c;其视频帧存储顺序与显示顺序不同
[2]. 解码器的输入是packet队列&#xff0c;视频帧解码顺序与存储顺序相同&#xff0c;是按dts递增的顺序。dts是解码时间戳&#xff0c;因此存储顺序解码顺序都是dts递增的顺序。avcodec_send_packet()就是将视频文件中的packet序列依次发送给解码器。发送packet的顺序如IPBBPBB。
[3]. 解码器的输出是frame队列&#xff0c;frame输出顺序是按pts递增的顺序。pts是解码时间戳。pts与dts不一致的问题由解码器进行了处理&#xff0c;用户程序不必关心。从解码器接收frame的顺序如IBBPBBP。
[4]. 解码器中会缓存一定数量的帧&#xff0c;一个新的解码动作启动后&#xff0c;向解码器送入好几个packet解码器才会输出第一个packet&#xff0c;这比较容易理解&#xff0c;因为解码时帧之间有信赖关系&#xff0c;例如IPB三个帧被送入解码器后&#xff0c;B帧解码需要依赖I帧和P帧&#xff0c;所在在B帧输出前&#xff0c;I帧和P帧必须存在于解码器中而不能删除。理解了这一点&#xff0c;后面视频frame队列中对视频帧的显示和删除机制才容易理解。
[5]. 解码器中缓存的帧可以通过冲洗(flush)解码器取出。冲洗(flush)解码器的方法就是调用avcodec_send_packet(..., NULL)&#xff0c;然后多次调用avcodec_receive_frame()将缓存帧取尽。缓存帧取完后&#xff0c;avcodec_receive_frame()返回AVERROR_EOF。ffplay中&#xff0c;是通过向解码器发送flush_pkt(实际为NULL)&#xff0c;每次seek操作都会向解码器发送flush_pkt。

如何确定解码器的输出frame与输入packet的对应关系呢&#xff1f;可以对比frame->pkt_pos和pkt.pos的值&#xff0c;这两个值表示packet在视频文件中的偏移地址&#xff0c;如果这两个变量值相等&#xff0c;表示此frame来自此packet。调试跟踪这两个变量值&#xff0c;即能发现解码器输入帧与输出帧的关系。为简便&#xff0c;就不贴图了。

4.3 视频同步到音频

视频同步到音频是ffplay的默认同步方式。在视频播放线程中实现。视频播放函数video_refresh()实现了视频显示(包含同步控制)&#xff0c;是非常核心的一个函数&#xff0c;理解起来也有些难度。这个函数的调用过程如下&#xff1a;

1
2
3
4
main() -->
event_loop() -->
refresh_loop_wait_event() -->
video_refresh()

函数实现如下&#xff1a;

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
/* called to display each frame */
static void video_refresh(void *opaque, double *remaining_time)
{VideoState *is &#61; opaque;double time;Frame *sp, *sp2;if (!is->paused && get_master_sync_type(is) &#61;&#61; AV_SYNC_EXTERNAL_CLOCK && is->realtime)check_external_clock_speed(is);// 音频波形图显示if (!display_disable && is->show_mode !&#61; SHOW_MODE_VIDEO && is->audio_st) {time &#61; av_gettime_relative() / 1000000.0;if (is->force_refresh || is->last_vis_time &#43; rdftspeed last_vis_time &#61; time;}*remaining_time &#61; FFMIN(*remaining_time, is->last_vis_time &#43; rdftspeed - time);}// 视频播放if (is->video_st) {
retry:if (frame_queue_nb_remaining(&is->pictq) &#61;&#61; 0) { // 所有帧已显示// nothing to do, no picture to display in the queue} else { // 有未显示帧double last_duration, duration, delay;Frame *vp, *lastvp;/* dequeue the picture */lastvp &#61; frame_queue_peek_last(&is->pictq); // 上一帧&#xff1a;上次已显示的帧vp &#61; frame_queue_peek(&is->pictq); // 当前帧&#xff1a;当前待显示的帧if (vp->serial !&#61; is->videoq.serial) {frame_queue_next(&is->pictq);goto retry;}// lastvp和vp不是同一播放序列(一个seek会开始一个新播放序列)&#xff0c;将frame_timer更新为当前时间if (lastvp->serial !&#61; vp->serial)is->frame_timer &#61; av_gettime_relative() / 1000000.0;// 暂停处理&#xff1a;不停播放上一帧图像if (is->paused)goto display;/* compute nominal last_duration */last_duration &#61; vp_duration(is, lastvp, vp); // 上一帧播放时长&#xff1a;vp->pts - lastvp->ptsdelay &#61; compute_target_delay(last_duration, is); // 根据视频时钟和同步时钟的差值&#xff0c;计算delay值time&#61; av_gettime_relative()/1000000.0;// 当前帧播放时刻(is->frame_timer&#43;delay)大于当前时刻(time)&#xff0c;表示播放时刻未到if (time frame_timer &#43; delay) {// 播放时刻未到&#xff0c;则更新刷新时间remaining_time为当前时刻到下一播放时刻的时间差*remaining_time &#61; FFMIN(is->frame_timer &#43; delay - time, *remaining_time);// 播放时刻未到&#xff0c;则不更新rindex&#xff0c;把上一帧再lastvp再播放一遍goto display;}// 更新frame_timer值is->frame_timer &#43;&#61; delay;// 校正frame_timer值&#xff1a;若frame_timer落后于当前系统时间太久(超过最大同步域值)&#xff0c;则更新为当前系统时间if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)is->frame_timer &#61; time;SDL_LockMutex(is->pictq.mutex);if (!isnan(vp->pts))update_video_pts(is, vp->pts, vp->pos, vp->serial); // 更新视频时钟&#xff1a;时间戳、时钟时间SDL_UnlockMutex(is->pictq.mutex);// 是否要丢弃未能及时播放的视频帧if (frame_queue_nb_remaining(&is->pictq) > 1) { // 队列中未显示帧数>1(只有一帧则不考虑丢帧)Frame *nextvp &#61; frame_queue_peek_next(&is->pictq); // 下一帧&#xff1a;下一待显示的帧duration &#61; vp_duration(is, vp, nextvp); // 当前帧vp播放时长 &#61; nextvp->pts - vp->pts// 1. 非步进模式&#xff1b;2. 丢帧策略生效&#xff1b;3. 当前帧vp未能及时播放&#xff0c;即下一帧播放时刻(is->frame_timer&#43;duration)小于当前系统时刻(time)if(!is->step && (framedrop>0 || (framedrop && get_master_sync_type(is) !&#61; AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer &#43; duration){is->frame_drops_late&#43;&#43;; // framedrop丢帧处理有两处&#xff1a;1) packet入队列前&#xff0c;2) frame未及时显示(此处)frame_queue_next(&is->pictq); // 删除上一帧已显示帧&#xff0c;即删除lastvp&#xff0c;读指针加1(从lastvp更新到vp)goto retry;}}// 字幕播放......// 删除当前读指针元素&#xff0c;读指针&#43;1。若未丢帧&#xff0c;读指针从lastvp更新到vp&#xff1b;若有丢帧&#xff0c;读指针从vp更新到nextvpframe_queue_next(&is->pictq);is->force_refresh &#61; 1;if (is->step && !is->paused)stream_toggle_pause(is);}
display:/* display picture */if (!display_disable && is->force_refresh && is->show_mode &#61;&#61; SHOW_MODE_VIDEO && is->pictq.rindex_shown)video_display(is); // 取出当前帧vp(若有丢帧是nextvp)进行播放}is->force_refresh &#61; 0;if (show_status) { // 更新显示播放状态......}
}

视频同步到音频的基本方法是&#xff1a;如果视频超前音频&#xff0c;则不进行播放&#xff0c;以等待音频&#xff1b;如果视频落后音频&#xff0c;则丢弃当前帧直接播放下一帧&#xff0c;以追赶音频。
此函数执行流程参考如下流程图&#xff1a;

步骤如下&#xff1a;
[1] 根据上一帧lastvp的播放时长duration&#xff0c;校正等到delay值&#xff0c;duration是上一帧理想播放时长&#xff0c;delay是上一帧实际播放时长&#xff0c;根据delay值可以计算得到当前帧的播放时刻
[2] 如果当前帧vp播放时刻未到&#xff0c;则继续显示上一帧lastvp&#xff0c;并将延时值remaining_time作为输出参数供上级调用函数处理
[3] 如果当前帧vp播放时刻已到&#xff0c;则立即显示当前帧&#xff0c;并更新读指针

在video_refresh()函数中&#xff0c;调用了compute_target_delay()来根据视频时钟与主时钟的差异来调节delay值&#xff0c;从而调节视频帧播放的时刻。

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
// 根据视频时钟与同步时钟(如音频时钟)的差值&#xff0c;校正delay值&#xff0c;使视频时钟追赶或等待同步时钟
// 输入参数delay是上一帧播放时长&#xff0c;即上一帧播放后应延时多长时间后再播放当前帧&#xff0c;通过调节此值来调节当前帧播放快慢
// 返回值delay是将输入参数delay经校正后得到的值
static double compute_target_delay(double delay, VideoState *is)
{double sync_threshold, diff &#61; 0;/* update delay to follow master synchronisation source */if (get_master_sync_type(is) !&#61; AV_SYNC_VIDEO_MASTER) {/* if video is slave, we try to correct big delays byduplicating or deleting a frame */// 视频时钟与同步时钟(如音频时钟)的差异&#xff0c;时钟值是上一帧pts值(实为&#xff1a;上一帧pts &#43; 上一帧至今流逝的时间差)diff &#61; get_clock(&is->vidclk) - get_master_clock(is);// delay是上一帧播放时长&#xff1a;当前帧(待播放的帧)播放时间与上一帧播放时间差理论值// diff是视频时钟与同步时钟的差值/* skip or repeat frame. We take into account thedelay to compute the threshold. I still don&#39;t knowif it is the best guess */// 若delay AV_SYNC_THRESHOLD_MAX&#xff0c;则同步域值为AV_SYNC_THRESHOLD_MAX// 若AV_SYNC_THRESHOLD_MIN max_frame_duration) {if (diff <&#61; -sync_threshold) // 视频时钟落后于同步时钟&#xff0c;且超过同步域值delay &#61; FFMAX(0, delay &#43; diff); // 当前帧播放时刻落后于同步时钟(delay&#43;diff<0)则delay&#61;0(视频追赶&#xff0c;立即播放)&#xff0c;否则delay&#61;delay&#43;diffelse if (diff >&#61; sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD) // 视频时钟超前于同步时钟&#xff0c;且超过同步域值&#xff0c;但上一帧播放时长超长delay &#61; delay &#43; diff; // 仅仅校正为delay&#61;delay&#43;diff&#xff0c;主要是AV_SYNC_FRAMEDUP_THRESHOLD参数的作用&#xff0c;不作同步补偿else if (diff >&#61; sync_threshold) // 视频时钟超前于同步时钟&#xff0c;且超过同步域值delay &#61; 2 * delay; // 视频播放要放慢脚步&#xff0c;delay扩大至2倍}}av_log(NULL, AV_LOG_TRACE, "video: delay&#61;%0.3f A-V&#61;%f\n",delay, -diff);return delay;
}

compute_target_delay()的输入参数delay是上一帧理想播放时长duration&#xff0c;返回值delay是经校正后的上一帧实际播放时长。为方便描述&#xff0c;下面我们将输入参数记作duration(对应函数的输入参数delay)&#xff0c;返回值记作delay(对应函数返回值delay)。
本函数实现功能如下&#xff1a;
[1] 计算视频时钟与音频时钟(主时钟)的偏差diff&#xff0c;实际就是视频上一帧pts减去音频上一帧pts。所谓上一帧&#xff0c;就是已经播放的最后一帧&#xff0c;上一帧的pts可以标识视频流/音频流的播放时刻(进度)。
[2] 计算同步域值sync_threshold&#xff0c;同步域值的作用是&#xff1a;若视频时钟与音频时钟差异值小于同步域值&#xff0c;则认为音视频是同步的&#xff0c;不校正delay&#xff1b;若差异值大于同步域值&#xff0c;则认为音视频不同步&#xff0c;需要校正delay值。
同步域值的计算方法如下&#xff1a;
若duration 若duration > AV_SYNC_THRESHOLD_MAX&#xff0c;则同步域值为AV_SYNC_THRESHOLD_MAX
若AV_SYNC_THRESHOLD_MIN [3] delay校正策略如下&#xff1a;
a) 视频时钟落后于同步时钟且落后值超过同步域值&#xff1a;
a1) 若当前帧播放时刻落后于同步时钟(delay&#43;diff<0)则delay&#61;0(视频追赶&#xff0c;立即播放)&#xff1b;
a2) 否则delay&#61;duration&#43;diff
b) 视频时钟超前于同步时钟且超过同步域值&#xff1a;
b1) 上一帧播放时长过长(超过最大值)&#xff0c;仅校正为delay&#61;duration&#43;diff&#xff1b;
b2) 否则delay&#61;duration×2&#xff0c;视频播放放慢脚步&#xff0c;等待音频
c) 视频时钟与音频时钟的差异在同步域值内&#xff0c;表明音视频处于同步状态&#xff0c;不校正delay&#xff0c;则delay&#61;duration

对上述视频同步到音频的过程作一个总结&#xff0c;参考下图&#xff1a;

 

图中&#xff0c;小黑圆圈是代表帧的实际播放时刻&#xff0c;小红圆圈代表帧的理论播放时刻&#xff0c;小绿方块表示当前系统时间(当前时刻)&#xff0c;小红方块表示位于不同区间的时间点&#xff0c;则当前时刻处于不同区间时&#xff0c;视频同步策略为&#xff1a;
[1] 当前时刻在T0位置&#xff0c;则重复播放上一帧&#xff0c;延时remaining_time后再播放当前帧
[2] 当前时刻在T1位置&#xff0c;则立即播放当前帧
[3] 当前时刻在T2位置&#xff0c;则忽略当前帧&#xff0c;立即显示下一帧&#xff0c;加速视频追赶
上述内容是为了方便理解进行的简单而形象的描述。实际过程要计算相关值&#xff0c;根据compute_target_delay()和video_refresh()中的策略来控制播放过程。

4.4 音频同步到视频

音频同步到视频的方式&#xff0c;在音频播放线程中&#xff0c;实现代码在audio_decode_frame()及synchronize_audio()中。
函数调用关系如下&#xff1a;

1
2
3
sdl_audio_callback() -->
audio_decode_frame() -->
synchronize_audio()

以后有时间再补充分析过程。

4.5 音视频同步到外部时钟


推荐阅读
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文讨论了如何使用GStreamer来删除H264格式视频文件中的中间部分,而不需要进行重编码。作者提出了使用gst_element_seek(...)函数来实现这个目标的思路,并提到遇到了一个解决不了的BUG。文章还列举了8个解决方案,希望能够得到更好的思路。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 【shell】网络处理:判断IP是否在网段、两个ip是否同网段、IP地址范围、网段包含关系
    本文介绍了使用shell脚本判断IP是否在同一网段、判断IP地址是否在某个范围内、计算IP地址范围、判断网段之间的包含关系的方法和原理。通过对IP和掩码进行与计算,可以判断两个IP是否在同一网段。同时,还提供了一段用于验证IP地址的正则表达式和判断特殊IP地址的方法。 ... [详细]
  • 欢乐的票圈重构之旅——RecyclerView的头尾布局增加
    项目重构的Git地址:https:github.comrazerdpFriendCircletreemain-dev项目同步更新的文集:http:www.jianshu.comno ... [详细]
  • 纠正网上的错误:自定义一个类叫java.lang.System/String的方法
    本文纠正了网上关于自定义一个类叫java.lang.System/String的错误答案,并详细解释了为什么这种方法是错误的。作者指出,虽然双亲委托机制确实可以阻止自定义的System类被加载,但通过自定义一个特殊的类加载器,可以绕过双亲委托机制,达到自定义System类的目的。作者呼吁读者对网上的内容持怀疑态度,并带着问题来阅读文章。 ... [详细]
  • 本文介绍了响应式页面的概念和实现方式,包括针对不同终端制作特定页面和制作一个页面适应不同终端的显示。分析了两种实现方式的优缺点,提出了选择方案的建议。同时,对于响应式页面的需求和背景进行了讨论,解释了为什么需要响应式页面。 ... [详细]
  • 使用圣杯布局模式实现网站首页的内容布局
    本文介绍了使用圣杯布局模式实现网站首页的内容布局的方法,包括HTML部分代码和实例。同时还提供了公司新闻、最新产品、关于我们、联系我们等页面的布局示例。商品展示区包括了车里子和农家生态土鸡蛋等产品的价格信息。 ... [详细]
  • 本文介绍了在Cpp中将字符串形式的数值转换为int或float等数值类型的方法,主要使用了strtol、strtod和strtoul函数。这些函数可以将以null结尾的字符串转换为long int、double或unsigned long类型的数值,且支持任意进制的字符串转换。相比之下,atoi函数只能转换十进制数值且没有错误返回。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • 本文介绍了机器学习手册中关于日期和时区操作的重要性以及其在实际应用中的作用。文章以一个故事为背景,描述了学童们面对老先生的教导时的反应,以及上官如在这个过程中的表现。同时,文章也提到了顾慎为对上官如的恨意以及他们之间的矛盾源于早年的结局。最后,文章强调了日期和时区操作在机器学习中的重要性,并指出了其在实际应用中的作用和意义。 ... [详细]
  • IOS开发之短信发送与拨打电话的方法详解
    本文详细介绍了在IOS开发中实现短信发送和拨打电话的两种方式,一种是使用系统底层发送,虽然无法自定义短信内容和返回原应用,但是简单方便;另一种是使用第三方框架发送,需要导入MessageUI头文件,并遵守MFMessageComposeViewControllerDelegate协议,可以实现自定义短信内容和返回原应用的功能。 ... [详细]
author-avatar
白开水冲清茶
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有