ffplay播放器音视频同步原理

*****************************************************************************

* 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()中音频时钟的更新

    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时间,最后在正确的时间渲染视频,实现同步播放。

参考

[1] http://osdl.sourceforge.net/main/documentation/rendering/SDL-audio.html


版权声明:本文为博主原创文章,未经博主允许请勿转载。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ITRonnie

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值