ffplay音视频同步分析

理论

一定要有基础知识再来看。

深入理解音视频pts,dts,time_base以及时间数学公式-优快云博客

FFmpeg中的时间单位

ffplay是基于ffmpeg api开发的播放器,而ffmpeg是以秒做为时间基

ffmpeg中的内部计时单位(时间基)
#define AV_TIME_BASE 1 000 000 

ffmpeg内部时间基的分数表示,实际上它是AV_TIME_BASE的倒数
#define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE}

基本公式 在头部标题 基础内容有提到

ffplay定义时钟概念

⾳频、视频、外部时钟 


typedef struct Clock {
    double pts; // 当前帧(待播放)显示时间戳,播放后,当前帧变成上一帧
    double pts_drift; //两时间差值,可以理解为持续时间
    double last_updated; // 最后一次更新的系统时钟
    double speed; // 时钟速度控制,用于控制播放速度
    int serial; // clock is based on a packet with this serial
    int paused; // = 1 说明是暂停状态
    int *queue_serial; /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

api和设计原理

        在做同步的时候,我们需要⼀个"时钟"的概念,⾳频、视频、外部时钟都有⾃⼰独⽴的时钟,各⾃ set各⾃的时钟,以谁为基准(master), 其他的则只能get该时钟进⾏同步。比如

aclk起始:10ms  set_clock(&is->audclk, 10ms, is->audclk.serial);
vclk起始:20ms   set_clock(&is->vidclk, 20ms, is->vidclk.serial);
eclk起始:30ms   ...


过了10ms 
is->av_sync_type = AV_SYNC_VIDEO_MASTER;
printf("AUDIO get master_clock = %0.3lf\n", get_master_clock(is)); //20-30ms内 系统调用时间
is->av_sync_type = AV_SYNC_VIDEO_MASTER;
printf("VIDEO get master_clock = %0.3lf\n", get_master_clock(is));//30-40ms内 与楼上相差10ms
总之每个时钟有自己的物理时间,那这种是如何设计的?

static void set_clock(Clock *c, double pts, int serial)
{
    double time = av_gettime_relative() / 1000000.0;
    set_clock_at(c, pts, serial, time); // 修正外部时钟
}
static void set_clock_at(Clock *c, double pts, int serial, double time)
{
    c->pts = pts;               // 当前帧的pts
    c->last_updated = time;     // 最后更新的时间,实际上是当前的一个系统时间
    c->pts_drift = c->pts - time;// 当前帧pts和系统时间的差值,正常播放情况下两者的差值应该是比较固定的,因为两者都是以时间为基准进行线性增长
    static int s_print_interval = 0;
    if(s_print_interval++ > 50)  // 用来观察pts_drift是怎么变化的,一般暂停/快进/快退其值才会大变动,如果一直是长速播放则相对值是稳定的
    {
        s_print_interval = 0;
        //printf("pts_drift = %0.3lf\n", c->pts_drift );
    }

    c->serial = serial;
}


static double get_clock(Clock *c)
{
    if (*c->queue_serial != c->serial)
            return NAN;
    if (c->paused) {    //如果当前是暂停状态,则返回最新的pts即可,因为暂停时时间没走
        return c->pts;
    } else {        // 如果当前正处在播放状态,则返回的时间为最新的pts + 更新pts之后流逝的时间
        double time = av_gettime_relative() / 1000000.0;
        
        // 正常速度播放时,speed=1.0
        /* 如果是正常速度播放,speed =1.0,则为c->pts_drift + time,即是c->pts - time1 + time2,即是可以
         * 计算出来当前时钟的位置因为time都是一直在走的
         */
        /* 并不支持真正的倍速播放,只是在使用外部时钟时的一个微调
         *
         */
        // printf("base %0.3lf, \n", (time - c->last_updated) * (1.0 - c->speed));
        // (time - c->last_updated) * (1.0 - c->speed) 外部时钟变速时有影响 调节播放速度

        //getclock pst = c->pts_drift + time   (time - c->last_updated)从上次更新到现在的时间差
        return c->pts_drift + time  - (time - c->last_updated) * (1.0 - c->speed);
    }
}

举例

ffplay 音视频同步规则

已经熟悉了api,现在我们来看看ffplay是如何实现音视频同步的!ffplay提供三种同步策略

⾳视频的同步策略,⼀般有如下⼏种:

以⾳频为基准,同步视频到⾳频(AV_SYNC_AUDIO_MASTER)

视频慢了则丢掉部分视频帧(视觉->画⾯跳帧)

视频快了则继续渲染上⼀帧。

以视频为基准,同步⾳频到视频(AV_SYNC_VIDEO_MASTER)

⾳频慢了则加快播放速度(或丢掉部分⾳频帧,丢帧极容易听出来断⾳)

⾳频快了则放慢播放速度(或重复上⼀帧 ) ⾳频改变播放速度时涉及到重采样

以外部时钟为基准,同步⾳频和视频到外部时钟(AV_SYNC_EXTERNAL_CLOCK) 前两者的综合,根据外部时钟改变播放速度

视频和⾳频各⾃输出,即不作同步处理(FREE RUN)

  技术层如何实现?

在这里先进行总体概述,具体细节看《实现细节》

以⾳频为基准(最常用) 主体关注视频的帧,通过修改frame_time显示时间,来控制同步。

比如:当前画面落后音频,那就需要减少frame_time,也有drop帧机制对落后的帧进行“减少播放时长”,实现视频提速。

当前画面超过音频,那就需要增加frame_time,对超前的帧长播放(不是一帧画面一直播放,是增加每帧的一点时间有个阈值区间,达到落后音频的时间)。

以视频为基准,主体关注音频的采样点,通过修改wanted_nb_samples控制音频帧播放,来控制同步。

⾳频慢了则加快播放速度,只需减少nb_samples的值。

⾳频快了则放慢播放速度,只需增大nb_samples的值。

以外部时钟为基准

依赖于已同步的audio或video的Clock video,不断通过校正⽅法extclk计算diff值。(音和视频这两者都需要extclk拿值“对时间”) 

小总结:

音频为基准 则关注frame_time

视频为基准,则关注wanted_nb_samples

外部时钟,不断set get计算diff实现两者同步

实现细节

以音频为基准

先来回顾视频刷新一帧逻辑
1 upload_texture          ffplay.c 1059 0x408f96  根据frame中的像素格式与SDL⽀持的像素格式的匹配,将图像数据输出到 tex
2 video_image_display     ffplay.c 1195 0x409797     取出当前需要播放帧,使用sdl渲染显示
3 video_display           ffplay.c 1549 0x40a8a2     显示一帧图像 设置纹理 渲染器 窗口
4 video_refresh           ffplay.c 1948 0x40b8bc     显示一帧图像,重点是音视频同步规则。lastvp vp nextvp 检查这三帧的合法性
5 refresh_loop_wait_event ffplay.c 3787 0x411217     注册输入设备监听事件,不断取出事件队列处理。也就是不断更新视频的帧
6 event_loop              ffplay.c 3834 0x41143c 
7 main                    ffplay.c 4339 0x412b09 
 

static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
...

//如果以audio或外部时钟同步,则需要比对主时钟调整待显示帧vp要等待的时间。 
//如果以video同步,则delay直接等于last_duration。
            delay = compute_target_delay(last_duration, is);
compute_target_delay:
//A. 只要主时钟不是video,就需要作同步校正 计算frame播放时长
    if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) 
        diff = get_clock(&is->vidclk) - get_master_clock(is);
...

      if (time < is->frame_timer + delay) { //判断是否继续显示上一帧
                // 当前系统时刻还未到达上一帧的结束时刻,那么还应该继续显示上一帧。
                // 计算出最小等待时间
                *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                goto display;
            }

...
//B. 更新vidclk,同时更新extclk
      if (!isnan(vp->pts))
                update_video_pts(is, vp->pts, vp->pos, vp->serial);
                

}

以视频为基准

⾳频的时钟设置在sdl_audio_callback:

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len){
     audio_callback_time = av_gettime_relative(); 
...
    static int audio_decode_frame(VideoState *is){
		wanted_nb_samples = synchronize_audio(is, af->frame->nb_samples);
synchronize_audio:
//C. 只要主时钟不是audio,就需要作同步校正
    if (get_master_sync_type(is) != AV_SYNC_AUDIO_MASTER) 
        diff = get_clock(&is->audclk) - get_master_clock(is);//diff是个正数或负数

	}

...
if (!isnan(is->audio_clock)) {
 //pts = 最终播放clock - 播放buffer+stream buffer +剩余可用 / 每秒字节数  (最终time-duration数据字节长度/每秒字节数)
//time = 刚刚计算的累计时间
         set_clock_at(&is->audclk
                        , is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec 
                        , is->audio_clock_serial
                        , audio_callback_time / 1000000.0);//更新音频时钟
//D. 更新audclk,同时更新extclk
        sync_clock_to_slave(&is->extclk, &is->audclk);
    }
}

以外部时钟为基准 

外部时钟为主的同步策略是这样的:video输出和audio输出时都需要作校正,校正的⽅法是参考 extclk计算diff值。因此abcd都会触发

在⾳视频同步基础概念中我们分析过Clock是需要⼀直对时 以保持pts_drift估算出来的pts不会偏差太远,并且get_clock的返回值实际是这⼀Clock对应的流的pts。问题是外部时钟(extclk)是如何对时的?

答案就在B和D步骤中。他们底层都会调用sync_clock_to_slave



/// @brief 从时钟的pts和serial对主时钟对时。
/// @param c 
/// @param slave 
static void sync_clock_to_slave(Clock *c, Clock *slave)
{
    double clock = get_clock(c);
    double slave_clock = get_clock(slave);
    if (!isnan(slave_clock) && (isnan(clock) || fabs(clock - slave_clock) > AV_NOSYNC_THRESHOLD))//范围超过10 重新设置
        set_clock(c, slave_clock, slave->serial);
}

既然要把video和audio同步到extclk,我们⽤的extclk校正video和audio,得到更新后的audclk和vidclk,却⼜反过来⽤audclk和vidclk去对时extclk。 分明就是蛋要鸡来⽣,鸡要蛋来敷嘛。

ffplay在计算估算值时,有一个 !isnan(diff)条件。这在第⼀帧的⾳频或视频显示前是不成⽴的,也就⽆需做 同步校正。在第⼀帧视频或⾳频显示后,此时extclk得到对时,接下来就可以进⼊正常的同步“循环”了。

 小总结:

以音频为基准

A. 只要主时钟不是video,就需要作同步校正  (改变frame_time)

B. 更新vidclk,同时更新extclk

以视频为基准

C. 只要主时钟不是audio,就需要作同步校正        (改变nb_sample)

D. 更新audclk,同时更新extclk

以外部时钟为基准 

ABCD

 总结

ffplay音视频同步选项

ffplay -sync 

audio:视频同步到⾳频。上⼀节中的A被触发,video输出需要作同步,同步的参考 (get_master_clock)是audclk.

video:⾳频同步到视频。上⼀节中的C被触发,audio输出需要作同步,同步的参考是vidclk。

ext:视频和⾳频都同步到外部时钟,上⼀节中的A和C都被触发,同步的参考是extclk

不论选择的是哪⼀个选项,B和D始终都有执⾏。

使用推荐

推荐使用 ffplay -sync audio 也是默认同步模式

媒体流⾥⾯只有视频成分,这个时候才会⽤以视频为基准。

ext在seek的时候体验⾮常差,没有必要选择这种同步⽅式。

学习资料分享

0voice · GitHub

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值