ffmpeg 2.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;
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音视频同步的分析的更多相关文章
- FFmpeg简易播放器的实现-音视频同步
本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10284653.html 基于FFmpeg和SDL实现的简易视频播放器,主要分为读取视频文 ...
- ffmpeg转码MPEG2-TS的音视频同步机制分析
http://blog.chinaunix.net/uid-26000296-id-3483782.html 一.FFmpeg忽略了adaptation_field()数据 FFmpeg忽略了包含PC ...
- ffplay的音视频同步分析
以前工作中参与了一些音视频程序的开发,不过使用的都是芯片公司的SDK,没有研究到更深入一层,比如说音视频同步是怎么回事.只好自己抽点时间出来分析开源代码了,做音视频编解码的人都知道ffmpeg,他在各 ...
- ffplay(2.0.1)中的音视频同步
最近在看ffmpeg相关的一些东西,以及一些播放器相关资料和代码. 然后对于ffmpeg-2.0.1版本下的ffplay进行了大概的代码阅读,其中这里把里面的音视频同步,按个人的理解,暂时在这里作个笔 ...
- (转)ffplay的音视频同步分析之视频同步到音频
以前工作中参与了一些音视频程序的开发,不过使用的都是芯片公司的SDK,没有研究到更深入一层,比如说音视频同步是怎么回事.只好自己抽点时间出来分析开源代码了,做音视频编解码的人都知道ffmp ...
- vlc源码分析(五) 流媒体的音视频同步
vlc播放流媒体时实现音视频同步,简单来说就是发送方发送的RTP包带有时间戳,接收方根据此时间戳不断校正本地时钟,播放音视频时根据本地时钟进行同步播放.首先了解两个概念:stream clock和sy ...
- 通俗的解释下音视频同步里pcr作用
PCR同步在非硬件精确时钟源的情况还是谨慎使用,gstreamer里面采用PCR同步,但是发现好多ffmpeg转的片儿,或者是CP方的片源,pcr打得很粗糙的,老是有跳帧等现象.音视频同步,有三种方法 ...
- Android 音视频同步机制
一.概述 音视频同步(avsync),是影响多媒体应用体验质量的一个重要因素.而我们在看到音视频同步的时候,最先想到的就是对齐两者的pts,但是实际使用中的各类播放器,其音视频同步机制都比这些复杂的多 ...
- [SimplePlayer] 8. 音视频同步
音频与视频在播放当中可能会由于种种原因(如:音视频并非在同一时间开始播放,或视频由于解码任务繁重导致输出图像延迟等)导致音频与视频的播放时间出现偏差,这种就是音视频的同步问题,本文会对音视频同步进行讨 ...
随机推荐
- PHP 中文字符串相关
1.字符串长度 中文字符串使用strlen() 获取字符串长度时一个UTF8的中文字符是3个字节长度:使用mb_strlen(),选定内码为UTF8,则会将一个中文字符当作长度1来计算 在对含中文字符 ...
- xmanager
[root@upright91 run]# ./runBenchmark.sh updbtpcc.properties sqlTableCreates Exception in thread &quo ...
- 富文本是在modal框中弹出显示的问题
记录一下,在用tinymce富文本的时候,由于是用在modal 上的,始终无法获取焦点,后来才发现问题出在tinymce在modal前创建了,所以导致这个问题,解决方案就是用 v-if="v ...
- HTTP/FTP压力测试工具siege
HTTP/FTP压力测试工具siege 压力测试可以检测服务器的承载能力.针对HTTP和FTP服务,Kali Linux提供专项工具siege.该工具可以模拟多个用户同时访问同一个网站的多个网页, ...
- Django配置参数可选总结
一.可选字段参数 null blank core db_index editable primary_key radio_admin unique True or False db_colum hel ...
- [HDU6196]happy happy happy
题目大意: 有一个长度为n的数列,A和B两个人轮流从两端取数,B先取,A想使分数严格小于B且尽量接近B,问两人分数之差最小是多少. 思路: 折半搜索,先预处理出长度为part的最大差最小差,再预处理出 ...
- 最小生成树 Prim(普里姆)算法和Kruskal(克鲁斯特尔)算法
Prim算法 1.概览 普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树.意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (gra ...
- hdu 5289 Assignment(2015多校第一场第2题)RMQ+二分(或者multiset模拟过程)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=5289 题意:给你n个数和k,求有多少的区间使得区间内部任意两个数的差值小于k,输出符合要求的区间个数 ...
- Android中利用ant进行多渠道循环批量打包
公司负责Android开发的小伙伴学习能力稍微偏弱,交代给他的自动化打包的任务,弄了好久依然没有成效.无奈只好亲自出手. 没有想到过程很顺利,我完全按照如下文章的步骤进行: 主要参考: Android ...
- 【10.5校内测试】【DP】【概率】
转移都很明显的一道DP题.按照不优化的思路,定义状态$dp[i][j][0/1]$表示吃到第$i$天,当前胃容量为$j$,前一天吃(1)或不吃(0)时能够得到的最大价值. 因为有一个两天不吃可以复原容 ...