10.基于FFMPEG+SDL2播放video---音视频同步(参考音频时钟)

2024-05-27 09:38

本文主要是介绍10.基于FFMPEG+SDL2播放video---音视频同步(参考音频时钟),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

继续FFMPEG学习之路。。。

参考资料:

  1. An ffmpeg and SDL Tutorial

文章目录

  • 1 综述
  • 2 音视频同步
  • 3 DTS 和 PTS
  • 4 音频时钟
  • 5 视频PTS
  • 6 同步
  • 7 不足
  • 8 工程

1 综述

前面在写了使用FFMPEG+SDL2播放音频,视频的demo,接下来则需要将音频视频合入同时进行播放,在简单的将两份代码合入之后,调试了一番,发现音频视频可以正常播放,但是并没有同步,两者之间的独立的两个部分,这样就会导致画面和人的口型对不上,看着很不舒服,这时候就需要音视频同步了。

所谓的音视频同步,顾名思义就是要音频的播放速度跟上视频的播放速度,或者说视频的播放速度跟上音频的播放速度,这样就会使当前的画面和播放的声音一致。

2 音视频同步

我们知道,视频有帧率的概念,表示一秒显示的帧数,例如25FPS表示一秒显示25帧图像;音频有采样率的概念,表示每秒播放的样本的个数,例如一段音频音频参数为:48khz 32bit, 则每秒播放的音频字节数为 48000*(32/8)= 192000 。因此如果我们保持每秒视频播放25帧,音频播放192000字节数据,这样即便音频和视频播放是独立的,理论上也应该可以达到同步的目的。理想是美好的,但是随着时间的逐渐增大,误差就会逐渐增大,慢慢就会出现音视频不会同步的现象。

那么如何才能做到音视频同步呐?既然音频和视频都有与时间相关的一些概念,那么能否有一个独立的时间参数,音频和视频都参考这个独立的时间参数,播放快了就减慢播放,播放慢了就加快播放,这样音视频就可以参考这个时间参数实现同步播放。

独立的时间参数可以有一下三个选择:
参考音频时钟:即以音频的播放速度为准,将视频同步到音频上,视频播放快了就减慢播放速度,视频播放慢了就加快播放速度。
参考视频时钟:即以视频的播放速度为准,将音频同步到视频上。
参考外部时钟:即设置一个外部的独立时钟,将音频和视频同步到此时钟上。

本文中的同步是参考音频时钟的。

3 DTS 和 PTS

要实现上面讲的参考音频时钟,那么使用每秒播放25帧的这个参数就不容易实现了,因此我们需要视频中更精确的时间量来和音频时钟同步。在视频中,有DTS和PTS的概念,DTS(Decoding Time Stamp)解码时间戳,用于告诉解码器解码的顺序;PTS(Presentation Time Stamp)显示时间戳,用于表示解码后的数据显示顺序。

关于DTS和PTS,网上有很多详细的资料,这里不再描述,我们只需要知道我们可以使用PTS和音频时钟进行参考,从而判断下一帧的时间,从而实现音视频同步。

4 音频时钟

首先,我们先了解音频时钟相关的。
既然以音频时钟为准,那么我们就需要实时的更新音频时钟,方便视频可以获取到最新的时钟,因此在audio_decode_frame函数中,每取出一个packet,就要更新audio clock,如下:

		/* if update, update the audio clock w/pts */if(pkt->pts != AV_NOPTS_VALUE) {/* 获取真正的时间 */pState->audioClock = av_q2d(pState->pAudioStream->time_base)*pkt->pts;//printf("pts is %f, av is %f, clock is %f\n", pkt->pts, av_q2d(pState->pAudioStream->time_base),//	pState->audioClock );}

根据前面的文章,我们知道可能一个音频packet里面包含多帧音频数据,因此在此函数中,每解码一帧的数据,就要更新audio clock,如下:

			/* Keep audio_clock up-to-date */pts = pState->audioClock;*pts_ptr = pts;n = 2 * pState->pAudioStream->codec->channels;pState->audioClock += (double)data_size /(double)(n * pState->pAudioStream->codec->sample_rate);

其中,data_size为当前帧的大小, n * pState->pAudioStream->codec->sample_rate 为每秒播放的音频数据量, (double)data_size /
(double)(n * pState->pAudioStream->codec->sample_rate);
则是当前解码的音频帧需要播放的时间,单位s.

注意:关于上面pts, dts, av_q2d相关知识,可以参考:https://blog.csdn.net/bixinwei22/article/details/78770090
这篇博文已经介绍的很详细。

当视频需要参考时钟的时候,却不能直接返回audio clock,因为上面介绍了当前的audio clock为当前音频帧播放完时候的时钟,但是如果当前缓冲区里面还有音频数据,就需要减去这些音频数据占据的时间,才是当前音频的clock,如下:

double getAudioClock(VideoState *pState) 
{double pts;int hwBufSize = 0;        //当前剩余的要播放的数据int bytesPerSec = 0;int n = 0;pts = pState->audioClock; /* maintained in the audio thread */hwBufSize = pState->audioBufSize - pState->audioBufIndex;n = pState->pAudioStream->codec->channels * 2;if(pState->pAudioStream) {bytesPerSec = pState->pAudioStream->codec->sample_rate * n;}if(bytesPerSec) {pts -= (double)hwBufSize / bytesPerSec;}return pts;
}

其中pts -= (double)hwBufSize / bytesPerSec; 即为减去缓冲区里面剩余音频数据要播放需要的时间。

5 视频PTS

根据博文:https://blog.csdn.net/bixinwei22/article/details/78770090 里面介绍,当我们获取到视频帧的PTS后,就可以得到其显示时间(单位s),如下:
time(second) = st->duration * av_q2d(st->time_base)

在我们的解码线程中,在将解码后的数据放入到PacketQueue队列前,需要获取其显示时间,如下:

		/* get PTS */if (packet->dts != AV_NOPTS_VALUE){pts = av_frame_get_best_effort_timestamp(pFrame);}else{pts = 0;}pts *= av_q2d(pState->pVideoStream->time_base);if(got_picture){pts = synchronizeVideo(pState, pFrame, pts);if ((queuePicture(pState, pFrame, pts)) < 0)break;}

上面的流程为,获取视频PTS—>获取显示时间—>矫正时间—>放入队列

上面的矫正时间,是有可能调用av_frame_get_best_effort_timestamp时候没有获取到正确的PTS,那么就需要在synchronizeVideo中进行矫正。

6 同步

现在我们已经获取到每帧视频的显示时间,并且能够实时的同步音频时钟,接下来就是视频显示的该如何参考音频时钟?

我们的videoRefreshTimer函数的作用是刷新定时器并将视频显示到屏幕上,因此我们的同步工作就在这个函数里面进行。

所谓的同步其实是根据时间戳和音频时钟来计算下一帧视频显示的时间,设置定时器,从而通过快了变慢下一帧播放,慢了加快下一帧播放来实现同步。

大致思路为:
① 用当前PTS减去上一帧的PTS,获取一个值delay
② 将当前要显示的视频的pts减去当前音频的时钟,获取一个值diff
③ 将delay 和diff进行比较,如果diff小于0,说明当前要显示的视频播放的慢了,下一帧需要提前播放了;如果diff大于delay,说明当前要显示的视频播放的快了,下一帧需要慢点播放

代码如下:

			delay = pVideoPic->pts - pState->frameLastPTS;if ((delay <= 0) || (delay >= 1.0)){/* if incorrect delay, use previous one */delay = pState->frameLastDelay;}pState->frameLastDelay = delay;pState->frameLastPTS = pVideoPic->pts;/* update delay to sync to audio */refClock = getAudioClock(pState);diff = pVideoPic->pts - refClock;/* Skip or repeat the frame. Take delay into accountFFPlay still doesn't "know if this is the best guess." */syncThreshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;if(fabs(diff) < AV_NOSYNC_THRESHOLD) {if(diff <= -syncThreshold)       //视频显示的慢了,下一帧快一点{delay = 0;} else if(diff >= syncThreshold)   //视频显示的快了,下一帧慢一点{delay = 2 * delay;}}pState->frameTimer += delay;/* computer the REAL delay */actualDelay = pState->frameTimer - (av_gettime() / 1000000.0);if (actualDelay <= 0.01){actualDelay = 0.01;}schduleRefresh(pState, (int)(actualDelay * 1000 + 0.5));

其中,frameTimer 为视频播放到现在的延迟时间总和,这个值减去当前的时间即为下一帧的播放时间;AV_NOSYNC_THRESHOLD为0.01,这个是参考ffplay里面来做的,保证值不能小于0.01,并且设置最小的刷新值为0.01.

如上,便是音视频同步的整个过程了。

7 不足

在代码中只是实现了基本的功能,在一些细节方面没有优化:
①去初始化功能没有做好
②代码在一个cpp文件中,太冗杂,可以按照其功能进行拆分
③没有添加必要的打印信息
④代码编解码细节方面还可以优化

接下来,则需要对这些方面进行修改。

8 工程

最后放上完整的工程,在vs2010上测试ok.
基于FFMPEG_SDL2_音视频播放_参考音频时钟

这篇关于10.基于FFMPEG+SDL2播放video---音视频同步(参考音频时钟)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

探索蓝牙协议的奥秘:用ESP32实现高质量蓝牙音频传输

蓝牙(Bluetooth)是一种短距离无线通信技术,广泛应用于各种电子设备之间的数据传输。自1994年由爱立信公司首次提出以来,蓝牙技术已经经历了多个版本的更新和改进。本文将详细介绍蓝牙协议,并通过一个具体的项目——使用ESP32实现蓝牙音频传输,来展示蓝牙协议的实际应用及其优点。 蓝牙协议概述 蓝牙协议栈 蓝牙协议栈是蓝牙技术的核心,定义了蓝牙设备之间如何进行通信。蓝牙协议

时间服务器中,适用于国内的 NTP 服务器地址,可用于时间同步或 Android 加速 GPS 定位

NTP 是什么?   NTP 是网络时间协议(Network Time Protocol),它用来同步网络设备【如计算机、手机】的时间的协议。 NTP 实现什么目的?   目的很简单,就是为了提供准确时间。因为我们的手表、设备等,经常会时间跑着跑着就有误差,或快或慢的少几秒,时间长了甚至误差过分钟。 NTP 服务器列表 最常见、熟知的就是 www.pool.ntp.org/zo

[FPGA][基础模块]跨时钟域传播脉冲信号

clk_a 周期为10ns clk_b 周期为34ns 代码: module pulse(input clk_a,input clk_b,input signal_a,output reg signal_b);reg [4:0] signal_a_widen_maker = 0;reg signal_a_widen;always @(posedge clk_a)if(signal_a)

Android 10.0 系统开机重启桌面时钟小部件widget加载慢解决方案

1.前言 在10.0的系统rom产品定制化开发中,在Launcher3桌面系统默认会有时钟widget小部件显示在首屏的,但是发现在开机过程 中会显示的好慢,等进入桌面了 还没显示,所以接下来分析下相关的源码流程,来实现相应的功能 2.系统开机重启桌面时钟小部件widget加载慢解决方案的核心类 frameworks\base\services\appwidget\java\com\andr

Linux-笔记 线程同步机制

目录 前言 实现 信号量(Semaphore) 计数型信号量 二值信号量  信号量的原语操作 无名信号量的操作函数 例子 互斥锁(mutex) 互斥锁的操作函数 例子 自旋锁 (Spinlock) 自旋锁与互斥锁的区别 自旋锁的操作函数 例子 前言         线程同步是为了对共享资源的访问进行保护,确保数据的一致性,由于进程中会有多个线程的存在,

java同步锁以及级别升级的理解

首先简单说下先偏向锁、轻量级锁、重量级锁三者各自的应用场景: 偏向锁:只有一个线程进入临界区;轻量级锁:多个线程交替进入临界区;重量级锁:多个线程同时进入临界区。 还要明确的是,偏向锁、轻量级锁都是JVM引入的锁优化手段,目的是降低线程同步的开销。比如以下的同步代码块:   synchronized (lockObject) { // do something } 上述同步代码块

基于ZYNQ7000的交叉编译工具链Qt+OpenCV+ffmpeg等库支持总结

最近刚刚接触XILINX的ZYNQ板,刚接触没有十天。XILINX定位它为SOC,我也很认同,起码比TI定位MPU为SOC强很多。据说今年TI的最新产品也加入了ZYNQ板。 之前的MIPS处理器设计与实现的项目就算做告一段落,搞了将近7个月,成果显著,收获颇多,最近打算搞搞ZYNQ。 之前MIPS也有一套交叉编译工具,不过是老师提供的,自己也尝试搞了搞,太辛苦了,而且也没什么成果,因为我

FFmpeg源码:ff_ctz / ff_ctz_c函数分析

一、ff_ctz函数的作用 ff_ctz定义在FFmpeg源码目录的libavutil/intmath.h 下: #ifndef ff_ctz#define ff_ctz ff_ctz_c/*** Trailing zero bit count.** @param v input value. If v is 0, the result is undefined.* @return

线程间通信方式(互斥(互斥锁)与同步(无名信号量、条件变量))

1通信机制:互斥与同步 线程的互斥通过线程的互斥锁完成; 线程的同步通过无名信号量或者条件变量完成。 2  互斥 2.1 何为互斥?         互斥是在多个线程在访问同一个全局变量的时候,先让这个线程争抢锁的资源,那个线程争抢到资源,它可以访问这个变量,没有争抢到资源的线程不能够访问这个变量。那这种只有一个线程能够访问到这个变量的现象称之为线程间互斥。 2.2互斥锁API 1.

音视频开发基础知识(1)——图像基本概念

像素 **像素是图像的基本单元,一个个像素就组成了图像。你可以认为像素就是图像中的一个点。**在下面这张图中,你可以看到一个个方块,这些方块就是像素。 分辨率 图像(或视频)的分辨率是指图像的大小或尺寸。我们一般用像素个数来表示图像的尺寸。比如说一张1920x1080的图像,前者1920指的是该图像的宽度方向上有1920个像素点,而后者1080指的是图像的高 度方向上有1080个像素点。