音视频流媒体开发【二十八】ffplay播放器-音视频同步基础

2023-03-18  本文已影响0人  AlanGe

音视频流媒体开发-目录

11 ⾳视频同步基础

由于⾳频和视频的输出不在同⼀个线程,⽽且,也不⼀定会同时解出同⼀个pts的⾳频帧和视频帧。更有甚者,编码或封装的时候可能pts还是不连续的,或有个别错误的。因此,在进⾏⾳频和视频的播放时,需要对⾳频和视频的播放速度、播放时刻进⾏控制,以实现⾳频和视频保持同步,即所谓的⾳视频同步。

在ffplay中,⾳频(audio)和视频(video)有各⾃的输出线程,其中⾳频的输出线程是sdl的⾳频输出回调线程,video的输出线程是程序的主线程。

11.1 ⾳视频同步策略

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

由于⼈⽿对于声⾳变化的敏感度⽐视觉⾼,因此,⼀般采样的策略是将视频同步到⾳频,即对画⾯进⾏适当的丢帧或重复以追赶或等待⾳频。

特殊地,有时候会碰到⼀些特殊封装(或者有问题的封装),此时就不作同步处理,各⾃为主时钟,进⾏播放。

在ffplay中实现了上述前3种的同步策略。由 sync 参数控制:

1 { "sync", HAS_ARG | OPT_EXPERT, { .func_arg = opt_sync }, "set audiovideo sync. type (type=audio/video/ext)", "type" },

⽐如
ffplay source.200kbps.768x320.flv -sync video
设置以video master

11.2 ⾳视频同步概念

在深⼊代码了解其实现前,需要先简单了解下⼀些结构体和概念。读者也可以选择在后⽂阅读中回头查阅本节。

当视频流中没有 B 帧时,通常 DTS 和 PTS 的顺序是⼀致的。但存在B帧的时候两者的顺序就不⼀致了。
(1)pts是presentation timestamp的缩写,即显示时间戳,⽤于标记⼀个帧的呈现时刻,它的单位由timebase决定。timebase的类型是结构体AVRational(⽤于表示分数):

typedef struct AVRational{
  int num; ///< Numerator
  int den; ///< Denominator
} AVRational;

如 timebase={1, 1000} 表示千分之⼀秒(毫秒),那么pts=1000,即为pts*1/1000 = 1秒,那么这⼀帧就需要在第⼀秒的时候呈现。

将AVRatioal结构转换成double

static inline double av_q2d(AVRational a){
  return a.num / (double) a.den;
}

计算时间戳

timestamp(秒) = pts * av_q2d(st->time_base)

计算帧时⻓

time(秒) = st->duration * av_q2d(st->time_base)

不同时间基之间的转换

int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq)

在ffplay中,将pts转化为秒,⼀般做法是: pts * av_q2d(timebase)

(2)在做同步的时候,我们需要⼀个"时钟"的概念,⾳频、视频、外部时钟都有⾃⼰独⽴的时钟,各⾃set各⾃的时钟,以谁为基准(master), 其他的则只能get该时钟进⾏同步,ffplay定义的结构体是Clock:

typedef struct Clock {
    double pts; // 时钟基础, 当前帧(待播放)显示时间戳,播放后,当前帧变成上⼀帧
    // 当前pts与当前系统时钟的差值, audio、video对于该值是独⽴的
    double pts_drift; // clock base minus time at which we updated the clock
    // 当前时钟(如视频时钟)最后⼀次更新时间,也可称当前时钟时间
    double last_updated; // 最后⼀次更新的系统时钟
    double speed; // 时钟速度控制,⽤于控制播放速度
    // 播放序列,所谓播放序列就是⼀段连续的播放动作,⼀个seek操作会启动⼀段新的播放序列
    int serial; // clock is based on a packet with this serial
    int paused; // = 1 说明是暂停状态
    // 指向packet_serial
    int *queue_serial; /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

这个时钟的⼯作原理是这样的:

  1. 需要不断“对时”。对时的⽅法 set_clock_at(Clock *c, double pts, int serial,double time) ,需要⽤pts、serial、time(系统时间)进⾏对时。
  2. 获取的时间是⼀个估算值。估算是通过对时时记录的pts_drift估算的。pts_drift是最精华的设计,⼀定要理解。

可以看这个图来帮助理解:

图中央是⼀个时间轴(time是⼀直在按时间递增),从左往右看。⾸先我们调⽤ set_clock 进⾏⼀次对时,假设这时的 pts 是落后时间 time 的,那么计算 pts_drift = pts - time ,计算出pts和time的相对差值。

接着,过了⼀会⼉,且在下次对时前,通过 get_clock 来查询时间,因为set_clock时的 pts 已经过时,不能直接拿set_clock时的pts当做这个时钟的时间。不过我们前⾯计算过 pts_drift ,也就是 pts和 time 的差值,所以我们可以通过当前时刻的时间来估算当前时刻的pts: pts = time + pts_drift 。

⼀般time会取CLOCK_MONOTONIC(单调递增的时钟),即系统开机到现在的时间.

ffplay使⽤ffmpeg提供的av_gettime_relative()函数。

再来⼀个图

11.3 FFmpeg中的时间单位

AV_TIME_BASE
AV_TIME_BASE_Q
时间基转换公式

11.4 ⾳视频时间换算的问题

11.5 不同结构体的time_base/duration分析

ffmpeg存在多个时间基准(time_base),对应不同的阶段(结构体),每个time_base具体的值不⼀样,ffmpeg提供函数在各个time_base中进⾏切换。

AVStream的time_base是在demuxer或者muxer内设置的,以TS,FLV,MP4为例⼦:

11.6 不同结构体的PTS/DTS分析

不同结构体下,pts和dts使⽤哪个time_base来表示?

AVPacket
AVFrame

11.6 ffplay中PTS的转换流程分析

⼤家注意到没有,ffplay在重新封装AVFrame的时候⾃⼰也有pts和duration两个变量

Video Frame PTS的获取

PTS校正
frame->pts = frame->best_effort_timestamp;
这⾥为什么不⽤AVFrame中的pts来直接计算呢?其实⼤多数情况下AVFrame的pts和best_effort_timestamp值是⼀样的

Audio Frame PTS的获取

ffplay有3次对于Audio的pts进⾏转换

上一篇下一篇

猜你喜欢

热点阅读