前面幾篇文章聊了聊FFmpeg的基礎知識,我也是接觸FFmpeg不久,除了時間處理之外,很多高深(濾鏡)操作都沒接觸到。在學習時間處理的時候,都是通過在ffmpeg目前提供的avfilter基礎上面修修補補(補充各種debug log)來驗證想法。 而這次我將嘗試新創建一個avfilter,來實現一個新濾鏡。 完整的代碼可參考 https://andy-zhangtao.github.io/ffmpeg-examples/
因為我是新手,所以本着先易后難的原則(其實是不會其它高深API的操作),從fade濾鏡入手來仿制一個new fade(就起名叫做ifade)。
目標
fade
是一個淡入淡出的濾鏡,可以通過參數設置fade type(in表示淡入, out表示淡出),在視頻的頭部和尾部添加淡入淡出效果。 在使用過程中,fade有一些使用限制。
- 淡入只能從片頭開始設置(00:00:00.0位置起)
- 淡出只能從片尾開始設置
- 一次只能設置一個類型
如果想在一個視頻中間設置多次淡入淡出效果,那么只能先分割視頻,分別應該fade之后在合並(可能還有其它方式,可我沒找到)。如果想一次實現多個fade效果,那么就要通過-filter-complex來組合多個fade,並合理安排調用順序,稍顯麻煩。
這次,ifade就嘗試支持在同一個視頻中實現多次fade效果。ifade計划完成的目標是:
- 一次支持設置一個類型(淡入/淡出)
- 一次支持設置多個fade時間點
- 支持fade時長
分析
先看看原版fade
是如何實現的。
1 static int filter_frame(AVFilterLink *inlink, AVFrame *frame)
2 {
3 AVFilterContext *ctx = inlink->dst;
4 FadeContext *s = ctx->priv;
5 double frame_timestamp = frame->pts == AV_NOPTS_VALUE ? -1 : frame->pts * av_q2d(inlink->time_base);
6
7 // Calculate Fade assuming this is a Fade In
8 if (s->fade_state == VF_FADE_WAITING) {
9 s->factor=0;
10 if (frame_timestamp >= s->start_time/(double)AV_TIME_BASE
11 && inlink->frame_count_out >= s->start_frame) {
12 // Time to start fading
13 s->fade_state = VF_FADE_FADING;
14
15 // Save start time in case we are starting based on frames and fading based on time
16 if (s->start_time == 0 && s->start_frame != 0) {
17 s->start_time = frame_timestamp*(double)AV_TIME_BASE;
18 }
19
20 // Save start frame in case we are starting based on time and fading based on frames
21 if (s->start_time != 0 && s->start_frame == 0) {
22 s->start_frame = inlink->frame_count_out;
23 }
24 }
25 }
26 if (s->fade_state == VF_FADE_FADING) {
27 if (s->duration == 0) {
28 // Fading based on frame count
29 s->factor = (inlink->frame_count_out - s->start_frame) * s->fade_per_frame;
30 if (inlink->frame_count_out > s->start_frame + s->nb_frames) {
31 s->fade_state = VF_FADE_DONE;
32 }
33
34 } else {
35 // Fading based on duration
36 s->factor = (frame_timestamp - s->start_time/(double)AV_TIME_BASE)
37 * (float) UINT16_MAX / (s->duration/(double)AV_TIME_BASE);
38 if (frame_timestamp > s->start_time/(double)AV_TIME_BASE
39 + s->duration/(double)AV_TIME_BASE) {
40 s->fade_state = VF_FADE_DONE;
41 }
42 }
43 }
44 if (s->fade_state == VF_FADE_DONE) {
45 s->factor=UINT16_MAX;
46 }
47
48 s->factor = av_clip_uint16(s->factor);
49
50 // Invert fade_factor if Fading Out
51 if (s->type == FADE_OUT) {
52 s->factor=UINT16_MAX-s->factor;
53 }
54
55 if (s->factor < UINT16_MAX) {
56 if (s->alpha) {
57 ctx->internal->execute(ctx, filter_slice_alpha, frame, NULL,
58 FFMIN(frame->height, ff_filter_get_nb_threads(ctx)));
59 } else if (s->is_packed_rgb && !s->black_fade) {
60 ctx->internal->execute(ctx, filter_slice_rgb, frame, NULL,
61 FFMIN(frame->height, ff_filter_get_nb_threads(ctx)));
62 } else {
63 /* luma, or rgb plane in case of black */
64 ctx->internal->execute(ctx, filter_slice_luma, frame, NULL,
65 FFMIN(frame->height, ff_filter_get_nb_threads(ctx)));
66
67 if (frame->data[1] && frame->data[2]) {
68 /* chroma planes */
69 ctx->internal->execute(ctx, filter_slice_chroma, frame, NULL,
70 FFMIN(frame->height, ff_filter_get_nb_threads(ctx)));
71 }
72 }
73 }
74
75 return ff_filter_frame(inlink->dst->outputs[0], frame);
76 }
不想貼代碼,但發現不貼代碼好像很難表述清楚。-_-!
fade
在處理fame時最關鍵的是三種狀態和一個變量因子。
三種狀態:
- VF_FADE_WAITING 待渲染, 初始狀態
- VF_FADE_FADING 渲染中
- VF_FADE_DO 渲染結束
變量因子:
- factor 控制效果強度
假設現在設置的是淡入效果(如果是淡出效果,52行會實現一個反轉)): s->fade_state
初始化狀態是VF_FADE_WAITING
,濾鏡工作時就會進入第八行的判斷,此時將s->factor
設置為0。如果我們假設淡入的背景顏色是黑色(默認色),當s->factor==0
時,渲染強度最大,此時渲染出的就是一個純黑的畫面。
第八行的if判斷是一個全局初始化,一旦進入之后,s->fade_status
就會被修改為VF_FADE_FADING
狀態。
而26到43行的判斷,是為了找到渲染結束時間點。通過不停的判斷每幀的frame_timestamp和start_time+duration之間的關系(通過start_frame同理),來決定是否結束渲染。start_time
是由fade st=xxx
來設定的,當到達結束時間點后,將s->fade_status
變更為VF_FADE_DO,即可結束渲染(其實是將s->factor
置為UINT16-MAX,這樣就不會進入到第55行的渲染邏輯)。
fade大致的處理流程如下:
+------------------------------------------------------------------------------------------------------------- +
| |
| |----------------------------------------------------------|------------------|--------------------> |
|time 0 st st+duration |
| |
|status VF_FADE_WAITING |
| VF_FADE_FADING |
| VF_FADE_DO |
|factor 0 0 0 0 0 0 100 500 4000 ... 65535 65535 65535 65535|
| |
+--------------------------------------------------------------------------------------------------------------+
在0->st
這段時間內,status一直是VF_FADE_FADING狀態,factor是0。 這段時間內渲染出來的全是黑色。到達st點后,開始逐步調整factor的值(不能一次性的調整到UINT16-MAX,要不就沒有逐漸明亮的效果了),直到st+duration
這個時間后,在將factor
調整為UINT16-MAX。以后流經fade的幀就原樣流轉到ff_filter_frame
了。
改造
分析完fade
的處理邏輯之后,如果要實現ifade
的效果,那么應該是下面的流程圖:
+------------------------------------------------------------------------------------------------------------------+
| A B C D |
| |-----------------------------|------------------|----------------|------------------|-------------------->|
|time 0 st1 st2-duration st2 st2+duration |
| |
|status VF_FADE_FADING |
| VF_FADE_DO |
| |
| VF_FADE_FADING |
| VF_FADE_DO |
|factor 0 0 0 65535 65535 0 0 0 0 0 0 0 0 100 500 4000 ... 65535 |
| |
+------------------------------------------------------------------------------------------------------------------+
從0-A點
仍然是fade
原始邏輯。到達A點之后,將s->fade_status
改完VF_FADE_DO
表示關閉渲染。 當到達B點時(距離st2還有duration的時間點),開始將s->factor
調整為0. 這是為了模擬出畫面從暗到亮的效果。同時s->fade_status
再次置為VF_FADE_FADING
狀態,到達C點是開始重新計算s->factor
的值,將畫面逐漸變亮。
可以看出ifade
就是利用s->fade_status
重復利用現有的處理邏輯來實現多次淡入的效果。
實現
上面分析完之后,就可以動手寫代碼了。 具體代碼就不貼出來了,可以直接看源碼。 下面就說幾個在ffmpeg 4.x中需要注意的地方:
-
添加新avfilter
- 在
libavfilter/Makefile
中添加新filter名稱。OBJS-$(CONFIG_IFADE_FILTER) += vf_ifade.o
- 在
libavfilter/allfilter.c
中添加新filter.extern AVFilter ff_vf_ifade
- 在
-
重新生成makefile
- 重新根據實際情況執行
configure
,生成最新的makefile腳本
- 重新根據實際情況執行
然后就是漫長的等待。
在編寫filter時,ffmpeg提供了AVFILTER_DEFINE_CLASS
這個宏來生成默認的avclass
和options
,所以一定要注意class名稱和options名稱要和宏定義中的名字保持一致,否則會導致編譯失敗。