【FFmpeg实战】FFplay音视频同步

news2024/11/23 18:42:41

作者:Mirs
链接:https://www.jianshu.com/p/d7ead3a5f2bd

PTS的由来

音视频同步依赖的一个东西就是pts(persentation time stamp )显示时间戳 告诉我们该什么时间显示这一帧 ,那么,这个东西是从哪里来的呢?

刨根问底栏目组将带你深度挖掘

PTS是在拍摄的时候打进去的时间戳,假如我们现在拍摄一段小视频(别想歪啊),什么特效都不加,那么走的就是以下的步骤

img

我们根据这个图可以知道,PTS是在录制的时候就打进Frame里的

音视频同步的方式

在ffplay中 音视频同步有三种方式

1.以视频为基准,同步音频到视频

  • 音频慢了就加快音频的播放速度,或者直接丢掉一部分音频帧
  • 音频快了就放慢音频的播放速度

2.以音频为基准,同步视频到音频

  • 视频慢了则加快播放或丢掉部分视频帧
  • 视频快了则延迟播放

3.以外部时钟为准,同步音频和视频到外部时钟

  • 根据外部时钟改版音频和视频的播放速度

视频基准

如果以视频为基准进行同步,那么我们就要考虑可能出现的情况,比如:

掉帧

此时的音频应该怎么做呢?通常的方法有

  1. 音频也丢掉相应的音频帧(会有断音,比如你说了一句,我的天啊,好漂亮的草原啊 很不凑巧丢了几帧视频,就成,,,卧槽!)
  2. 音频加快速度播放(此处可以直接用Audition加快个几倍速的播放一首音乐)

音频基准

如果以音频为基准进行同步,很不幸的碰到了掉帧的情况,那么视频应该怎么做呢?通常也有两种做法

1.视频丢帧 (画面跳帧,丢的多的话,俗称卡成PPT)

2.加快播放速度(画面加快播放)

外部时钟为基准

假如以外部时钟为基准,如果音视频出现了丢帧,怎么办呢?

如果丢帧较多,直接重新初始化外部时钟 (pts和时钟进行对比,超过一定阈值重设外部时钟,比如1s)

音视频时间换算

PTS 时间换算

之前我们稍微讲过pts的时间换算,pts换算成真正的秒是用以下操作

realTime = pts * av_q2d(stream.time_base)

stream是当前的视频/音频流

我们这里主要讲一下在音频解码pts可能会遇到的情况,有时候音频帧的pts会以1/采样率为单位,像

pts1 = 0 pts2 = 1024 pts3 = 2048

像我们例子中的这个视频,我们在解码一帧音频之后打印出来他的pts

std::cout<<"audio pts : "<pts<<std::endl;

img

我们知道当前视频的音频采样率为44100,那么这个音频帧pts的单位就是1/44100,那么

pts1 = 0 * 1 / 44100 = 0 pts2 = 1024 * 1 / 44100 = 0.232 pts3 = 2048 * 1 / 44100 = 0.464

音频流的time_base里面正是记录了这个值,我们可以通过debug来看一下

img

利用

realTime = pts * av_q2d(stream.time_base)

我们可以直接算出来当前音频帧的pts

img

另外需要注意

在ffplay中做音视频同步,都是以秒为单位

音视频帧播放时间换算

音频帧播放时间计算

音频帧的播放和音频的属性有关系,是采用

采样点数 * 1 / 采样率

来计算,像AAC当个通道采样是1024个采样点,那么

  • 如果是44.1khz,那么一帧的播放时长就是 1024 * 1 / 44100 = 23.3毫秒
  • 如果是48khz,那么一帧的播放时长就是 1024 * 1 / 48000 = 21.33毫秒

视频帧的播放时间计算

视频帧的播放时间也有两个计算方式

  1. 利用1/帧率获取每个帧平均播放时间,这个方式有一个很大的缺点就是,不能动态响应视频帧的变化,比如说我们做一些快速慢速的特效,有的厂商或者SDK(我们的SDK不是)是直接改变视频帧的增加/减少视频帧之间的pts间距来实现的,这就导致在一些拿帧率计算显示时间的播放器上发现是整体(快/慢)了,达不到想要的效果;还有一种情况就是丢帧之后,时间显示仍然是固定的
  2. 相邻帧相减这大程度上避免利用帧率去算的各种弊端,但是缺点是使用起来比较复杂,尤其是暂停/Seek之类的操作的时候需要进行一些时间差值的计算

时间校正

视频时间校正

在看ffplay的时候我们会发现,他在里面默认情况下是用了

frame->pts = frame->best_effort_timestamp;

其实大多数情况下pts和best_effort_timestamp的值是一样的,这个值是利用各种探索方法去计算当前帧的视频戳

音频时间校正

音频的pts获取比视频的要复杂一点,在ffplay中对音频的pts做了三次修改

frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb); 将其由stream->time_base转为(1/采样率)(decoder_decode_frame()中)

af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb); 将其由(1/采样率)转换为秒 (audio_thread()中)

is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec 根据实际输入进SDL2播放的数据长度做调整 (sdl_audio_callback中)

ffplay 时钟框架

ffplay中的时钟框架主要依靠Clock结构体和相应的方法组成

/** 时钟结构体 **/
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;

/** 初始化时钟 **/
static void init_clock(Clock *c, int *queue_serial);

/** 获取当前时钟 **/
static double get_clock(Clock *c);

/** 设置时钟  内部调用set_clock_at()**/
static void set_clock(Clock *c, double pts, int serial);

/** 设置时钟 **/
static void set_clock_at(Clock *c, double pts, int serial, double time);

/** 设置时钟速度 **/
static void set_clock_speed(Clock *c, double speed);

/** 音/视频设置时钟的时候都回去跟外部时钟进行对比,防止丢帧或者丢包情况下时间差距比较大而进行的纠偏 **/
static void sync_clock_to_slave(Clock *c, Clock *slave);

/** 获取做为基准的类型  音频 外部时钟 视频 **/
static int get_master_sync_type(VideoState *is);

/** 获取主时间轴的时间 **/
static double get_master_clock(VideoState *is);

/** 检查外部时钟的速度 **/
static void check_external_clock_speed(VideoState *is);

这个时钟框架也是比较简单,可以直接去看FFplay的源码,这里就不过多的叙述

音视频同步时间轴

在ffplay中,我们不管是以哪个方式做为基准,都是有一个时间轴

img

就像这样子,有一个时钟一直在跑,所谓基于音频、视频、外部时间 做为基准,也就是将那个轴的的时间做为时间轴的基准,另一个在轴参照主时间轴进行同步

假如是以音频为基准,视频同步音频的方式,那么就是音频在每播放一帧的时候,就去将当前的时间同步到时间轴,视频参考时间轴做调整

音频时钟设置

音频时钟的设置的话需要考虑注意 硬件缓存数据 设置音频时钟的时候需要将

pts - 硬件缓冲数据的播放时间

详情参考 ffplay 中

sdl_audio_callback(void *opaque, Uint8 *stream, int len)

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); 

这是就是将音频的pts - 硬件缓冲区里剩下的时间设置到了音频的时钟里

视频时钟设置

视频时钟设置的话就比较简单了,直接设置pts,在ffplay中 queue_picture(VideoState *is, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)内,我们可以直接看到 vp->pts = pts;,然后在video_refresh里面update_video_pts(is, vp->pts, vp->pos, vp->serial);去调用了set_clock

static void update_video_pts(VideoState *is, double pts, int64_t pos, int serial) {
    /* update current video pts */
    set_clock(&is->vidclk, pts, serial);
    sync_clock_to_slave(&is->extclk, &is->vidclk);
}

音视频同步操作

音视频在同步上出的处理我们上面有简单讲到过,我们这里来详细看一下他具体是真么做的

音频同步操作

音频的同步操作是在audio_decode_frame()中的wanted_nb_samples = synchronize_audio(is, af->frame->nb_samples);,注意synchronize_audio

方法,我们来看他注释

/* return the wanted number of samples to get better sync if sync_type is video
 * or external master clock
 *
 * 如果同步类型为视频或外部主时钟,则返回所需的采样数来更好的同步。
 *
 * */
static int synchronize_audio(VideoState *is, int nb_samples)

这个方法里面的操作有点多,我这边简单说一下这个方法,主要是利用音频时钟与主时钟相减得到差值(需要先判断音频是不是主时间轴),然后返回如果要同步需要的采样数,在audio_decode_frame()中用len2 = swr_convert(is->swr_ctx, out, out_count, in, af->frame->nb_samples);进行重采样,然后才在sdl_audio_callback()中进行播放

视频同步操作

视频同步操作的主要步骤是在video_refresh()方法中,我们来看一下关键的地方

   /* compute nominal last_duration 根据当前帧和上一帧的pts计算出来上一帧显示的持续时间 */
            last_duration = vp_duration(is, lastvp, vp);
            /** 计算当前帧需要显示的时间 **/
            delay = compute_target_delay(last_duration, is);

            /** 获取当前的时间 **/
            time= av_gettime_relative()/1000000.0;
            /** 如果当前时间小于显示时间 则直接进行显示**/
            if (time < is->frame_timer + delay) {
                *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                goto display;
            }

            /** 更新视频的基准时间 **/
            is->frame_timer += delay;
            /** 如果当前时间与基准时间偏差大于 AV_SYNC_THRESHOLD_MAX 则把视频基准时间设置为当前时间 **/
            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->pos, vp->serial);
            SDL_UnlockMutex(is->pictq.mutex);

            /** 如果队列中有未显示的帧,如果开启了丢帧处理或者不是以视频为主时间轴,则进行丢帧处理 **/
            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;
                }
            }

到这里,ffplay中主要的音视频同步就讲完了,建议去看一下ffplay的源码,多体会体会 印象才会比较深刻,说实话ffplay中同步的操作是比较复杂的,我们在平常开发中要根据自己的实际业务进行一些简化和改进的,下一章我们就来写一个以音频为基准的视频播放器

  >>> 音视频开发 视频教程: https://ke.qq.com/course/3202131?flowToken=1031864 
  >>> 音视频开发学习资料、教学视频,免费分享有需要的可以自行添加学习交流群 739729163 领取

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/725404.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Jvm创建对象之内存分配-JVM(七)

上篇文章介绍了jvm创建&#xff0c;会校验是否已加载类&#xff0c;没有则加载&#xff0c;通过之前学的源码&#xff0c;classLoader加载完之后&#xff0c;虚拟机开始给类分配内存&#xff0c;指针移动分配和free链表分配&#xff0c;解决并发分配情况用cap和TLAB方法。之后设…

编写LED灯的驱动,创建三个设备文件,每个设备文件和一个LED灯绑定,当操作这个设备文件时只能控制设备文件对应的这盏灯

.编写LED灯的驱动&#xff0c;创建三个设备文件&#xff0c;每个设备文件和一个LED灯绑定&#xff0c;当操作这个设备文件时只能控制设备文件对应的这盏灯 实验现象 test.c #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <s…

【linux】使用 sftp 替换 vsftpd

为什么使用 sftp&#xff1f; 1. 基于 ssh 协议&#xff0c;保证数据安全性 2. 大多数防火墙不会阻止 ssh 协议 3. 完全基于文件系统权限对用户进行管理 4. 配置比 vsftpd 简单 另外&#xff0c;我在集成商工作服务于各大甲方&#xff0c;在厦门的一些企业&#xff0c;如&…

一步一步指导如何使用 FastSAM进行图像分割

它以50倍的速度实现了与SAM方法相当的性能。 (SAM) 是一个强大的视觉基础模型,可以根据用户交互提示分割图像中的任何对象。SAM 一经发布就因其准确性而在计算机视觉社区中获得了巨大的关注。然而,SAM 广泛使用计算量大的Transformer (ViT) 架构限制了其实际应用,特别是在…

C# NPOI操作Excel汇总

C#操作Excel有多种方法&#xff0c;如通过数据库的方式来读写Excel的OleDb方式&#xff0c;但是OleDb方式需要安装微软office&#xff0c;还可以通过COM组件方式操作Excel&#xff0c;也需要安装微软Excel。如果不想安装微软办公套餐可以使用ClosedXML、EPPlus、NPOI。本文主要…

【网络系统集成】VLAN实验

1.实验名称:VLAN实验 2.实验目的 在PacketTracer中进行模拟VLAN实验,完成“不同交换机相同VLAN间通讯”实验、“单臂路由”实验与“三层交换实现VLAN间通讯”实验,加深对VLAN间通讯相关知识的理解与掌握。 3.实验内容 3.1不同交换机相同VLAN间通讯 (1)拓扑结构图

STM32——关于时钟源的实际使用及解释

1、STM32内部有5个时钟源&#xff0c;分别为HSI、HSE、LSE、LSI、PLL。 HSE&#xff1a;高速外部时钟&#xff0c;可接石英谐振器、陶瓷谐振器&#xff0c;或者接外部时钟源&#xff0c;其频率范围为4MHZ~16MHZ。 LSE&#xff1a; 低速外部时钟&#xff0c;接频率为32.768KHZ…

ATFX国际:小非农ADP数据来袭,加息预期或再度升温

ATFX国际&#xff1a;每月发布一次的ADP数据是国际金融市场的大事件&#xff0c;它能够对周五发布的非农就业报告起到相对准确的前瞻作用。今日晚间20:15&#xff0c;美国6月ADP就业人数将发布&#xff0c;前值为增加27.8万人&#xff0c;预期值增加22.8万人&#xff0c;市场预…

【Quartus FPGA】EMIF DDR3 IP 仿真记录

EMIF (External Memory Interface) 是 Quartus 平台提供的 IP&#xff0c;用于实现高速存储器件接口与控制器。通过 Intel Quartus Prime 软件&#xff0c;可以很方便地实现 EMIF IP 电路。本文记录了使用 EMIF 实现 DDR3 控制器的仿真过程&#xff0c;软件平台为 Quartus Prim…

软件测试体系方案

目录 前言&#xff1a; 1. 引言 1.1 目标 1.2 背景 1.3 术语和定义 2. 测试体系完善 2.1 项目启动 2.2 测试计划 2.3 需求分析 2.4 测试设计 2.5 测试执行 2.6 测试记录 2.7 缺陷跟踪 2.8 测试结束 2.9 测试总结 3. 测试管理规划 3.1 测试人员 3.2 测试环境 …

骑行,怎么样高效而省力的摇车?

大家好&#xff0c;今天我们来聊一聊自行车运动中的摇车技巧。我们知道&#xff0c;摇车是自行车运动中一种非常高效的发力方式&#xff0c;那么如何做到高效而省力的摇车呢&#xff1f; 首先&#xff0c;我们要了解摇车的原理。摇车&#xff0c;其实就是通过腿部蹬踏的方式&am…

(02)Cartographer源码无死角解析-(78) ROS数据发布→2D点云数据、tf、机器人tracking frame轨迹发布

讲解关于slam一系列文章汇总链接:史上最全slam从零开始&#xff0c;针对于本栏目讲解(02)Cartographer源码无死角解析-链接如下: (02)Cartographer源码无死角解析- (00)目录_最新无死角讲解&#xff1a;https://blog.csdn.net/weixin_43013761/article/details/127350885 文…

【MTK】ES7210、ES7243E Driver调试

文章目录 1.概要2.整体架构流程3. ES7210、ES7243E Driver4. 调试过程中的问题点小结1.概要 由于项目需要实现 4 路MIC 以及 2 路Speaker回采输入android系统,硬件是一个ES7210用来采集4路MIC,一个ES7243E用来采集2路Speaker回采,组成类似6路麦克风输入系统。系统SoC无法支持…

“前端刘德华”Pink老师送签名图书啦

就算成功的概率为1%又如何呢&#xff0c;如太阳系般波澜壮阔&#xff0c;也只有0.14%产生了生命&#xff0c;平凡的我们绝大多数也终将如整个太阳系的99.86%一般化作死寂。 但这不重要朋友&#xff0c;今天是黑马疯狂星期四&#xff0c;Pink老师开讲了&#xff01;&#xff01…

跑马灯实验(stm32)

目录 LED的功能代码led.cled.h硬件相关说明 main.c代码的一些介绍BSRR和BRR 实验结果 说明&#xff1a;以下内容参考正点原子的相关资料 LED的功能代码 led.c void LED_Init(void) {GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_A…

单月涨粉30w,小红书涨粉秘诀是什么?

6月&#xff0c;小红书平台又涌现出哪些优质博主&#xff1f;品牌在投放种草方面有何亮眼表现&#xff1f; 为洞察小红书平台的内容创作趋势及品牌营销策略&#xff0c;新红推出6月月度榜单&#xff0c;从创作者、品牌、品类多方面入手&#xff0c;解析月榜数据&#xff0c;为从…

耗时半月,终于把牛客网上的软件测试面试八股文整理成PDF合集!

大家好&#xff0c;最近收到不少小伙伴的留言&#xff0c;反映现在的面试难度越来越高&#xff0c;要背的八股文越来越多了&#xff0c;考察的知识点也越来越细致&#xff0c;明摆着就是想让我们“徒手造航母”嘛&#xff01;对程序员们来说确实是一大挑战。 因此&#xff0c;…

Win10快捷方式添加到开始菜单或磁贴

打开Windows文件夹&#xff0c;进入该目录(用户名替换为当前用户)&#xff1a;C:\Users\[你的用户名]\AppData\Roaming\Microsoft\Windows\Start Menu\Programs 将应用的快捷方式复制到此目录下&#xff0c;即可展示在开始菜单中可以右键在将快捷方式固定到磁贴

【UnityDOTS 小知识】在DOTS中实例化Prefab的方法

在DOTS中实例化Prefab的方法 前言 实例化Prefab的方法常规方法&#xff1a; 1.利用Baker的方式&#xff0c;以及getEntity方法&#xff0c;将prefab转化为一个对应的Entity原型&#xff0c;再利用EntityManager或ECB的Instantiate方法实例化这个Entity原型得到对应Prefab的实…

LSTD: A Low-Shot Transfer Detector for Object Detection论文阅读笔记

LSTD: A Low-Shot Transfer Detector for Object Detection论文阅读笔记 提出low-shot Transfer detector&#xff0c;来解决标注样本数据不足的情况。利用source domain知识&#xff0c;来构建高效的target-domain检测器&#xff0c;仅需要很少的训练样本。 提出了一个高效的…