zoukankan      html  css  js  c++  java
  • [ffmpeg] 多输入滤波同步方式(framesync)

    滤波也不总是单一的输入,也存在对多个输入流进行滤波的需求,最常见的就是对视频添加可视水印,水印的组成通常为原视频以及作为水印的图片或者小动画,在ffmpeg中可以使用overlay滤波器进行水印添加。

    对于多视频流输入的滤波器,ffmpeg提供了一个名为framesync的处理方案。framesync为滤波器分担了不同线路的输入的帧同步任务,并为滤波器提供同步过后的帧,使得滤波器专注于滤波处理。

    Extend Mode

    由于各个视频流可能长短不一,可能起始或者结束时间也不同,为了应对由此产生的各种需求,framesync为每个输入流的起始以及结束都提供了3种可选的扩展方式

    Mode before(流开始前) after(流结束后)
    EXT_STOP 在这个流开始前的这段时间不可以进行滤波处理。如果有多个流都指定了before=EXT_STOP,那么以时间线最后的流为准。 在这个流结束后滤波处理必须停止。如果有多个流都指定了after=EXT_STOP,那么以时间线最前的流为准。
    EXT_NULL 其余的流可以在缺少了该流的情况下执行滤波处理。 其余的流可以在缺少了该流的情况下执行滤波处理。
    EXT_INFINITY 在这个流开始前的这段时间,提供这一个流的第一帧给滤波器进行处理。 在这个流结束后的这段时间,提供这一个流的最后一帧给滤波器进行处理。

    Sync

    在framesync所提供的同步服务中,滤波器可以为输入流设置同步等级,同步等级最高的输入流会被当作同步基准。

    image

    如上图所示,不同的输入流可能有不同的帧率,因此有必要对输入的流进行同步。上面的例子中,input stream 1的同步级别最高,因此以该流为同步基准,即每次得到input stream 1的帧时,可以进行滤波处理。滤波处理所提供的帧为各个流最近所获得的帧,在上面的例子中,当input stream 1获得序号为2的帧时,input stream 2刚刚所获得的帧序号为3,input stream 3刚刚所获得的帧序号为1,因此滤波时framesync所提供的帧分别为stream 1的2、stream 2的3、stream 3的1。

    Example

    滤波器调用framesync需要执行如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    typedef struct Context {
        FFFrameSync fs;           //Context involves FFFrameSync
    } Context;
     
    static int process_frame(FFFrameSync *fs)
    {
        Context *s = fs->opaque;
     
        AVFrame *in1, *in2, *in3;
        int ret;
     
        //get frame before filtering
        if ((ret = ff_framesync_get_frame(&s->fs, 0, &in1, 0)) < 0 ||
            (ret = ff_framesync_get_frame(&s->fs, 1, &in2, 0)) < 0 ||
            (ret = ff_framesync_get_frame(&s->fs, 2, &in3, 0)) < 0)
     
        //filtering
    }
     
    //Before filtering, we can only get timebase in function config_output.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    //See avfilter_config_links
    static int config_output(AVFilterLink *outlink)
    {
        FFFrameSyncIn *in;
     
        ret = ff_framesync_init(&s->fs, ctx, 3);          //init framesync
        if (ret < 0)
            return ret;
     
        //set inputs parameter: timebase, sync level, before mode, after mode
        in = s->fs.in;                                   
        in[0].time_base = srclink1->time_base;
        in[1].time_base = srclink2->time_base;
        in[2].time_base = srclink3->time_base;
        in[0].sync   = 2;
        in[0].before = EXT_STOP;
        in[0].after  = EXT_STOP;
        in[1].sync   = 1;
        in[1].before = EXT_NULL;
        in[1].after  = EXT_INFINITY;
        in[2].sync   = 1;
        in[2].before = EXT_NULL;
        in[2].after  = EXT_INFINITY;
     
        //save Context to fs.opaque which will be used on filtering
        s->fs.opaque   = s;
         
        //filtering function
        s->fs.on_event = process_frame;
     
        return ff_framesync_configure(&s->fs);       //framesync configure
    }
     
    static int activate(AVFilterContext *ctx)
    {
        RemapContext *s = ctx->priv;
        return ff_framesync_activate(&s->fs);       //call filtering function if frame ready
    }
     
     
    static av_cold void uninit(AVFilterContext *ctx)
    {
        RemapContext *s = ctx->priv;
     
        ff_framesync_uninit(&s->fs);
    }
     
    static const AVFilterPad remap_inputs[] = {
        {
            .name         = "source 1",
            .type         = AVMEDIA_TYPE_VIDEO,
            .config_props = config_input,
        },
        {
            .name         = "source 2",
            .type         = AVMEDIA_TYPE_VIDEO,
        },
        {
            .name         = "source 3",
            .type         = AVMEDIA_TYPE_VIDEO,
        },
        { NULL }
    };
     
    static const AVFilterPad remap_outputs[] = {
        {
            .name          = "default",
            .type          = AVMEDIA_TYPE_VIDEO,
            .config_props  = config_output,
        },
        { NULL }
    };

    可以发现使用framesync有如下要求:

    1. 在滤波器的参数结构体(Context)内包含FFFramesync结构体。
    2. 在进行滤波处理时,调用ff_framesync_get_frame来获得framesync同步后的帧。
    3. 在config_output时或之前调用ff_framesync_init来进行framesync初始化。
    4. 在config_output时设置各个输入的time base,extend mode,sync level,并调用ff_framesync_configure进行配置。
    5. 在config_output时或之前设置fs->opaque=context(参数结构体),用于后续滤波处理。
    6. 在config_output时或之前设置用于回调的滤波处理函数fs->on_event=process_frame。
    7. 在activate时调用ff_framesync_activate。在该函数内部如果frame ready,就会执行回调函数。

    framesync的同步实现

    framesync的同步实现主要集中在ff_framesync_activate所调用的framesync_advance函数当中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    static int framesync_advance(FFFrameSync *fs)
    {
        while (!(fs->frame_ready || fs->eof)) {
            ret = consume_from_fifos(fs);
            if (ret <= 0)
                return ret;
        }
        return 0;
    }

    framesync_advance内是一个循环,退出该循环需要满足任意如下一个条件:

    • fs->frame_ready==1。代表接下来可以执行滤波处理。
    • fs->eof==1。代表结束整个滤波处理。
    • ret = consume_from_fifos(fs) <= 0。返回值小于0代表出错;返回值等于0代表目前无法都从所有的输入流中得到帧。

    从consume_from_fifos开始分析,我们将会对framesync的同步机制有详细的了解。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    static int consume_from_fifos(FFFrameSync *fs)
    {
        AVFilterContext *ctx = fs->parent;
        AVFrame *frame = NULL;
        int64_t pts;
        unsigned i, nb_active, nb_miss;
        int ret, status;
     
        nb_active = nb_miss = 0;
        for (i = 0; i < fs->nb_in; i++) {
            if (fs->in[i].have_next || fs->in[i].state == STATE_EOF)
                continue;
            nb_active++;
            ret = ff_inlink_consume_frame(ctx->inputs[i], &frame);
            if (ret < 0)
                return ret;
            if (ret) {
                av_assert0(frame);
                framesync_inject_frame(fs, i, frame);
            } else {
                ret = ff_inlink_acknowledge_status(ctx->inputs[i], &status, &pts);
                if (ret > 0) {
                    framesync_inject_status(fs, i, status, pts);
                } else if (!ret) {
                    nb_miss++;
                }
            }
        }
        if (nb_miss) {
            if (nb_miss == nb_active && !ff_outlink_frame_wanted(ctx->outputs[0]))
                return FFERROR_NOT_READY;
            for (i = 0; i < fs->nb_in; i++)
                if (!fs->in[i].have_next && fs->in[i].state != STATE_EOF)
                    ff_inlink_request_frame(ctx->inputs[i]);
            return 0;
        }
        return 1;
    }

    在consume_from_fifos返回1代表目前已经从所有的输入流中获得了帧。

    1. 如果已经从某个输入获得了帧,则不需要再次去获取。
    2. 如果某个输入流还未获得帧,则会调用ff_inlink_comsume_frame尝试从输入link中获取帧。
    3. 如果得到了帧,就会调用framesync_inject_frame把从输入流中获得的帧存放在fs->in[i].frame_next中,并用fs->in[i].have_next表示第i个输入流已经获得了帧。
    4. 如果没有获得帧,则调用ff_inlink_acknowledge_status检查是否出错或者EOF,是则表明该输入流结束,不是则表明前面的滤波器实例无法为我们提供帧。
    5. 由于无法获得我们所需要的帧,因此要调用ff_inlink_request_frame向前面的滤波器实例发出请求。
    6. 只有当从所有的输入流都得到帧后,consume_from_fifos才会返回1。

    consume_from_fifos返回1的时候,所有输入流的帧缓存fs->in[i].frame_next都存储了一帧,该帧缓存标志fs->in[i].have_next的值都为1。然后进行下列同步处理:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    static int framesync_advance(FFFrameSync *fs)
    {
        unsigned i;
        int64_t pts;
        int ret;
     
        while (!(fs->frame_ready || fs->eof)) {
            ret = consume_from_fifos(fs);
            if (ret <= 0)
                return ret;
     
            pts = INT64_MAX;
            for (i = 0; i < fs->nb_in; i++)   //get the least pts frame
                if (fs->in[i].have_next && fs->in[i].pts_next < pts)
                    pts = fs->in[i].pts_next;
            if (pts == INT64_MAX) {
                framesync_eof(fs);
                break;
            }
            for (i = 0; i < fs->nb_in; i++) {
                if (fs->in[i].pts_next == pts ||
                    (fs->in[i].before == EXT_INFINITY &&
                     fs->in[i].state == STATE_BOF)) {
                    av_frame_free(&fs->in[i].frame);
                    fs->in[i].frame      = fs->in[i].frame_next; //move from frame_next to frame
                    fs->in[i].pts        = fs->in[i].pts_next;
                    fs->in[i].frame_next = NULL;
                    fs->in[i].pts_next   = AV_NOPTS_VALUE;
                    fs->in[i].have_next  = 0;
                    fs->in[i].state      = fs->in[i].frame ? STATE_RUN : STATE_EOF;
                    if (fs->in[i].sync == fs->sync_level && fs->in[i].frame)//the highest level frame
                        fs->frame_ready = 1;
                    if (fs->in[i].state == STATE_EOF &&
                        fs->in[i].after == EXT_STOP)
                        framesync_eof(fs);
                }
            }
            if (fs->frame_ready)
                for (i = 0; i < fs->nb_in; i++)
                    if ((fs->in[i].state == STATE_BOF &&
                         fs->in[i].before == EXT_STOP))
                        fs->frame_ready = 0;
            fs->pts = pts;
        }
        return 0;
    }

    这里我们把frame_next当作从上一滤波器实例中获取的帧缓存,frame当作接下来会用于进行滤波处理的帧缓存。

    1. 从所缓存的帧(frame_next)中提取pts最小的一帧。
    2. 存放到用于提供给滤波器的缓存中(frame = frame_next)。
    3. 把这一帧所在输入流帧缓存设置为空(frame_next = NULL)。
    4. 如果这一帧所在的输入流是同步级别最高的流,表明此时在frame中该同步级别最高的流所输入的帧的pts最大,符合我们前面的同步描述,因此设置frame_ready = 1,表明接下来可以进行滤波处理。
    5. 如果这一帧所在的输入流不是同步级别最高的流,则需要继续执行下一循环(执行consume_from_fifos)。

    以我们前面所展示的图片为例

    image

    每次都把frame_next中pts最小的一帧放入frame时,同时也表明在frame中新所放入的一帧永远是pts最大的一帧。当被放入到frame中的帧是属于最高同步等级的输入流的时候,可以执行滤波处理。如果我们把这一帧的pts定义为同步pts,此时其余的输入流中的帧的pts尽管比同步pts小,不过也是各自输入流中最大的,这与我们前面所说的同步处理是一致的。

    framesync的实现总结来说就是循环执行:

    1. 从输入流中提取帧填补空缺的frame_next。
    2. 当所有输入流的frame_next都被写入帧后(即所有输入流的have_next都为1)consume_from_fifos才会返回1,然后进行各个流之间的pts比较。
    3. 接下来把pts最小的帧从frame_next存入frame,如此一来该frame_next又会出现空缺。

    这种实现方式能保证所有的帧都是以pts从小到大由frame_next移入frame的,能防止帧被遗漏。

    参考:

    [1]:https://www.cnblogs.com/TaigaCon/p/10193627.html

  • 相关阅读:
    LeetCode 1110. Delete Nodes And Return Forest
    LeetCode 473. Matchsticks to Square
    LeetCode 886. Possible Bipartition
    LeetCode 737. Sentence Similarity II
    LeetCode 734. Sentence Similarity
    LeetCode 491. Increasing Subsequences
    LeetCode 1020. Number of Enclaves
    LeetCode 531. Lonely Pixel I
    LeetCode 1091. Shortest Path in Binary Matrix
    LeetCode 590. N-ary Tree Postorder Traversal
  • 原文地址:https://www.cnblogs.com/lidabo/p/15410337.html
Copyright © 2011-2022 走看看