在抖音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;    // 频率:一秒钟播放多少个sampleif (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数据精度为16bitwanted_spec.silence = 0;// wanted_spec.samples指定了buffer A和B中的sample的数量,这里指定buffer A和B大概包含了1/30秒的sampleswanted_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 Bif (is->audio_buf_index >= is->audio_buf_size) { // audio_buf中的数据已经全拷到stream中,需要拿新的audio_bufaudio_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;elseis->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 byduplicating 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 thedelay to compute the threshold. I still don't knowif 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 timedelay = 2 * delay;       // video frame来早了,增加渲染前的等待时间,让前一帧渲染两次}}......return delay;
}

总结
现在来看,音视频同步的机制还是很直观的:音频更新音频时钟,然后根据视频帧时间戳与音频时钟的差别计算渲染前的sleep时间,最后在正确的时间渲染视频,实现同步播放。

在抖音APP源码中如何实现播放器的音视频同步相关推荐

  1. 安卓APP源码和报告——音乐播放器

    课 程 设 计 报 告 院 系: 专 业: 题 目: 科 目: 学 生: 指导教师: 完成时间: 目 录 1. 引言1 1.1 目的1 1.2 背景1 2. 需求分析1 3. 系统设计1 3.1总体设 ...

  2. 一对一软件开发:在一对一社交app源码中加入这个功能,很有用...

    一对一软件开发在年内倍受欢迎,随着社交app的盛行,越来越多的人愿意涉足社交行业,但传统社交平台需要大量资金支持运营,而一对一直播社交平台由于其机制限制,对带宽使用较少.对平台能承受的并发量要求低.所 ...

  3. 云豹短视频app源码中关于php--thinkcmf配置语言包的说明

    云豹科技经常接到国外客户的单子,因此在语言模式设置上,需要实现灵活切换,以便不同语言的人员使用.云豹短视频app源码采用PHP做后台,Thinkcmf做框架,Thinkcmf本身是具有多语言模式的,默 ...

  4. 云豹短视频app源码中,标签选择功能的实现

    在国内任意短视频app中,都能够看到"贴标签功能",云豹短视频app源码作为紧跟行业潮流的成品商业源码,自然也有该功能的实现,在云豹短视频app源码中,标签选择功能的实现要求是这样 ...

  5. Android直播APP源码中排行榜功能如何实现

    刚进公司的时候,听技术人员说起直播APP源码中的"排行榜"功能,小编最先想到的是学生时期的成绩排行,上榜的沾沾自喜到下次考试,下榜的哭哭啼啼,其实就算上榜也并没有什么实质性的奖励, ...

  6. 云豹短视频app源码中,用户定位与位置筛选功能如何实现

    在云豹短视频app源码中,用户定位与位置筛选功能部分,使用的是类似通讯录样式的字母排序方式,以及右侧导航直达的跳转模式,这一设置的目的是尽量以最简洁的方式UI.用户最熟悉的排序方式便于用户使用该功能. ...

  7. 视频直播APP源码开发iOS音频播放流程

    视频直播APP源码开发iOS音频播放流程 概览 随着移动互联网的发展,如今的手机早已不是打电话.发短信那么简单了,播放音乐.视频.录音.拍照等都是很常用的功能.在iOS中对于多媒体的支持是非常强大的, ...

  8. html5 mp3播放器源码,HTML5自定义mp3播放器源码

    audio对象 src兼容.ogg .wav .mp3 width autoplay loop muted静音 播放play() var myAudio = new Audio(); myAudio. ...

  9. 【流媒体开发】VLC Media Player - Android 平台源码编译 与 二次开发详解 (提供详细800M下载好的编译源码及eclipse可调试播放器源码下载)

    作者 : 韩曙亮  博客地址 : http://blog.csdn.net/shulianghan/article/details/42707293 转载请注明出处 : http://blog.csd ...

最新文章

  1. ​“手把手撕LeetCode题目,扒各种算法套路的裤子”
  2. 公开平等的企业文化是OKR落地第一步
  3. linux文件系统的链接,用户指南:Linux文件系统的链接
  4. 弹出提示对话框并重定向网页
  5. bzoj#2125. 最短路
  6. samba登陆密码不正确
  7. Opencv--学习Opencv比较好的网址
  8. linux下 apache启动、停止、重启命令
  9. 如何在一台没有网的电脑上安装anaconda_简述验证Anaconda是否安装成功的两种方式...
  10. php文件下载IE文件名乱码问题
  11. select下拉框option默认选中(php模板渲染)
  12. zipf定律 齐普夫定律
  13. 能盾智能化应急响应管理平台
  14. Debian9开机:firmware: failed to load rtl_nic/rtl8168g-3.fw (-2)
  15. (混沌序列统计特性)Maurer通用统计测试---matlab
  16. 深圳小汽车摇号结果采集
  17. 《求职》第四部分 - 操作系统篇 - 操作系统常见问题
  18. duang,duang!!duang.java.mustReadTips
  19. 「重磅」微信小程序再也不能打开APP了
  20. 线性代数 --- 什么叫线性组合 Linear Combination(个人笔记扫描版)

热门文章

  1. 语音识别MFCC系列(四)——MFCC特征参数提取
  2. 爱奇艺播放视频声音和画面不同步解决办法
  3. 我的电子书共享站,欢迎大家访问下载经典电子书
  4. Vscode中注释快捷键设置
  5. 九.mini2440实现I2C协议裸机程序(完成)
  6. vr全景拍摄与合成培训哪家好?自学好还是培训好?
  7. 抖音点赞最多的标题_抖音的标题应该怎么来写才能获得点赞多?
  8. RecyclerView中notifyDataSetChanged刷新总结
  9. Phabricator 切换使用 Bitnami 容器镜像
  10. 修复照片怎么弄?我来教你几招