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所提供的同步服務中,濾波器可以為輸入流設定同步等級,同步等級最高的輸入流會被當作同步基準。
如上圖所示,不同的輸入流可能有不同的幀率,因此有必要對輸入的流進行同步。上面的例子中,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有如下要求:
- 在濾波器的引數結構體(Context)內包含FFFramesync結構體。
- 在進行濾波處理時,呼叫ff_framesync_get_frame來獲得framesync同步後的幀。
- 在config_output時或之前呼叫ff_framesync_init來進行framesync初始化。
- 在config_output時設定各個輸入的time base,extend mode,sync level,並呼叫ff_framesync_configure進行配置。
- 在config_output時或之前設定fs->opaque=context(引數結構體),用於後續濾波處理。
- 在config_output時或之前設定用於回撥的濾波處理函式fs->on_event=process_frame。
- 在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代表目前已經從所有的輸入流中獲得了幀。
- 如果已經從某個輸入獲得了幀,則不需要再次去獲取。
- 如果某個輸入流還未獲得幀,則會呼叫ff_inlink_comsume_frame嘗試從輸入link中獲取幀。
- 如果得到了幀,就會呼叫framesync_inject_frame把從輸入流中獲得的幀存放在fs->in[i].frame_next中,並用fs->in[i].have_next表示第i個輸入流已經獲得了幀。
- 如果沒有獲得幀,則呼叫ff_inlink_acknowledge_status檢查是否出錯或者EOF,是則表明該輸入流結束,不是則表明前面的濾波器例項無法為我們提供幀。
- 由於無法獲得我們所需要的幀,因此要呼叫ff_inlink_request_frame向前面的濾波器例項發出請求。
- 只有當從所有的輸入流都得到幀後,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; }
- 從所快取的幀(fs->in[i].frame_next)中提取pts最小的一幀。
- 存放到用於提供給濾波器的快取中(fs->in[i].frame = fs->in[i].frame_next)。
- 把這一幀所在輸入流幀快取設定為空(fs->in[i].frame_next = NULL)。
- 如果這一幀所在的輸入流是同步級別最高的流,則設定frame_ready = 1,表明可以進行濾波處理。
- 如果這一幀所在的輸入流不是同步級別最高的流,則需要繼續執行下一迴圈(執行consume_from_fifos)。
以我們前面所展示的圖片為例
每次都把frame_next中pts最小的一幀放入frame,這也表示在frame中最後放入的一幀永遠是最大的一幀。當被放入到frame中的幀是屬於最高同步等級的輸入流的時候,可以執行濾波處理。如果我們把這一幀的pts定義為同步pts,此時其餘的輸入流中的幀的pts儘管比同步pts小,不過也是各自輸入流中最大的,這與我們前面所說的同步處理是一致的。
framesync的實現總結來說就是迴圈執行:
- 從輸入流中提取幀填補空缺的fs->in[i].frame_next。
- 當所有輸入流的fs->in[i].frame_next都被寫入幀後(即fs->in[i].have_next)consume_from_fifos才會返回1,然後進行各個流之間的pts比較。
- 接下來把pts最小的幀從fs->in[i].frame_next存入fs->in[i].frame,如此一來該frame_next就會空缺。
這種實現方式能保證所有的幀都是以pts從小到大由frame_next移入frame的,能防止幀被遺漏。