ffplay源码分析4-音视频同步
本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10307089.html
ffplay 是 FFmpeg 工程自带的简单播放器,使用 FFmpeg 提供的解码器和 SDL 库进行视频播放。本文基于 FFmpeg 工程 8.0 版本进行分析,其中 ffplay 源码清单如下:
https://github.com/FFmpeg/FFmpeg/blob/n8.0/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 音频流和帧率为 25 FPS 的 H.264 视频流为例,来看一下理想情况下音视频的同步过程:
一个 AAC 音频 frame 每个声道包含 1024 个采样点 (也可能是 2048,参“FFmpeg 关于 nb_smples, frame_size 以及 profile 的解释”),则一个音频 frame 的播放时长 (duration) 为:(1024/44100)×1000ms = 23.22ms;一个 H.264 视频 frame 播放时长 (duration) 为:1000ms/25 = 40ms。声卡虽然是以音频采样点为播放单位,但通常我们每次往声卡缓冲区送一个音频 frame,每送一个音频 frame 更新一下音频的播放时刻,即每隔一个音频 frame 时长更新一下音频时钟,实际上 ffplay 就是这么做的。我们暂且把一个音频时钟更新点记作其播放点,理想情况下,音视频完全同步,则音视频播放过程如下图所示:
4.1 音视频同步模式
音视频同步的方式基本是确定一个时钟 (音频时钟、视频时钟、外部时钟) 作为主时钟,非主时钟的音频或视频时钟为从时钟。在播放过程中,主时钟作为同步基准,不断判断从时钟与主时钟的差异,调节从时钟,使从时钟追赶 (落后时) 或等待 (超前时) 主时钟。按照主时钟的不同种类,可以将音视频同步模式分为如下三种:
音频同步到视频,视频时钟作为主时钟。
视频同步到音频,音频时钟作为主时钟。
音视频同步到外部时钟,外部时钟作为主时钟。
ffplay 中同步模式的定义如下:
enum {
AV_SYNC_AUDIO_MASTER, /* default choice */
AV_SYNC_VIDEO_MASTER,
AV_SYNC_EXTERNAL_CLOCK, /* synchronize to an external clock */
};
4.2 time_base
time_base 是 PTS 和 DTS 的时间单位,也称时间基。不同的封装格式 time_base 不一样,转码过程中的不同阶段 time_base 也不一样。以 mpegts 封装格式为例,假设视频帧率为 25 FPS。编码数据包 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 (单位是秒)。
AVRational 类型定义如下:
/**
* Rational number (pair of numerator and denominator).
*/
typedef struct AVRational{
int num; ///< Numerator
int den; ///< Denominator
} AVRational;
容器层的 time_base (AVStream.time_base) 定义如下:
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;
...
} AVStream;
视频层的 time_base (AVCodecContext.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: unused.
*/
AVRational time_base;
...
} AVCodecContext;
time_base 是一个 AVRational 类型的分数,av_q2d() 函数可则将分数转换为 double 类型。因此有如下计算:
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.3 PTS/DTS/解码过程
4.3.1 PTS 和 DTS
DTS (Decoding Time Stamp, 解码时间戳),表示压缩帧的解码时刻。PTS (Presentation Time Stamp, 显示时间戳),表示将压缩帧解码后得到的原始帧的显示时刻。音频中 DTS 和 PTS 是相同的。视频中由于 B 帧需要双向预测,B 帧依赖于其前和其后的帧,因此含 B 帧的视频解码顺序与显示顺序不同,其 DTS 和 PTS 不同。当然,不含 B 帧的视频,其 DTS 和 PTS 是相同的。下图以一个开放式 GOP 示意图为例,说明视频流的解码顺序和显示顺序:
图中 [0]、[1] 等表示 GOP 中帧的采集/显示顺序序号,每个方格表示一帧图像 (原始帧或编码帧),原始帧与编码帧一一对应。采集顺序指图像传感器采集原始信号得到图像帧的顺序。编码顺序指编码器编码后图像帧的顺序。存储到磁盘的本地视频文件中图像帧的顺序与编码顺序相同。传输顺序指编码后的流在网络中传输过程中图像帧的顺序。解码顺序指解码器解码图像帧的顺序。显示顺序指图像帧在显示器上显示的顺序。采集顺序与显示顺序相同。编码顺序、传输顺序和解码顺序相同。
以图中“B[1]”帧为例进行说明,“B[1]”帧解码时需要参考“I[0]”帧和“P[3]”帧,因此“P[3]”帧必须比“B[1]”帧先解码。这就导致了解码顺序和显示顺序的不一致,后显示的帧需要先解码。采集的时候图像还没有 I、P、B 类型,也没有 DTS 和 PTS,编码后才有这些信息。DTS 和 PTS 是解码器使用的,编码器编码生成的视频流(裸流)是不含 DTS 和 PTS 的,但是解码器可以根据视频流的内容 (例如 H.264 语法结构) 计算生成 DTS 和 PTS,这个 DTS 和 PTS 一般会被放在视频封装格式里。
上述内容可参考“视频编解码基础概念”
理解了含 B 帧视频流解码顺序与显示顺序的不同,才容易理解解码函数 decoder_decode_frame() 中对视频解码的处理:avcodec_send_packet() 按解码顺序向解码器发送 packet,avcodec_receive_frame() 按显示顺序从解码器接收 frame。这里面,是解码器将 B 帧解码后做了重排序后才输出的,重排序过程由解码器处理,不需要用户程序费心。一般有 B 帧解码能力的解码器都会实现 B 帧重排序功能,可能也有少数解码器不支持 B 帧重排序,那就需要由用户程序来实现 B 帧重排序功能,这种情况我们暂不考虑。
4.3.2 decoder_decode_frame()
decoder_decode_frame() 是非常核心的一个函数,代码本身并不难理解。decoder_decode_frame() 是一个通用函数,可以解码音频帧、视频帧和字幕帧,本节着重关注视频帧解码过程。音频帧解码过程参考注释。
// 从packet_queue中取一个packet,解码生成frame
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
int ret = AVERROR(EAGAIN);
for (;;) {
// 本函数被各解码线程(音频、视频、字幕)首次调用时,d->pkt_serial等于-1,d->queue->serial等于1
if (d->queue->serial == d->pkt_serial) {
do {
if (d->queue->abort_request)
return -1;
// 3. 从解码器接收frame
switch (d->avctx->codec_type) {
case AVMEDIA_TYPE_VIDEO:
// 3.1 一个视频packet含一个视频frame
// 解码器缓存一定数量的packet后,才有解码后的frame输出
// frame输出顺序是按pts的顺序,如IBBPBBP
// frame->pkt_pos变量是此frame对应的packet在视频文件中的偏移地址,值同pkt.pos
ret = avcodec_receive_frame(d->avctx, frame);
if (ret >= 0) {
if (decoder_reorder_pts == -1) { // 未在命令行改变参数,则decoder_reorder_pts为默认值-1
frame->pts = frame->best_effort_timestamp;
} else if (!decoder_reorder_pts) {
frame->pts = frame->pkt_dts;
}
}
break;
case AVMEDIA_TYPE_AUDIO:
// 3.2 一个音频packet含一至多个音频frame,每次avcodec_receive_frame()返回一个frame,此函数返回。
// 下次进来此函数,继续获取一个frame,直到avcodec_receive_frame()返回AVERROR(EAGAIN),
// 表示解码器需要填入新的音频packet
ret = avcodec_receive_frame(d->avctx, frame);
if (ret >= 0) {
// 时基转换,从d->avctx->pkt_timebase时基转换到1/frame->sample_rate时基
AVRational tb = (AVRational){1, frame->sample_rate};
if (frame->pts != AV_NOPTS_VALUE)
frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb);
else if (d->next_pts != AV_NOPTS_VALUE)
frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb);
if (frame->pts != AV_NOPTS_VALUE) {
d->next_pts = frame->pts + frame->nb_samples;
d->next_pts_tb = tb;
}
}
break;
}
if (ret == AVERROR_EOF) {
d->finished = d->pkt_serial;
// 复位解码器内部状态/刷新内部缓冲区,解码结束时调用此函数
avcodec_flush_buffers(d->avctx);
return 0;
}
if (ret >= 0)
return 1; // 成功解码得到一个视频帧或一个音频帧,则返回
} while (ret != AVERROR(EAGAIN));
}
do {
if (d->queue->nb_packets == 0) // packet_queue为空则等待
SDL_CondSignal(d->empty_queue_cond);
if (d->packet_pending) { // 有未处理的packet则先处理
d->packet_pending = 0;
} else {
int old_serial = d->pkt_serial;
// 1. 取出一个packet。使用pkt对应的serial赋值给d->pkt_serial
if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)
return -1;
if (old_serial != d->pkt_serial) {
// 复位解码器内部状态/刷新内部缓冲区,seek操作时调用此函数
avcodec_flush_buffers(d->avctx);
d->finished = 0;
d->next_pts = d->start_pts;
d->next_pts_tb = d->start_pts_tb;
}
}
if (d->queue->serial == d->pkt_serial)
break;
av_packet_unref(d->pkt);
} while (1);
if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {
int got_frame = 0;
ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, d->pkt);
if (ret < 0) {
ret = AVERROR(EAGAIN);
} else {
if (got_frame && !d->pkt->data) {
d->packet_pending = 1;
}
ret = got_frame ? 0 : (d->pkt->data ? AVERROR(EAGAIN) : AVERROR_EOF);
}
av_packet_unref(d->pkt);
} else {
if (d->pkt->buf && !d->pkt->opaque_ref) {
FrameData *fd;
d->pkt->opaque_ref = av_buffer_allocz(sizeof(*fd));
if (!d->pkt->opaque_ref)
return AVERROR(ENOMEM);
fd = (FrameData*)d->pkt->opaque_ref->data;
fd->pkt_pos = d->pkt->pos;
}
// 2. 将packet发送给解码器
// 发送packet的顺序是按dts递增的顺序,如IPBBPBB
// pkt.pos变量可以标识当前packet在视频文件中的地址偏移
if (avcodec_send_packet(d->avctx, d->pkt) == 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 = 1;
} else {
av_packet_unref(d->pkt);
}
}
}
}
基本流程为:从视频 packet 队列中取一个 packet,将取得的 packet 发送给解码器,从解码器接收解码后的 frame,此 frame 作为函数的输出参数供上级函数处理。
注意如下几点:
[1]. 含 B 帧的视频文件,其视频帧存储顺序与显示顺序不同
[2]. 解码器的输入是 packet 队列,视频帧解码顺序与存储顺序相同,是按 dts 递增的顺序。dts 是解码时间戳,因此存储顺序解码顺序都是 dts 递增的顺序。将视频文件中的 packet 序列依次取出,再调用 avcodec_send_packet() 发送给解码器。发送 packet 的顺序如 IPBBPBB。
[3]. 解码器的输出是 frame 队列,frame 输出顺序是按 pts 递增的顺序。pts 是解码时间戳。pts 与 dts 不一致的问题由解码器进行了处理,用户程序不必关心。从解码器接收 frame 的顺序如 IBBPBBP。
[4]. 解码器中会缓存一定数量的帧,一个新的解码动作启动后,向解码器送入好几个 packet 解码器才会输出第一个 packet,这比较容易理解,因为解码时帧之间有依赖关系,例如 I、P、B 三个帧被送入解码器后,B 帧解码需要依赖 I 帧和 P 帧,所以在 B 帧输出前,I 帧和 P 帧必须存在于解码器中而不能删除。理解了这一点,后面视频 frame 队列中对视频帧的显示和删除机制才容易理解。
[5]. 解码器中缓存的帧可以通过冲洗 (flush) 解码器取出。冲洗 (flush) 解码器的方法就是调用 avcodec_send_packet(..., NULL)
,然后多次调用 avcodec_receive_frame() 将缓存帧取尽。缓存帧取完后,avcodec_receive_frame() 返回 AVERROR_EOF。
如何确定解码器的输出 frame 与输入 packet 的对应关系呢?可以对比 frame->pkt_pos 和 pkt.pos 的值,这两个值表示 packet 在视频文件中的偏移地址,如果这两个变量值相等,表示此 frame 来自此 packet。调试跟踪这两个变量值,即能发现解码器输入帧与输出帧的关系。
4.3.3 如何获取 pkt_pos
在 FFmpeg 4.4 版本中,AVFrame 中的 pkt_pos 是能直接使用的,很简单。但后面 FFmpeg 版本升级后,AVFrame 中的 pkt_pos 和 pkt_size 字段已经被废弃了,改用通过自定义用户数据的方式来获取这些信息。我们来看一下 FFmpeg 8.0 版本中 pkt_pos 的获取方法 (即如何获取解码后的每个 frame 对应的编码流中的 packet 的偏移地址),先把相关代码都摘取出来:
解码后要获取 frame 所对应的 packet 在编码流中的位置,其实就是要获取 AVPacket 中的 pos 字段。可以利用 AVPacket 中的自定义用户数据 opaque_ref 来实现,在应用层使 AVPacket.opaque_ref 中包含 AVPacket.pos 信息。
typedef struct AVPacket {
...
int64_t pos; ///< byte position in stream, -1 if unknown
...
/**
* AVBufferRef for free use by the API user. FFmpeg will never check the
* contents of the buffer ref. FFmpeg calls av_buffer_unref() on it when
* the packet is unreferenced. av_packet_copy_props() calls create a new
* reference with av_buffer_ref() for the target packet's opaque_ref field.
*
* This is unrelated to the opaque field, although it serves a similar
* purpose.
*/
AVBufferRef *opaque_ref;
...
} AVPacket;
在 AVFrame 结体定义中,可以看到 pkt_pos 和 pkt_size 已经废弃。在解码过程中,解码器内部会将 AVPacket.opaque_ref 拷贝(实际是引用计数方式)到 AVFrame.opaque_ref,这样用户层就能通过 AVFrame.opaque_ref 拿到 AVPacket.pos 信息。
typedef struct AVFrame {
...
#if FF_API_FRAME_PKT
/**
* reordered pos from the last AVPacket that has been input into the decoder
* - encoding: unused
* - decoding: Read by user.
* @deprecated use AV_CODEC_FLAG_COPY_OPAQUE to pass through arbitrary user
* data from packets to frames
*/
attribute_deprecated
int64_t pkt_pos;
#endif
...
#if FF_API_FRAME_PKT
/**
* size of the corresponding packet containing the compressed
* frame.
* It is set to a negative value if unknown.
* - encoding: unused
* - decoding: set by libavcodec, read by user.
* @deprecated use AV_CODEC_FLAG_COPY_OPAQUE to pass through arbitrary user
* data from packets to frames
*/
attribute_deprecated
int pkt_size;
#endif
...
/**
* Frame owner's private data.
*
* This field may be set by the code that allocates/owns the frame data.
* It is then not touched by any library functions, except:
* - a new reference to the underlying buffer is propagated by
* av_frame_copy_props() (and hence by av_frame_ref());
* - it is unreferenced in av_frame_unref();
* - on the caller's explicit request. E.g. libavcodec encoders/decoders
* will propagate a new reference to/from @ref AVPacket "AVPackets" if the
* caller sets @ref AV_CODEC_FLAG_COPY_OPAQUE.
*
* @see opaque the plain pointer analogue
*/
AVBufferRef *opaque_ref;
...
} AVFrame;
再来看一下 ffplay 代码中的实现:
ffplay 中定义了如下数据类型,用于存储 pkt_pos:
typedef struct FrameData {
int64_t pkt_pos;
} FrameData;
在视频解码线程中,在将一个 packet 送进解码器之前,会先为 packet 中的 opaque_ref 分配内存,并将 packet 中的 pos 字段值存入 opaque_ref 对象中。
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
int ret = AVERROR(EAGAIN);
for (;;) {
...
if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {
...
} else {
if (d->pkt->buf && !d->pkt->opaque_ref) {
FrameData *fd;
d->pkt->opaque_ref = av_buffer_allocz(sizeof(*fd));
if (!d->pkt->opaque_ref)
return AVERROR(ENOMEM);
fd = (FrameData*)d->pkt->opaque_ref->data;
fd->pkt_pos = d->pkt->pos;
}
if (avcodec_send_packet(d->avctx, d->pkt) == AVERROR(EAGAIN)) {
...
}
...
}
}
}
在视频播放线程中,直接取出 frame->opaque_ref 中的 pkt_pos 值,frame->opaque_ref 实际是通过引用计数机制指向 packet->opaque_ref,这是在解码器内部完成的。
static int video_thread(void *arg)
{
...
for (;;) {
// 从packet队列中取出一个packet解码得到一个frame
ret = get_video_frame(is, frame);
...
// 将frame送入滤镜输入端
ret = av_buffersrc_add_frame(filt_in, frame);
if (ret < 0)
goto the_end;
while (ret >= 0) {
FrameData *fd;
...
ret = av_buffersink_get_frame_flags(filt_out, frame, 0);
...
fd = frame->opaque_ref ? (FrameData*)frame->opaque_ref->data : NULL;
...
ret = queue_picture(is, frame, pts, duration, fd ? fd->pkt_pos : -1, is->viddec.pkt_serial);
...
}
...
}
...
}
解码器内部将 AVPacket.opaque_ref 拷贝到 AVFrame.opaque_ref 的前提是为解码器设置了 AV_CODEC_FLAG_COPY_OPAQUE 标志。ffplay 的代码中是有设置这个标志的,如下:
static int stream_component_open(VideoState *is, int stream_index)
{
...
if (!av_dict_get(opts, "threads", NULL, 0))
av_dict_set(&opts, "threads", "auto", 0);
if (stream_lowres)
av_dict_set_int(&opts, "lowres", stream_lowres, 0);
av_dict_set(&opts, "flags", "+copy_opaque", AV_DICT_MULTIKEY);
...
}
AVCodecContext 中 flags 成员与 AV_CODEC_FLAG_COPY_OPAQUE 相关的选项定义在 libavcodec/options_table.h 文件中,如下:
static const AVOption avcodec_options[] = {
...
{"flags", NULL, OFFSET(flags), AV_OPT_TYPE_FLAGS, {.i64 = DEFAULT }, 0, UINT_MAX, V|A|S|E|D, .unit = "flags"},
...
{"copy_opaque", "propagate opaque values", 0, AV_OPT_TYPE_CONST, {.i64 = AV_CODEC_FLAG_COPY_OPAQUE}, .unit = "flags"},
...
}
这里涉及到 AVOption 的用法,不展开了。
4.4 视频同步到音频
视频同步到音频是 ffplay 的默认同步方式,同步过程在视频播放线程中实现。
4.4.1 video_refresh()
video_refresh() 函数的调用过程如下:
main()
|-> event_loop()
|-> refresh_loop_wait_event()
|-> video_refresh()
video_refresh() 实现了视频播放功能 (包含同步控制),是非常核心的一个函数,理解起来也有些难度。函数实现如下:
static void video_refresh(void *opaque, double *remaining_time)
{
VideoState *is = opaque;
double time;
Frame *sp, *sp2;
if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)
check_external_clock_speed(is);
// 音频波形图显示
if (!display_disable && is->show_mode != SHOW_MODE_VIDEO && is->audio_st) {
time = av_gettime_relative() / 1000000.0;
if (is->force_refresh || is->last_vis_time + rdftspeed < time) {
video_display(is);
is->last_vis_time = time;
}
*remaining_time = FFMIN(*remaining_time, is->last_vis_time + rdftspeed - time);
}
// 视频播放
if (is->video_st) {
retry:
if (frame_queue_nb_remaining(&is->pictq) == 0) { // 无未显示帧
// 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); // 上一帧:上次已显示的帧,即ri帧
vp = frame_queue_peek(&is->pictq); // 当前帧:当前待显示的帧,即ris帧
if (vp->serial != is->videoq.serial) {
frame_queue_next(&is->pictq);
goto retry;
}
// lastvp和vp不是同一播放序列(一个seek会开始一个新播放序列),将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); // 上一帧播放时长:vp->pts - lastvp->pts
delay = compute_target_delay(last_duration, is); // 根据视频时钟和同步时钟的差值,计算delay值
time= av_gettime_relative()/1000000.0;
// 当前帧播放时刻(is->frame_timer+delay)大于当前时刻(time),表示播放时刻未到
if (time < is->frame_timer + delay) {
// 播放时刻未到,则更新刷新时间remaining_time为当前时刻到下一播放时刻的时间差
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
// 播放时刻未到,则不更新rindex。(窗口大小变化或用户按刷新键force_refresh生效)把上一帧lastvp再播放一遍,否则无动作。
goto display;
}
// 更新frame_timer值
is->frame_timer += delay;
// 校正frame_timer值:若frame_timer落后于当前系统时间太久(超过最大同步域值),则更新为当前系统时间
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->serial); // 更新视频时钟:时间戳、时钟时间
SDL_UnlockMutex(is->pictq.mutex);
// 是否要丢弃未能及时播放的视频帧
if (frame_queue_nb_remaining(&is->pictq) > 1) { // 队列中未显示帧数>1(只有一帧则不考虑丢帧)
Frame *nextvp = frame_queue_peek_next(&is->pictq); // 下一帧:下一待显示的帧
duration = vp_duration(is, vp, nextvp); // 当前帧vp播放时长 = nextvp->pts - vp->pts
// 此处if()的三个条件:
// 1) 非步进模式;2) 丢帧策略生效;
// 3) 下一帧播放时刻(is->frame_timer+duration)小于当前系统时刻(time),也就是下一帧都
// 该播放了,那就丢了当前帧vp,直接播放下一帧
if(!is->step && (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration){
// framedrop丢帧处理有两处:
// 1) 解码后frame的pts和同步时钟偏差超过同步域值;
// 2) 播放太慢,frame未及时显示(此处)
is->frame_drops_late++;
frame_queue_next(&is->pictq); // 删除上一帧已显示帧,即删除lastvp,读指针加1(从lastvp更新到vp)
goto retry;
}
}
// 字幕播放
if (is->subtitle_st) {
while (frame_queue_nb_remaining(&is->subpq) > 0) {
sp = frame_queue_peek(&is->subpq);
if (frame_queue_nb_remaining(&is->subpq) > 1)
sp2 = frame_queue_peek_next(&is->subpq);
else
sp2 = NULL;
if (sp->serial != is->subtitleq.serial
|| (is->vidclk.pts > (sp->pts + ((float) sp->sub.end_display_time / 1000)))
|| (sp2 && is->vidclk.pts > (sp2->pts + ((float) sp2->sub.start_display_time / 1000))))
{
if (sp->uploaded) {
int i;
for (i = 0; i < sp->sub.num_rects; i++) {
AVSubtitleRect *sub_rect = sp->sub.rects[i];
uint8_t *pixels;
int pitch, j;
if (!SDL_LockTexture(is->sub_texture, (SDL_Rect *)sub_rect, (void **)&pixels, &pitch)) {
for (j = 0; j < sub_rect->h; j++, pixels += pitch)
memset(pixels, 0, sub_rect->w << 2);
SDL_UnlockTexture(is->sub_texture);
}
}
}
frame_queue_next(&is->subpq);
} else {
break;
}
}
}
// 更新读指针:删除ri帧,然后将ri指针加1,此时ri帧指向vp帧但还未显示
// 若前一步骤中未丢帧,读指针从lastvp更新到vp;若有丢帧,读指针从vp更新到nextvp
// 考虑前一步未丢帧的情况,读指针(ri帧位置)从lastvp(ri帧)更新到vp(ris帧),即ri帧后移了一位
// 此处是先更新了ri指针,新的ri帧还未显示,等下一步video_display()调用完成后,新ri帧就变成了最后一次已显示帧
frame_queue_next(&is->pictq);
is->force_refresh = 1;
if (is->step && !is->paused)
stream_toggle_pause(is); // 逐帧播放模式下,播放一帧画面后暂停
}
display:
/* display picture */
// 此函数外部窗口大小变化或用户按刷新键,或此函数内部帧播放时间已到,则is->force_refresh为1
// 从if分支走到这里:通常无动作,若force_refresh为1,就从视频队列中取出上一帧显示
// 从else分支走到这里:通常取vp帧显示,若force_refresh为0(帧播放时间未到)则无动作
if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
video_display(is); // 取出当前帧vp(若有丢帧是nextvp)进行播放
}
is->force_refresh = 0;
if (show_status) { // 更新显示播放状态
AVBPrint buf;
static int64_t last_time;
int64_t cur_time;
int aqsize, vqsize, sqsize;
double av_diff;
cur_time = av_gettime_relative();
if (!last_time || (cur_time - last_time) >= 30000) {
aqsize = 0;
vqsize = 0;
sqsize = 0;
if (is->audio_st)
aqsize = is->audioq.size;
if (is->video_st)
vqsize = is->videoq.size;
if (is->subtitle_st)
sqsize = is->subtitleq.size;
av_diff = 0;
if (is->audio_st && is->video_st)
av_diff = get_clock(&is->audclk) - get_clock(&is->vidclk);
else if (is->video_st)
av_diff = get_master_clock(is) - get_clock(&is->vidclk);
else if (is->audio_st)
av_diff = get_master_clock(is) - get_clock(&is->audclk);
av_bprint_init(&buf, 0, AV_BPRINT_SIZE_AUTOMATIC);
av_bprintf(&buf,
"%7.2f %s:%7.3f fd=%4d aq=%5dKB vq=%5dKB sq=%5dB \r",
get_master_clock(is),
(is->audio_st && is->video_st) ? "A-V" : (is->video_st ? "M-V" : (is->audio_st ? "M-A" : " ")),
av_diff,
is->frame_drops_early + is->frame_drops_late,
aqsize / 1024,
vqsize / 1024,
sqsize);
if (show_status == 1 && AV_LOG_INFO > av_log_get_level())
fprintf(stderr, "%s", buf.str);
else
av_log(NULL, AV_LOG_INFO, "%s", buf.str);
fflush(stderr);
av_bprint_finalize(&buf, NULL);
last_time = cur_time;
}
}
}
视频同步到音频的基本方法是:如果视频和音频播放进度差不多 (小于同步域值),则播放当前帧;如果视频超前音频,则不进行播放,以等待音频;如果视频落后音频,则丢弃当前帧直接播放下一帧,以追赶音频。
此函数执行流程参考如下流程图:
步骤如下:
[1] 根据上一帧 lastvp 的播放时长 duration,校正 delay 值,duration 是上一帧理想播放时长,delay 是上一帧实际播放时长,根据 delay 值可以计算得到当前帧的播放时刻
[2] 如果当前帧 vp 播放时刻未到,则继续显示上一帧 lastvp,并将延时值 remaining_time 作为输出参数供上级调用函数处理
[3] 如果当前帧 vp 播放时刻已到,则立即显示当前帧,并更新读指针
视频播放太慢时还会做丢帧处理,ffplay 中 framedrop 处理有两处,另一处丢帧是在视频解码线程中,framedrop 机制详参 3.4.2 节。
4.4.2 compute_target_delay()
在 video_refresh() 函数中,调用了 compute_target_delay() 来根据视频时钟与同步时钟 (音频时钟) 的差异来调节 delay 值,从而调节视频帧播放的时刻。
// 根据视频时钟与同步时钟(如音频时钟)的差值,校正delay值,使视频时钟追赶或等待同步时钟
// 输入参数delay是上一帧播放时长,即上一帧播放后应延时多长时间后再播放当前帧,通过调节此值来调节当前帧播放快慢
// 返回值delay是将输入参数delay经校正后得到的值
static double compute_target_delay(double delay, VideoState *is)
{
double sync_threshold, diff = 0;
/* update delay to follow master synchronisation source */
if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) {
/* if video is slave, we try to correct big delays by
duplicating or deleting a frame */
// 视频时钟与同步时钟(如音频时钟)的差异,时钟值是上一帧pts值(实为:上一帧pts + 上一帧至今流逝的时间差)
diff = get_clock(&is->vidclk) - get_master_clock(is);
// delay是上一帧播放时长:当前帧(待播放的帧)播放时间与上一帧播放时间差理论值
// diff是视频时钟与同步时钟的差值
/* skip or repeat frame. We take into account the
delay to compute the threshold. I still don't know
if it is the best guess */
// 若delay < AV_SYNC_THRESHOLD_MIN,则同步域值为AV_SYNC_THRESHOLD_MIN
// 若delay > AV_SYNC_THRESHOLD_MAX,则同步域值为AV_SYNC_THRESHOLD_MAX
// 若AV_SYNC_THRESHOLD_MIN < delay < AV_SYNC_THRESHOLD_MAX,则同步域值为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); // 当前帧播放时刻落后于同步时钟(delay+diff<0)则delay=0(视频追赶,立即播放),否则delay=delay+diff
else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD) // 视频时钟超前于同步时钟,且超过同步域值,但上一帧播放时长超长
delay = delay + diff; // 仅仅校正为delay=delay+diff,主要是AV_SYNC_FRAMEDUP_THRESHOLD参数的作用
else if (diff >= sync_threshold) // 视频时钟超前于同步时钟,且超过同步域值
delay = 2 * delay; // 视频播放要放慢脚步,delay扩大至2倍
}
}
av_log(NULL, AV_LOG_TRACE, "video: delay=%0.3f A-V=%f\n",
delay, -diff);
return delay;
}
compute_target_delay() 的输入参数 delay 是上一帧理想播放时长 duration,返回值 delay 是经校正后的上一帧实际播放时长。为方便描述,下面我们将输入参数记作 duration (对应函数的输入参数 delay),返回值记作 delay (对应函数返回值 delay)。compute_target_delay() 函数实现功能如下:
[1] 计算视频时钟与音频时钟 (主时钟) 的偏差 diff,实际就是视频上一帧 pts 减去音频上一帧 pts。所谓上一帧,就是已经播放的最后一帧,上一帧的 pts 可以标识视频流/音频流的播放时刻 (进度)。
[2] 计算同步域值 sync_threshold,同步域值的作用是:若视频时钟与音频时钟差异值小于同步域值,则认为音视频是同步的,不校正 delay;若差异值大于同步域值,则认为音视频不同步,需要校正 delay 值。同步域值的计算方法为:若 duration < AV_SYNC_THRESHOLD_MIN,则同步域值为 AV_SYNC_THRESHOLD_MIN;若 duration > AV_SYNC_THRESHOLD_MAX,则同步域值为 AV_SYNC_THRESHOLD_MAX;若 AV_SYNC_THRESHOLD_MIN < duration < AV_SYNC_THRESHOLD_MAX,则同步域值为 duration。
[3] delay校正策略如下:
a) 视频时钟落后于同步时钟且落后值超过同步域值:
a1) 若当前帧播放时刻落后于同步时钟 (delay+diff<0) 则 delay=0 (视频追赶,立即播放);
a2) 否则 delay=duration+diff
b) 视频时钟超前于同步时钟且超过同步域值:
b1) 上一帧播放时长过长 (超过最大值),仅校正为 delay=duration+diff;
b2) 否则 delay=duration×2,视频播放放慢脚步,等待音频
c) 视频时钟与音频时钟的差异在同步域值内,表明音视频处于同步状态,不校正 delay,则 delay=duration
对上述视频同步到音频的过程作一个总结,参考下图:
图中,小黑圆圈是代表帧的实际播放时刻,小红圆圈代表帧的理论播放时刻,小绿方块表示当前系统时间 (当前时刻),小红方块表示位于不同区间的时间点,则当前时刻处于不同区间时 (将小绿方块移动到 T0、T1、T2 位置),视频同步策略为:
[1] 当前时刻在 T0 位置,则重复播放上一帧,延时 remaining_time 后再播放当前帧
[2] 当前时刻在 T1 位置,则立即播放当前帧
[3] 当前时刻在 T2 位置,则忽略当前帧,立即显示下一帧,加速视频追赶
上述内容是为了方便理解进行的简单而形象的描述。实际过程要计算相关值,根据 compute_target_delay() 和 video_refresh() 中的策略来控制播放过程。
4.4.3 video_display()
video_refresh() 函数内大部分工作是在判断有没有视频帧要显示,视频帧应该在什么时候显示,即音视频同步的工作,当一个视频帧需要显示时,就会调用 video_display() 函数。
video_display() 函数是一个通用函数,视频流可以调用它显示视频帧,音频流也可以调用它显示音频波形图。函数实现比较简单,如下:
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);
}
SDL 负责视频帧的渲染显示,video_display() 执行如下步骤实现图像显示:
SDL_RenderClear(); // 使用特定颜色清空当前渲染目标
video_image_display(is)->SDL_RenderCopy(); // 使用图像数据更新当前渲染目标
SDL_RenderPresent(renderer); // 执行渲染,更新屏幕显示
4.5 音频同步到视频
音频同步到视频的方式,在音频播放线程中,实现代码在 audio_decode_frame() 及 synchronize_audio() 中。
函数调用关系如下:
sdl_audio_callback()
|-> audio_decode_frame()
|-> synchronize_audio()
实际应用中,一般采用视频同步到音频的同步方式,音频时钟作为同步主时钟。ffplay 默认也是采用视频同步到音频的方式。音频同步到视频的方式用得较少,暂不关注。