音视频同步原理及实现(转载)

news/2024/11/16 13:11:17/文章来源:https://www.cnblogs.com/zeliangzhang/p/18304610

# 音视频同步原理及实现

本文主要描述音视频同步原理,及常见的音视频同步方案,并以代码示例,展示如何以音频的播放时长为基准,将视频同步到音频上以实现视音频的同步播放。内容如下:

* 1.音视频同步简单介绍

* 2.DTS和PTS简介

* 2.1I/P/B帧

* 2.2时间戳DTS、PTS

* 3.常用同步策略

* 4.音视频同步简单示例代码

## 1.音视频同步简单介绍

对于一个播放器,一般来说,其基本构成均可划分为以下几部分:<br/>

<h4>数据接收(网络/本地)->解复用->音视频解码->音视频同步->音视频输出</h4><br/>

基本框架如下图所示:

为什么需要音视频同步?

媒体数据经过解复用流程后,音频/视频解码便是独立的,也是独立播放的。而在音频流和视频流中,其播放速度都是有相关信息指定的:

* 视频:帧率,表示视频一秒显示的帧数。

* 音频:采样率,表示音频一秒播放的样本的个数。

从帧率及采样率,即可知道视频/音频播放速度。声卡和显卡均是以一帧数据来作为播放单位,如果单纯依赖帧率及采样率来进行播放,在理想条件下,应该是同步的,不会出现偏差。

以一个44.1KHz的AAC音频流和24FPS的视频流为例:

一个AAC音频frame每个声道包含1024个采样点,则一个frame的播放时长(duration)为:(1024/44100)×1000ms = 23.22ms;一个视频frame播放时长(duration)为:1000ms/24 = 41.67ms。<br/>

理想情况下,音视频完全同步,音视频播放过程如下图所示:

但实际情况下,如果用上面那种简单的方式,慢慢的就会出现音视频不同步的情况,要不是视频播放快了,要么是音频播放快了。可能的原因如下:

1. 一帧的播放时间,难以精准控制。音视频解码及渲染的耗时不同,可能造成每一帧输出有一点细微差距,长久累计,不同步便越来越明显。(例如受限于性能,42ms才能输出一帧)

2. 音频输出是线性的,而视频输出可能是非线性,从而导致有偏差。

3. 媒体流本身音视频有差距。(特别是TS实时流,音视频能播放的第一个帧起点不同)

所以,解决音视频同步问题,引入了时间戳:

首先选择一个参考时钟(要求参考时钟上的时间是线性递增的);

编码时依据参考时钟上的给每个音视频数据块都打上时间戳;

播放时,根据音视频时间戳及参考时钟,来调整播放。

所以,视频和音频的同步实际上是一个动态的过程,同步是暂时的,不同步则是常态。以参考时钟为标准,放快了就减慢播放速度;播放快了就加快播放的速度。

接下来,我们介绍媒体流中时间戳的概念。

## 2.DTS和PTS简介

### 2.1I/P/B帧

在介绍DTS/PTS之前,我们先了解I/P/B帧的概念。I/P/B帧本身和音视频同步关系不大,但理解其概念有助于我们了解DTS/PTS存在的意义。

视频本质上是由一帧帧画面组成,但实际应用过程中,每一帧画面会进行压缩(编码)处理,已达到减少空间占用的目的。

>编码方式可以分为帧内编码和帧间编码。<br/>

>内编码方式:<br/>

>即只利用了单帧图像内的空间相关性,对冗余数据进行编码,达到压缩效果,而没有利用时间相关性,不使用运动补偿。所以单靠自己,便能完整解码出一帧画面。<br/>

>帧间编码:<br/>

>由于视频的特性,相邻的帧之间其实是很相似的,通常是运动矢量的变化。利用其时间相关性,可以通过参考帧运动矢量的变化来预测图像,并结合预测图像与原始图像的差分,便能解码出原始图像。<br/>

>所以,帧间编码需要依赖其他帧才能解码出一帧画面。

由于编码方式的不同,视频中的画面帧就分为了不同的类别,其中包括:I 帧、P 帧、B 帧。I 帧、P 帧、B 帧的区别在于:

>* I 帧(Intra coded frames):<br/>

>I 帧图像采用帧I 帧使用帧内压缩,不使用运动补偿,由于 I 帧不依赖其它帧,可以独立解码。I 帧图像的压缩倍数相对较低,周期性出现在图像序列中的,出现频率可由编码器选择。

>* P 帧(Predicted frames):<br/>

>P 帧采用帧间编码方式,即同时利用了空间和时间上的相关性。P 帧图像只采用前向时间预测,可以提高压缩效率和图像质量。P 帧图像中可以包含帧内编码的部分,即 P 帧中的每一个宏块可以是前向预测,也>可以是帧内编码。

>* B 帧(Bi-directional predicted frames):<br/>

>B 帧图像采用帧间编码方式,且采用双向时间预测,可以大大提高压缩倍数。也就是其在时间相关性上,还依赖后面的视频帧,也正是由于 B 帧图像采用了后面的帧作为参考,因此造成视频帧的传输顺序和显示顺序是不同的。

也就是说,一个 I 帧可以不依赖其他帧就解码出一幅完整的图像,而 P 帧、B 帧不行。P 帧需要依赖视频流中排在它前面的帧才能解码出图像。B 帧则需要依赖视频流中排在它前面或后面的I/P帧才能解码出图像。

对于I帧和P帧,其解码顺序和显示顺序是相同的,但B帧不是,如果视频流中存在B帧,那么就会打乱解码和显示顺序。

正因为解码和显示的这种非线性关系,所以需要DTS、PTS来标识帧的解码及显示时间。

### 2.2时间戳DTS、PTS

* DTS(Decoding Time Stamp):即解码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。

* PTS(Presentation Time Stamp):即显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。

当视频流中没有 B 帧时,通常 DTS 和 PTS 的顺序是一致的。但如果有 B 帧时,就回到了我们前面说的问题:解码顺序和播放顺序不一致了,即视频输出是非线性的。

比如一个视频中,帧的显示顺序是:I B B P,因为B帧解码需要依赖P帧,因此这几帧在视频流中的顺序可能是:I P B B,这时候就体现出每帧都有 DTS 和 PTS 的作用了。DTS 告诉我们该按什么顺序解码这几帧图像,PTS 告诉我们该按什么顺序显示这几帧图像。顺序大概如下:

从流分析工具看,流中P帧在B帧之前,但显示确实在B帧之后。

需要注意的是:虽然 DTS、PTS 是用于指导播放端的行为,但它们是在编码的时候由编码器生成的。

以我们最常见的TS为例:

>TS流中,PTS/DTS信息在打流阶段生成在PES层,主要是在PES头信息里。<br/>

>![im

>标志第一位是PTS标识,第二位是DTS标识。<br/>

>标志:<br/>

>00,表示无PTS无DTS;<br/>

>01,错误,不能只有DTS没有PTS;<br/>

>10,有PTS;<br/>

>11,有PTS和DTS。<br/>

>PTS有33位,但是它不是直接的33位数据,而是占了5个字节,PTS分别在这5字节中取。<br/>

>TS的I/P帧携带PTS/DTS信息,B帧PTS/DTS相等,进保留PTS;由于声音没有用到双向预测,它的解码次序就是它的显示次序,故它只有PTS。

>TS的编码器中有一个系统时钟STC(其频率是27MHz),此时钟用来产生指示音视频的正确显示和解码时间戳。<br/>

>PTS域在PES中为33bits,是对系统时钟的300分频的时钟的计数值。它被编码成为3个独立的字段:<br/>

>PTS[32…30][29…15][14…0]。<br/>

>DTS域在PES中为33bits,是对系统时钟的300分频的时钟的计数值。它被编码成为3个独立的字段:<br/>

>DTS[32…30][29…15][14…0]。<br/>

>因此,对于TS流,PTS/DTS时间基均为1/90000秒(27MHz经过300分频)。<br/>

>PTS对于TS流的意义不仅在于音视频同步,TS流本身不携带duration(可播放时长)信息,所以计算duration也是根据PTS得到。

附上TS流解析PTS示例:

```C++

#define MAKE_WORD(h, l) (((h) << 8) | (l))

//packet为PES

int64_t get_pts(const uint8_t *packet)

{

const uint8_t *p = packet;

if(packet == NULL) {

return -1;

}

if(!(p[0] == 0x00 && p[1] == 0x00 && p[2] == 0x01)) { //pes sync word

return -1;

}

p += 3; //jump pes sync word

p += 4; //jump stream id(1) pes length(2) pes flag(1)

int pts_pts_flag = *p >> 6;

p += 2; //jump pes flag(1) pes header length(1)

if (pts_pts_flag & 0x02) {

int64_t pts32_30, pts29_15, pts14_0, pts;

pts32_30 = (*p) >> 1 & 0x07;

p += 1;

pts29_15 = (MAKE_WORD(p[0],p[1])) >> 1;

p += 2;

pts14_0 = (MAKE_WORD(p[0],p[1])) >> 1;

p += 2;

pts = (pts32_30 << 30) | (pts29_15 << 15) | pts14_0;

return pts;

}

return -1;

}

```

## 3.常用同步策略

前面已经说了,实现音视频同步,在播放时,需要选定一个参考时钟,读取帧上的时间戳,同时根据的参考时钟来动态调节播放。现在已经知道时间戳就是PTS,那么参考时钟的选择一般来说有以下三种:

1. 将视频同步到音频上:就是以音频的播放速度为基准来同步视频。

2. 将音频同步到视频上:就是以视频的播放速度为基准来同步音频。

3. 将视频和音频同步外部的时钟上:选择一个外部时钟为基准,视频和音频的播放速度都以该时钟为标准。

当播放源比参考时钟慢,则加快其播放速度,或者丢弃;快了,则延迟播放。

这三种是最基本的策略,考虑到人对声音的敏感度要强于视频,频繁调节音频会带来较差的观感体验,且音频的播放时钟为线性增长,所以一般会以音频时钟为参考时钟,视频同步到音频上。

在实际使用基于这三种策略做一些优化调整,例如:

* 调整策略可以尽量采用渐进的方式,因为音视频同步是一个动态调节的过程,一次调整让音视频PTS完全一致,没有必要,且可能导致播放异常较为明显。

* 调整策略仅仅对早到的或晚到的数据块进行延迟或加快处理,有时候是不够的。如果想要更加主动并且有效地调节播放性能,需要引入一个反馈机制,也就是要将当前数据流速度太快或太慢的状态反馈给“源”,让源去放慢或加快数据流的速度。

* 对于起播阶段,特别是TS实时流,由于视频解码需要依赖第一个I帧,而音频是可以实时输出,可能出现的情况是视频PTS超前音频PTS较多,这种情况下进行同步,势必造成较为明显的慢同步。处理这种情况的较好方法是将较为多余的音频数据丢弃,尽量减少起播阶段的音视频差距。

## 4.音视频同步简单示例代码

代码参考ffplay实现方式,同时加入自己的修改。以audio为参考时钟,video同步到音频的示例代码:

1. 获取当前要显示的video PTS,减去上一帧视频PTS,则得出上一帧视频应该显示的时长delay;

2. 当前video PTS与参考时钟当前audio PTS比较,得出音视频差距diff;

3. 获取同步阈值sync_threshold,为一帧视频差距,范围为10ms-100ms;

4. diff小于sync_threshold,则认为不需要同步;否则delay+diff值,则是正确纠正delay;

5. 如果超过sync_threshold,且视频落后于音频,那么需要减小delay(FFMAX(0, delay + diff)),让当前帧尽快显示。<br/>

如果视频落后超过1秒,且之前10次都快速输出视频帧,那么需要反馈给音频源减慢,同时反馈视频源进行丢帧处理,让视频尽快追上音频。因为这很可能是视频解码跟不上了,再怎么调整delay也没用。

6. 如果超过sync_threshold,且视频快于音频,那么需要加大delay,让当前帧延迟显示。<br/>

将delay*2慢慢调整差距,这是为了平缓调整差距,因为直接delay+diff,会让画面画面迟滞。<br/>

如果视频前一帧本身显示时间很长,那么直接delay+diff一步调整到位,因为这种情况再慢慢调整也没太大意义。<br/>

7.考虑到渲染的耗时,还需进行调整。frame_timer为一帧显示的系统时间,frame_timer+delay- curr_time,则得出正在需要延迟显示当前帧的时间。

```C++

{

video->frameq.deQueue(&video->frame);

//获取上一帧需要显示的时长delay

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;

}

// 根据视频PTS和参考时钟调整delay

double ref_clock = audio->get_audio_clock();

double diff = current_pts - ref_clock;// diff < 0 :video slow,diff > 0 :video fast

//一帧视频时间或10ms,10ms音视频差距无法察觉

double sync_threshold = FFMAX(MIN_SYNC_THRESHOLD, FFMIN(MAX_SYNC_THRESHOLD, delay)) ;

audio->audio_wait_video(current_pts,false);

video->video_drop_frame(ref_clock,false);

if (!isnan(diff) && fabs(diff) < NOSYNC_THRESHOLD) // 不同步

{

if (diff <= -sync_threshold)//视频比音频慢,加快

{

delay = FFMAX(0, delay + diff);

static int last_delay_zero_counts = 0;

if(video->frame_last_delay <= 0)

{

last_delay_zero_counts++;

}

else

{

last_delay_zero_counts = 0;

}

if(diff < -1.0 && last_delay_zero_counts >= 10)

{

printf("maybe video codec too slow, adjust video&audio\n");

#ifndef DORP_PACK

audio->audio_wait_video(current_pts,true);//差距较大,需要反馈音频等待视频

#endif

video->video_drop_frame(ref_clock,true);//差距较大,需要视频丢帧追上

}

}

//视频比音频快,减慢

else if (diff >= sync_threshold && delay > SYNC_FRAMEDUP_THRESHOLD)

delay = delay + diff;//音视频差距较大,且一帧的超过帧最常时间,一步到位

else if (diff >= sync_threshold)

delay = 2 * delay;//音视频差距较小,加倍延迟,逐渐缩小

}

video->frame_last_delay = delay;

video->frame_last_pts = current_pts;

double curr_time = static_cast<double>(av_gettime()) / 1000000.0;

if(video->frame_timer == 0)

{

video->frame_timer = curr_time;//show first frame ,set frame timer

}

double actual_delay = video->frame_timer + delay - curr_time;

if (actual_delay <= MIN_REFRSH_S)

{

actual_delay = MIN_REFRSH_S;

}

usleep(static_cast<int>(actual_delay * 1000 * 1000));

//printf("actual_delay[%lf] delay[%lf] diff[%lf]\n",actual_delay,delay,diff);

// Display

SDL_UpdateTexture(video->texture, &(video->rect), video->frame->data[0], video->frame->linesize[0]);

SDL_RenderClear(video->renderer);

SDL_RenderCopy(video->renderer, video->texture, &video->rect, &video->rect);

SDL_RenderPresent(video->renderer);

video->frame_timer = static_cast<double>(av_gettime()) / 1000000.0 ;

av_frame_unref(video->frame);

//update next frame

schedule_refresh(media, 1);

}

原文作者: wusc'blog

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

外卖霸王餐系统怎么快速盈利赚钱?

微客云外卖霸王餐系统,作为近年来外卖行业中的一股新兴力量,以其独特的商业模式和营销策略,迅速吸引了大量消费者的目光。该系统通过提供显著的折扣和返利,让顾客能够以极低的价格甚至免费享受到美味的外卖,同时,也为外卖商家带来了可观的收益和市场曝光度。那么,外卖霸…

博客园配置留存

博客园配置留存 博客皮肤:SimpleMeory 侧边栏公告 <script type="text/javascript">window.cnblogsConfig = {info: {blogIcon: https://pic.cnblogs.com/avatar/3370073/20240107144051.png, // 请自己添加一个图片链接(网页标签),如果不需要的话可以删掉na…

ResXManager 使用

1.在扩展中安装ResXManager 2.添加一个资源文件Resource.resx 3.工具中找到ResXManager打开点击左边的刷新按钮加载资源文件,勾选需要使用的资源文件4.添加需要本地化资源 资源名称一般选择使用英文,为了使各个区域人能够理解 5.选择需要本地化的语言 添加的每一项语言都会加…

开源工作流引擎该如何扩展?扩展哪些功能?

目前主流的开源流程引擎有activiti、flowable、camunda等,这几个开源流程引擎的版本很多,哪个开源流程引擎哪个版本的功能更多、性能更好,该如何选择。 无论您选择哪一个开源流程引擎,都不可能直接拿来即用,均需要做一定的扩展开发和集成开发,才能满足项目真正的需求。以…

汽车通用微控制器S32K324NHT1MPBIR、S32K324NHT1MMMSR、S32K314EHT1MMMSR可为汽车开发软件提高安全性,简化开发工作,

S32K3系列与S32汽车平台兼容,实现了无缝软件重复使用和灵活性,适用于车身、区域控制和电气化应用。S32K3系列32位微控制器(MCU)提供基于Arm Cortex-M7的MCU,支持单核、双核和锁步内核配置。S32K3系列具有内核、内存和外设数量方面的可扩展性,能够实现高性能和功能安全,符合…

0188-输出宏和错误信息

环境Time 2022-11-13 WSL-Ubuntu 22.04 QEMU 6.2.0 Rust 1.67.0-nightly VSCode 1.73.1前言 说明 参考:https://os.phil-opp.com/vga-text-mode 目标 可以使用 println! 宏向屏幕输出错误,发送错误时,可以打印错误信息。 Cargo.toml 其中的 spin 为全局锁需要,lazy_static …

聊聊springboot项目脱离配置中心,如何实现属性动态刷新

前言 如果大家有开发过微服务项目,那对配置中心应该是耳熟能详了,配置中心有个很有用的能力,就是热更新属性,即不重启服务,就能做到属性的动态变更。而我们今天讲的话题是,怎么样不使用配置中心,也能达到如上的效果 如何实现属性的热更新 如果我们属性是配置在配置文件中…

关于docker使用中的问题

问题:今天发现一个服务报503,得知这个服务使用docker部署,部署在了test环境服务器中, 开始排查: 1.docker ps -a看了一下容器还在,状态正常。 2.docker logs -f 容器名 确认的日志也是正常的。但容器是另一个别的服务的,下图所示,我的服务是 op 这个服务叫mini 3.查了一…

架构与思维:微服务架构的思想本质

我们为什么需要微服务架构,它一定是为了解决我们某些问题才出现了。这篇文章我们讨论下微服务架构模式所解决的问题,带来的挑战,以及他的核心思想本质。 1 早期的服务架构上图是一个典型的服务分层架构: Client: 调用方是browser web或者App 应用层: 实现计算层的业务逻辑…

[羊城杯2020]easyphp 1

.htaccess的利用,rce,绕过<?php$files = scandir(./); foreach($files as $file) {if(is_file($file)){if ($file !== "index.php") {unlink($file);}}}if(!isset($_GET[content]) || !isset($_GET[filename])) {highlight_file(__FILE__);die();}$content = $…

科研单位如何安全又高效地使用文件摆渡U盘?

科研单位是专门从事科学研究与技术开发工作的机构,通常拥有高水平的科研团队和先进的科研设备。这些机构按照属性可以分为中央直属科研单位和地方科研单位。中央直属科研单位包括中国科学院、中国工程院等机构,以及原国家部委附属的科研单位、国家“211”和“985”高校及其所…

spi概念

目录一、SPI概念1. 引脚定义2. 数据收发3. 工作模式4. 数据格式 一、SPI概念串行外设接口(Serial Peripheral Interface)的简称也叫做SPI. 是一种高速的、全双工同步通信的一种接口. 串行外设接口一般是需要4根线来进行通信(NSS、MISO、MOSI、SCK)。 如果打算实现单向通信(…