FFplay源码分析-音视频同步2

2024-06-24 01:58

本文主要是介绍FFplay源码分析-音视频同步2,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《FFmpeg原理》的社群来了,想加入社群的朋友请购买 VIP 版,VIP 版有更高级的内容与答疑服务。


本系列 以 ffmpeg4.2 源码为准,下载地址:链接:百度网盘 提取码:g3k8

FFplay 源码分析系列以一条简单的命令开始,ffplay -i a.mp4。a.mp4下载链接:百度网盘,提取码:nl0s 。


上一篇文章已经讲解完 音频播放线程函数 sdl_audio_callback() 内部逻辑,本篇文章开始讲解 视频播放线程的内部逻辑。

音视频同步方式是 AV_SYNC_AUDIO_MASTER,以音频为主时钟,本文基于这种同步方式做讲解。

视频播放线程,实际上就是就是 main() -> event_loop()。就是主线程。

下面开始分析 event_loop() 的内部逻辑,流程图如下:

在这里插入图片描述

event_loop() 里面的逻辑是这样的,在死循环里面等待键盘事件出现,如果没有键盘事件,就执行 video_refresh() 播放视频帧。事件处理的逻辑比较简单,这里就不仔细分析了。下面直接从 video_refresh() 视频播放 开始讲解。

video_refresh() 里面播放视频帧的逻辑比较奇怪,它是先用 frame_queue_next() 偏移 FrameQueue 队列的读索引 rindex,再执行 video_display() 来显示上一帧的数据。因为已经偏移了 rindex,所以待播放帧就变成了上一帧。

ffplay 它不是先播放视频帧,再偏移 读索引 rindex。它这样是为了可以通用一些逻辑,例如暂停的时候,缩小播放窗口,也需要调 video_display() 来刷新上一帧数据到SDL texture内存,但是却不用偏移 读索引rindex,可以跳过 偏移rindex。

video_refresh() 里面还有两个重点需要讲解:

1,is->frame_timer 跟 is->force_refresh 的作用。

is->frame_timer 可以理解为 窗口正在显示的帧 的播放时刻,就是说这帧是在什么系统时间播放的,音视频同步会用到系统时间作为刻度表。这个 is->frame_timer 表示的就是 在 14:00 (下午2点)的时候,播放了 第二帧视频,第二帧的 pts 是2。这里为什么不用 clock 里面的时间?我再想想。

is->force_refresh 控制是否需要调用 video_display()。video_display 的作用是 用 AVFrame:: data 重新渲染SDL render。在 reflesh_loop_wait_event() 里面循环的时候,remaining_time 被赋值为 REFRESH_RATE,也就说至少经历 0.01s 循环一次。但为了降低渲染频率,不是每 0.01s 秒就取上一帧渲染到SDL。在调 video_display() 之前,是用了一个判断,我流程图没画出来,请看代码:

if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)video_display(is);

上面的 if 条件,最重要的是 force_refresh,是否需要渲染。force_refresh 有几个场景的赋值变化,如下。

  • video_reflesh 里面 顺利拿到 下一帧,force_reflesh 置为1。

    ffplay.c 1680行
    frame_queue_next(&is->pictq);
    is->force_refresh = 1;
    
  • 窗口缩放事件,需要重新渲染。

    case SDL_WINDOWEVENT_EXPOSED:cur_stream->force_refresh = 1;
    

然后在 video_display() 之后,还会把 force_refresh 改回去成0,减少不必要的渲染。在暂停状态下,窗口大小没变,视频播放线程 event_loop 是不会每隔0.01s 就取上一帧渲染SDL的,因为没必要。这也就是之前文章所说的,FrameQueue 里面的 keep_last 跟 index_shown 的作用,保留上一帧在队列不销毁。

2,compute_target_delay(),计算出窗口正在显示的帧需要持续显示多久。

compute_target_delay 是 video_refresh() 里面最重要的函数,视频向音频同步的算法,就是在这里实现的。

本文命令是以音频为主时钟的,compute_target_delay 的代码不过几十行,但每一行代码都比较复杂。下面开始讲解:

diff = get_clock(&is->vidclk) - get_master_clock(is);

上面这行代码 是计算 视频时钟 与 音频时钟的差异, diff 大于 0 代表 视频 比 音频 播放快了, diff 小于 0 代表 视频 比 音频 播放快了,diff 的单位是秒。

为什么这两个时钟相减就能得出,视频比音频快多少,或者慢多少呢?这个问题需要仔细研究一下 struct Clock 的结构以及赋值场景。

static void set_clock(Clock *c, double pts, int serial)
{double time = av_gettime_relative() / 1000000.0; //注意这里set_clock_at(c, pts, serial, time);
}
typedef struct Clock {double pts;           /* clock base */double pts_drift;     /* clock base minus time at which we updated the clock */double last_updated;double speed;int serial;           /* clock is based on a packet with this serial */int paused;int *queue_serial;    /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

struct Clock 里面最重要的字段就是 pts 跟 pts_drift,他们的单位都是秒,pts_drift = pts - last_updated。

pts 存的是视频帧AVFrame的pts,last_updated 是通过 av_gettime_relative() 得到的,所以 pts_drift 计算出来的值是一个很大的负数。

一个很大的负数,其实看不出 pts_drift 要实现的功能。我改动一下 pts_drift 的实现,让他的功能更容易明白。

在main() 入口的时候,加一句代码 strat_time = av_gettime_relative() ,用 av_gettime_relative() 获取一个程序启动的时间。

然后 pts_drift = pts - last_updated - strat_time ,因为 last_updated 是用 av_gettime_relative() 获取的最新的时间。所以还可以简写成这样。

pts_drift = pts - (av_gettime_relative() - strat_time) , 也就是 pts_drift = pts - 系统消逝的时间。

没错, pts_drift 的实际意义就是 视频帧的 pts 与当前系统消逝时间的差距。视频中的pts的单位是秒。

什么是系统消逝时间,还是用 a.mp4 举例,a.mp4 是24帧的视频,也就是每隔 0.04 秒播放一帧,也就是说,在main()开始执行后,过了0.04s 就应该播放第一帧视频,过了 0.08s 就应该播放第二帧视频。0.04 跟 0.08 就是 av_gettime_relative() - strat_time 得到的。视频帧的AVFrame的pts肯定是 0.04s,0.08s的值,但是,但是 av_gettime_relative() - strat_time 不一定就是 0.04 或者 0.08 之类的,因为系统卡顿等等因素,可能 在0.05秒的时候才播放第一帧,也就是 av_gettime_relative() - strat_time = 0.05s 的时候播放第一帧,此时此刻,视频的 pts_drift 是不是等于 0.04 -0.05 = -0.01,也就是说视频播放比系统时间慢了 0.01s 秒。没错,音视频同步里面 系统时间是一个基准。

音频的 pts_drift 同理,也是代表音频帧pts跟系统时间的差距,当前播放的音频帧比系统时间慢多少或者快多少。

如果 音频的 pts_drift 是 0.03s,音频帧pts比系统时间快0.03s秒。

如果 视频的 pts_drift 是 -0.01s,视频帧pts比系统时间慢0.01s秒。

diff = get_clock(&is->vidclk) - get_master_clock(is);

那此时 compute_target_delay() 里面计算的 diff 就是 -0.04s,视频比音频慢 0.04s 秒。


再举个例子,还是 播放 a.mp4,音频每隔 0.02s 播放一帧,视频每隔 0.04s 播放一帧。

音频视频
第一帧00
第二帧0.020.04 (14:00:00:09)
第三帧0.040.08
第四帧0.060.12
第五帧0.08 (14:00:00:10)0.16
第六帧0.100.20
第七帧0.12(14:00:00:14)0.24

在 14:00:00 (下午2点)的时候开始执行main()函数,过了0.1s 到 14:00:00:10 的时候,音频已经播放到第5帧,音频的第5帧的pts是0.08s,所以音频比系统时间慢了0.02s,为什么慢是因为系统卡顿。然后视频的第3帧的pts也是0.08,也就是说,音视频完全同步的情况下,播放完音频的第5帧之后,需要立即播放视频的第三帧,这样才是完全同步,但是 视频的第二帧是在 14:00:00:09 的时候才播放的,按帧率播放,视频第二帧本来应该在 14:00:00:04 的时候播放的,在 00:09 才播放第二帧,说明视频的播放慢于系统时间 0.05s,

视频的播放慢于系统时间 0.05s,音频比系统时间慢了0.02s,那就是 视频比音频慢 0.03s秒,所以在 14:00:00:09 播放第二帧视频的时候,第二帧视频本来应该持续显示 0.04s 的,因为按帧率算嘛,一帧视频显示 0.04s秒再显示下一帧。但是因为视频比音频慢了0.03s,第二帧视频不能显示 0.04s这么长时间,他只能显示 0.01s秒,因为到 14:00:00:10 的时候已经开始播放第五帧音频了,所以需要立即显示第三帧视频。

这个就是 compute_target_delay() 函数做的事情,传进去的参数 delay 是 0.04s,delay是上一帧应该持续显示的时长,然后计算出视频慢于音频多少,就是 diff 等于负多少。

如果视频比音频慢,diff 就会是负数,delay +diff 就会导致delay减少,例如上面的例子从 0.04s 减少到 0.01s。

如果视频比音频快,diff 就会是正数,delay +diff 就会导致delay拉长,拉长会导致重复播放上一帧视频。

我上面在 main() 加的 start_time,在 diff = get_clock(&is->vidclk) - get_master_clock(is); 的时候其实是可以对消掉的,实际上等于什么都没改,只是方便理解。

现在已经知道怎么计算出diff,然后也知道diff的作用,但是 compute_target_delay 用 diff的方式有点复杂,还是需要继续讲解一下,请看下面代码。

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 + diff;else if (diff >= sync_threshold)delay = 2 * delay;
}

上面这些逻辑判断的意思是。

1,如果音视频差距大于 max_frame_duration 不进行同步,不管。

2,sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));

这段代码的意思是,让 sync_threshold 在 0.04 ~ 0.1 之间取值,具体看delay的值。

3,在本文的命令里,sync_threshold 的值是 0.04166,也就是一帧视频的时间。如果视频慢于音频 0.03s ,diff = -0.03,那 diff <= -sync_threshold 就是假。

如果视频慢于音频 0.03s ,那上面的任何 if 条件都不会跑进去,就是不进行同步。这里同步逻辑就是,如果视频慢于音频的时间,比一帧的时间还短,不进行同步。所以我上面的那个14:00:00 (下午2点) 0.03s,减少到 0.01s的那个例子的时间假设有点问题,不要介意,原理就是那样。

4,如果视频慢于音频 0.05s,那 diff <= -sync_threshold 就是真,就会跑进去 delay = FFMAX(0, delay + diff),这样delay 就等于 0,也就是说,如果视频慢于音频的时间,比一帧的时间长,就设置delay为0,正在显示的帧马上消失,立即显示下一帧视频。

5,如果视频快于音频 0.03s,上面的任何 if 条件都不会跑进去。不进行同步,也就是说,如果视频快于音频的时间,比一帧视频的时间还短,不进行同步。

6,如果视频快于音频 0.05s,就会跑进去 delay = delay + diff;,也就是说,如果视频快于音频的时间,比一帧视频的时间长,长多少时间就延长多少上一帧的显示时间。这个判断还有个 AV_SYNC_FRAMEDUP_THRESHOLD 限制,AV_SYNC_FRAMEDUP_THRESHOLD 等于 0.1,1秒10帧,也就是低帧率这个条件不生效。

6,如果视频快于音频 0.05s,同时,视频流又是1秒5帧那种低帧率视频,就会跑进去 delay = 2 * delay,直接double。

上面已经举例把视频同步的所有情况都列举完了。


视频同步,计算出 delay 的值之后,还有两个逻辑,决定是重复显示上一帧,还是播放刚刚取出来的当前帧,还是丢弃刚刚取出来的当前帧。如图:

在这里插入图片描述

ffplay.c 1622行
if (time < is->frame_timer + delay) {*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);goto display;
}

如果上一帧还没显示完,不需要取下一帧来播放。

代码继续跑。

if (frame_queue_nb_remaining(&is->pictq) > 1) {Frame *nextvp = frame_queue_peek_next(&is->pictq);duration = vp_duration(is, vp, nextvp);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;}
}

这里的逻辑需要注意,因为之前计算出来的 delay 是一个 大于等于 0 的数,但是有可能视频已经落后于音频2个视频帧的时间了,

例如 音频播放线程 已经播放到 第七帧 0.12(14:00:00:14) 了,但视频播放线程因为一些原因卡住了,才播放到 第二帧 0.04 (14:00:00:14),视频比音频慢了0.08s,2个帧的时间,但是delay只能返回 0,delay不能是负数。所以需要上面那段代码判断。

判断如果队列里面有超过 2 个帧,而且视频比音频慢 2 个帧时间以上,为什么是2个帧,是因为 frame_timer 已经重新赋值了,那刚刚取出来的当前帧不用去播放,直接丢弃,继续取队列的下一帧。

这个逻辑是为了处理视频慢于音频太多时间,用来丢视频帧的。

所以 video_reflesh() 里面的视频同步逻辑重点总结如下。

1,视频如果慢于音频一个视频帧以内的时间,不进行同步。为什么差距一个视频帧之内不同步?也是非常容易推理到的。

例如 14:00:00:10 的时候播放第五帧音频,但是到 14:00:00:12 的时候才播放第三帧视频,第三帧视频跟第五帧音频应该同时间播放,但是视频慢了 0.02s秒,那 0.02秒的时间差距需要同步吗?视频一帧的时间都需要显示0.04s秒,这个0.02s连一帧都不够,太小了,察觉不出来。所以不同步是情有可原的。

2,视频如果慢于音频1个视频帧的时间,但是不慢于2个视频帧的时间,delay 置为 0,窗口现在播放的帧立即消失。下一帧显示。

3,视频如果慢于音频2个视频帧的时间,delay 置为 0,窗口现在播放的帧立即消失。下一帧丢弃,取下下一帧显示。以此类推。

4,视频如果快于音频,拉长上一帧的显示时间。


还有一个函数没有讲解,就是 video_display() 里面的video_image_display()。

video_image_display() 的内部逻辑如下:

  1. frame_queue_peek_last(), 拿到上一帧视频。
  2. calculate_display_rect(),计算 SDL_Rect,里面使用了 sar 这个参数,sar 的含义简单来说就是 把宽高按 sar 的比例拉伸播放,这个是显示设备像素设计不同导致的,推荐阅读:ffmpeg解析出的视频参数PAR,DAR,SAR的意义 跟 theory-videoaspectratios
  3. upload_texture(),把 视频帧数据更新到 is->vid_texture,更新到 SDL texture里面。
  4. SDL_RenderCopyEx() ,把 SDL texture 纹理 拷贝到 SDL render 渲染器里面

ffplay 源码分析,event_loop() 视频播放线程分析完毕。

©版权所属:弦外之音。

由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。

这篇关于FFplay源码分析-音视频同步2的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1088873

相关文章

基于MySQL Binlog的Elasticsearch数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张M

服务器集群同步时间手记

1.时间服务器配置(必须root用户) (1)检查ntp是否安装 [root@node1 桌面]# rpm -qa|grep ntpntp-4.2.6p5-10.el6.centos.x86_64fontpackages-filesystem-1.41-1.1.el6.noarchntpdate-4.2.6p5-10.el6.centos.x86_64 (2)修改ntp配置文件 [r

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

MOLE 2.5 分析分子通道和孔隙

软件介绍 生物大分子通道和孔隙在生物学中发挥着重要作用,例如在分子识别和酶底物特异性方面。 我们介绍了一种名为 MOLE 2.5 的高级软件工具,该工具旨在分析分子通道和孔隙。 与其他可用软件工具的基准测试表明,MOLE 2.5 相比更快、更强大、功能更丰富。作为一项新功能,MOLE 2.5 可以估算已识别通道的物理化学性质。 软件下载 https://pan.quark.cn/s/57

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

衡石分析平台使用手册-单机安装及启动

单机安装及启动​ 本文讲述如何在单机环境下进行 HENGSHI SENSE 安装的操作过程。 在安装前请确认网络环境,如果是隔离环境,无法连接互联网时,请先按照 离线环境安装依赖的指导进行依赖包的安装,然后按照本文的指导继续操作。如果网络环境可以连接互联网,请直接按照本文的指导进行安装。 准备工作​ 请参考安装环境文档准备安装环境。 配置用户与安装目录。 在操作前请检查您是否有 sud