十、详解FFplay音视频同步
[TOC]
开始前的BB
有些没有接触过的童鞋可能还不知道音视频同步是什么意思,大家印象中应该看到过这样的视频,画面中的人物说话和声音出来的不在一起,小时候看有些电视台转播的港片的时候(别想歪 TVB)有时候就会遇到 明明声音已经播出来了,但是播的图像比声音慢了很多,看的极为不舒服,这个时候就发生了音视频不同步的情况,而音视频同步,就是让声音与画面对应上
这里有个知识点需要记一下
人对于图像和声音的接受灵敏程度不一样,人对音频比对视频敏感;视频放快一点,可能察觉的不是特别明显,但音频加快或减慢,人耳听的很敏感
PTS的由来
音视频同步依赖的一个东西就是pts(persentation time stamp )显示时间戳 告诉我们该什么时间显示这一帧 ,那么,这个东西是从哪里来的呢?
刨根问底栏目组将带你深度挖掘
PTS是在拍摄的时候打进去的时间戳,假如我们现在拍摄一段小视频(别想歪啊),什么特效都不加,那么走的就是以下的步骤
我们根据这个图可以知道,PTS是在录制的时候就打进Frame里的
音视频同步的方式
在ffplay中 音视频同步有三种方式
- 以视频为基准,同步音频到视频
- 音频慢了就加快音频的播放速度,或者直接丢掉一部分音频帧
- 音频快了就放慢音频的播放速度
- 以音频为基准,同步视频到音频
- 视频慢了则加快播放或丢掉部分视频帧
- 视频快了则延迟播放
- 以外部时钟为准,同步音频和视频到外部时钟
- 根据外部时钟改版音频和视频的播放速度
视频基准
如果以视频为基准进行同步,那么我们就要考虑可能出现的情况,比如:
掉帧
此时的音频应该怎么做呢?通常的方法有
- 音频也丢掉相应的音频帧(会有断音,比如你说了一句,我的天啊,好漂亮的草原啊 很不凑巧丢了几帧视频,就成,,,卧槽!)
- 音频加快速度播放(此处可以直接用Audition加快个几倍速的播放一首音乐)
音频基准
如果以音频为基准进行同步,很不幸的碰到了掉帧的情况,那么视频应该怎么做呢?通常也有两种做法
1.视频丢帧 (画面跳帧,丢的多的话,俗称卡成PPT)
2.加快播放速度(画面加快播放)
外部时钟为基准
假如以外部时钟为基准,如果音视频出现了丢帧,怎么办呢?
如果丢帧较多,直接重新初始化外部时钟 (pts和时钟进行对比,超过一定阈值重设外部时钟,比如1s)
音视频时间换算
PTS 时间换算
之前我们稍微讲过pts的时间换算,pts换算成真正的秒是用以下操作
realTime = pts * av_q2d(stream.time_base)
stream是当前的视频/音频流
我们这里主要讲一下在音频解码pts可能会遇到的情况,有时候音频帧的pts会以1/采样率为单位,像
pts1 = 0
pts2 = 1024
pts3 = 2048
像我们例子中的这个视频,我们在解码一帧音频之后打印出来他的pts
std::cout<<'audio pts : '<<frame->pts<<std::endl;
我们知道当前视频的音频采样率为44100,那么这个音频帧pts的单位就是1/44100
,那么
pts1 = 0 * 1 / 44100 = 0
pts2 = 1024 * 1 / 44100 = 0.232
pts3 = 2048 * 1 / 44100 = 0.464
音频流的time_base里面正是记录了这个值,我们可以通过debug来看一下
利用
realTime = pts * av_q2d(stream.time_base)
我们可以直接算出来当前音频帧的pts
另外需要注意
在ffplay中做音视频同步,都是以秒为单位
音视频帧播放时间换算
音频帧播放时间计算
音频帧的播放和音频的属性有关系,是采用
采样点数 * 1 / 采样率
来计算,像AAC当个通道采样是1024个采样点,那么
- 如果是44.1khz,那么一帧的播放时长就是 1024 * 1 / 44100 = 23.3毫秒
- 如果是48khz,那么一帧的播放时长就是 1024 * 1 / 48000 = 21.33毫秒
视频帧的播放时间计算
视频帧的播放时间也有两个计算方式
- 利用
1/帧率
获取每个帧平均播放时间,这个方式有一个很大的缺点就是,不能动态响应视频帧的变化,比如说我们做一些快速慢速的特效,有的厂商或者SDK(我们的SDK不是)是直接改变视频帧的增加/减少视频帧之间的pts间距来实现的,这就导致在一些拿帧率计算显示时间的播放器上发现是整体(快/慢)了,达不到想要的效果;还有一种情况就是丢帧之后,时间显示仍然是固定的 - 相邻帧相减 这大程度上避免利用帧率去算的各种弊端,但是缺点是使用起来比较复杂,尤其是暂停/Seek之类的操作的时候需要进行一些时间差值的计算
时间校正
视频时间校正
在看ffplay
的时候我们会发现,他在里面默认情况下是用了
frame->pts = frame->best_effort_timestamp;
其实大多数情况下pts
和best_effort_timestamp
的值是一样的,这个值是利用各种探索方法去计算当前帧的视频戳
音频时间校正
音频的pts获取比视频的要复杂一点,在ffplay中对音频的pts做了三次修改
frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb);
将其由stream->time_base转为(1/采样率)(decoder_decode_frame()
中)af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
将其由(1/采样率)转换为秒 (audio_thread()
中)is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec
根据实际输入进SDL2播放的数据长度做调整 (sdl_audio_callback
中)
ffplay 时钟框架
ffplay中的时钟框架主要依靠Clock
结构体和相应的方法组成
/** 时钟结构体 **/typedef struct Clock { double pts; /* clock base 时间基准*/ double pts_drift; /* clock base minus time at which we updated the clock 时间基减去更新时钟的时间 */ double last_updated; double speed; int serial; /* clock is based on a packet with this serial */ int paused; int *queue_serial; /* pointer to the current packet queue serial, used for obsolete clock detection */} Clock;/** 初始化时钟 **/static void init_clock(Clock *c, int *queue_serial);/** 获取当前时钟 **/static double get_clock(Clock *c);/** 设置时钟 内部调用set_clock_at()**/static void set_clock(Clock *c, double pts, int serial);/** 设置时钟 **/static void set_clock_at(Clock *c, double pts, int serial, double time);/** 设置时钟速度 **/static void set_clock_speed(Clock *c, double speed);/** 音/视频设置时钟的时候都回去跟外部时钟进行对比,防止丢帧或者丢包情况下时间差距比较大而进行的纠偏 **/static void sync_clock_to_slave(Clock *c, Clock *slave);/** 获取做为基准的类型 音频 外部时钟 视频 **/static int get_master_sync_type(VideoState *is);/** 获取主时间轴的时间 **/static double get_master_clock(VideoState *is);/** 检查外部时钟的速度 **/static void check_external_clock_speed(VideoState *is);
这个时钟框架也是比较简单,可以直接去看FFplay的源码,这里就不过多的叙述
音视频同步时间轴
在ffplay
中,我们不管是以哪个方式做为基准,都是有一个时间轴
就像这样子,有一个时钟一直在跑,所谓基于音频、视频、外部时间 做为基准,也就是将那个轴的的时间做为时间轴的基准,另一个在轴参照主时间轴进行同步
假如是以音频为基准,视频同步音频的方式,那么就是音频在每播放一帧的时候,就去将当前的时间同步到时间轴,视频参考时间轴做调整
音频时钟设置
音频时钟的设置的话需要考虑注意 硬件缓存数据 设置音频时钟的时候需要将
pts - 硬件缓冲数据的播放时间
详情参考 ffplay 中
sdl_audio_callback(void *opaque, Uint8 *stream, int len)
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);
这是就是将音频的pts - 硬件缓冲区里剩下的时间设置到了音频的时钟里
视频时钟设置
视频时钟设置的话就比较简单了,直接设置pts,在ffplay中
queue_picture(VideoState *is, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)
内,我们可以直接看到 vp->pts = pts;
,然后在video_refresh
里面update_video_pts(is, vp->pts, vp->pos, vp->serial);
去调用了set_clock
static void update_video_pts(VideoState *is, double pts, int64_t pos, int serial) { /* update current video pts */ set_clock(&is->vidclk, pts, serial); sync_clock_to_slave(&is->extclk, &is->vidclk);}
音视频同步操作
音视频在同步上出的处理我们上面有简单讲到过,我们这里来详细看一下他具体是真么做的
音频同步操作
音频的同步操作是在audio_decode_frame()
中的wanted_nb_samples = synchronize_audio(is, af->frame->nb_samples);
,注意synchronize_audio
方法,我们来看他注释
/* return the wanted number of samples to get better sync if sync_type is video * or external master clock * * 如果同步类型为视频或外部主时钟,则返回所需的采样数来更好的同步。 * * */static int synchronize_audio(VideoState *is, int nb_samples)
这个方法里面的操作有点多,我这边简单说一下这个方法,主要是利用音频时钟与主时钟相减得到差值(需要先判断音频是不是主时间轴),然后返回如果要同步需要的采样数,在audio_decode_frame()
中用len2 = swr_convert(is->swr_ctx, out, out_count, in, af->frame->nb_samples);
进行重采样,然后才在sdl_audio_callback()
中进行播放
视频同步操作
视频同步操作的主要步骤是在video_refresh()
方法中,我们来看一下关键的地方
/* compute nominal last_duration 根据当前帧和上一帧的pts计算出来上一帧显示的持续时间 */ last_duration = vp_duration(is, lastvp, vp); /** 计算当前帧需要显示的时间 **/ delay = compute_target_delay(last_duration, is); /** 获取当前的时间 **/ time= av_gettime_relative()/1000000.0; /** 如果当前时间小于显示时间 则直接进行显示**/ if (time < is->frame_timer + delay) { *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time); goto display; } /** 更新视频的基准时间 **/ is->frame_timer += delay; /** 如果当前时间与基准时间偏差大于 AV_SYNC_THRESHOLD_MAX 则把视频基准时间设置为当前时间 **/ if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX) is->frame_timer = time; /** 更新视频时间轴 **/ SDL_LockMutex(is->pictq.mutex); if (!isnan(vp->pts)) update_video_pts(is, vp->pts, vp->pos, vp->serial); SDL_UnlockMutex(is->pictq.mutex); /** 如果队列中有未显示的帧,如果开启了丢帧处理或者不是以视频为主时间轴,则进行丢帧处理 **/ if (frame_queue_nb_remaining(&is->pictq) > 1) { Frame *nextvp = frame_queue_peek_next(&is->pictq); duration = vp_duration(is, vp, nextvp); if(!is->step && (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration){ is->frame_drops_late++; frame_queue_next(&is->pictq); goto retry; } }
到这里,ffplay中主要的音视频同步就讲完了,建议去看一下ffplay的源码,多体会体会 印象才会比较深刻,说实话ffplay中同步的操作是比较复杂的,我们在平常开发中要根据自己的实际业务进行一些简化和改进的,下一章我们就来写一个以音频为基准的视频播放器
未完持续...