文章目录
-
-
- [一 音视频同步](#一 音视频同步)
- 二、变量解析
- 三、delay实现详解
- code
-
一 音视频同步
让视频帧的显示节奏与音频播放节奏保持一致,避免"画面快/慢于声音"。
为此,ffplay:
- 以音频时钟为主时钟(默认);
- 动态调整每帧视频的实际显示时长(不是固定按 PTS 差);
- 利用一个虚拟的
frame_timer来模拟"理想播放进度"。
二、变量解析
帧率(Frames Per Second,简称 FPS)是指每秒钟显示的图像帧数。
25 FPS 表示:每秒播放 25 张画面;
10 FPS 表示:每秒播放 10 张画面。
帧率越高,画面越流畅;帧率越低,画面越"卡顿"。
delay 指的是相邻两帧之间的时间间隔(单位:秒)。
帧率与 delay 的关系为:
d e l a y = 1 F P S delay = \frac{1}{FPS} delay=FPS1
25 FPS → delay = 1 / 25 = 0.04 秒 = 40 毫秒
10 FPS → delay = 1 / 10 = 0.1 秒 = 100 毫秒
帧率越高,帧与帧之间的时间间隔越短(delay 越小);
帧率越低,delay 越大。
| 变量 | 含义 |
|---|---|
delay |
当前帧理论应显示的时长(单位:秒),初始为 nextvp->pts - vp->pts |
diff |
vidclk - audclk:视频当前时间减去音频当前时间(diff < 0 视频落后,diff > 0 视频超前) |
sync_threshold |
同步容忍阈值(通常 40~100ms),小于此值不调整,防止抖动 |
frame_timer |
"理想情况下,当前帧应该显示到什么时刻"(累积 delay 的虚拟时钟) |
time |
当前真实系统时间(秒) |
- time: 当前系统时间(单位:秒)
- is->frame_timer: 上一帧实际开始显示的时刻(系统时间)
- delay: 上一帧应该持续显示多久(由 PTS 差 + 同步校正得出)
- is->frame_timer + delay: 上一帧应该结束显示的时刻
✅ 注意:这里讨论的是"上一帧"的显示生命周期,不是当前帧!
c
time = av_gettime_relative() / 1000000.0; // 获取当前时间(秒)
if (time < is->frame_timer + delay) {
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display; // 不换帧,继续显示上一帧
}
情景假设
上一帧(lastvp)在 t = 10.0 秒 开始显示 → frame_timer = 10.0
它应该显示 0.04 秒(约 25fps)→ delay = 0.04
所以它应该 在 t = 10.04 秒结束显示
现在系统时间是 t = 10.02 秒
此时:
time (10.02) < frame_timer + delay (10.04) → 条件成立!
那么,"继续显示上一帧"是什么意思?
答案:这一帧还没到该消失的时候,所以不能换新帧!
视频播放不是"有帧就立刻显示",而是要严格按时间表。
即使下一帧(vp)已经解码好了、放在队列里了,也不能提前显示。
必须等到上一帧的显示时间窗口完全结束。
不同时间的定义和使用:
| 时间类型 | 来源 | 单位 | 用途 |
|---|---|---|---|
time |
av_gettime_relative() |
秒(double) |
判断"现在真实时间" |
vp->pts |
解码后的视频帧 | 秒(经av_q2d转换) |
帧的"理想播放时刻" |
is->frame_timer |
上次换帧时记录的time |
秒 | 标记上一帧开始显示的系统时间 |
audio_clock |
音频回调更新 | 秒 | 主同步源(默认) |
表格采用对齐式设计,通过冒号指定对齐方式(默认左对齐)。时间类型列使用代码块标记强调技术术语,单位列明确标注数据类型或转换函数。
在播放过程中,time(当前系统时间) 作为现实世界的基准,用于衡量播放进度是否"准时";vp->pts 表示每一帧在媒体时间轴上的"理想播放时刻";is->frame_timer 记录了上一帧实际开始显示时的系统时间,结合计算出的 delay(由 PTS 差和同步策略决定),确定当前帧应持续显示到何时;而 audio_clock(通常来自主音频流)作为默认的主时钟,代表"正确的播放进度"。播放器通过不断比较 time 与 frame_timer + delay 来决定是否换帧,并利用 audio_clock 与视频 PTS 的偏差动态调整 delay,从而让视频"追赶"或"等待"音频,最终实现音画同步。
三、delay实现详解
| 场景 | diff | 处理方式 | 效果 |
|---|---|---|---|
| 音频卡顿(视频超前) | +0.15s | 延长 delay(若帧较长) | 视频暂停一帧等音频 |
| 视频解码慢(视频落后) | -0.2s | delay → 0,甚至丢帧 | 快速跳帧追赶 |
| 正常播放 | ±0.02s | 不处理 | 流畅播放 |
c
static double compute_target_delay(double delay, VideoState *is)
{
double sync_threshold, diff = 0;
1. 视频主同步?直接返回原始 delay
c
if (is->av_sync_type == AV_SYNC_VIDEO_MASTER)
return delay;
极少使用。此时视频自己走自己的节奏,音频去追视频。默认是 AV_SYNC_AUDIO_MASTER,所以这行通常跳过。
2. 计算视频 vs 音频的时钟差
c
diff = get_clock(&is->vidclk) - get_clock(&is->audclk);
get_clock() 返回的是基于 PTS + 系统时间校准后的"当前播放时间"。
- 音频播到 10.5 秒,视频显示的是 10.3 秒 →
diff = -0.2(视频落后 200ms) - 音频 10.5 秒,视频 10.7 秒 →
diff = +0.2(视频超前)
3. 动态设置同步阈值
c
sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
宏定义(通常):
c
#define AV_SYNC_THRESHOLD_MIN 0.04 // 40ms
#define AV_SYNC_THRESHOLD_MAX 0.1 // 100ms
目的:帧率越低(delay 越大),允许的偏差越大。
- 25fps(
delay=0.04s)→threshold ≈ 40ms - 10fps(
delay=0.1s)→threshold = 100ms
4. 主同步逻辑:根据 diff 调整 delay
c
if (!isnan(diff) && fabs(diff) < is->max_frame_duration) {
排除无效时钟(如未初始化);max_frame_duration 通常是 10 秒,防止极端 PTS 跳变导致误判。
情况 A:视频落后音频(diff ≤ -threshold)
c
if (diff <= -sync_threshold) {
delay = FFMAX(0, delay + diff);
}
diff 是负数,比如 -0.08(落后 80ms),delay=0.04
新 delay = 0.04 + (-0.08) = -0.04 → 被 FFMAX(0, ...) 截断为 0
效果:立即显示下一帧(甚至可能连续跳多帧,配合 framedrop)
情况 B:视频超前音频(diff ≥ threshold 且 delay 较大)
c
else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD) {
delay = delay + FFMIN(diff, sync_threshold);
}
AV_SYNC_FRAMEDUP_THRESHOLD 通常为 0.1(100ms)
- 高帧率视频(如 60fps)即使超前也不复制帧,而是靠后续自然同步。
- 低帧率视频(如 5fps,
delay=0.2),diff=0.15→ 新delay = 0.2 + min(0.15, 0.1) = 0.3
效果:当前帧多显示 100ms,等音频追上
video_refresh() 中如何使用这个 delay?
c
last_duration = nextvp->pts - vp->pts; // 原始帧间隔
delay = compute_target_delay(last_duration, is); // 调整后的实际显示时长
frame_timer 是虚拟播放进度锚点 :
c
time = av_gettime_relative() / 1000000.0; // 当前真实时间(秒)
if (time < is->frame_timer + delay) {
// 还没到换帧时间 → 继续显示当前帧
*remaining_time = FFMIN(delay - (time - is->frame_timer), *remaining_time);
} else {
// 到时间了 → 换帧!
is->frame_timer += delay; // 推进虚拟时钟
if (is->frame_timer < time)
is->frame_timer = time; // 防止累计误差过大
frame_queue_next(&is->pictq); // 出队下一帧
is->force_refresh = 1; // 标记需要重绘
}
code
c
// video.c 中的核心函数
static double compute_target_delay(double delay, VideoState *is)
{
double sync_threshold, diff = 0;
// 如果是视频主同步(很少用),直接返回原始帧间隔
if (is->av_sync_type == AV_SYNC_VIDEO_MASTER)
return delay;
// 获取视频时钟与音频时钟的差值
diff = get_clock(&is->vidclk) - get_clock(&is->audclk);
// 同步阈值:取 [0.04, min(0.1, delay)] 之间的值
sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN,
FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
if (!isnan(diff) && fabs(diff) < is->max_frame_duration) {
if (diff <= -sync_threshold) {
// 视频落后音频 → 缩短显示时间(尽快显示下一帧)
delay = FFMAX(0, delay + diff);
} else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD) {
// 视频超前音频 → 延长显示时间(等音频追上)
delay = delay + FFMIN(diff, sync_threshold);
}
// 注意:轻微超前(< threshold)时不处理,避免抖动
}
return delay;
}
配合调用它的 video_refresh 函数中的逻辑:
c
编辑
// 在 video_refresh() 中
last_duration = nextvp->pts - vp->pts; // 理论帧间隔
delay = compute_target_delay(last_duration, is); // 实际应显示多久
time = av_gettime_relative() / 1000000.0;
if (time < is->frame_timer + delay) {
// 还没到显示下一帧的时间 → 继续显示当前帧
*remaining_time = FFMIN(delay - (time - is->frame_timer), *remaining_time);
} else {
// 到时间了 → 换帧,并更新 frame_timer
is->frame_timer += delay;
if (is->frame_timer < time) is->frame_timer = time;
frame_queue_next(&is->pictq);
is->force_refresh = 1;
}