在抖音APP源码中音频和视频的播放是在不同线程中进行的,而且音频和视频都有自己的时间戳,所以需要同步机制保障音画同步。
抖音APP源码有多种机制可以做到音视频同步:a. 音频同步于视频。b. 视频同步于音频。c.音视频都同步于基准时钟。ffplay默认采用视频同步于音频的方式,下面结合ffmpeg 3.1.1源代码分析一下ffplay的音视频同步原理。
总体来看,视频同步于音频的机制主要包括两个部分:1、音频时钟的更新。2、视频帧渲染与音频时钟的同步(根据当前音频时钟调整视频帧渲染的时刻,实现同步)。
音频时钟更新
这里说的音频时钟,是指抖音APP源码当前播放的音频的时间戳。根据“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音频播放原理
简单来说,在抖音APP源码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()中音频时钟的更新
is->audclk即音频时钟,计算公式为:
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是当前拿到的最新的audio sample的时间戳,在sdl_audio_callback()中,未播放的音频数据包括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。 具体代码和注释如下:
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;
......
}
视频渲染与音频时钟的同步
视频帧的渲染函数调用关系:
main() -> event_loop() -> refresh_loop_wait_event() -> video_refresh() -> video_display() -> video_image_display()
视频的同步操作主要在refresh_loop_wait_event()和video_refresh()中,refresh_loop_wait_event()中,在视频帧渲染前先等待remainning_time,remainning_time为当前时刻距video frame显示时刻的时间差,首先sleep(remainning_time)让帧在正确的时间显示。
那么remainning_time的计算就是同步的关键,remainning_time在video_refresh中计算。
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time); 其中is->frame_timer为上一帧的渲染时间,delay为当前帧与上一帧渲染时间差, time为当前实际时间,所以is->frame_timer + delay - time为当前帧渲染之前的等待时间。
delay通过compute_target_delay()计算的。
相关代码及注释如下:
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时间,最后在正确的时间渲染视频,实现同步播放。