音视频学习:同步原理及实现

news2024/11/30 10:52:11

1.音视频同步简单介绍

对于一个播放器,一般来说,其基本构成均可划分为以下几部分:
数据接收(网络/本地)->解复用->音视频解码->音视频同步->音视频输出。
基本框架如下图所示:
在这里插入图片描述
为什么需要音视频同步?
媒体数据经过解复用流程后,音频/视频解码便是独立的,也是独立播放的。而在音频流和视频流中,其播放速度都是有相关信息指定的:

  • 视频:帧率,表示视频一秒显示的帧数。
  • 音频:采样率,表示音频一秒播放的样本的个数。
    在这里插入图片描述
    从帧率及采样率,即可知道视频/音频播放速度。声卡和显卡均是以一帧数据来作为播放单位,如果单纯依赖帧率及采样率来进行播放,在理想条件下,应该是同步的,不会出现偏差。
    以一个44.1KHz的AAC音频流和24FPS的视频流为例:

一个AAC音频frame每个声道包含1024个采样点,则一个frame的播放时长(duration)为:(1024/44100)×1000ms = 23.22ms;一个视频frame播放时长(duration)为:1000ms/24 = 41.67ms。理想情况下,音视频完全同步,音视频播放过程如下图所示:
在这里插入图片描述
但实际情况下,如果用上面那种简单的方式,慢慢的就会出现音视频不同步的情况,要不是视频播放快了,要么是音频播放快了。可能的原因如下:

  1. 一帧的播放时间,难以精准控制。音视频解码及渲染的耗时不同,可能造成每一帧输出有一点细微差距,长久累计,不同步便越来越明显。(例如受限于性能,42ms才能输出一帧)
  2. 音频输出是线性的,而视频输出可能是非线性,从而导致有偏差。
  3. 媒体流本身音视频有差距。(特别是TS实时流,音视频能播放的第一个帧起点不同)

所以,解决音视频同步问题,引入了时间戳:
首先选择一个参考时钟(要求参考时钟上的时间是线性递增的);
编码时依据参考时钟上的给每个音视频数据块都打上时间戳;
播放时,根据音视频时间戳及参考时钟,来调整播放。
所以,视频和音频的同步实际上是一个动态的过程,同步是暂时的,不同步则是常态。以参考时钟为标准,放快了就减慢播放速度;播放快了就加快播放的速度。

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

2.DTS和PTS简介

2.1I/P/B帧

在介绍DTS/PTS之前,我们先了解I/P/B帧的概念。I/P/B帧本身和音视频同步关系不大,但理解其概念有助于我们了解DTS/PTS存在的意义。
视频本质上是由一帧帧画面组成,但实际应用过程中,每一帧画面会进行压缩(编码)处理,已达到减少空间占用的目的。

编码方式可以分为帧内编码和帧间编码。

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

帧间编码:
由于视频的特性,相邻的帧之间其实是很相似的,通常是运动矢量的变化。利用其时间相关性,可以通过参考帧运动矢量的变化来预测图像,并结合预测图像与原始图像的差分,便能解码出原始图像。所以,帧间编码需要依赖其他帧才能解码出一帧画面。

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

I 帧(Intra coded frames):

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

P 帧(Predicted frames):

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

B 帧(Bi-directional predicted frames):

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头信息里。
在这里插入图片描述
在这里插入图片描述
标志第一位是PTS标识,第二位是DTS标识。
标志:
00,表示无PTS无DTS;
01,错误,不能只有DTS没有PTS;
10,有PTS;
11,有PTS和DTS。
PTS有33位,但是它不是直接的33位数据,而是占了5个字节,PTS分别在这5字节中取。
在这里插入图片描述
S的I/P帧携带PTS/DTS信息,B帧PTS/DTS相等,进保留PTS;由于声音没有用到双向预测,它的解码次序就是它的显示次序,故它只有PTS。

TS的编码器中有一个系统时钟STC(其频率是27MHz),此时钟用来产生指示音视频的正确显示和解码时间戳。
PTS域在PES中为33bits,是对系统时钟的300分频的时钟的计数值。它被编码成为3个独立的字段:
PTS[32…30][29…15][14…0]。
DTS域在PES中为33bits,是对系统时钟的300分频的时钟的计数值。它被编码成为3个独立的字段:
DTS[32…30][29…15][14…0]。
因此,对于TS流,PTS/DTS时间基均为1/90000秒(27MHz经过300分频)。
PTS对于TS流的意义不仅在于音视频同步,TS流本身不携带duration(可播放时长)信息,所以计算duration也是根据PTS得到。

附上TS流解析PTS示例:

#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)),让当前帧尽快显示。如果视频落后超过1秒,且之前10次都快速输出视频帧,那么需要反馈给音频源减慢,同时反馈视频源进行丢帧处理,让视频尽快追上音频。因为这很可能是视频解码跟不上了,再怎么调整delay也没用。
  6. 如果超过sync_threshold,且视频快于音频,那么需要加大delay,让当前帧延迟显示。
    将delay*2慢慢调整差距,这是为了平缓调整差距,因为直接delay+diff,会让画面画面迟滞。
    如果视频前一帧本身显示时间很长,那么直接delay+diff一步调整到位,因为这种情况再慢慢调整也没太大意义。
  7. 考虑到渲染的耗时,还需进行调整。frame_timer为一帧显示的系统时间,frame_timer+delay- curr_time,则得出正在需要延迟显示当前帧的时间。
{
    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);
}
音视频开发 视频教程:https://ke.qq.com/course/3202131?flowToken=1031864(免费订阅不迷路)
音视频开发学习资料、教学视频,免费分享有需要的可以自行添加学习交流群:739729163 领取

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

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

相关文章

Python学习之批量word文档转pdf并统计其页码

pypdf2是一个Python模块&#xff0c;可以用来读取、写入和操作PDF文件。要安装pypdf2模块&#xff0c;请按照以下步骤操作&#xff1a; 确保你已经安装了Python。你可以在终端或命令提示符中输入python --version来检查Python是否已安装。 pypdf2模块的安装&#xff1a; Modu…

【2023年电工杯数学建模竞赛B题人工智能对大学生学习影响的评价】完整思路分析+完整代码

1.问题背景与描述 这道题整体还是相对简单的&#xff0c;比较适合新手&#xff0c;选的人多对应获奖数量也会多&#xff0c;所以不要纠结于选题&#xff0c;就选你看上去能做的就好 2.问题分析 2.1 问题一的分析 对附件2中所给数据进行分析和数值化处理&#xff0c;并给出处…

GitHub标星11.9k兼职项目!基于SpringBoot + VUE电商-分销商城系统(附源码)

商城介绍 商城系统支持商家入驻&#xff0c;后端基于SpringBoot 研发&#xff0c;前端使用 Vue、uniapp开发&#xff0c; 系统全端全部代码开源 前后端分离&#xff0c;支持分布式部署&#xff0c;支持Docker&#xff0c;各个API独立&#xff0c;并且有独立的消费者。 商城 …

【VR】手柄定位技术

1. 关于Quest Pro头显、控制器的规格分析&#xff08;终篇&#xff09;及Quest 3分辨率 &#xff08;2022年07月29日&#xff09;被认为是“Quest Pro”的高端一体机Project Cambria将于今年秋季正式发布。对于一直关注和分享所述设备情报的YouTuber布拉德利林奇&#xff08;B…

python知识点总结(1)

声明&#xff1a;python不同于其他语言的最厉害之处就在于其他的第三方库可以实现不同的功能&#xff0c;但是在学习其他第三方库之前&#xff0c;我们还需要学习完python语言的基础。基础我们都学过&#xff0c;但是总有一些知识点平时很少使用&#xff0c;再碰到很陌生。这里…

云原生之深入解析Docker容器退出码的含义和产生原因

一、前言 为什么我的容器没有运行?回答这个问题之前,需要知道 Docker 容器为什么退出?退出码会提示容器停止运行的情况?本文列出最常见的退出码,来回答两个重要问题:这些退出码是什么意思?导致该退出码的动作是什么?exit code:代表一个进程的返回码,通过系统调用 exi…

URLConnection(三)

文章目录 1. 配置连接2. protected URL url3. protected boolean connected4. protected boolean allowUserInteraction5. protected boolean doInput5. protected boolean doOutput6. protected boolean isModifiedSince7. protected boolean useCaches8. 超时 1. 配置连接 U…

基于Springboot + vue的云盘系统

目录 一. &#x1f981; 前言二. &#x1f981; 主要技术栈三. &#x1f981; 架构搭建1. 项目搭建效果2. 各部分作用 四. &#x1f981; 主要功能1.功能图2. 主要功能2.1 分片上传文件2.2 存储分享记录 五. &#x1f981; 效果显示 一. &#x1f981; 前言 本项目是在B站学习的…

SSM 如何使用 RabbitMQ 实现消息队列

SSM 如何使用 RabbitMQ 实现消息队列 简介 在分布式系统中&#xff0c;消息队列是一种常见的通信方式&#xff0c;可以实现不同服务之间的异步通信和解耦。RabbitMQ 是一个开源的消息队列软件&#xff0c;本文将介绍如何在 SSM 框架中使用 RabbitMQ 实现消息队列。 本文将使…

python 自动化学习(三) 句柄获取、模拟按键、opencv安装

一、什么是句柄 句柄是在操作系统中的一种标识符&#xff0c;相当于我们每个人的身份证一样&#xff0c;句柄在电脑中也是有唯一性的&#xff0c;我们启动的每一个程序都有自己的句柄号&#xff0c;表示自己的身份 为什么要说句柄&#xff0c;我们如果想做自动化操作时&#xf…

【数据库复习】第六章 关系数据理论 2

若R∈BCNF 所有非主属性对每一个码都是完全函数依赖 所有的主属性对每一个不包含它的码&#xff0c;也是完全函数依赖 没有任何属性完全函数依赖于非码的任何一组属性 多值依赖 Teaching具有唯一候选码(C&#xff0c;T&#xff0c;B)&#xff0c; 即全码&#xff0c; ∈3NF …

JAVA商城源码-B2B2C商城系统-独立部署,一套源码终身可用

在现在电商迅速占领市场的时代里&#xff0c;选择开发商城系统已经成为了一种趋势&#xff0c;现在开发搭建商城系统有很多编程语言可以选择&#xff0c;目前在电商里市面上受到很多商家企业的喜爱的便是Java商城系统&#xff0c;那为什么要选择Java电商系统呢&#xff1f; 1、…

linuxOPS基础_服务器构成

服务器的重要结构组成 家用电脑组成&#xff1a; CPU、主板、内存条、显卡、硬盘、电源、风扇、网卡、显示器、机箱、键盘鼠标等等。 CPU CPU是电脑的大脑&#xff0c; CPU发展史&#xff1a; 32 位CPU&#xff1a;最大的内存寻址地址2^32&#xff0c;大约4G的大小。 CP…

js 常用函数 push()、pop()、shift()、unshift()、slice()、splice() 等

文章目录 1. join() 函数2.push() 函数3. pop() 函数4.shift() 函数5.unshift() 函数6.sort() 函数7. reverse() 函数8. concat() 函数9.slice() 函数10. splice() 函数11. indexOf() & lastIndexOf() 函数 最近对前端一些函数的用法还不是很熟悉&#xff0c;有一些函数容易…

手持式网络性能测试仪应用于哪些领域及可以完成什么工作?

首先明辰智航国产网络一点通有千兆和万兆以手持式网络性能测试仪&#xff0c;两款仪器可以应用于以下领域&#xff1a; 电信运营商&#xff1a;用于测试网络质量、信号强度、带宽、时延、丢包率等参数&#xff0c;以便优化网络性能和提高用户满意度。 企业网络管理&#xff1a…

【什么是iMessage苹果推?】什么是苹果推信?什么是苹果推?

挑选得当的IM推送平台&#xff1a;选择合用于PC真个IM推送平台 开辟或集成API&#xff1a;依照所选平台的开发文档&#xff0c;利用响应的编程语言&#xff08;如Python、Java等&#xff09;开发或集成API&#xff0c;以便与平台举行交互和节制。API可用于建立、办理和发送消息…

STM32F103 USB实现虚拟串口

STM32F103 USB实现虚拟串口 最近买了一个STM32F103C8T6最小核心板&#xff0c;使用CubeIDE无法识别该芯片&#xff0c;发现该芯片的flash是128Kbytes&#xff0c;ST的标准库是64Kbytes&#xff0c;奇怪啊&#xff01;也许是国产替代的&#xff0c;国产化太先进了&#xff0c;导…

CCIG:智能文档处理「新未来」

文章目录 ⭐️ CCIG大会简介⭐️ 领先世界的智能文档处理技术&#x1f31f; 智能图像处理&#xff1a;为文字识别 "增质提效" 筑基✨ 切边增强 - 提升文档图像质量✨ 弯曲矫正 - 解决图像畸变问题✨ 去摩尔纹 - 保证图像信息完整 &#x1f31f; 图像预处理整体效果展…

汇编基础知识

1.汇编工程流程: 汇编指令--->编译器--->机器码--->计算机 2.汇编语言组成: 1.汇编指令 2.伪指令 3.其他符号 3.存储器: 存放指令与数据的容器,也叫内存. 存储器被划分为多个单元,并且从0开始按钮顺序编号,这些编号视为存储器的存储单元的地址. 4.指令与…

《Cocos Creator游戏实战》老虎机抽奖效果实现思路

在线体验地址 Cocos Creator | SlotMachine Cocos Store 购买地址&#xff08;如果没有显示&#xff0c;那就是还在审核&#xff09;&#xff1a; https://store.cocos.com/app/detail/4958微店购买地址&#xff1a; https://weidian.com/item.html?itemID6338406353运行效果…