ijkplayer源码分析(4)——渲染流程

2,177 阅读5分钟

回顾一下video_refresh_thread的大概创建使用流程。

20210128154342391.png

video_refresh_thread

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;
}

内部一个while循环,每次循环10ms,触发video_refresh条件为

is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh)

show_mode不为空,并且当前非暂停状态或者进行强制刷新

音视频同步

实现音视频同步,在播放时,需要选定一个参考时钟,读取帧上的时间戳,同时根据的参考时钟来动态调节播放。现在已经知道时间戳就是PTS,那么参考时钟的选择一般来说有以下三种:

  • 将视频同步到音频上:就是以音频的播放速度为基准来同步视频。
  • 将音频同步到视频上:就是以视频的播放速度为基准来同步音频。
  • 将视频和音频同步外部的时钟上:选择一个外部时钟为基准,视频和音频的播放速度都以该时钟为标准。

当播放源比参考时钟慢,则加快其播放速度,或者丢弃;快了,则延迟播放。

这三种是最基本的策略,考虑到人对声音的敏感度要强于视频,频繁调节音频会带来较差的观感体验,且音频的播放时钟为线性增长,所以一般会以音频时钟为参考时钟,视频同步到音频上。 在实际使用基于这三种策略做一些优化调整,例如:

  • 调整策略可以尽量采用渐进的方式,因为音视频同步是一个动态调节的过程,一次调整让音视频PTS完全一致,没有必要,且可能导致播放异常较为明显
  • 调整策略仅仅对早到的或晚到的数据块进行延迟或加快处理,有时候是不够的。如果想要更加主动并且有效地调节播放性能,需要引入一个反馈机制,也就是要将当前数据流速度太快或太慢的状态反馈给“源”,让源去放慢或加快数据流的速度。
  • 对于起播阶段,特别是TS实时流,由于视频解码需要依赖第一个I帧,而音频是可以实时输出,可能出现的情况是视频PTS超前音频PTS较多,这种情况下进行同步,势必造成较为明显的慢同步。处理这种情况的较好方法是将较为多余的音频数据丢弃,尽量减少起播阶段的音视频差距。

video_refresh

 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 */
            //根据pts计算上一帧的显示时间,就是当前帧pts-上一帧pts
            last_duration = vp_duration(is, lastvp, vp);
            //计算出延迟时间,也就是最终需要显示的延迟时间。
            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;
            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 && (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;
                }
            }

其中比较关键的方法compute_target_delay,根据上一帧的显示时间,计算出delay

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 */
        sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
        /* -- by bbcallen: replace is->max_frame_duration with AV_NOSYNC_THRESHOLD */
        if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) {
            if (diff <= -sync_threshold)
                delay = FFMAX(0, delay + diff);
            else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
                delay = delay + diff;
            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;
}

整理下大概结论如下

  • 获取当前要显示的video PTS,减去上一帧视频PTS,则得出上一帧视频应该显示的时长delay
  • 当前video PTS与参考时钟当前audio PTS比较,得出音视频差距diff
  • 获取同步阈值sync_threshold,为一帧视频差距,范围为10ms-100ms
  • 如果超过sync_threshold,且视频落后于音频(diff <= -sync_threshold),那么需要减小delay(FFMAX(0, delay + diff)),让当前帧尽快显示
  • 如果超过sync_threshold,且视频快于音频,那么需要加大delay,让当前帧延迟显示。 将delay*2慢慢调整差距,这是为了平缓调整差距,因为直接delay+diff,会让画面画面迟滞。 如果视频前一帧本身显示时间很长,那么直接delay+diff一步调整到位,因为这种情况再慢慢调整也没太大意义

通过上述逻辑同步之后,接下来就是进行显示了

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);
}
​

video_image_display2

static void video_image_display2(FFPlayer *ffp)
{
    VideoState *is = ffp->is;
    Frame *vp;
    Frame *sp = NULL;
    vp = frame_queue_peek_last(&is->pictq);
    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的display_overlay显示方法,那么这个对象是在哪里赋值的呢?在初始化的时候,ijkmp_android_create方法之中

IjkMediaPlayer *ijkmp_android_create(int(*msg_loop)(void*))
{
    IjkMediaPlayer *mp = ijkmp_create(msg_loop);
    // ...
    mp->ffplayer->vout = SDL_VoutAndroid_CreateForAndroidSurface();
    // ...
}

之后通过setSurface(),创建ANativeWindow,并赋值给SDL_Vout_Opaque

补充一下几个结构体的定义 SDL_Vout表示一个上下文,主要负责对overlay的控制和显示

struct SDL_Vout {
    SDL_mutex *mutex;
​
    SDL_Class       *opaque_class;
    SDL_Vout_Opaque *opaque;
    SDL_VoutOverlay *(*create_overlay)(int width, int height, int frame_format, SDL_Vout *vout);
    void (*free_l)(SDL_Vout *vout);
    int (*display_overlay)(SDL_Vout *vout, SDL_VoutOverlay *overlay);
​
    Uint32 overlay_format;
};

SDL_VoutOverlay理解为一块图像数据,宽高/格式等

struct SDL_VoutOverlay {
    int w; /**< Read-only */
    int h; /**< Read-only */
    Uint32 format; /**< Read-only */
    int planes; /**< Read-only */
    Uint16 *pitches; /**< in bytes, Read-only */
    Uint8 **pixels; /**< Read-write */
​
    int is_private;
​
    int sar_num;
    int sar_den;
​
    SDL_Class               *opaque_class;
    SDL_VoutOverlay_Opaque  *opaque;
​
    void    (*free_l)(SDL_VoutOverlay *overlay);
    int     (*lock)(SDL_VoutOverlay *overlay);
    int     (*unlock)(SDL_VoutOverlay *overlay);
    void    (*unref)(SDL_VoutOverlay *overlay);
​
    int     (*func_fill_frame)(SDL_VoutOverlay *overlay, const AVFrame *frame);
};

display_overlay

从代码中可以看到,根据overlay->format的不同,提供了3中不同的渲染方式,MediaCodecEGLNativeWindow

static int func_display_overlay_l(SDL_Vout *vout, SDL_VoutOverlay *overlay)
{
    SDL_Vout_Opaque *opaque = vout->opaque;
    ANativeWindow *native_window = opaque->native_window;
​
    if (!native_window) {
        if (!opaque->null_native_window_warned) {
            opaque->null_native_window_warned = 1;
            ALOGW("func_display_overlay_l: NULL native_window");
        }
        return -1;
    } else {
        opaque->null_native_window_warned = 1;
    }
​
    if (!overlay) {
        ALOGE("func_display_overlay_l: NULL overlay");
        return -1;
    }
​
    if (overlay->w <= 0 || overlay->h <= 0) {
        ALOGE("func_display_overlay_l: invalid overlay dimensions(%d, %d)", overlay->w, overlay->h);
        return -1;
    }
​
    switch(overlay->format) {
    case SDL_FCC__AMC: {
        // only ANativeWindow support
        IJK_EGL_terminate(opaque->egl);
        return SDL_VoutOverlayAMediaCodec_releaseFrame_l(overlay, NULL, true);
    }
    case SDL_FCC_RV24:
    case SDL_FCC_I420:
    case SDL_FCC_I444P10LE: {
        // only GLES support
        if (opaque->egl)
            return IJK_EGL_display(opaque->egl, native_window, overlay);
        break;
    }
    case SDL_FCC_YV12:
    case SDL_FCC_RV16:
    case SDL_FCC_RV32: {
        // both GLES & ANativeWindow support
        if (vout->overlay_format == SDL_FCC__GLES2 && opaque->egl)
            return IJK_EGL_display(opaque->egl, native_window, overlay);
        break;
    }
    }
​
    // fallback to ANativeWindow
    IJK_EGL_terminate(opaque->egl);
    return SDL_Android_NativeWindow_display_l(native_window, overlay); 
}