ijkplayer播放器剖析(六)视频同步与渲染机制分析

一、引言:
在前面的博客中,将音频解码播放及视频解码都分析了,这篇博客将单独针对视频同步及渲染来分析,看下ijkplayer是如何做的。本博客分析的同步方式为以音频为主,视频去同步音频。

二、同步前提的确认:
ijkplayer的同步前提跟其他的播放器略有不同,在ijkplayer中,会创建用于维护音频,视频的时钟及一个外部时钟,所有的同步操作都是基于这三个时钟来进行的。具体的变量如下:

Clock audclk
Clock vidclk
Clock extclk
那么,对于同步而言,我们需要确认的是,音频和视频的时钟是何时更新的,只要知道了各自的时钟,那么就只需要去分析同步策略了,先来看视频。
在视频渲染线程video_refresh中,对于即将渲染的下一帧,会在渲染前更新pts:

vp = frame_queue_peek(&is->pictq);

然后将数据的pts及当前系统时间设置到vidclk:

SDL_LockMutex(is->pictq.mutex);
if (!isnan(vp->pts))
    update_video_pts(is, vp->pts, vp->pos, vp->serial);
SDL_UnlockMutex(is->pictq.mutex);

看一下update_video_pts:

static void update_video_pts(VideoState *is, double pts, int64_t pos, int serial) {
    
    
    /* update current video pts */
    set_clock(&is->vidclk, pts, serial);
    sync_clock_to_slave(&is->extclk, &is->vidclk);
}

我们前面知道,视频是去同步音频的,显然,音频pts的更新至关重要,看一下音频pts是如何更新的:audclk的时钟更新是在往audiotrack中写入数据的时候。
我们看一下ff_ffplay.c中用于往audiotrack中写数据的回调函数sdl_audio_callback

    if (!isnan(is->audio_clock)) {
    
    
        set_clock_at(&is->audclk, is->audio_clock - (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec - SDL_AoutGetLatencySeconds(ffp->aout), is->audio_clock_serial, ffp->audio_callback_time / 1000000.0);
        sync_clock_to_slave(&is->extclk, &is->audclk);
    }

音频pts更新的首要条件是判断is->audio_clock是否有效,那么这个is->audio_clock是怎么更新的呢?这个值是在往audiotrack中写数据时,获取待写入数据audio_decode_frame中去更新的:

audio_decode_frame@ff_ffplay.c:

static int audio_decode_frame(FFPlayer *ffp)
{
    
    
...
 /* update the audio clock with the pts */
 if (!isnan(af->pts))
     is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate;
 else
     is->audio_clock = NAN;
....
}

当有效帧中含有效pts时,将进入if循环,audio_clock等于当前帧pts加上当前帧的采样总点数 / 采样率两者之和,当前帧的总点数 /采样率即当前帧的持续时间。得到了这个值之后,再看前面是如何更新音频时钟audclk的:

set_clock_at(&is->audclk, is->audio_clock - (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec - SDL_AoutGetLatencySeconds(ffp->aout), is->audio_clock_serial, ffp->audio_callback_time / 1000000.0);

注意第二个入参是一个实时pts,因为上面我们分析了audio_clock是在pts的基础上加上整个帧的持续时间的,故这里会用audio_clock减去已经写入到audiotrack中的buffer数据持续时间,并且还进行了latency校准,可以说,ijkplayer在音频时钟的更新上做的非常细节。将校准后的音频时钟更新之后,下面就是同步策略的分析了。

三、同步策略分析:
ijkplayer视频同步音频的策略在video_refresh@ff_ffplay.c中实现,我们先看下video_refresh是如何调入的:

static int video_refresh_thread(void *arg)
{
    
    
    FFPlayer *ffp = arg;
    VideoState *is = ffp->is;
    double remaining_time = 0.0;
    /* 如果不退出播放 */
    while (!is->abort_request) {
    
    
    	/* 是否需要睡眠 */
        if (remaining_time > 0.0)
            av_usleep((int)(int64_t)(remaining_time * 1000000.0));
        remaining_time = REFRESH_RATE;
        if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
        	/* 调用同步及渲染函数 */
            video_refresh(ffp, &remaining_time);
    }

    return 0;
}

ijkplayer专门创建了一个video_refresh_thread用于处理视频渲染,看代码中,有一个变量remaining_time用于处理是否睡眠,实际上这个变量的值是由后面的同步策略来决定的,一帧视频需要渲染多长时间,均由这个值来判定,看一下同步处理及渲染的函数video_refresh

扫描二维码关注公众号,回复: 14837697 查看本文章
static void video_refresh(FFPlayer *opaque, double *remaining_time)
{
    
    
    FFPlayer *ffp = opaque;
    VideoState *is = ffp->is;
    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 (!ffp->display_disable && is->show_mode != SHOW_MODE_VIDEO && is->audio_st) {
    
    
		...
    }

	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);
            vp = frame_queue_peek(&is->pictq);

            /* 视频缓冲队列若更新,将丢弃本帧 */
            if (vp->serial != is->videoq.serial) {
    
    
                frame_queue_next(&is->pictq);
                goto retry;
            }

            if (lastvp->serial != vp->serial)
                is->frame_timer = av_gettime_relative() / 1000000.0;

            if (is->paused)
                goto display;

            /* compute nominal last_duration */
            /* 1.计算理论上的两帧时间间隔:通过pts直接相减获得 */
            last_duration = vp_duration(is, lastvp, vp);
            /* 2.两帧时间间隔校准:获得校准后的的两帧播放间隔 */
            delay = compute_target_delay(ffp, last_duration, is);

            /* 更新当前系统时间 */
            time= av_gettime_relative()/1000000.0;
            if (isnan(is->frame_timer) || time < is->frame_timer)
                is->frame_timer = time;
            /* 当前系统时间如果小于计算出来的下一帧显示时间,将持续等待 */
            if (time < is->frame_timer + delay) {
    
    
                *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                goto display;
            }
            /* 若已经到达该下一帧的显示时间,则更新显示时间 */
            is->frame_timer += delay;
            /* 如果当前系统时间大于下一帧渲染时间超过100ms,则直接更新播放时间为当前系统时间 */
            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的最后面:如果当前系统时间大于预估的下一帧渲染时间的话,同时上层option设置了丢帧,则直接丢帧进入下一个retry */
                if(!is->step && (ffp->framedrop > 0 || (ffp->framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration) {
    
    
                    frame_queue_next(&is->pictq);
                    goto retry;
                }
            }

            /* 字幕处理 */
            if (is->subtitle_st) {
    
    
                while (frame_queue_nb_remaining(&is->subpq) > 0) {
    
    
                    sp = frame_queue_peek(&is->subpq);

                    if (frame_queue_nb_remaining(&is->subpq) > 1)
                        sp2 = frame_queue_peek_next(&is->subpq);
                    else
                        sp2 = NULL;

                    if (sp->serial != is->subtitleq.serial
                            || (is->vidclk.pts > (sp->pts + ((float) sp->sub.end_display_time / 1000)))
                            || (sp2 && is->vidclk.pts > (sp2->pts + ((float) sp2->sub.start_display_time / 1000))))
                    {
    
    
                        if (sp->uploaded) {
    
    
                            ffp_notify_msg4(ffp, FFP_MSG_TIMED_TEXT, 0, 0, "", 1);
                        }
                        frame_queue_next(&is->subpq);
                    } else {
    
    
                        break;
                    }
                }
            }

            /* release该帧并开启渲染 */
            frame_queue_next(&is->pictq);
            is->force_refresh = 1;

            SDL_LockMutex(ffp->is->play_mutex);
            if (is->step) {
    
    
                is->step = 0;
                if (!is->paused)
                    stream_update_pause_l(ffp);
            }
            SDL_UnlockMutex(ffp->is->play_mutex);
        }
display:
        /* display picture */
        if (!ffp->display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
            video_display2(ffp);
    }        
}

当视频队列中有待处理帧时,ijkplayer会将上一帧和当前待显示帧出列,通过这两帧附带的码流信息进行同步和校准,来看下具体是如何做同步的,首先通过vp_duration来得到一个直接的时间差值:

static double vp_duration(VideoState *is, Frame *vp, Frame *nextvp) {
    
    
    if (vp->serial == nextvp->serial) {
    
    
    	/* 直接由两帧的pts相减获得 */
        double duration = nextvp->pts - vp->pts;
        if (isnan(duration) || duration <= 0 || duration > is->max_frame_duration)
            return vp->duration;
        else
            return duration;
    } else {
    
    
        return 0.0;
    }
}

如果视频帧是连续的,则初步计算出来的两帧持续时间为pts的差值。这个差值还需要去校准,之后才能作为是判断播放还是丢帧的依据,可以说,同步校准是播放器最技术的内容。ijkplayer的同步校准是通过compute_target_delay函数来实现的,其原理主要是通过比对ijkplayer自己维护的视频和音频时钟的差值,来确定视频帧的播放时长(延长,缩短或者直接丢弃)

static double compute_target_delay(FFPlayer *ffp, double delay, VideoState *is)
{
    
    
    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 */
        /* 音视频时间差(单位:秒) = 当前视频时钟 - 当前音频时钟 */
        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 */
        /* 确定一个ijkplayer认为的同步区间:40ms~100ms */
        sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
        /* -- by bbcallen: replace is->max_frame_duration with AV_NOSYNC_THRESHOLD */
        /* 音视频的时间差在100s内都去做同步操作 */
        if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) {
    
    
            /* 1.视频落后音频40ms~100ms区间内:缩短这两帧的显示间隔 */
            if (diff <= -sync_threshold)
                delay = FFMAX(0, delay + diff);
            /* 2.视频超前音频至少40ms~100ms且视频前后两帧的时间间隔大于150ms:增加两帧间的播放间隔以等待音频 */
            else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
                delay = delay + diff;
            /* 3.视频超前音频40ms~100ms且两帧间的间隔较小:延长一个delay */
            else if (diff >= sync_threshold)
                delay = 2 * delay;
        }
    }

    if (ffp) {
    
    
        ffp->stat.avdelay = delay;
        ffp->stat.avdiff  = diff;
    }
#ifdef FFP_SHOW_AUDIO_DELAY
    av_log(NULL, AV_LOG_TRACE, "video: delay=%0.3f A-V=%f\n",
            delay, -diff);
#endif

    return delay;
}

入参delay是前面计算出来的视频前后两帧的pts差值,diff则是当前音视频的时钟差,sync_threshold是一个同步阈值,ijkplayer将通过这个同步阈值来决定视频帧的持续时长,其值是通过delay来动态得到的,通过计算,可以看到,其同步阈值的范围为40~100ms,计算出来了同步阈值之后,就是去进行同步的判断操作了,这里我比较好奇的是,ijkplayer的同步操作在100s以内都会去做的,不确定是否是单位的问题,来看一下ijkplayer罗列出的三种情况:

/* 音视频的时间差在100s内都去做同步校准操作 */
if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) {
    
    
    /* 1.视频落后音频40ms~100ms区间内:缩短这两帧的显示间隔 */
    if (diff <= -sync_threshold)
        delay = FFMAX(0, delay + diff);
    /* 2.视频超前音频至少40ms~100ms且视频前后两帧的时间间隔大于150ms:增加两帧间的播放间隔以等待音频 */
    else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
        delay = delay + diff;
    /* 3.视频超前音频40ms~100ms且两帧间的间隔较小:延长一个delay */
    else if (diff >= sync_threshold)
        delay = 2 * delay;
}

因为diff值为视频时钟减去音频时钟,如果该值为负,则表明是视频落后音频,如果该值为正,则是视频超前音频,先看情形一,如果视频此时已经落后音频了,则需要加快显示下一帧,那么自然就需要缩短当前帧的显示时长,故直接在diff的基础上加上两帧间隔,如果其值依旧小于0的话,就取零。再看视频超前音频的情形,这里分为了两种,如果视频超前时间较长,则增加当前帧的显示时间,等待音频,如果较短的话,则直接将当前帧显示时长增加一倍即可。最终,将该值返回。
回到外面的video_refresh,在通过计算和校准拿到最终的显示时长后,接下来需要做的就是去送显,is->frame_timer是ijkplayer用于维护当前视频帧显示时间的变量,将这个变量加上前面校准后的delay即是下一帧的视频显示时间,如果此时的系统时间小于下一帧的显示时间的话,那么直接等待即可,但是如果大于了下一帧的系统时间的话,就需要考虑丢帧了:

/* 如果还有至少两帧待渲染:计算出下一帧的播放时间 */
if (frame_queue_nb_remaining(&is->pictq) > 1) {
    
    
    Frame *nextvp = frame_queue_peek_next(&is->pictq);
    duration = vp_duration(is, vp, nextvp);
    /* 直接看if的最后面:如果当前系统时间大于预估的下一帧播放时间的话,同时上层option设置了丢帧,则直接丢帧进入下一个retry */
    if(!is->step && (ffp->framedrop > 0 || (ffp->framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration) {
    
    
        frame_queue_next(&is->pictq);
        goto retry;
    }
}

ijkplayer所有关于同步的策略就分析完了,下面就是去看ijkplayer如何进行送显的了。

四、渲染函数分析:
ijkplayer的视频渲染是由video_display2函数来完成的:

/* display the current picture, if any */
static void video_display2(FFPlayer *ffp)
{
    
    
    VideoState *is = ffp->is;
    if (is->video_st)
        video_image_display2(ffp);
}
static void video_image_display2(FFPlayer *ffp)
{
    
    
    VideoState *is = ffp->is;
    Frame *vp;
    Frame *sp = NULL;

    vp = frame_queue_peek_last(&is->pictq);
	
	 if (vp->bmp) {
    
    
		...
		SDL_VoutDisplayYUVOverlay(ffp->vout, vp->bmp);
		...
	}
	...
}

看一下SDL_VoutDisplayYUVOverlay

int SDL_VoutDisplayYUVOverlay(SDL_Vout *vout, SDL_VoutOverlay *overlay)
{
    
    
    if (vout && overlay && vout->display_overlay)
        return vout->display_overlay(vout, overlay);

    return -1;
}

这里又是通过函数指针去实现的,先确认下vout的创建,在ijkplayer的create阶段,会去创建surface,在SDL_VoutAndroid_CreateForANativeWindow (ijksdl_vout_android_nativewindow.c)中会去实例化vout:
在这里插入图片描述
确认下vout的几个函数指针指向:

vout->opaque_class    = &g_nativewindow_class;
vout->create_overlay  = func_create_overlay;
vout->free_l          = func_free_l;
vout->display_overlay = func_display_overlay;

渲染函数是由func_display_overlay来完成的:

static int func_display_overlay(SDL_Vout *vout, SDL_VoutOverlay *overlay)
{
    
    
    SDL_LockMutex(vout->mutex);
    int retval = func_display_overlay_l(vout, overlay);
    SDL_UnlockMutex(vout->mutex);
    return retval;
}
static int func_display_overlay_l(SDL_Vout *vout, SDL_VoutOverlay *overlay)
{
    
    
	...
    switch(overlay->format) {
    
    
    case SDL_FCC__AMC: {
    
    
        // only ANativeWindow support
        IJK_EGL_terminate(opaque->egl);
        /* 反射到mediacodec去进行渲染 */
        return SDL_VoutOverlayAMediaCodec_releaseFrame_l(overlay, NULL, true);
    }
    ...
}

往下追一下,SDL_VoutOverlayAMediaCodec_releaseFrame_l->SDL_VoutAndroid_releaseBufferProxyP_l->SDL_VoutAndroid_releaseBufferProxy_l

static int SDL_VoutAndroid_releaseBufferProxy_l(SDL_Vout *vout, SDL_AMediaCodecBufferProxy *proxy, bool render)
{
    
    
	...
	sdl_amedia_status_t amc_ret = SDL_AMediaCodec_releaseOutputBuffer(opaque->acodec, proxy->buffer_index, render); 
	...
}

这里就可以看到还是调用的mediacodec的releaseOutputBuffer接口来进行视频帧的渲染了。

五、补充:
视频渲染的函数分析并不复杂,但是有一个地方需要注意,那就是SDL_FCC__AMC这个case是如何确认的?首先,在视频解码线程func_run_sync中,会去调用ffp_queue_picture函数:

int ffp_queue_picture(FFPlayer *ffp, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)
{
    
    
    return queue_picture(ffp, src_frame, pts, duration, pos, serial);
}
static int queue_picture(FFPlayer *ffp, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)
{
    
    
	...
	alloc_picture(ffp, src_frame->format);
	...
}
static void alloc_picture(FFPlayer *ffp, int frame_format)
{
    
    
	...
	SDL_VoutSetOverlayFormat(ffp->vout, ffp->overlay_format);
    vp->bmp = SDL_Vout_CreateOverlay(vp->width, vp->height,
                                   frame_format,
                                   ffp->vout);
    ...
}

跟进到SDL_Vout_CreateOverlay

SDL_VoutOverlay *SDL_Vout_CreateOverlay(int width, int height, int frame_format, SDL_Vout *vout)
{
    
    
    if (vout && vout->create_overlay)
        return vout->create_overlay(width, height, frame_format, vout);

    return NULL;
}

在前面我们已经分析了vout的函数指针,看一下其函数实现:

static SDL_VoutOverlay *func_create_overlay(int width, int height, int frame_format, SDL_Vout *vout)
{
    
    
    SDL_LockMutex(vout->mutex);
    SDL_VoutOverlay *overlay = func_create_overlay_l(width, height, frame_format, vout);
    SDL_UnlockMutex(vout->mutex);
    return overlay;
}
static SDL_VoutOverlay *func_create_overlay_l(int width, int height, int frame_format, SDL_Vout *vout)
{
    
    
    switch (frame_format) {
    
    
    case IJK_AV_PIX_FMT__ANDROID_MEDIACODEC:
        return SDL_VoutAMediaCodec_CreateOverlay(width, height, vout);
    default:
        return SDL_VoutFFmpeg_CreateOverlay(width, height, frame_format, vout);
    }
}

这里又需要确认frame_format,这个值是在填充buffer的时候写入的:

amc_fill_frame@ijkmedia\ijkplayer\android\pipeline\ffpipenode_android_mediacodec_vdec.c:
static int amc_fill_frame(
    IJKFF_Pipenode            *node,
    AVFrame                   *frame,
    int                       *got_frame,
    int                        output_buffer_index,
    int                        acodec_serial,
    SDL_AMediaCodecBufferInfo *buffer_info)
{
    
    
	...
	frame->format = IJK_AV_PIX_FMT__ANDROID_MEDIACODEC;
	...
}

确认了format,进入到mediacodec的case:

SDL_VoutOverlay *SDL_VoutAMediaCodec_CreateOverlay(int width, int height, SDL_Vout *vout)
{
    
    
	...
	overlay->format       = SDL_FCC__AMC;
	...
}

这里就是最终确认使用mediacodec来进行送显的依据。

六、总结:
ijkplayer的同步原理就是比对视频和音频的时钟差值,来确音频和视频谁超前,然后通过视频前后两帧的pts差值来决定执行哪种策略,最后就是将校准后的这个值同当前系统时间对比来确认最终的送显时间:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/achina2011jy/article/details/116046380