近期学习播放器的一些东西。所以接触了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;

double time;

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 {

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;

redisplay = 0;

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,

则说明视频播放太快。此时须要通过计算delay来调整视频的播放速度假设

diff<AV_SYNC_THRESHOLD_MIN || diff>AV_SYNC_THRESHOLD_MAX 则不用调整delay?*/

diff =get_clock(&is->vidclk) - get_master_clock(is);

/* skip or repeatframe. We take into account the

delay to computethe threshold. I still don't know

if it is the bestguess */

sync_threshold=FFMAX(AV_SYNC_THRESHOLD_MIN,FFMIN(AV_SYNC_THRESHOLD_MAX,delay));

if (!isnan(diff)&& fabs(diff) < is->max_frame_duration) {

if (diff <=-sync_threshold)

delay =FFMAX(0, delay + diff);

else if (diff >= sync_threshold&& delay > AV_SYNC_FRAMEDUP_THRESHOLD)

delay = delay+ diff;

else if (diff>= sync_threshold)

delay = 2 *delay;

}

}

av_dlog(NULL, "video:delay=%0.3f A-V=%f\n",

delay, -diff);

return delay;

}

(c)看了以上的分析,是不是对于怎样将视频同步到音频有了一个了解,那么音频clock是在哪里设置的呢?继续看代码。sdl_audio_callback 分析

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)

{

VideoState *is = opaque;

int audio_size, len1;

/*当前系统时间*/

audio_callback_time =av_gettime_relative();

/*len为SDL中audio buffer的大小,单位是字节。该大小是我们在打开音频设备时设置*/

while (len > 0) {

/*假设audiobuffer中的数据少于SDL须要的数据,则进行解码*/

if(is->audio_buf_index >= is->audio_buf_size) {

audio_size = audio_decode_frame(is);

if (audio_size <0) {

/* if error,just output silence */

is->audio_buf      =is->silence_buf;

is->audio_buf_size =sizeof(is->silence_buf) / is->audio_tgt.frame_size *is->audio_tgt.frame_size;

}

else

{

if(is->show_mode != SHOW_MODE_VIDEO)

update_sample_display(is, (int16_t *)is->audio_buf, audio_size);

is->audio_buf_size = audio_size;

}

is->audio_buf_index = 0;

}

/*推断解码后的数据是否满足SDL须要*/

len1 =is->audio_buf_size - is->audio_buf_index;

if (len1 > len)

len1 = len;

memcpy(stream,(uint8_t *)is->audio_buf + is->audio_buf_index, len1);

len -= len1;

stream += len1;

is->audio_buf_index+= len1;

}

is->audio_write_buf_size = is->audio_buf_size -is->audio_buf_index;

/* Let's assume the audiodriver that is used by SDL has two periods. */

if(!isnan(is->audio_clock))

{

/*set_clock_at第二个參数是计算音频已经播放的时间,相当于video中的上一帧的播放时间,假设不同过SDL。比如直接使用linux下的dsp设备进行播放,那么我们能够通         过ioctl接口获取到驱动的audiobuffer中还有多少数据没播放,这样,我们通过音频的採样率和位深,能够非常精确的算出音频播放到哪个点了,可是此处的计算方法有点让人         看不懂*/

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);

sync_clock_to_slave(&is->extclk, &is->audclk);

}

}

三、总结

音视频同步。拿以音频为基准为例,事实上就是将视频当前的播放时间和音频当前的播放时间作比較,假设视频播放过快,则通过加大延迟或者反复播放来使速度降下来,假设慢了。则通过减小延迟或者丢帧来追赶音频播放的时间点,并且关键就在于音视频时间的比較以及延迟的计算。

四、还存在的问题

关于sdl_audio_callback中 set_clock_at第二个參数的计算。为什么要那么做。还不是非常明确,也有可能那仅仅是一种如果的算法。仅仅是经验,并没有什么为什么。但也有可能是其它。希望明确的人给解释一下。大家互相学习。互相进步。

邓旭光 于2015年3月17日

Ps:转摘请注明出处

ffmpeg 2.3版本号, 关于ffplay音视频同步的分析的更多相关文章

  1. FFmpeg简易播放器的实现-音视频同步

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10284653.html 基于FFmpeg和SDL实现的简易视频播放器,主要分为读取视频文 ...

  2. ffmpeg转码MPEG2-TS的音视频同步机制分析

    http://blog.chinaunix.net/uid-26000296-id-3483782.html 一.FFmpeg忽略了adaptation_field()数据 FFmpeg忽略了包含PC ...

  3. ffplay的音视频同步分析

    以前工作中参与了一些音视频程序的开发,不过使用的都是芯片公司的SDK,没有研究到更深入一层,比如说音视频同步是怎么回事.只好自己抽点时间出来分析开源代码了,做音视频编解码的人都知道ffmpeg,他在各 ...

  4. ffplay(2.0.1)中的音视频同步

    最近在看ffmpeg相关的一些东西,以及一些播放器相关资料和代码. 然后对于ffmpeg-2.0.1版本下的ffplay进行了大概的代码阅读,其中这里把里面的音视频同步,按个人的理解,暂时在这里作个笔 ...

  5. (转)ffplay的音视频同步分析之视频同步到音频

          以前工作中参与了一些音视频程序的开发,不过使用的都是芯片公司的SDK,没有研究到更深入一层,比如说音视频同步是怎么回事.只好自己抽点时间出来分析开源代码了,做音视频编解码的人都知道ffmp ...

  6. vlc源码分析(五) 流媒体的音视频同步

    vlc播放流媒体时实现音视频同步,简单来说就是发送方发送的RTP包带有时间戳,接收方根据此时间戳不断校正本地时钟,播放音视频时根据本地时钟进行同步播放.首先了解两个概念:stream clock和sy ...

  7. 通俗的解释下音视频同步里pcr作用

    PCR同步在非硬件精确时钟源的情况还是谨慎使用,gstreamer里面采用PCR同步,但是发现好多ffmpeg转的片儿,或者是CP方的片源,pcr打得很粗糙的,老是有跳帧等现象.音视频同步,有三种方法 ...

  8. Android 音视频同步机制

    一.概述 音视频同步(avsync),是影响多媒体应用体验质量的一个重要因素.而我们在看到音视频同步的时候,最先想到的就是对齐两者的pts,但是实际使用中的各类播放器,其音视频同步机制都比这些复杂的多 ...

  9. [SimplePlayer] 8. 音视频同步

    音频与视频在播放当中可能会由于种种原因(如:音视频并非在同一时间开始播放,或视频由于解码任务繁重导致输出图像延迟等)导致音频与视频的播放时间出现偏差,这种就是音视频的同步问题,本文会对音视频同步进行讨 ...

随机推荐

  1. 《JavaScript高级程序设计》学习笔记(4)——引用类型

    欢迎关注本人的微信公众号"前端小填填",专注前端技术的基础和项目开发的学习. 本节内容对应<JavaScript高级程序设计>的第五章内容. 在ECMAScript中, ...

  2. sizeof运算符和strlen函数的区别

    1.sizeof是运算符,而strlen是函数. 2.sizeof操作符的运算结果为size_t,他在头文件中的typedef为unsigned int.该类型保证能容纳所操作对象的最大字节大小. 3 ...

  3. (转)@SuppressWarnings的使用、作用、用法

    在java编译过程中会出现很多警告,有很多是安全的,但是每次编译有很多警告影响我们对error的过滤和修改,我们可以在代码中加上 @SuppressWarnings(“XXXX”) 来解决 例如:@S ...

  4. skynet newservice API参考

    local skynet = require("skynet") skynet.start(start_func) c服务snlua启动后执行的第一个lua文件里面的主逻辑必定是s ...

  5. html5中关于input使用方法的改变

    測试环境:Firefox 10.0.Safari 5.1.Opera 11.61, Chrome 14.0.835.202 自己測试的时候都有写在form表单里,有提交button验证.由于对博客使用 ...

  6. Python全栈之路----常用模块----random模块

    程序中有很多地方需要用到随机字符,比如登陆网站的随机验证码,通过random模块可以很容易生成随机字符串. >>> import random >>> random ...

  7. 在MyEclipse中使用javadocAPI文档

    开始啦 1.打开MyEclipse,选中要导出的项目,右击Exprot弹出窗口,选择java----javadoc点击next到下一界面. 2.选出要导出的项目或要添加的项目,在browse中选择路径 ...

  8. C++ Templates STL标准模板库的基本概念

    STL标准库包括几个重要的组件:容器.迭代器和算法.迭代器iterator,用来在一个对象群集的元素上进行遍历操作.这个对象群集或许是一个容器,或许是容器的一部分.迭代器的主要好处是,为所有的容器提供 ...

  9. com/mysql/jdbc/Driver : Unsupported major.minor version 52.0

    解决方案: 1.jdk7+老版5.0驱动com/mysql/jdbc/Driver 2.jdk8+新版6.0驱动com/mysql/cj/jdbc/Driver

  10. HDU 5645 DZY Loves Balls 水题

    DZY Loves Balls 题目连接: http://acm.hdu.edu.cn/showproblem.php?pid=5645 Description DZY loves playing b ...