音视频同步
at 5个月前 ca 音视频 pv 335 by touch
视频:帧率,表示视频一秒显示的帧数。
音频:采样率,表示音频一秒播放的sample数。
声卡和显卡均是以一帧数据来作为播放单位,如果单纯依赖帧率及采样率来进行播放,在理想条件下,应该是同步的,不会出现偏差。
一个AAC音频frame每声道1024个sample,一帧播放时长duration=(1024/44100)×1000ms = 23.22ms;一个视频frame播放时长duration=1000ms/24 = 41.67ms。声卡虽然是以音频采样点为播放单位,但通常我们每次往声卡缓冲区送一个音频frame,每送一个音频frame更新一下音频的播放时刻,即每隔一个音频frame时长更新一下音频时钟,实际上ffplay就是这么做的。理想情况下,音视频完全同步,音视频播放过程如下图所示:
但实际情况,往往不同步,原因如下:
1一帧的播放时间,难以精准控制。音视频解码及渲染的耗时不同,可能造成每一帧输出有一点细微差距,长久累计,不同步便越来越明显。(例如受限于性能,42ms才能输出一帧)
2音频输出是线性的,而视频输出可能是非线性,从而导致有偏差。
3媒体流本身音视频有差距。(特别是TS实时流,音视频能播放的第一个帧起点不同)
所以,解决音视频同步问题,引入了时间戳:
首先选择一个参考时钟,时间是线性递增的;编码时依据参考时钟给每个音视频数据块都打上时间戳;播放时,根据音视频时间戳PTS及参考时钟,来调整播放。视频和音频的同步实际上是一个动态的过程,以参考时钟为标准,放快了就减慢播放速度;播放快了就加快播放的速度。
没有B帧的情况下,DTS=PTS。从流分析工具elecard eye查看,流中P帧在B帧之前,但显示在B帧之后。
参考时钟的选择一般来说有以下三种:
以音频为基准,将视频同步到音频。人对音频更敏感,音频播放时钟线性增长,常用做法
将音频同步到视频
选择一个外部时钟为基准,视频和音频的播放速度都以该时钟为标准。
ffplay音视频同步方式,以audio为参考时钟,video同步到音频:
获取当前要显示的video PTS,减去上一帧视频PTS,则得出上一帧视频应该显示的时长delay;
当前video PTS与参考时钟当前audio PTS比较,得出音视频差距diff;
获取同步阈值sync_threshold,为一帧视频差距,范围为10ms-100ms;
diff小于sync_threshold,则认为不需要同步;否则delay+diff值,则是正确纠正delay;
如果超过sync_threshold,且视频落后于音频,那么需要减小delay(FFMAX(0, delay + diff)),让当前帧尽快显示。
如果视频落后超过1秒,且之前10次都快速输出视频帧,那么需要反馈给音频源减慢,同时反馈视频源进行丢帧处理,让视频尽快追上音频。因为这很可能是视频解码跟不上了,再怎么调整delay也没用。
如果超过sync_threshold,且视频快于音频,那么需要加大delay,让当前帧延迟显示。
将delay*2慢慢调整差距,这是为了平缓调整差距,因为直接delay+diff,会让画面画面迟滞。
如果视频前一帧本身显示时间很长,那么直接delay+diff一步调整到位,因为这种情况再慢慢调整也没太大意义。
考虑到渲染的耗时,还需进行调整。frame_timer为一帧显示的系统时间,frame_timer+delay- curr_time,则得出正在需要延迟显示当前帧的时间。
小黑圆圈是代表帧的实际播放时刻,小红圆圈代表帧的理论播放时刻,小绿方块表示当前系统时间(当前时刻),小红方块表示位于不同区间的时间点,则当前时刻处于不同区间时,视频同步策略为:
[1] 当前时刻在T0位置,则重复播放上一帧,延时remaining_time后再播放当前帧
[2] 当前时刻在T1位置,则立即播放当前帧
[3] 当前时刻在T2位置,则忽略当前帧,立即显示下一帧,加速视频追赶
上述内容是为了方便理解进行的简单而形象的描述。实际过程要计算相关值,根据compute_target_delay()和video_refresh()中的策略来控制播放过程。
{ video->frameq.deQueue(&video->frame); //获取上一帧需要显示的时长delay double current_pts = *(double *)video->frame->opaque; double delay = current_pts - video->frame_last_pts; if (delay <= 0 || delay >= 1.0) { delay = video->frame_last_delay; } // 根据视频PTS和参考时钟调整delay double ref_clock = audio->get_audio_clock(); double diff = current_pts - ref_clock;// diff < 0 :video slow,diff > 0 :video fast //一帧视频时间或10ms,10ms音视频差距无法察觉 double sync_threshold = FFMAX(MIN_SYNC_THRESHOLD, FFMIN(MAX_SYNC_THRESHOLD, delay)) ; audio->audio_wait_video(current_pts,false); video->video_drop_frame(ref_clock,false); if (!isnan(diff) && fabs(diff) < NOSYNC_THRESHOLD) // 不同步 { if (diff <= -sync_threshold)//视频比音频慢,加快 { delay = FFMAX(0, delay + diff); static int last_delay_zero_counts = 0; if(video->frame_last_delay <= 0) { last_delay_zero_counts++; } else { last_delay_zero_counts = 0; } if(diff < -1.0 && last_delay_zero_counts >= 10) { printf("maybe video codec too slow, adjust video&audio\n"); #ifndef DORP_PACK audio->audio_wait_video(current_pts,true);//差距较大,需要反馈音频等待视频 #endif video->video_drop_frame(ref_clock,true);//差距较大,需要视频丢帧追上 } } //视频比音频快,减慢 else if (diff >= sync_threshold && delay > SYNC_FRAMEDUP_THRESHOLD) delay = delay + diff;//音视频差距较大,且一帧的超过帧最常时间,一步到位 else if (diff >= sync_threshold) delay = 2 * delay;//音视频差距较小,加倍延迟,逐渐缩小 } video->frame_last_delay = delay; video->frame_last_pts = current_pts; double curr_time = static_cast<double>(av_gettime()) / 1000000.0; if(video->frame_timer == 0) { video->frame_timer = curr_time;//show first frame ,set frame timer } double actual_delay = video->frame_timer + delay - curr_time; if (actual_delay <= MIN_REFRSH_S) { actual_delay = MIN_REFRSH_S; } usleep(static_cast<int>(actual_delay * 1000 * 1000)); //printf("actual_delay[%lf] delay[%lf] diff[%lf]\n",actual_delay,delay,diff); // Display SDL_UpdateTexture(video->texture, &(video->rect), video->frame->data[0], video->frame->linesize[0]); SDL_RenderClear(video->renderer); SDL_RenderCopy(video->renderer, video->texture, &video->rect, &video->rect); SDL_RenderPresent(video->renderer); video->frame_timer = static_cast<double>(av_gettime()) / 1000000.0 ; av_frame_unref(video->frame); //update next frame schedule_refresh(media, 1); }
版权声明
本文仅代表作者观点,不代表码农殇立场。
本文系作者授权码农殇发表,未经许可,不得转载。