FFmpeg学习6:视音频同步

2024-05-04 04:38
文章标签 学习 ffmpeg 同步 视音频

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

https://www.cnblogs.com/wangguchangqing/p/5900426.html


在上一篇文章中,视频和音频是各自独立播放的,并不同步。本文主要描述了如何以音频的播放时长为基准,将视频同步到音频上以实现视音频的同步播放的。主要有以下几个方面的内容

  • 视音频同步的简单介绍
  • DTS 和 PTS
  • 计算视频中Frame的显示时间
  • 获取Audio clock(audio的播放时长)
  • 将视频同步到音频上,实现视音频同步播放

视音频同步简单介绍

一般来说,视频同步指的是视频和音频同步,也就是说播放的声音要和当前显示的画面保持一致。想象以下,看一部电影的时候只看到人物嘴动没有声音传出;或者画面是激烈的战斗场景,而声音不是枪炮声却是人物说话的声音,这是非常差的一种体验。
在视频流和音频流中已包含了其以怎样的速度播放的相关数据,视频的帧率(Frame Rate)指示视频一秒显示的帧数(图像数);音频的采样率(Sample Rate)表示音频一秒播放的样本(Sample)的个数。可以使用以上数据通过简单的计算得到其在某一Frame(Sample)的播放时间,以这样的速度音频和视频各自播放互不影响,在理想条件下,其应该是同步的,不会出现偏差。但,理想条件是什么大家都懂得。如果用上面那种简单的计算方式,慢慢的就会出现音视频不同步的情况。要不是视频播放快了,要么是音频播放快了,很难准确的同步。这就需要一种随着时间会线性增长的量,视频和音频的播放速度都以该量为标准,播放快了就减慢播放速度;播放快了就加快播放的速度。所以呢,视频和音频的同步实际上是一个动态的过程,同步是暂时的,不同步则是常态。以选择的播放速度量为标准,快的等待慢的,慢的则加快速度,是一个你等我赶的过程。

播放速度标准量的的选择一般来说有以下三种:

  • 将视频同步到音频上,就是以音频的播放速度为基准来同步视频。视频比音频播放慢了,加快其播放速度;快了,则延迟播放。
  • 将音频同步到视频上,就是以视频的播放速度为基准来同步音频。
  • 将视频和音频同步外部的时钟上,选择一个外部时钟为基准,视频和音频的播放速度都以该时钟为标准。

DTS和PTS

上面提到,视频和音频的同步过程是一个你等我赶的过程,快了则等待,慢了就加快速度。这就需要一个量来判断(和选择基准比较),到底是播放的快了还是慢了,或者正以同步的速度播放。在视音频流中的包中都含有DTS和PTS,就是这样的量(准确来说是PTS)。DTS,Decoding Time Stamp,解码时间戳,告诉解码器packet的解码顺序;PTS,Presentation Time Stamp,显示时间戳,指示从packet中解码出来的数据的显示顺序。
视音频都是顺序播放的,其解码的顺序不应该就是其播放的顺序么,为啥还要有DTS和PTS之分呢。对于音频来说,DTS和PTS是相同的,也就是其解码的顺序和解码的顺序是相同的,但对于视频来说情况就有些不同了。
视频的编码要比音频复杂一些,特别的是预测编码是视频编码的基本工具,这就会造成视频的DTS和PTS的不同。这样视频编码后会有三种不同类型的帧:

  • I帧 关键帧,包含了一帧的完整数据,解码时只需要本帧的数据,不需要参考其他帧。
  • P帧 P是向前搜索,该帧的数据不完全的,解码时需要参考其前一帧的数据。
  • B帧 B是双向搜索,解码这种类型的帧是最复杂,不但需要参考其一帧的数据,还需要其后一帧的数据。

I帧的解码是最简单的,只需要本帧的数据;P帧也不是很复杂,值需要缓存上一帧的数据即可,总体来说都是线性,其解码顺序和显示顺序是一致的。B帧就比较复杂了,需要前后两帧的顺序,并且不是线性的,也是造成了DTS和PTS的不同的“元凶”,也是在解码后有可能得不到完整Frame的原因。(更多I,B,P帧的信息可参考)
假如一个视频序列,要这样显示I B B P,但是需要在B帧之前得到P帧的信息,因此帧可能以这样的顺序来存储I P B B,这样其解码顺序和显示的顺序就不同了,这也是DTS和PTS同时存在的原因。DTS指示解码顺序,PTS指示显示顺序。所以流中可以是这样的:

Stream : I P B B
DTS      1 2 3 4
PTS      1 4 2 3

通常来说只有在流中含有B帧的时候,PTS和DTS才会不同。

计算视频Frame的显示时间

在计算某一帧的显示时间之前,现来弄清楚FFmpeg中的时间单位:时间基(TIME BASE)。在FFmpeg中存在这多个不同的时间基,对应着视频处理的不同的阶段(分布于不同的结构体中)。在本文中使用的是AVStream的时间基,来指示Frame显示时的时间戳(timestamp)。

/*** This is the fundamental unit of time (in seconds) in terms* of which frame timestamps are represented.**/
AVRational time_base;

可以看出,AVStream中的time_base是以秒为单位,表示frame显示的时间,其类型为AVRational。 AVRational是一个分数,其声明如下:

/*** rational number numerator/denominator*/
typedef struct AVRational{int num; ///< numeratorint den; ///< denominator
} AVRational;

num为分子,den为分母。
PTS为一个uint64_t的整型,其单位就是time_base。表示视频长度的duration也是一个uint64_t,那么使用如下方法就可以计算出一个视频流的时间长度:

time(second) = st->duration * av_q2d(st->time_base)

st为一个AVStream的指针,av_q2d将一个AVRational转换为双精度浮点数。同样的方法也可以得到视频中某帧的显示时间

timestamp(second) = pts * av_q2d(st->time_base)

也就是说,得到了Frame的PTS后,就可以得到该frame显示的时间戳。

得到Frame的PTS

通过上面的描述知道,如果有了Frame的PTS就计算出帧的显示的时间。下面的代码展示了在从packet中解码出frame后,如何得到frame的PTS

ret = avcodec_receive_frame(video->video_ctx, frame);
if (ret < 0 && ret != AVERROR_EOF)continue;if ((pts = av_frame_get_best_effort_timestamp(frame)) == AV_NOPTS_VALUE)pts = 0;pts *= av_q2d(video->stream->time_base);pts = video->synchronize(frame, pts);frame->opaque = &pts;

注意,这里的pts是double型,因为将其乘以了time_base,代表了该帧在视频中的时间位置(秒为单位)。有可能存在调用av_frame_get_best_effort_timestamp得不到一个正确的PTS,这样的情况放在函数synchronize中处理。

double VideoState::synchronize(AVFrame *srcFrame, double pts)
{double frame_delay;if (pts != 0)video_clock = pts; // Get pts,then set video clock to itelsepts = video_clock; // Don't get pts,set it to video clockframe_delay = av_q2d(stream->codec->time_base);frame_delay += srcFrame->repeat_pict * (frame_delay * 0.5);video_clock += frame_delay;return pts;
}

video_clock是视频播放到当前帧时的已播放的时间长度。在synchronize函数中,如果没有得到该帧的PTS就用当前的video_clock来近似,然后更新video_clock的值。

到这里已经知道了video中frame的显示时间了(秒为单位),下面就描述如果得到Audio的播放时间,并以此时间为基准来安排video中显示时间。

获取Audio Clock

Audio Clock,也就是Audio的播放时长,可以在Audio时更新Audio Clock。在函数audio_decode_frame中解码新的packet,这是可以设置Auddio clock为该packet的PTS

if (pkt.pts != AV_NOPTS_VALUE)
{audio_state->audio_clock = av_q2d(audio_state->stream->time_base) * pkt.pts;
}

由于一个packet中可以包含多个帧,packet中的PTS比真正的播放的PTS可能会早很多,可以根据Sample Rate 和 Sample Format来计算出该packet中的数据可以播放的时长,再次更新Audio clock 。

// 每秒钟音频播放的字节数 sample_rate * channels * sample_format(一个sample占用的字节数)
audio_state->audio_clock += static_cast<double>(data_size) / (2 * audio_state->stream->codec->channels *            audio_state->stream->codec->sample_rate);

上面乘以2是因为sample format是16位的无符号整型,占用2个字节。
有了Audio clock后,在外面获取该值的时候却不能直接返回该值,因为audio缓冲区的可能还有未播放的数据,需要减去这部分的时间

double AudioState::get_audio_clock()
{int hw_buf_size = audio_buff_size - audio_buff_index;int bytes_per_sec = stream->codec->sample_rate * audio_ctx->channels * 2;double pts = audio_clock - static_cast<double>(hw_buf_size) / bytes_per_sec;return pts;
}

用audio缓冲区中剩余的数据除以每秒播放的音频数据得到剩余数据的播放时间,从Audio clock中减去这部分的值就是当前的audio的播放时长。

同步

现在有了video中Frame的显示时间,并且得到了作为基准时间的音频播放时长Audio clock ,可以将视频同步到音频了。

  • 用当前帧的PTS - 上一播放帧的PTS得到一个延迟时间
  • 用当前帧的PTS和Audio Clock进行比较,来判断视频的播放速度是快了还是慢了
  • 根据上一步额判断结果,设置播放下一帧的延迟时间。

使用要播放的当前帧的PTS和上一帧的PTS差来估计播放下一帧的延迟时间,并根据video的播放速度来调整这个延迟时间,以实现视音频的同步播放。
具体实现:

// 将视频同步到音频上,计算下一帧的延迟时间
// 使用要播放的当前帧的PTS和上一帧的PTS差来估计播放下一帧的延迟时间,并根据video的播放速度来调整这个延迟时间
double current_pts = *(double*)video->frame->opaque;
double delay = current_pts - video->frame_last_pts;
if (delay <= 0 || delay >= 1.0)delay = video->frame_last_delay;video->frame_last_delay = delay;
video->frame_last_pts = current_pts;// 根据Audio clock来判断Video播放的快慢
double ref_clock = media->audio->get_audio_clock();double diff = current_pts - ref_clock;// diff < 0 => video slow,diff > 0 => video quickdouble threshold = (delay > SYNC_THRESHOLD) ? delay : SYNC_THRESHOLD;// 调整播放下一帧的延迟时间,以实现同步
if (fabs(diff) < NOSYNC_THRESHOLD) // 不同步
{if (diff <= -threshold) // 慢了,delay设为0delay = 0;else if (diff >= threshold) // 快了,加倍delaydelay *= 2;
}
video->frame_timer += delay;
double actual_delay = video->frame_timer - static_cast<double>(av_gettime()) / 1000000.0;
if (actual_delay <= 0.010)actual_delay = 0.010; // 设置一下帧播放的延迟
schedule_refresh(media, static_cast<int>(actual_delay * 1000 + 0.5));

frame_last_ptsframe_last_delay是上一帧的PTS以及设置的播放上一帧时的延迟时间。

  • 首先根据当前播放帧的PTS和上一播放帧的PTS估算出一个延迟时间。
  • 用当前帧的PTS和Audio clock相比较判断此时视频播放的速度是快还是慢了
  • 视频播放过快则加倍延迟,过慢则将延迟设置为0
  • frame_timer保存着视频播放的延迟时间总和,这个值和当前时间点的差值就是播放下一帧的真正的延迟时间
  • schedule_refresh 设置播放下一帧的延迟时间。

Summary

本文主要描述如何利用audio的播放时长作为基准,将视频同步到音频上以实现视音频的同步播放。视音频的同步过程是一个动态过程,快者等待,慢则加快播放,在这样的你等我赶的过程过程中实现同步播放。
本文代码:https://github.com/brookicv/FSplayer


这篇关于FFmpeg学习6:视音频同步的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


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

相关文章

Mysql的主从同步/复制的原理分析

《Mysql的主从同步/复制的原理分析》:本文主要介绍Mysql的主从同步/复制的原理分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录为什么要主从同步?mysql主从同步架构有哪些?Mysql主从复制的原理/整体流程级联复制架构为什么好?Mysql主从复制注意

Android学习总结之Java和kotlin区别超详细分析

《Android学习总结之Java和kotlin区别超详细分析》Java和Kotlin都是用于Android开发的编程语言,它们各自具有独特的特点和优势,:本文主要介绍Android学习总结之Ja... 目录一、空安全机制真题 1:Kotlin 如何解决 Java 的 NullPointerExceptio

Python使用FFmpeg实现高效音频格式转换工具

《Python使用FFmpeg实现高效音频格式转换工具》在数字音频处理领域,音频格式转换是一项基础但至关重要的功能,本文主要为大家介绍了Python如何使用FFmpeg实现强大功能的图形化音频转换工具... 目录概述功能详解软件效果展示主界面布局转换过程截图完成提示开发步骤详解1. 环境准备2. 项目功能结

SpringBoot使用ffmpeg实现视频压缩

《SpringBoot使用ffmpeg实现视频压缩》FFmpeg是一个开源的跨平台多媒体处理工具集,用于录制,转换,编辑和流式传输音频和视频,本文将使用ffmpeg实现视频压缩功能,有需要的可以参考... 目录核心功能1.格式转换2.编解码3.音视频处理4.流媒体支持5.滤镜(Filter)安装配置linu

Mac备忘录怎么导出/备份和云同步? Mac备忘录使用技巧

《Mac备忘录怎么导出/备份和云同步?Mac备忘录使用技巧》备忘录作为iOS里简单而又不可或缺的一个系统应用,上手容易,可以满足我们日常生活中各种记录的需求,今天我们就来看看Mac备忘录的导出、... 「备忘录」是 MAC 上的一款常用应用,它可以帮助我们捕捉灵感、记录待办事项或保存重要信息。为了便于在不同

查看MySql主从同步的偏移量方式

《查看MySql主从同步的偏移量方式》:本文主要介绍查看MySql主从同步的偏移量方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 1.mysql的主从同步方案mysqlphp为了在实现读写分离,主库写,从库读mysql的同步方案主要是通过从库读取主库的binl

重新对Java的类加载器的学习方式

《重新对Java的类加载器的学习方式》:本文主要介绍重新对Java的类加载器的学习方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、介绍1.1、简介1.2、符号引用和直接引用1、符号引用2、直接引用3、符号转直接的过程2、加载流程3、类加载的分类3.1、显示

Android NDK版本迭代与FFmpeg交叉编译完全指南

《AndroidNDK版本迭代与FFmpeg交叉编译完全指南》在Android开发中,使用NDK进行原生代码开发是一项常见需求,特别是当我们需要集成FFmpeg这样的多媒体处理库时,本文将深入分析A... 目录一、android NDK版本迭代分界线二、FFmpeg交叉编译关键注意事项三、完整编译脚本示例四

MySQL主从同步延迟问题的全面解决方案

《MySQL主从同步延迟问题的全面解决方案》MySQL主从同步延迟是分布式数据库系统中的常见问题,会导致从库读取到过期数据,影响业务一致性,下面我将深入分析延迟原因并提供多层次的解决方案,需要的朋友可... 目录一、同步延迟原因深度分析1.1 主从复制原理回顾1.2 延迟产生的关键环节二、实时监控与诊断方案

Java学习手册之Filter和Listener使用方法

《Java学习手册之Filter和Listener使用方法》:本文主要介绍Java学习手册之Filter和Listener使用方法的相关资料,Filter是一种拦截器,可以在请求到达Servl... 目录一、Filter(过滤器)1. Filter 的工作原理2. Filter 的配置与使用二、Listen