ffmpeg音视频同步---视频同步到音频时钟

做者:Huatiangit

github:https://github.com/Huatiangithub

邮箱: 773512457@qq.com缓存

平台:Fedora 25 (64bit)bash

音视频同步简单介绍

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

播放速度标准量的的选择通常来讲有如下三种:函数

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

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指示显示顺序。因此流中能够是这样的:ui

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)。spa

/**
    * 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; ///< numerator
    int 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:

avcodec_decode_video2(is->video_st->codec, pFrame, &frameFinished, packet);

        if(packet->dts == AV_NOPTS_VALUE && packet->pts && packet->pts != AV_NOPTS_VALUE){
            pts = packet->pts;
        }else if(packet->dts != AV_NOPTS_VALUE){
            pts = packet->dts;
        }else{
            pts = 0;
        }
        pts *= av_q2d(is->video_st->time_base);

        if(frameFinished){
            pts = synchronize_video(is, pFrame, pts);
            if(queue_picture(is, pFrame, pts) < 0)
                break;
        }

注意,这里的pts是double型,由于将其乘以了time_base,表明了该帧在视频中的时间位置(秒为单位)。有可能dts和pts都没有获得正确的,这种状况放到synchronize_video中处理:

double synchronize_video(VideoState *is, AVFrame *src_frame, double pts){

    double frame_delay;

    if(pts != 0){
        is->video_clock = pts;
    }else{
        pts = is->video_clock;
    }

    frame_delay = av_q2d(is->video_st->codec->time_base);

    frame_delay += src_frame->repeat_pict * (frame_delay * 0.5);
    is->video_clock += frame_delay;

    return pts;
}

video_clock 是视频播放到当前帧时的已播放的时间长度。在 synchronize_video 函数中,若是没有获得该帧的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) {
          is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts;
        }

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

is->audio_clock += (double)resampled_data_size /
                (double)(2 * is->audio_st->codec->channels * is->audio_st->codec->sample_rate);

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

double get_audio_clock(VideoState *is){
    double pts;
    int hw_buf_size, bytes_per_sec, n;

    pts = is->audio_clock;
    hw_buf_size = is->audio_buf_size - is->audio_buf_index;
    bytes_per_sec = 0;
    n = is->audio_st->codec->channels * 2;

    if(is->audio_st){
        bytes_per_sec = is->audio_st->codec->sample_rate * n;
    }

    if(bytes_per_sec){
        pts -= (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的播放速度来调整这个延迟时间,以实现视音频的同步播放。

相关代码:

void video_refresh_timer(void *userdata)
{

    VideoState *is = (VideoState *)userdata;
    VideoPicture *vp;
    double actual_delay, delay, sync_threshold, ref_clock, diff;

    if(is->video_st)
    {
        if(is->pictq_size == 0)
        {
            schedule_refresh(is, 1);
        }
        else
        {
            vp = &is->pictq[is->pictq_rindex];

            //设置延迟,首先和上次的pts对比得出延迟,更新延迟和pts;
            //经过与音频时钟比较,获得更精确的延迟
            //最后与外部时钟对比,得出最终可用的延迟,并刷新视频
            delay = vp->pts - is->frame_last_pts;
            if(delay <= 0 || delay >= 1.0){
                delay = is->frame_last_delay;//若是延迟不正确,咱们使用上一个延迟
            }

            is->frame_last_delay = delay;
            is->frame_last_pts = vp->pts;

            ref_clock = get_audio_clock(is);
            diff = vp->pts - ref_clock;

            sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
            if(fabs(diff) < AV_NOSYNC_THRESHOLD){
                if(diff <= -sync_threshold){//音频快于视频
                    delay = 0;
                }else if(diff >= sync_threshold){//视频快于音频
                    delay = 2 * delay;
                }
            }

            is->frame_timer += delay;

            actual_delay = is->frame_timer - (av_gettime() / 1000000.0);
            if(actual_delay < 0.010){
                actual_delay = 0.010;
            }

            schedule_refresh(is, (int)(actual_delay * 1000 + 0.5));

            video_display(is);

            if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE)
            {
                is->pictq_rindex = 0;
            }

            SDL_LockMutex(is->pictq_mutex);
            is->pictq_size--;
            SDL_CondSignal(is->pictq_cond);
            SDL_UnlockMutex(is->pictq_mutex);
        }
    }
    else
    {
        schedule_refresh(is, 100);
    }
}

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

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

本文代码下载地址:

https://github.com/Huatian/ffmpeg-tutorial 

相关文章
相关标签/搜索