ffplay播放器音视频同步原理
来源:互联网 发布:新疆知信科技有限公司 编辑:程序博客网 时间:2024/05/17 21:01
*****************************************************************************
* ffplay系列博客: *
* ffplay播放器原理剖析 *
* ffplay播放器音视频同步原理 *
* ffplay播放控制代码分析 *
* 视频主观质量对比工具(Visual comparision tool based on ffplay) *
*****************************************************************************
根据之前的博客“ffplay播放器原理的剖析”,我们知道了音频和视频的播放是在不同线程中进行的,而且音频和视频都有自己的时间戳,所以需要同步机制保障音画同步。
有多种机制可以做到音视频同步:a. 音频同步于视频。b. 视频同步于音频。c.音视频都同步于基准时钟。ffplay默认采用视频同步于音频的方式,下面结合ffmpeg 3.1.1源代码分析一下ffplay的音视频同步原理。
总体来看,视频同步于音频的机制主要包括两个部分:1、音频时钟的更新。2、视频帧渲染与音频时钟的同步(根据当前音频时钟调整视频帧渲染的时刻,实现同步)。
音频时钟更新
这里说的音频时钟,是指当前播放的音频的时间戳。根据“ffplay播放器原理的剖析”,音频播放的函数调用关系为:SDL音频驱动 -> sdl_audio_callback(),因此音频时钟的更新在sdl_audio_callback()中。
sdl_audio_callback()是ffplay往SDL驱动指定的buffer中拷贝音频数据的函数,SDL音频驱动不断的调用sdl_audio_callback()来持续获取音频数据,达到流畅播放的效果。由于SDL音频驱动会缓冲一定量的数据,所以当前SDL播放的音频的时间戳要早于在sdl_audio_callback()中填充的音频数据的时间戳,为了弄清ffplay中音频播放时间戳的计算公式,有必要弄清SDL音频播放的原理。
SDL音频播放原理
简单来说,SDL音频驱动播放音频采用“双buffer机制[1]”:一个buffer用于音频播放(声卡从中读取数据进行播放),另一个buffer用于数据填充 (用户自定义的callback函数往里填充音频数据,在ffplay中就是sdl_audio_callback函数)。举例来说,假设两个buffer分别为A和B,A和B大小一致,那么音频播放机制如下:
(1)、初始填充A、B为静音的音频数据。
(2)、音频驱动调用声卡开始播放A中的数据。
(3)、音频驱动调用回调函数(sdl_audio_callback()),在回调函数中应用程序往B中填充数据。
(4)、音频驱动等待声卡播放完A,接着调用声卡播放B中的数据(注意此时要保证(2)已经完成)。
(5)、音频驱动调用回调函数(sdl_audio_callback()),在回调函数中应用程序往A中填充数据。
(6)、音频驱动等待声卡播放完B,接着调用声卡播放A中的数据(注意此时要保证(4)已经完成)。
... 如此循环 ...
根据上面的分析,双buffer机制可以使 写buffer和读buffer独立互不干扰,不产生访问竞争问题,当然前提是sdl_audio_callback()能及时往buffer里填满所需数据。
这里buffer A和B的大小是比较重要的参数:buffer太大或导致播放延迟(因为需要等到A填充满了之后才开始播放),buffer太小或导致sdl_audio_callback()来不及往buffer中填充数据,导致部分音频被Skip的后果。在ffplay中,设定一秒钟大概调用30次sdl_audio_callback()函数,应该很好的权衡了buffer大小的问题,下面分析一下ffplay中初始化SDL音频驱动的代码。
ffplay中SDL音频驱动参数的初始化分析
ffplay中音频设备初始化的函数调用关系为read_thread() -> stream_component_open() -> audio_open(),在audio_open函数中对SDL音频驱动进行初始化。
一些音频相关概念:
- 音频format:每个音频sample数据的精度,一般为8bit或者16bit,类似于视频中每个像素的比特位深。
- 声道数:几个声道(mono单声道, stero双声道,5.1声道etc)。
- 音频Sample的Size:每个音频Sample的大小,比如双声道16bit,则SampleSize = 16 * 2 = 32 bits。
- 音频频率freq:表示一秒钟播放多少个sample,单位为Hz或者kHz,一般CD音质为44100Hz(44.1kHz)
下面是audio_open()的代码,加了相关注释便于理解。函数返回了buffer A和B的大小,赋给了is->audio_hw_buf_size。
static int audio_open(void *opaque, int64_t wanted_channel_layout, int wanted_nb_channels, int wanted_sample_rate, struct AudioParams *audio_hw_params){ SDL_AudioSpec wanted_spec, spec; const char *env; static const int next_nb_channels[] = {0, 0, 1, 6, 2, 6, 4, 6}; static const int next_sample_rates[] = {0, 44100, 48000, 96000, 192000}; int next_sample_rate_idx = FF_ARRAY_ELEMS(next_sample_rates) - 1; env = SDL_getenv("SDL_AUDIO_CHANNELS"); if (env) { wanted_nb_channels = atoi(env); wanted_channel_layout = av_get_default_channel_layout(wanted_nb_channels); } if (!wanted_channel_layout || wanted_nb_channels != av_get_channel_layout_nb_channels(wanted_channel_layout)) { wanted_channel_layout = av_get_default_channel_layout(wanted_nb_channels); wanted_channel_layout &= ~AV_CH_LAYOUT_STEREO_DOWNMIX; } wanted_nb_channels = av_get_channel_layout_nb_channels(wanted_channel_layout); wanted_spec.channels = wanted_nb_channels;// 声道数 wanted_spec.freq = wanted_sample_rate; // 频率:一秒钟播放多少个sample if (wanted_spec.freq <= 0 || wanted_spec.channels <= 0) { av_log(NULL, AV_LOG_ERROR, "Invalid sample rate or channel count!\n"); return -1; } while (next_sample_rate_idx && next_sample_rates[next_sample_rate_idx] >= wanted_spec.freq) next_sample_rate_idx--; wanted_spec.format = AUDIO_S16SYS; // 每个sample数据精度为16bit wanted_spec.silence = 0; // wanted_spec.samples指定了buffer A和B中的sample的数量,这里指定buffer A和B大概包含了1/30秒的samples wanted_spec.samples = FFMAX(SDL_AUDIO_MIN_BUFFER_SIZE, 2 << av_log2(wanted_spec.freq / SDL_AUDIO_MAX_CALLBACKS_PER_SEC)); wanted_spec.callback = sdl_audio_callback; // 指定SDL音频驱动的回调函数 wanted_spec.userdata = opaque; while (SDL_OpenAudio(&wanted_spec, &spec) < 0) { ...... // 这里是一些调整,忽略 } ...... audio_hw_params->fmt = AV_SAMPLE_FMT_S16; audio_hw_params->freq = spec.freq; audio_hw_params->channel_layout = wanted_channel_layout; audio_hw_params->channels = spec.channels; audio_hw_params->frame_size = av_samples_get_buffer_size(NULL, audio_hw_params->channels, 1, audio_hw_params->fmt, 1); audio_hw_params->bytes_per_sec = av_samples_get_buffer_size(NULL, audio_hw_params->channels, audio_hw_params->freq, audio_hw_params->fmt, 1); if (audio_hw_params->bytes_per_sec <= 0 || audio_hw_params->frame_size <= 0) { av_log(NULL, AV_LOG_ERROR, "av_samples_get_buffer_size failed\n"); return -1; } return spec.size; // 返回的是buffer A和B的Size,赋值给了VideoState->audio_hw_buf_size}
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(); //获取当前系统时间 while (len > 0) { // 往stream填充长度为len的数据,stream就是buffer A或者buffer B if (is->audio_buf_index >= is->audio_buf_size) { // audio_buf中的数据已经全拷到stream中,需要拿新的audio_buf audio_size = audio_decode_frame(is); // 从audio sample queue中拿新的数据来播放 if (audio_size < 0) { /* if error, just output silence */ is->audio_buf = NULL; is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE / 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; } len1 = is->audio_buf_size - is->audio_buf_index; if (len1 > len) len1 = len; if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME) //往stream中拷贝长度为len的数据 memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1); else { memset(stream, 0, len1); if (!is->muted && is->audio_buf) SDL_MixAudio(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1, is->audio_volume); } len -= len1; //更新is->audio_buf的未拷贝到stream中的数据(剩余数据)的长度 stream += len1; is->audio_buf_index += len1; //更新is->audio_buf_index,指向audio_buf中未被拷贝到stream的数据(剩余数据)的起始位置 } is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index; // audio_write_buf_size为audio_buf中剩余数据的size /* Let's assume the audio driver that is used by SDL has two periods. */ if (!isnan(is->audio_clock)) { // // 计算当前播放的音频的时间戳,这里的计算公式理解起来稍微费劲一些。 // is->audio_clock是当前拿到的最新的audio sample的时间戳,在audio_decode_frame函数中计算的。 // 此时,未播放的音频数据包括buffer A 和 buffer B中的数据加上is->audio_buf中的剩余数据 // 所以当前播放的时间戳相对于is->audio_clock要落后(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec。 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); }}static int audio_decode_frame(VideoState *is){ ...... if (!isnan(af->pts)) // 更新当前拿到的数据的时间戳 is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate; else is->audio_clock = NAN; ......}
视频渲染与音频时钟的同步
static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) { double remaining_time = 0.0; // 调用SDL_PeepEvents前先调用SDL_PumpEvents,将输入设备的事件抽到事件队列中 SDL_PumpEvents(); while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_ALLEVENTS)) { // 从事件队列中拿一个事件,放到event中,如果没有事件,则进入循环中 if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) { SDL_ShowCursor(0); //隐藏鼠标 cursor_hidden = 1; } // remaining_time就是用来进行音视频同步的。 // 在video_refresh函数中,根据当前帧显示时刻(display time)和实际时刻(actual time)计算需要sleep的时间,保证帧按时显示 if (remaining_time > 0.0) // 如果视频来的太早,则sleep一段时间之后再来显示 av_usleep((int64_t)(remaining_time * 1000000.0)); remaining_time = REFRESH_RATE; if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh)) video_refresh(is, &remaining_time); SDL_PumpEvents(); }}
/* called to display each frame */static void video_refresh(void *opaque, double *remaining_time){ VideoState *is = opaque; double time; Frame *sp, *sp2; if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime) check_external_clock_speed(is); ...... if (is->video_st) {retry: if (frame_queue_nb_remaining(&is->pictq) == 0) { // nothing to do, no picture to display in the queue } else { double last_duration, duration, delay; Frame *vp, *lastvp; /* dequeue the picture */ lastvp = frame_queue_peek_last(&is->pictq); //取Video Frame Queue上一帧图像 vp = frame_queue_peek(&is->pictq); //取Video Frame Queue当前帧图像 ...... if (is->paused) goto display; /* compute nominal last_duration */ last_duration = vp_duration(is, lastvp, vp); //计算两帧之间的时间间隔 delay = compute_target_delay(last_duration, is); //计算当前帧与上一帧渲染的时间差 time= av_gettime_relative()/1000000.0; //is->frame_timer + delay是当前帧渲染的时刻,如果当前时间还没到帧渲染的时刻,那就要sleep了 if (time < is->frame_timer + delay) { // remaining_time为需要sleep的时间 *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time); goto display; } is->frame_timer += delay; 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; } } ...... frame_queue_next(&is->pictq); is->force_refresh = 1; //显示当前帧 if (is->step && !is->paused) stream_toggle_pause(is); }display: /* display picture */ if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown) video_display(is); } is->force_refresh = 0; ......}compute_target_delay用来计算当前帧和前一帧渲染的时间差。
static double compute_target_delay(double delay, VideoState *is){ // delay传递进来的参数为当前帧和上一帧时间戳间的时间差,是两帧之间正常播放的时间间隔 double sync_threshold, diff = 0; /* update delay to follow master synchronisation source */ if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) { /* if video is slave, we try to correct big delays by duplicating or deleting a frame */ // is->vldclk为当前帧的渲染时间,get_master_clock(is)其实返回的是is->audclk,为音频时钟(正在播放的音频的时间戳) // 所以diff为视频相对于音频时钟的时间差,diff > 0表示视频来的早, diff < 0表示视频来的迟了 diff = get_clock(&is->vidclk) - get_master_clock(is); /* skip or repeat frame. We take into account the delay to compute the threshold. I still don't know if it is the best guess */ 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) // video frame来的迟了,减少等待时间 delay = FFMAX(0, delay + diff); else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD) delay = delay + diff; // video frame来早了,增加渲染前的等待时间 else if (diff >= sync_threshold) // last frame displayed two frame time delay = 2 * delay; // video frame来早了,增加渲染前的等待时间,让前一帧渲染两次 } } ...... return delay;}
总结
现在来看,音视频同步的机制还是很直观的:音频更新音频时钟,然后根据视频帧时间戳与音频时钟的差别计算渲染前的sleep时间,最后在正确的时间渲染视频,实现同步播放。
参考
[1] http://osdl.sourceforge.net/main/documentation/rendering/SDL-audio.html
版权声明:本文为博主原创文章,未经博主允许请勿转载。
- ffplay播放器音视频同步原理
- ffplay视频播放器原理
- 【FFplay】零基础读懂视频播放器控制原理——ffplay播放器源代码分析
- 音视频同步(播放)原理
- 音视频同步(播放)原理
- 音视频同步(播放)原理
- 音视频同步(播放)原理
- 音视频同步(播放)原理
- 音视频同步(播放)原理
- 音视频同步(播放)原理
- 音视频同步(播放)原理
- 音视频同步(播放)原理
- 音视频同步(播放)原理
- ffplay之音视频同步
- ffplay播放器原理剖析
- 零基础读懂视频播放器控制原理——ffplay播放器源代码分析
- 零基础读懂视频播放器控制原理: ffplay 播放器源代码分析
- Ffplay视频播放流程
- java.lang.NoClassDefFoundError: Could not initialize class sun.awt.X11GraphicsEnvironment
- 刚学的类与对象的思想(不知道对不对,求大神指导)
- MySQL学习笔记
- Android Room联合AsyncListUtil实现RecyclerView分页加载ORM数据
- yii2中分页的样式设置
- ffplay播放器音视频同步原理
- Windows2008 r2 x64下安装FTP工具File Zilla server报错:could not load tls libraries filezilla
- windows xampp localhost can visit ip also ,but vhost can't
- Libcoap安装和使用
- 云星数据---Scala实战系列(精品版)】:Scala入门教程044-Scala实战源码-Scala 元组Tuple操作
- JSP页面中<%!%>与<%%>与<%=%>
- MPU6050开发 -- 数据分析
- JavaScript 闭包
- Linux root初始密码设置