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

    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


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