1.⾳视频同步基础
1.2 简介
看视频时,要是声音和画面不同步,体验会大打折扣。之所以会出现这种情况,和音视频数据的处理过程密切相关。音频和视频的输出不在同一个线程,就像两个工人在不同车间工作,而且不一定会同时 “生产” 出同一时间点(pts,Presentation Time Stamp,即显示时间戳)的音频帧和视频帧。更麻烦的是,在编码或封装阶段,pts 可能不连续,甚至存在错误。所以,在播放音视频时,必须对它们的播放速度、播放时刻进行精准控制,这就是音视频同步的关键所在。
在 ffplay 这个常用的音视频播放工具中,音频和视频分别有自己的输出线程。音频的输出线程是 sdl 的音频输出回调线程,好比是音频专属的 “快递员”,负责把处理好的音频数据送到播放设备;视频的输出线程则是程序的主线程,就像视频的 “大管家”,统筹着视频数据的播放流程。
为了实现音视频同步,常见的策略有以下几种:
-
**以音频为基准同步视频(AV_SYNC_AUDIO_MASTER)**
此策略将音频作为整个音视频同步的时间基准。音频数据在解码后,其时间戳(PTS)被视为标准时间轴。视频播放进程紧密围绕音频时间轴进行动态调整。
当系统检测到视频播放进度滞后于音频,即视频的 PTS 大于音频的 PTS 时,为了让视频尽快追赶音频的节奏,会选择性地丢弃部分视频帧。这种 “跳帧” 处理在视觉上可能表现为画面轻微跳跃,但相较于声音的异常,人眼对这种画面变化的敏感度较低,能在一定程度上保证音画同步。
反之,若视频播放速度快于音频,系统则会继续渲染上一帧,延长该帧的显示时间,从而降低视频的整体播放速度,使其与音频播放进度保持一致。这种以音频为主导的同步方式,在大多数常规音视频播放场景中被广泛应用,能有效利用人耳对声音变化更为敏感的特性,为用户带来相对自然流畅的视听体验。 -
以视频为基准同步音频(AV_SYNC_VIDEO_MASTER)
该策略以视频的播放进度和时间戳作为同步的核心依据。当音频播放进度落后于视频,即音频的 PTS 小于视频的 PTS 时,系统会加快音频的播放速度,或者选择丢弃部分音频帧来追赶视频。然而,丢弃音频帧极易导致声音出现断音现象,严重影响听觉体验,因此这种处理方式需谨慎使用。
当音频播放进度超前于视频时,系统会放慢音频的播放速度,或重复播放上一帧音频。在调整音频播放速度的过程中,涉及到音频重采样技术,即对音频样本的采样率、采样精度等参数进行重新调整,以保证音频在变速播放后仍能保持较好的音质,避免出现声音失真、变调等问题 。但这种同步方式由于人耳对声音变化的高敏感度**,在实际应用中相对较少,仅适用于一些对视频画面呈现要求极高、对音频同步精度要求相对宽松的特殊场景。** -
以外部时钟为基准同步(AV_SYNC_EXTERNAL_CLOCK)
该策略引入一个高精度的外部时钟源作为统一的时间基准,整个音视频播放系统中的音频和视频播放时序均参照此外部时钟进行校准。外部时钟可以是网络时间协议(NTP)服务器提供的时间,或是专业硬件设备(如时钟发生器)产生的稳定时钟信号。
音视频数据在解码后,其时间戳(PTS)会与外部时钟进行实时比对。系统通过精确计算两者的时间偏差,对音频和视频的播放时刻进行调整,确保音频采样点的输出与视频帧的显示在时间维度上严格对齐。这种同步方式尤其适用于对同步精度要求极高的场景,如远程视频会议、大型多机位直播等。在这些场景中,多个终端设备同时接收并播放音视频流,只有依赖统一的外部时钟,才能实现跨设备的精准音视频同步,避免出现音画错位的现象。 -
结合外部时钟调整播放速度
此策略是在以外部时钟为基准同步的基础上进行的优化升级。它不仅依据外部时钟来校准音视频的播放起始时刻,还会实时监测外部时钟与音视频播放进度之间的差异,并动态调整音视频的播放速度。
系统持续计算音视频播放进度与外部时钟的时间差,当发现音频或视频播放进度超前于外部时钟时,通过延长音频样本的播放间隔、增加视频帧的显示时长等方式降低播放速度;当播放进度滞后时,则通过缩短音频样本播放间隔、跳过部分视频帧等手段加快播放速度。这种动态调整机制类似于闭环控制系统,能够有效应对网络延迟波动、设备性能差异等因素导致的音视频播放节奏变化,在复杂的网络环境或异构设备组成的播放系统中,为用户提供稳定、流畅的音视频同步体验。
ffplay实现三种
{ "sync", HAS_ARG | OPT_EXPERT, { .func_arg = opt_sync }, "set audio-video sync. type (type=audio/video/ext)", "type" },# ffplay -sync audio input.mp4 使用办法
1.2 ⾳视频同步基本概念
基本概念
DTS(Decoding Time Stamp):即解码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这⼀帧的数据。
PTS(Presentation Time Stamp):即显示时间戳,这个时间戳⽤来告诉播放器该在什么时候显示
这⼀帧的数据。
timebase 时基:pts的值的真正单位
ffplay中的pts,ffplay在做⾳视频同步时使⽤秒为单位,使⽤double类型去标识pts,在ffmpeg内部不会⽤浮点数去标记pts。
Clock 时钟
AVRational 表示分数
typedef struct AVRational{int num; ///< Numeratorint den; ///< Denominator} AVRational;
timebase={1, 1000} 表示千分之⼀秒(毫秒),那么pts=1000,即为pts*1/1000 = 1秒
将AVRatioal结构转换成double
static inline double av_q2d(AVRational a){
return a.num / (double) a.den;
}
计算时间戳
timestamp(秒) = pts * av_q2d(st->time_base)
计算帧时⻓
time(秒) = st->duration * av_q2d(st->time_base)
不同时间基之间的转换
int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq)
补充
对比项 | 时间戳(Timestamp) | 帧时长(Duration) |
---|---|---|
计算对象 | 单个帧或数据包的显示时间点 | 整个流(或文件)的总播放时间 |
公式输入 | pts (帧的时间戳) | st->duration (流的总时长) |
结果含义 | 例如:第 10 秒显示的帧 | 例如:视频总时长为 120 分钟 |
单位 | 秒(相对于流的开始时间) | 秒(整个流的持续时间) |
用途 | 同步、进度显示、帧定位 | 总时长显示、处理时间估算 |
示例说明
假设一个视频流:
- 时间基
time_base = {1, 1000}
(即 1 个时间单位 = 0.001 秒)。 - 某帧的
pts = 5000
,则该帧的时间戳为:
5000 * 0.001 = 5
秒(即该帧在第 5 秒显示)。 - 流的总时长
duration = 20000
,则整个视频的时长为:
20000 * 0.001 = 20
秒(即视频总时长为 20 秒)。
Clock
typedef struct Clock {double pts; // 时钟基础, 当前帧(待播放)显示时间戳,播放后,当前帧变成上一帧// 当前pts与当前系统时钟的差值, audio、video对于该值是独立的double pts_drift; // clock base minus time at which we updated the clock// 当前时钟(如视频时钟)最后一次更新时间,也可称当前时钟时间double last_updated; // 最后一次更新的系统时钟double speed; // 时钟速度控制,用于控制播放速度// 播放序列,所谓播放序列就是一段连续的播放动作,一个seek操作会启动一段新的播放序列int serial; // clock is based on a packet with this serialint paused; // = 1 说明是暂停状态// 指向packet_serialint *queue_serial; /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;
static void set_clock_at(Clock *c, double pts, int serial, double time);
参数详解
- Clock *c
作用:指定要操作的时钟对象。- double pts
作用:设置时钟的时间戳值。- int serial
作用:标识时间戳的有效性,避免使用已过时的时间戳。- double time
作用:记录设置时钟的系统时间基准,用于后续时钟漂移计算。
工作原理
-
不断 “对时”
就像我们平时给手表对时间一样,这个时钟也需要定期校准。它有个叫 set_clock_at 的方法,要校准的时候,得告诉它三个信息:- pts :可以把它理解成某个事件(比如视频里某一帧该出现的时间)的时间标记。
- serial :可以看作是一种编号,用来区分不同的 “东西”(比如不同的视频流之类的 ),不过这里重点先在时间上。
- time :就是系统当前实实在在过了多久的时间,像从电脑开机到现在过了多少秒。用这三个信息就能给时钟校准啦。
-
估算时间
这个时钟显示的时间不是绝对精准的,而是个估算值。这里面最关键的设计就是 pts_drift 。
关键操作与计算
- set_clock 操作:在 PTS1 对应的时间点,进行了 set_clock 操作 。此时计算 pts_drift,公式为 pts_drift = PTS1 - time1 ,也就是用当前帧的 PTS1 减去此时的系统时间 time1 ,得到两者的时间差值 。
- 时间推进与 get_clock 操作:随着时间推进,系统时间到了 time2 ,此时进行 get_clock 操作 。为了估算当前系统时间 time2 对应的 PTS 时间(即参考时间 ),利用之前计算的 pts_drift 来计算:
首先将 pts_drift + time2 ,即 PTS1 - time1 + time2 。
进一步变形为 PTS1 + (time2 - time1) ,而 time2 - time1 就是这段时间的时长,用 duration 表示,所以最终得到 PTS1 + duration ,这就是当前系统时间对应的参考 PTS 时间 。
1.3 不同时间基
AVFormatContext
duration:表示整个码流的时长。在获取正常时长时,需要将其除以 AV_TIME_BASE ,得到的结果单位为秒。这一数值为了解整个媒体文件的时长提供了关键信息,在诸如播放器展示总时长等场景中发挥作用。
AVStream 的 time_base 设置
AVStream 的 time_base 是在解复用器(demuxer)或复用器(muxer)内进行设置的,以下以常见的 TS、FLV、MP4 格式为例:
- TS 格式:在 mpegts.c 和 mpegtsenc.c 中,通过 avpriv_set_pts_info(st, 33, 1, 90000) 进行设置。这里的参数设置,决定了 TS 流后续处理中时间相关计算的基准。在解码 TS 流时,解码器依据这个设定的 time_base ,准确地将时间戳信息转化为实际的播放时间,以确保音视频同步播放。
- FLV 格式:在 flvdec.c 中使用 avpriv_set_pts_info(st, 32, 1, 1000) ,在 flvenc.c 中使用 avpriv_set_pts_info(s->streams[i], 32, 1, 1000) 。在 FLV 流的解码过程中,time_base 参与到对音视频帧时间戳的解析和处理中。比如,对于视频帧,根据 time_base 可以准确计算出每一帧应该在何时显示,对于音频帧,能确定其采样点的播放时刻,从而保障解码后的音视频能正确同步播放。
- MP4 格式:在 mov.c 中通过 avpriv_set_pts_info(st, 64, 1, sc->time_scale) ,在 movenc.c 中通过 avpriv_set_pts_info(st, 64, 1, track->timescale) 进行设置。在 MP4 解码时,time_base 如同一个精准的 “时间指挥官”,协调着音视频帧的解码和输出顺序。例如,视频解码线程依据 time_base 来决定何时输出一帧画面,音频解码线程也基于此来控制音频样本的播放节奏,以实现音视频在播放时的完美同步。
AVPacket 结构体
在 AVPacket 中,各时间相关字段均以 所属 AVStream 的 time_base 为单位,具体如下:
- pts(Presentation Timestamp,显示时间戳):用于标识数据包对应内容在正常播放顺序下的显示时间点,其数值需结合 AVStream->time_base 换算为实际时间(秒),例如 实际时间(秒) = pts × AVStream->time_base.num / AVStream->time_base.den。
- dts(Decoding Timestamp,解码时间戳):指示数据包应被解码的时间点,在编码存在 B 帧等复杂场景时,dts 与 pts 可能不同,同样以 AVStream->time_base 为度量基准 。
- duration(持续时间):表示该数据包所包含内容的持续时长,同样基于 AVStream->time_base 进行量化,用于计算相邻数据包的时间间隔或解码后帧的显示时长。
AVFrame 结构体
AVFrame 中的时间相关字段涉及继承与转换,与 AVStream->time_base 紧密关联,具体规则如下:
- pts:代表解码后帧的显示时间戳,通常从对应 AVPacket 拷贝而来,同样以 AVStream->time_base 为单位。若原始 AVPacket 未提供有效 pts,则需通过其他逻辑(如推算)确定。
- pkt_pts 和 pkt_dts:直接拷贝自生成该帧的 AVPacket 中的 pts 和 dts,因此也以 AVStream->time_base 为单位 。这两个字段保留了数据包层面的时间戳信息,便于追溯帧的原始时间属性。
- duration:表示该帧的持续时长,与 AVPacket 类似,同样以 AVStream->time_base 为度量单位,用于精确控制帧的显示时长,在音视频同步计算中发挥重要作用。
1.4 ffplay的实际操作
typedef struct Frame {AVFrame *frame; // 指向数据帧AVSubtitle sub; // 用于字幕int serial; // 帧序列,在seek的操作时serial会变化double pts; // 时间戳,单位为秒double duration; // 该帧持续时间,单位为秒int64_t pos; // 该帧在输入文件中的字节位置int width; // 图像宽度int height; // 图像高读int format; // 对于图像为(enum AVPixelFormat),// 对于声音则为(enum AVSampleFormat)AVRational sar; // 图像的宽高比(16:9,4:3...),如果未知或未指定则为0/1int uploaded; // 用来记录该帧是否已经显示过?int flip_v; // =1则垂直翻转, = 0则正常播放
} Frame;
视频帧 PTS 的获取与校正
-
best_effort_timestamp 的作用:
代码中通过 frame->pts = frame->best_effort_timestamp; 校正视频帧的 pts。尽管多数情况下 AVFrame 的 pts 与 best_effort_timestamp 值相同,但 best_effort_timestamp 是通过多种启发式方法(如解码时的综合逻辑)估计出的时间戳,由 libavcodec 设置。 -
优势:在某些复杂场景(如输入流时间戳不规范、部分编码场景缺失 pts 时),best_effort_timestamp 能提供更可靠的时间参考,确保视频帧的显示时序正确,避免因原始 pts 异常导致的播放错乱或同步问题。
与 AVFrame->pts 的关系:AVFrame->pts 通常依赖于输入流的时间基,而 best_effort_timestamp 是经过解码层逻辑处理后的 “最佳估计”,在时间表示上可能更直接、准确,因此 FFplay 选择以此作为 Frame->pts 的来源。
在 FFplay 中,音频帧的 PTS(Presentation Time Stamp,显示时间戳)处理是实现音频同步的关键环节。这个过程涉及三次时间基转换和缓冲区延迟补偿,下面我将逐步拆解其逻辑:
Audio Frame PTS的获取
1. 第一次转换:从 AVStream->time_base
到 1/采样率
frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb);
- 目的:将原始的时间戳(以
AVStream->time_base
为单位)转换为以 样本数 为单位。 - 转换逻辑:
d->avctx->pkt_timebase
通常等于AVStream->time_base
,是输入流的时间基(例如{1, 90000}
)。tb
是目标时间基{1, 采样率}
(例如{1, 44100}
)。av_rescale_q
函数将pts
从pkt_timebase
转换为tb
,
- 意义:后续音频处理(如重采样、缓冲区操作)通常以样本数为单位,这一步为精确控制音频数据的时序奠定基础。
2. 第二次转换:从 1/采样率
到秒
af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
- 目的:将以样本数为单位的
pts
转换为以 秒 为单位的浮点数,便于与时钟同步。 - 转换逻辑:
av_q2d(tb)
将时间基{1, 采样率}
转换为小数值(例如1/44100 ≈ 0.0000226757
)。frame->pts * av_q2d(tb)
直接将样本数转换为秒数(例如 44100 个样本 → 1.0 秒)。
- 特殊处理:若
frame->pts
为AV_NOPTS_VALUE
(无效时间戳),则设为NAN
,表示时间未知。
3. 第三次调整:补偿音频缓冲区延迟
audio_pts = is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec;
- 目的:修正
audio_clock
,使其反映 实际播放时间(而非缓冲区填充时间)。 - 延迟来源:
2 * is->audio_hw_buf_size
:SDL 音频驱动通常维护两个缓冲区,这部分数据已提交但尚未播放。is->audio_write_buf_size
:当前audio_buf
中未提交给驱动的剩余数据。
- 计算逻辑:
is->audio_tgt.bytes_per_sec
表示每秒播放的字节数(例如 44100Hz × 2 字节/样本 × 2 声道 = 176400 B/s)。- 总延迟字节数除以
bytes_per_sec
得到延迟秒数,从audio_clock
中减去该值,得到实际播放位置的时间戳。
完整流程总结
- 从容器到样本数:通过
av_rescale_q
将AVPacket
的pts
转换为样本数,消除不同媒体流时间基的差异。 - 从样本数到秒:将样本数转换为秒,便于与时钟系统(如视频时钟)直接比较。
- 缓冲区延迟补偿:考虑音频驱动缓冲区的延迟,修正时钟以反映真实播放进度。
2 以⾳频为基准
2.1 音频主流程
/* Let's assume the audio driver that is used by SDL has two periods. */if (!isnan(is->audio_clock)) {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);}
- 橙色段:表示 SDL 内部的 audio_hw_buf_size(音频硬件缓冲区大小),代表已在 SDL 音频驱动内部、但尚未播放的音频数据占用空间。
- 绿色段:表示 SDL 外部 sdl_audio_callback 处理的 audio_hw_buf_size,即当前回调正在处理或准备填充到驱动的音频数据部分。
- 蓝色段:表示 audio_buf 剩余的 audio_write_buf_size,即解码后未被填充到 SDL 音频驱动缓冲区的剩余音频数据量。
流程分析
1 is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate;
audio_clock 代表的是当前帧最后一个样本的显示时间,即 audio_buf 结束位置的时间戳
物理意义:
若 af->pts=2.0 秒,帧持续时间为 0.023 秒,则 audio_clock=2.023 秒。这意味着当播放到 audio_buf 的末尾时,时间应推进到 2.023 秒。
2 剩余数据的时间戳修正
当 audio_buf 中有剩余数据(长度为 audio_write_buf_size 字节)时,实际播放数据的 pts 需要调整:
实际数据的pts = is->audio_clock - (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec;
修正原因:
audio_clock 是整个 audio_buf 的结束时间,但当前可能只播放了其中一部分,剩余数据尚未播放。
需要从 audio_clock 中减去剩余数据的播放时间,以得到当前正在播放的数据的实际时间戳。
修正公式解析:
is->audio_tgt.bytes_per_sec 表示每秒播放的字节数(例如 44100Hz × 2 字节 / 样本 × 2 声道 = 176400 B/s)。
(double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec 计算剩余数据的播放时间(秒)。
例如,若剩余数据为 88200 字节,则播放时间为 88200 / 176400 = 0.5 秒。若 audio_clock=3.0 秒,则实际播放数据的 pts=3.0 - 0.5 = 2.5 秒。
这里的 2 * is->audio_hw_buf_size 表示 SDL 驱动中两个未播放的硬件缓冲区,加上 audio_write_buf_size 得到总延迟,进一步修正时钟,确保与实际播放位置精确匹配。
因此
is->audio_clock -(double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size)/ is->audio_tgt.bytes_per_sec,
2.2 视频主流程
ffplay中将视频同步到⾳频的主要⽅案是,如果视频播放过快,则重复播放上⼀帧,以等待⾳频;如果视频播放过慢,则丢帧追赶⾳频。
这⼀部分的逻辑实现在视频输出函数 video_refresh 中
重点如何计算上一帧时长
这⾥与系统时刻的对⽐,引⼊了另⼀个概念——frame_timer。可以理解为帧显示时刻,如更新前,是上⼀帧lastvp的显示时刻;对于更新后( is->frame_timer += delay ),则为当前帧vp显示时刻。
上⼀帧显示时刻加上delay(还应显示多久(含帧本身时⻓))即为上⼀帧应结束显示的时刻
time1:系统时刻⼩于lastvp结束显示的时刻(frame_timer+dealy),即虚线圆圈位置。此时应该继续显示lastvp
time2:系统时刻⼤于lastvp的结束显示时刻,但⼩于vp的结束显示时刻(vp的显示时间开始于虚线圆圈,结束于⿊⾊圆圈)。此时既不重复显示lastvp,也不丢弃vp,即应显示vp
time3:系统时刻⼤于vp结束显示时刻(⿊⾊圆圈位置,也是nextvp预计的开始显示时刻)。此时应该丢弃vp。
计算delay
delay = compute_target_delay(last_duration, is);
坐标轴与参数定义:
坐标轴表示视频时钟(video clock)与音频时钟(audio clock)的差值 diff。diff = 0 代表两者完全同步。
坐标轴下方色块表示根据 diff 计算后返回的值,其中 delay 为传入参数,即上一帧(lastvp)的显示时长(frame duration)。sync_threshold 定义了一个允许的同步误差范围,在该范围内认为是 “准同步”,无需调整 lastvp 的显示时长。
- delay >AV_SYNC_THRESHOLD_MAX=0.1秒,则sync_threshold = 0.1秒
- delay <AV_SYNC_THRESHOLD_MIN=0.04秒,则sync_threshold = 0.04秒
- AV_SYNC_THRESHOLD_MIN = 0.0.4秒 <= delay <= AV_SYNC_THRESHOLD_MAX=0.1秒,则sync_threshold为delay本身
同步精度最好的范围是:-0.0.4秒~+0.04秒;
同步精度最差的范围是:-0.1秒~+0.1秒
同步逻辑分析:
- diff <= -sync_threshold:视频播放速度慢于音频,需适当丢帧。返回值为 MAX(0, delay + diff),确保至少更新画面为当前帧(vp),以追赶音频进度。
- diff >= sync_threshold 且 delay > AV_SYNC_FRAMEDUP_THRESHOLD(0.1 秒):视频播放快于音频,且当前帧显示时长超过 0.1 秒。返回 delay + diff,此时总时长 delay + diff >= 0.2 秒,具体显示时长由 diff 决定,确保视频与音频逐步对齐。
- diff >= sync_threshold 且 delay <= 0.1 秒:视频播放快于音频,且当前帧显示时长较短。返回 2 * delay,即重复显示 lastvp 一帧,通过延长该帧显示时间等待音频,使总显示时长不超过 0.2 秒。
- -sync_threshold < diff < +sync_threshold:处于允许的同步误差范围内,按正常 frame duration 显示视频,直接返回 delay,维持当前播放节奏。
同步策略总结:
该机制通过动态调整视频帧的显示方式(丢帧或重复帧)实现音视频同步。若视频过快,重复上一帧以等待音频;若视频过慢,丢弃部分帧以追赶音频。通过引入 frame_timer 标记帧的显示时刻和应结束显示时刻,并与系统时刻对比,决定具体操作。lastvp 的应结束显示时刻不仅考虑自身显示时长,还纳入了音视频时钟差值。此策略并非要求每时每刻完全同步,而是通过 “准同步” 差值区域(-sync_threshold 至 +sync_threshold)平衡同步精度与系统资源,提升同步效率与稳定性,确保用户感知上的音画协调。