ffmpeg2.3版本,关于ffplay⾳视频同步的分析
最近学习播放器的⼀些东西,所以接触了ffmpeg,看源码的过程中,就想了解⼀下ffplay是怎么处理⾳视频同步的,之前只⼤概知道通过pts来进⾏同步,但对于如何实现却不甚了解,所以想借助这个机会,从最直观的代码⼊⼿,详细分析⼀下如何处理⾳视频同步。在看代码的时候,刚开始脑袋⼀⽚混乱,对于ffplay.c⾥⾯的各种时间计算完全摸不着头脑,在⽹上查资料的过程中,发现关于分析ffplay⾳视频同步的东西⽐较少,要么就是ffplay版本太过于⽼旧,代码和现在最新版本已经不⼀样,要么就是简单的分析了⼀下,没有详细的讲清楚为什么要这么做。遂决定,在⾃⼰学习的过程中,记录下⾃⼰的分析思路,以供⼤家指正和参考。我⽤的ffmpeg版本是2.3, SDL版本为1.2.14,编译环境是windos xp下使⽤MinGw+msys.
⼀、先简单介绍下ffplay的代码结构。如下:
1.      Main函数中需要注意的有
(1)      av_register_all接⼝,该接⼝的主要作⽤是注册⼀些muxer、demuxer、coder、和decoder. 这些模块将是我们后续编解码的关键。每个demuxer和decoder都对应不同的格式,负责不同格式的demux和decode
(2)      stream_open接⼝,该接⼝主要负责⼀些队列和时钟的初始化⼯作,另外⼀个功能就是创建read_thread线程,该线程将负责⽂件格式的检测,⽂件的打开以及frame的读取⼯作,⽂件操作的主要⼯作都在这个线程⾥⾯完成
(3)      event_loop:事件处理,event_loop->refresh_loop_wait_event-> video_refresh,通过这个顺序进⾏视频的display
2.Read_thread线程
(1)  该线程主要负责⽂件操作,包括⽂件格式的检测,⾳视频流的打开和读取,它通过av_read_frame读取完整的⾳视频frame packet,并将它们放⼊对应的队列中,等待相应的解码线程进⾏解码
3. video_thread线程,该线程主要负责将packet队列中的数据取出并进⾏解码,然将解码完后的picture放⼊picture队列中,等待SDL进⾏渲染
4. sdl_audio_callback,这是ffplay注册给SDL的回调函数,其作⽤是进⾏⾳频的解码,并在SDL需要数据的时候,将解码后的⾳频数据写⼊SDL的缓冲区,SDL再调⽤audio驱动的接⼝进⾏播放。
5. video_refresh,该接⼝的作⽤是从picture队列中获取pic,并调⽤SDL进⾏渲染,⾳视频同步的关键
就在这个接⼝中
⼆、⾳视频的同步
要想了解⾳视频的同步,⾸先得去了解⼀些基本的概念,video的frame_rate. Pts, audio的frequency之类的东西,这些都是⽐较基础的,⽹上资料很多,建议先搞清楚这些基本概念,这样阅读代码才会做到⼼中有数,好了,闲话少说,开始最直观的源码分析吧,如下:
(1)      ⾸先来说下video和audio 的输出接⼝,video输出是通过调⽤video_refresh-> video_display-> video_image_display->
SDL_DisplayYUVOverlay来实现的。Audio是通过SDL回调sdl_audio_callback(该接⼝在打开⾳频时注册给SDL)来实现的。
(2)      ⾳视频同步的机制,据我所知有3种,(a)以⾳频为基准进⾏同步(b)以视频为基准进⾏同步(c)以外部时钟为基准进⾏同步。Ffplay 中默认以⾳频为基准进⾏同步,我们的分析也是基于此,其它两种暂不分析。
(3)      既然视频和⾳频的播放是独⽴的,那么它们是如何做到同步的,答案就是通过ffplay中⾳视频流各⾃维护的clock来实现,具体怎么做,我们还是来看代码吧。
(4)      代码分析:
(a)      先来看video_refresh的代码, 去掉了⼀些⽆关的代码,像subtitle和状态显⽰
static voidvideo_refresh(void *opaque, double *remaining_time)
{
VideoState *is = opaque;
SubPicture *sp, *sp2;
if (!is->paused &&get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime) check_external_clock_speed(is);
if(!display_disable && is->show_mode != SHOW_MODE_VIDEO &&is->audio_st)
{
time = av_gettime_relative() /1000000.0;
if (is->force_refresh ||is->last_vis_time + rdftspeed < time) {
video_display(is);
is->last_vis_time = time;
}
*remaining_time =FFMIN(*remaining_time, is->last_vis_time + rdftspeed - time);
}
if (is->video_st) {
int redisplay = 0;
if (is->force_refresh)
redisplay = pictq_prev_picture(is);
retry:
if (pictq_nb_remaining(is) == 0) {
/
/ nothing to do, no picture todisplay in the queue
} else {toggle rate
double last_duration, duration, delay;
VideoPicture *vp, *lastvp;
/* dequeue the picture */
lastvp =&is->pictq[is->pictq_rindex];
vp =&is->pictq[(is->pictq_rindex + is->pictq_rindex_shown) % VIDEO_PICTURE_QUEUE_SIZE]; if (vp->serial !=is->videoq.serial) {
pictq_next_picture(is);
is->video_current_pos = -1;
goto retry;
}
/*不管是vp的serial还是queue的serial, 在seek操作的时候才会产⽣变化,更准确的说,应该是packet 队列发⽣flush操作时*/
if (lastvp->serial !=vp->serial && !redisplay)
{
is->frame_timer =av_gettime_relative() / 1000000.0;
}
if (is->paused)
goto display;
/*通过pts计算duration,duration是⼀个videoframe的持续时间,当前帧的pts 减去上⼀帧的pts*/
/* compute nominal last_duration */
last_duration = vp_duration(is,lastvp, vp);
if (redisplay)
{
delay = 0.0;
}
/*⾳视频同步的关键点*/
else
delay =compute_target_delay(last_duration, is);
/*time 为系统当前时间,av_gettime_relative拿到的是1970年1⽉1⽇到现在的时间,也就是格林威治时间*/
time=av_gettime_relative()/1000000.0;
/*frame_timer实际上就是上⼀帧的播放时间,该时间是⼀个系统时间,⽽ frame_timer + delay 实际上就是当前这⼀帧的播放时间*/ if (time < is->frame_timer +delay && !redisplay) {
/*remaining 就是在refresh_loop_wait_event 中还需要睡眠的时间,其实就是现在还没到这⼀帧的播放
时间,我们需要睡眠等待*/ *remaining_time =FFMIN(is->frame_timer + delay - time,  *remaining_time);
return;
}
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);
/*视频帧的pts⼀般是从0开始,按照帧率往上增加的,此处pts是⼀个相对值,和系统时间没有关系,对于固定fps,⼀般是按照1/frame_rate的速度往上增加,可变fps暂            时没研究*/
if (!redisplay &&!isnan(vp->pts))
/*更新视频的clock,将当前帧的pts和当前系统的时间保存起来,这2个数据将和audio  clock的pts 和系统时间⼀起计算delay*/
update_video_pts(is,vp->pts, vp->pos, vp->serial);
SDL_UnlockMutex(is->pictq_mutex);
if (pictq_nb_remaining(is) > 1){
VideoPicture *nextvp =&is->pictq[(is->pictq_rindex + is->pictq_rindex_shown + 1) %VIDEO_PICTURE_QUEUE_SIZE];
duration = vp_duration(is, vp,nextvp);
/
*如果延迟时间超过⼀帧,并且允许丢帧,则进⾏丢帧处理*/
if(!is->step &&(redisplay || framedrop>0 || (framedrop && get_master_sync_type(is)!= AV_SYNC_VIDEO_MASTER)) && time >
is->frame_timer + duration){
if (!redisplay)
is->frame_drops_late++;
/*丢掉延迟的帧,取下⼀帧*/
pictq_next_picture(is);
redisplay = 0;
goto retry;
}
}
display:
/* display picture */
/*刷新视频帧*/
if (!display_disable &&is->show_mode == SHOW_MODE_VIDEO)
video_display(is);
pictq_next_picture(is);
if (is->step &&!is->paused)
stream_toggle_pause(is);
}
}
}
(b)      视频的播放实际上是通过上⼀帧的播放时间加上⼀个延迟来计算下⼀帧的计算时间的,例如上⼀帧的播放时间pre_pts是0,延迟delay 为33ms,那么下⼀帧的播放时间则为0+33ms,第⼀帧的播放时间我们可以轻松获取,那么后续帧的播放时间的计算,起关键点就在于delay,我们就是更具delay来控制视频播放的速度,从⽽达到与⾳频同步的⽬的,那么如何计算delay?接着看代码,compute_target_delay接⼝:
static doublecompute_target_delay(double delay, VideoState *is)
{
double sync_threshold,diff;
/* update delay to followmaster synchronisation source */
/*如果主同步⽅式不是以视频为主,默认是以audio为主进⾏同步*/
if(get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) {
/* if video is slave,we try to correct big delays by
duplicating ordeleting a frame */
/*get_clock(&is->vidclk)获取到的实际上是:从处理最后⼀帧开始到现在的时间加上最后⼀帧的pts,具体参考set_clock_at 和get_clock的代码
get_clock(&is->vidclk) ==is->vidclk.pts, av_gettime_relative() / 1000000.0 -is->vidclk.last_updated  +is->vidclk.pts*/
/*driff实际上就是已经播放的最近⼀个视频帧和⾳频帧pts的差值+ 两⽅系统的⼀个差值,⽤公式表达如下:
pre_video_pts: 最近的⼀个视频帧的pts
video_system_time_diff: 记录最近⼀个视频pts 到现在的时间,即av_gettime_relative()/ 1000000.0 - is->vidclk.last_updated
pre_audio_pts: ⾳频已经播放到的时间点,即已经播放的数据所代表的时间,通过已经播放的samples可以计算出已经播放的时间,在
sdl_audio_callback中被设置
audio_system_time_diff: 同video_system_time_diff
最终视频和⾳频的diff可以⽤下⾯的公式表⽰:
diff = (pre_video_pts-pre_audio_pts) +(video_system_time_diff -  audio_system_time_diff)
如果diff<0, 则说明视频播放太慢了,如果diff>0,

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。