音视频-ffplay的音视频同步

news/2024/7/10 21:54:00 标签: 音视频, ffmpeg

最近自己在做一个视频播放器,渲染视频帧时有些疑惑,所以特意来学习一下ffplay中是如何处理视频帧的渲染的!😊

我自己最初的理解是这样

1、只关心视频本身的时间戳,不考虑音视频同步以及其他的同步时钟,或者说以视频帧的时钟为主时钟,那就是渲染一帧视频,然后渲染线程休眠duration时长,再渲染下一帧。

2、视频帧的时钟需要和音频时钟或者其他的主时钟对其,这种情况就会比较复杂一点,每一次渲染都需要和主时钟来检测,当前帧应该立即渲染,延迟还是跳过等等。大致的思路如下:

        获取当前帧的时长,duration;

        然后获取视频时钟与主时钟的差值diff;

        不需要调整的最小阈值sync_min;不需要调整的最大阈值sync_max;小于sync_min时,不需要同步调整,位于sync_min和sync_max之间,需要做调整,大于sync_max时,可能需要跳帧。这个比较还需要考虑本身视频帧的duration,同时还需要从diff的

正负,也就是当前视频始终比主时钟提前还是滞后来判断。

现在来看看ffplay中,是如何来实现这个逻辑的。

ffplay定义了几个阈值如下:

/* no AV sync correction is done if below the minimum AV sync threshold */
#define AV_SYNC_THRESHOLD_MIN 0.04
/* AV sync correction is done if above the maximum AV sync threshold */
#define AV_SYNC_THRESHOLD_MAX 0.1
/* If a frame duration is longer than this, it will not be duplicated to compensate AV sync */
#define AV_SYNC_FRAMEDUP_THRESHOLD 0.1
/* no AV correction is done if too big error */
#define AV_NOSYNC_THRESHOLD 10.0

 比较清晰,从注释就可以看出来含义,不用过多说明。

现在从代码来进入正题,在ffplay.c中有个video_refresh(void *opaque, double *remaining_time),第一个参数就是VideoState,里面保存了播放中的所有数据和状态,第二个可以看到,传入的是一个double指针,这个值在当前修改之后,其值会保留,就是渲染下一帧之前的休眠时长。

static void video_refresh(void *opaque, double *remaining_time) {
    VideoState *is = opaque;
    ...

    // lastvp和vp都是AVFrame;
    // lastvp是上一次展示的视频帧,vp是当前即将展示的视频帧;
    if (lastvp->serial != vp->serial) {
        is->frame_timer = av_gettime_relative() / 1000000.0;
    }

    ...
    // 计算当前帧的时长;
    last_duration = vp_duration(is, lastvp, vp);
    // 计算当前帧的实际展示时长,这个里面是时钟对齐的主要逻辑;
    delay = compute_target_delay(last_duration, is);

    // 这之后就是记录视频展示的时间戳以及需要的休眠时间;
    time = av_gettime_relative() / 1000000.0;
    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;
    }

    ...
    // display picture;
    video_display(is);
    ...
}

正常情况下的主要流程就是以上,重点看看vp_duration和compute_target_delay这两个函数的细节。

首先是vp_duration:

static double vp_duration(VideoState *is, Frame *vp, Frame *nextvp) {
    if (vp->serial == nextvp->serial) {
        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;
    }
}

这个函数比较简单,两个Frame的serial正常情况下都是相等的,所以这个条件先跳过。使用当前帧与后一帧的差值来作为当前帧的展示时长,如果这个差值异常,就是用当前帧的duration。由于当前帧的duration是在解码之后算出来的,所以在能获取下一帧的情况下,优先使用这个差值。

接下来是compute_target_delay:

static double compute_target_delay(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));
        if (!isnan(diff) && fabs(diff) < is->max_frame_duration) {
            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;
        }
    }

    av_log(NULL, AV_LOG_TRACE, "video: delay=%0.3f A-V=%f\n",
            delay, -diff);

    return delay;
}

第一个参数delay实际上是前一步得到的当前帧的展示时长,以此为基础来计算具体的实际展示时长。 

1、如果视频时钟作为主时钟时,就直接返回当前delay;

2、视频时钟不是主时钟,就算当前时钟与主时钟的差值diff,然后开始来判断如何同步;

3、以已经定义同步调整上下限为区域,来确定当前要进行同步的阈值,sync_threshold;

4、max_frame_duration是当前视频帧连续的时长阈值,如果diff超过了这个范围,说明此时已经的时间戳已经不连续,此时不属于连续播放,所以不考虑这个情况。接下来的判断可以用下图来表示了:

 

 1、第一个判断,diff属于上图中1的范围,此时,diff+delay应该是在图中3和4的范围,在3范围时,说明当前视频时钟加上当前帧的展示时长仍后落后于主时钟,所以当前帧应该立即展示;如果在4范围,则说明当前帧的展示需要延长一段时间。

2、第一个条件不满足时,diff落在图中2的范围,并且当前帧的展示时长大于AV_SYNC_FRAMEDUP_THRESHOLD,表示此时不会通过复制当前帧来做同步操作,所以直接将diff+delay作为当前帧的实际展示时长。

3、最后一种情况,是当前帧的展示时长不大于AV_SYNC_FRAMEDUP_THRESHOLD,将展示时长的2倍作为延迟,这种情况相当于是复制了当前帧吧。

上面这个方法,主要是将视频时钟与当前主时钟的同步操作,然后还需要计算与前一次视频帧的渲染时间来确定当前帧的渲染已经线程需要需要休眠的时间。

time = av_gettime_relative() / 1000000.0;
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->serial);
SDL_UnlockMutex(is->pictq.mutex);

...

frame_queue_next(&is->pictq);

再回头来看以上代码,已知delay是前面计算完成的当前帧实际展示时长,首先time,是获取当的相对时间,is->frame_timer是上一次画面更新的相对时间。

所以第一个判断的逻辑的意义就是,上一次画面更新时间+当前帧时长超过了这个当前时间,表示当前应该展示当前帧,所以调用了直接展示当前帧的逻辑,同时也更新了线程的休眠时长。

如果以上判断不成立,就把当前帧的展示时长累加到frame_timer中,接下来的判断就是,累加当前帧时长之后,仍然没有达到当前时间,并且差值大于同步调整的最大阈值,就将当前的时间作为frame_timer,然后展示当前帧。

然后更新视频帧展示的时钟数据,并且将视频帧队列往后推进,绘制画面。

另外,看一下上面代码中vp和lastvp的来历。

lastvp = frame_queue_peek_last(&is->pictq);
vp = frame_queue_peek(&is->pictq);

// function implementations;
static Frame *frame_queue_peek_last(FrameQueue *f) {
    return &f->queue[f->rindex];
}

static Frame *frame_queue_peek(FrameQueue *f) {
    return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
}

可以看到,在rindex_shown为0的时候,vp和lastvp其实是同一个位置,在ffplay中这个rindex_shown在队列执行第一次next的时候,会被置为1,并且一直保持这个值。代码中也有使用lastvp、vp、nextvp(索引位置是(f->rindex + f->rindex_shown) + 1)来判断的逻辑,我个人的理解,这样来判断帧与帧之间的关系应该会更可靠一点吧。

以上方法是ffplay作者的思路,也给了我一些启发,从注释里面可以看出来作者其实也有一些疑惑的地方,当然我的疑惑可能比作者更多一点,但大体思路应该是类似,所以我要准备自己去实现一下这个东西,不知道会遇到点什么东西🤔。

如果有幸被大佬们看到,也希望指点一二,感激不尽🫡。


http://www.niftyadmin.cn/n/213928.html

相关文章

c语言引入另一个.c文件

目录结构 a.c #include <stdio.h>/*外部变量声明*/ extern int x ; extern int y ; int addtwonum() {return xy; } test.c #include <stdio.h> #include <windows.h>//解决中文乱码 #include "a.c" /*定义两个全局变量*/ int x1; int y2; int …

springboot、@Async实现异步调用

以注册送积分模拟异步调用 启动类添加注解 EnableAsync SpringBootApplication EnableAsync public class SpringbootTestApplication {public static void main(String[] args) {SpringApplication.run(SpringbootTestApplication.class, args);} } 被调用的类或方法添加注…

uniapp - 实现车牌号键盘与格子间隔显示组件,汽车牌照录入支持自定义样式、新能源等(附带组件完整源码,开箱即用,稍微改改就能用)

效果图 uniapp 全平台兼容,车牌号键盘输入、分格显示功能示例源码,注释很多! 可以直接复制一下,然后自己改改样式或功能就能使了。 示例源码 复制,运行。 &

Springboot应用整合Sentinel实现限流、熔断、降级笔记

可以使用官方的sentinel 也可使用进行持久化改造后的Sentinel &#xff0c;本文基于此进行记录。Sentinel持久化到Nacoshttps://blog.csdn.net/chenjian723122704/article/details/130098896 Sentinel版本为 1.8.6 Nacos版本为 2.2.0 关于网关限流、限流和熔断&#xff0c;我…

JAVA SMART系统-系统框架设计与开发

SMART系统是一个新型智能在线考试信息管理系统&#xff0c;该系统主要实现了学生在线考试与评估以及教师对学生在线考试信息的管理和维护。本文按照SMART系统的非功能性需求&#xff0c;基于Struts、Spring、Hibernate三种开源技术&#xff0c;构建了一个具有良好的可扩展性、可…

PMO在企业项目管理中起到的重要性

在现代企业中&#xff0c;项目管理助力企业在激烈的市场竞争中获得成功&#xff0c;而这需要一个专业化的项目管理办公室&#xff08;PMO&#xff09;来确保项目的顺利实施。在企业项目管理中&#xff0c;PMO扮演着至关重要的角色&#xff0c;下文我们将对其的重要作用进行探讨…

2023年全国最新保安员精选真题及答案47

百分百题库提供保安员考试试题、保安职业资格考试预测题、保安员考试真题、保安职业资格证考试题库等&#xff0c;提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻松过关。 471.机动车在消防栓或者消防队(站)门前&#xff08;&#xff09;米以内的路段&#x…

Android添加开机启动服务程序关于Selinux权限问题说明

(619条消息) Android7.1添加开机启动服务程序关于Selinux权限问题说明_平仄散人的博客-CSDN博客 service xxxx /system/bin/xxxx class main user root group root oneshot seclabel u:r:xxxx:s0 #这句是为加selinux权限添加的&#xff0c;android5.1以后不加则无法启动该服务…