1. 程式人生 > >ffmpeg多輸入濾波處理方式(framesync)

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需要執行如下程式碼:

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. 
//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函式當中。

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的同步機制有詳細的瞭解。

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。然後進行下列同步處理:

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;
}
  1. 從所快取的幀(fs->in[i].frame_next)中提取pts最小的一幀。
  2. 存放到用於提供給濾波器的快取中(fs->in[i].frame = fs->in[i].frame_next)。
  3. 把這一幀所在輸入流幀快取設定為空(fs->in[i].frame_next = NULL)。
  4. 如果這一幀所在的輸入流是同步級別最高的流,則設定frame_ready = 1,表明可以進行濾波處理。
  5. 如果這一幀所在的輸入流不是同步級別最高的流,則需要繼續執行下一迴圈(執行consume_from_fifos)。

以我們前面所展示的圖片為例

image

每次都把frame_next中pts最小的一幀放入frame,這也表示在frame中最後放入的一幀永遠是最大的一幀。當被放入到frame中的幀是屬於最高同步等級的輸入流的時候,可以執行濾波處理。如果我們把這一幀的pts定義為同步pts,此時其餘的輸入流中的幀的pts儘管比同步pts小,不過也是各自輸入流中最大的,這與我們前面所說的同步處理是一致的。

framesync的實現總結來說就是迴圈執行:

  1. 從輸入流中提取幀填補空缺的fs->in[i].frame_next。
  2. 當所有輸入流的fs->in[i].frame_next都被寫入幀後(即fs->in[i].have_next)consume_from_fifos才會返回1,然後進行各個流之間的pts比較。
  3. 接下來把pts最小的幀從fs->in[i].frame_next存入fs->in[i].frame,如此一來該frame_next就會空缺。

這種實現方式能保證所有的幀都是以pts從小到大由frame_next移入frame的,能防止幀被遺漏。