目录

简介

延伸——buffering机制的影响

延伸——时间转换位置

延伸——pause操作的影响

延伸——delay设置

总结

简介

播放器的音视频同步无外乎三种方式:视频往音频同步、音频往视频同步及音视频同时往系统时钟同步。大部分播放器都是采取第一种方式,例如ffplay、EXOPlayer、Nuplayer、ijkplayer等。而VLC就比较不合群了,它采用音视频同时往系统时钟同步的方式,下面结合代码简单介绍下音画同步的原理。

VLC专门有一个clock.c文件,用来维护时钟信息,其核心结构体如下,阐述了VLC音画同步的核心思想:

typedef struct

{

    mtime_t i_stream;

    mtime_t i_system;

} clock_point_t;

VLC将连续的时间变为离散的clock_point_t,每一个clock_point_t都记录了当前时间点的i_system和i_stream。i_system比较好理解,就是系统时间;i_stream则是片源的时间,或者理解为dts,例如,你在上午10点整播放一个视频,那么第一个clock_point_t的i_system就是上午十点,i_stream则是视频第一帧的dts(大概率是0)。VLC维护这两套时间基准,就是因为视频文件的音视频流只包含stream时间,需要将它转换为系统时间才能够跟系统时钟进行同步。

input_clock_t结构体内部维护了两个clock_point_t,分别名为ref和last。ref用来记录起播或seek后的第一个时间点,last用来记录最近的时间点。这两个clock_point_t的更新策略在input_clock_Update()函数中:

/*****************************************************************************

 * input_clock_Update: manages a clock reference

 *

 *  i_ck_stream: date in stream clock

 *  i_ck_system: date in system clock

 *****************************************************************************/

void input_clock_Update( input_clock_t *cl, vlc_object_t *p_log,

                         bool *pb_late,

                         bool b_can_pace_control, bool b_buffering_allowed,

                         mtime_t i_ck_stream, mtime_t i_ck_system )

{

    bool b_reset_reference = false;

    assert( i_ck_stream > VLC_TS_INVALID && i_ck_system > VLC_TS_INVALID );

    vlc_mutex_lock( &cl->lock );

    //b_has_reference在初始化和seek后被置为false

    //因此起播和seek后b_reset_reference为true

    if( !cl->b_has_reference )

    {

        /* */

        b_reset_reference= true;

    }

    //pts发生跳变

    else if( cl->last.i_stream > VLC_TS_INVALID &&

             ( (cl->last.i_stream - i_ck_stream) > CR_MAX_GAP ||

               (cl->last.i_stream - i_ck_stream) < -CR_MAX_GAP ) )

    {

        /* Stream discontinuity, for which we haven't received a

         * warning from the stream control facilities (dd-edited

         * stream ?). */

        msg_Warn( p_log, "clock gap, unexpected stream discontinuity" );

        cl->i_ts_max = VLC_TS_INVALID;

        /* */

        msg_Warn( p_log, "feeding synchro with a new reference point trying to recover from clock gap" );

        b_reset_reference= true;

    }

    /* */

    //记录起播和seek后的第一个时间点ref

    if( b_reset_reference )

    {

        cl->i_next_drift_update = VLC_TS_INVALID;

        AvgReset( &cl->drift );

        /* Feed synchro with a new reference point. */

        cl->b_has_reference = true;

        cl->ref = clock_point_Create( i_ck_stream,

                                      __MAX( cl->i_ts_max + CR_MEAN_PTS_GAP, i_ck_system ) );

        cl->b_has_external_clock = false;

    }

    ......   

    /* */

    //更新最近时间点last

    cl->last = clock_point_Create( i_ck_stream, i_ck_system );

    /* It does not take the decoder latency into account but it is not really

     * the goal of the clock here */

    /* There also should consider playback rate */

    //这里在介绍buffering机制中提到过,用来判断数据是否来晚

    const mtime_t i_ts_delay = cl->i_pts_delay + ClockGetTsOffset( cl );

    const mtime_t i_system_expected = ClockStreamToSystem( cl, i_ck_stream + AvgGet( &cl->drift ) );

    const mtime_t i_late = ( i_ck_system - i_ts_delay ) - i_system_expected;

    *pb_late = i_late > 0;

    if( i_late > 0 )

    {

        cl->late.pi_value[cl->late.i_index] = i_late;

        cl->late.i_index = ( cl->late.i_index + 1 ) % INPUT_CLOCK_LATE_COUNT;

    }

    vlc_mutex_unlock( &cl->lock );

}

demux在解到数据后,会通过数据的dts来更新pcr,然后通过es_out_SetPCR()通知es_out,es_out每次setPCR都会调用上面的input_clock_Update()函数来更新时间信息(这部分流程都可以在代码里直接找到,这里就不再展开赘述了)。至此我们可以知道,ref的stream就是demux解到第一帧数据的dts,ref的system就是demux解到第一帧数据的系统时间。last则是demux解到最近一帧的dts和系统时间。

stream时间转换成system时间的函数如下:

/*****************************************************************************

 * ClockStreamToSystem: converts a movie clock to system date

 *****************************************************************************/

static mtime_t ClockStreamToSystem( input_clock_t *cl, mtime_t i_stream )

{

    if( !cl->b_has_reference )

        return VLC_TS_INVALID;

    return ( i_stream - cl->ref.i_stream ) * cl->i_rate / INPUT_RATE_DEFAULT +

           cl->ref.i_system;

}

至此,我们已经可以将stream时间转换成系统时间了,假设当前系统时间是10s,此时我们从视频20s的位置起播(1倍速),时钟信息如下:

stream

20

21

22

23

24

25

stream_to_system 10 11 12 13 14 15
system 10 11 12 14 14 15

stream为23时的计算公式为:

(23 - 20)× 1 + 10 = 13;

即我们在系统时间是10s时从视频的20s开始以1倍速播放,视频21s的内容应该在系统时间11s时展示。。。以此类推。这样看来我们的视频确实能够同步到系统时钟进行播放了。

再举一个倍速播放的例子,假设当前系统时间是30s,此时我们从视频10s的位置起播(2倍速),时钟信息如下:

stream

10

11

12

13

14

stream_to_system 30 30.5 31 31.5 32
system 30 30.5 31 31.5 32

stream为13时的计算公式为:

(13 - 10) × 0.5 + 30 = 31.5;

可以看到,本来需要4s才能播放完的视频,实际只用了2s,达到了两倍速播放的效果。

延伸——buffering机制的影响

对播放器有一定了解的同学都知道,播放器都是有buffering机制的。上面讲的同步机制只适用于理想条件下,一旦播放器发生了rebuffering,这个过程中视频是不会播放的,但系统时间仍在稳定增长,那么就会导致buffering结束后,所有帧都晚了,导致丢帧,如下表所示:

stream

20

buffering 1s

21

22

23

24

stream_to_system 10 - 11 12 13 14
system 10 11 12 13 14 15

可以看到,buffering之后的所有帧都晚了1s,会出现大量的丢帧,这种情况肯定是不能容忍的。因此VLC将buffering的这段时间也考虑在内了,参考es_out.c的EsOutDecodersStopBuffering()函数:

/* */

//i_wakeup_delay是10ms的线程唤醒时间,i_current_date就是buffering结束的系统时间

const mtime_t i_wakeup_delay = 10*1000/* FIXME CLEANUP thread wake up time*/

const mtime_t i_current_date = p_sys->b_paused ? p_sys->i_pause_date : mdate();

input_clock_ChangeSystemOrigin( p_sys->p_pgrm->p_clock, true,

                                i_current_date + i_wakeup_delay - i_buffering_duration );

至于input_clock_ChangeSystemOrigin就很简单了,把传进来的参数i_system做了下微调就设置给clock了。

void input_clock_ChangeSystemOrigin( input_clock_t *cl, bool b_absolute, mtime_t i_system )

{

    vlc_mutex_lock( &cl->lock );

    assert( cl->b_has_reference );

    mtime_t i_offset;

    if( b_absolute )

    {

        i_offset = i_system - cl->ref.i_system - ClockGetTsOffset( cl );

    }

    else

    {

        if( !cl->b_has_external_clock )

        {

            cl->b_has_external_clock = true;

            cl->i_external_clock     = i_system;

        }

        i_offset = i_system - cl->i_external_clock;

    }

    cl->ref.i_system += i_offset;

    cl->last.i_system += i_offset;

    vlc_mutex_unlock( &cl->lock );

}

延伸——时间转换位置

VLC的时间戳转换是在decoder.c中进行的,decoder.c的DecoderFixTs()函数调用了clock.c的input_clock_ConvertTS(),获取转换后的时间戳、duration、rate等信息。DecoderFixTs()函数又被DecoderPlayVideo()函数和DecoderPlayAudio()函数调用,说明decoder是对解码后的数据进行的时间戳转换。这里明确时间戳转换的位置,主要是为了更好的理解下一部分内容。

延伸——pause操作的影响

pause和buffering的情况比较像,pause状态下系统时钟仍在不断增长,resume时需要把这部分系统时间加到ref.i_system上。我们看下VLC是怎么做的:

//代码逻辑可以说是十分清晰了,就是记录了下pause和resume的系统时间,两者相减就能得到pause的持续时间,将这部分时间加到ref.i_system上就能做到stream和system时钟同步了。

/*****************************************************************************

 * input_clock_ChangePause:

 *****************************************************************************/

void input_clock_ChangePause( input_clock_t *cl, bool b_paused, mtime_t i_date )

{

    vlc_mutex_lock( &cl->lock );

    assert( (!cl->b_paused) != (!b_paused) );

    if( cl->b_paused )

    {

        const mtime_t i_duration = i_date - cl->i_pause_date;

        if( cl->b_has_reference && i_duration > 0 )

        {

            cl->ref.i_system += i_duration;

            cl->last.i_system += i_duration;

        }

    }

    cl->i_pause_date = i_date;

    cl->b_paused = b_paused;

    vlc_mutex_unlock( &cl->lock );

}

到这部分位置和buffering的逻辑都是一样的,为什么要分开讲呢?我们再回顾下时间戳转换的位置是在decoder取得加码完的数据后,VLC从demux到output的框架大概如下:

demux---->block_fifo---->decoder---->picture_fifo----> output

时间戳转换发生在上图的decoder和picture_fifo之间,即clock.c的修正只对picture_fifo之前的数据起作用,然而picture_fifo中也是有缓存一部分解码后的数据的,这部分数据如果在pause-resume后不加修正,也会因为too late丢帧。这么简单的情况VLC肯定也考虑在内了,解码后的时间戳修正在video_output.c中:

static void ThreadChangePause(vout_thread_t *vout, bool is_paused, mtime_t date)

{

    assert(!vout->p->pause.is_on || !is_paused);

    if (vout->p->pause.is_on) {

        const mtime_t duration = date - vout->p->pause.date;

        if (vout->p->step.timestamp > VLC_TS_INVALID)

            vout->p->step.timestamp += duration;

        if (vout->p->step.last > VLC_TS_INVALID)

            vout->p->step.last += duration;

        //这里将pause的持续时间加给了fifo中的每一帧

        picture_fifo_OffsetDate(vout->p->decoder_fifo, duration);

        //vout已经从fifo中取出来的帧也不放过

        if (vout->p->displayed.decoded)

            vout->p->displayed.decoded->date += duration;

        //字幕也需要处理

        spu_OffsetSubtitleDate(vout->p->spu, duration);

        ThreadFilterFlush(vout, false);

    else {

        vout->p->step.timestamp = VLC_TS_INVALID;

        vout->p->step.last      = VLC_TS_INVALID;

    }

    vout->p->pause.is_on = is_paused;

    vout->p->pause.date  = date;

    vout_window_t *window = vout->p->window;

    if (window != NULL)

        vout_window_SetInhibition(window, !is_paused);

}

延伸——delay设置

在VLC var机制中提到了VLC可以通过var来设置一些属性或下发event,其中就有audio-delay和spu-delay,我们可以通过这两个属性对音频和字幕的同步进行微调。属性设置的流程这里也不展开了,delay值最终生效的位置也是在decoder.c的DecoderFixTs()函数中。

总结

至此,VLC的音画同步原理就大致介绍完了,可以看到VLC用clock.c来维护时钟系统,其中最重要的就是名为ref的clock_point_t,它内部保存了起播时的stream时间和system时间,解码后的帧都需要以此为基准做时间戳的转换。对比其他两种同步方式,系统时钟的增长不会因为pause或buffering而停止,因此这两种情况需要特殊处理保证音画同步播放。

VLC-Android音画同步原理相关推荐

  1. 深入理解Android音视频同步机制(二)ExoPlayer的avsync逻辑

    深入理解Android音视频同步机制(一)概述 深入理解Android音视频同步机制(二)ExoPlayer的avsync逻辑 深入理解Android音视频同步机制(三)NuPlayer的avsync ...

  2. 音视频同步原理解析;音频编码和解码原理

    视频流中的DTS/PTS到底是什么? DTS(解码时间戳)和PTS(显示时间戳)分别是解码器进行解码和显示帧时相对于SCR(系统参考)的时间戳.SCR可以理解为解码器应该开始从磁盘读取数据时的时间. ...

  3. 音视频同步原理[ffmpeg]

    音视频同步原理[ffmpeg] output_example.c 中AV同步的代码如下(我的代码有些修改),这个实现相当简单,不过挺说明问题. 阅读前希望大家先了解一下时间戳的概念. /* compu ...

  4. 探店视频批量剪辑神器,批量生成音画同步探店视频,好物视频和团购达人视频

    探店视频批量剪辑神器,批量生成音画同步探店视频,好物视频和团购达人视频 主要按场景分场景做剪辑.三级文件 夹方式.合成视频. 比如:做餐饮探店,首先是按门头,菜品,环境,活动.分为四大场景,每个场景会 ...

  5. Android 音视频变速原理

    视频倍速播放 假设视频帧率是24fps, 则播放器必须在1000/24 = 41.66ms 内 解封装 + 解码 + 渲染完一帧,一般只计算出把YUV数据从渲染队列中取出到渲染结束的时间(Render ...

  6. ffmpeg源码中ffplay音视频同步原理及实现

    音视频指南 文章目录 音视频指南 前言 一.音视频同步简单介绍? 二.基本概念解释 1.为什么需要视频压缩 2.什么是I帧.p帧.b帧 3.什么是DTS,PTS 4.其他概念解释 三.常用同步策略 四 ...

  7. 【gitHubDailyShare】通过真实录音,让动漫人物的嘴唇实现音画同步。开发者可将其应用于计算机游戏

    推荐 GitHub 上一款比较有意思的开源工具:Rhubarb Lip Sync,可通过真实录音,让动漫人物的嘴唇实现音画同步. 开发者可将其应用于计算机游戏.动画卡通角色.视频 Vlog 等场景上. ...

  8. 音画同步的几套方案的对比

    文章目录 一. 首先回顾业界的常用的三种方案的优缺点以及实现 二. 方案三的实现 基础补充 1. CADisplaylink取代视频线程回调,从视频队列中获取大于且最接近音频pts的一帧,小于这个pt ...

  9. 上计算机课用什么耳机,在电脑上录课程用什么软件好?实现音画同步,画板标注就选它...

    原标题:在电脑上录课程用什么软件好?实现音画同步,画板标注就选它 怎么在电脑上录制课程?对于每个人来说,学习都是一个长期积累的过程,即使你是早已离开学校为生活奔波的上班族,只要你愿意学习,网上的各种教 ...

最新文章

  1. idea 2018.2.2安装
  2. Google 系两公司联手,要让无人车少“犯错”
  3. UVA - 11491 Erasing and Winning(奖品的价值)(贪心)
  4. python爬虫脚本ie=utf-8_python脚本-共享文件爬虫
  5. Python dataframe列拆分多行与统计
  6. easyui input输入框的限制和校验条件
  7. Update From 用法
  8. hdu1166------树状数组(板子)
  9. 【产品工具使用】黑群晖史上最强安装教程
  10. Linux系统密码忘记教程
  11. ffmpeg命令分析-ss
  12. 【推理加速】博客翻译:利用融合conv和bn的方法加速模型
  13. android ndk 怎样调用第三方的so库文件。
  14. matlab学霸表白公式,一个理科学霸的表白:数学公式的超酷表白
  15. 动态规划求解机器人有多少种可能的路径
  16. 五子棋AI算法第三篇-Alpha Beta剪枝
  17. Linux-日志管理篇
  18. 使用AssetFileDescriptor 来读取(android)app的raw文件夹下的数据
  19. 【Unity编辑器扩展】(二)PSD转UGUI Prefab, 图层解析和碎图导出
  20. 计算机蓝屏 无法启动怎么办,电脑无法开机一直蓝屏怎么办

热门文章

  1. C++ cin输入空格
  2. 五道口考研之谈,困惑的人好好看看
  3. [凯立德]2014秋季版3321J0L(SP2)Android版
  4. 基因共表达网络分析java,好用的基因共表达网络分析工具
  5. java正则匹配英文句号_java正则表达式最简单 学习教程
  6. 初中在线测试软件,2020初中生成绩查询网址-2020年初中生成绩查询网址官网最新预约 v1.0-优盘手机站...
  7. TX2学习笔记(1)——NVIDIA Jetson TX2 开箱上电
  8. 30分钟带你精通Git使用
  9. SI4459ADY-T1-GE3 MOS管资料
  10. 点击按钮背景变灰色,松开恢复原来色