From patchwork Sat Jun 25 09:57:52 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Aman Karmani X-Patchwork-Id: 36443 Delivered-To: ffmpegpatchwork2@gmail.com Received: by 2002:a05:6a20:8b27:b0:88:1bbf:7fd2 with SMTP id l39csp826780pzh; Sat, 25 Jun 2022 03:01:37 -0700 (PDT) X-Google-Smtp-Source: AGRyM1ssVv8z1qnwxE3QKm1jpKXD91DhkYxG08sPo+ZrVaWMYXxQhpuEWOl+3qUuL0550IJ+AqpF X-Received: by 2002:a17:907:2d1f:b0:726:320d:3e0 with SMTP id gs31-20020a1709072d1f00b00726320d03e0mr3247212ejc.238.1656151296884; Sat, 25 Jun 2022 03:01:36 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1656151296; cv=none; d=google.com; s=arc-20160816; b=kAi/spkFO+x5TWfUCP1jvKU96J+U/Xr/IYt6717aYvai1O/Mw59zefN64yJcg4PIJr 9BBfC4KQCFEaNqyd4Qmp7q0UQfMBTRvh06bb8o3Hkl5Wj3w85sCPOihqQCQqefBdqJAk TOtf5vOw3DOpbaX8D+++K43Fan5LpaU+oIpZ+U8cBWU0HrFHI4VukcCpE+/l3f+QQqkz AkwDTN8V0loIHLhx+IVCtuAUNYEPebtElali87oI/UWE+e1gO7Vy3nccJr6QrMo2SbNj CfdK4NKf7+M1BW+oyjr+CgLzg1TYrZyv7NjFjazd7IS9Me8hKQ6Z8ooG6JLiIUhYmHXu WVPQ== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816; h=sender:errors-to:content-transfer-encoding:cc:reply-to :list-subscribe:list-help:list-post:list-archive:list-unsubscribe :list-id:precedence:subject:to:mime-version:fcc:date:references :in-reply-to:message-id:from:dkim-signature:delivered-to; bh=tpV9E7zc43IJJ/XdiWF3WhtrV3vBm2JI9WZuPz+tgek=; b=LR92s9WUZPCy2teWTMr/p8SyI9fD7PoZhFKzV8kvvyf2CWl3R7NdKlfmvONKhHOvq3 7lQlJ20jLRqIFFArtQx8bIpeYaCLLPYZ2ammmTRmXla+6S4LNQUv5bGj/gBTYbC/CShG u5tubUpLE3dyHF7tgcRWYyWsJDA87x4R3iwPgu5zxYH0an+EH9rov2V2v1qMjbv19PLJ q1XXErfdh9nzzQP52Pj+UOmQvg2QwF2A7wSWPyOxtEC/nOxmhrRy6XjSnzaJfZQYD10W 5atoZeoTjNnuh1wUCWBHrgcpJMPedyqarbajkkagvt7TnTMufbrVFEEPtuhrDIA5FpjN tMWQ== ARC-Authentication-Results: i=1; mx.google.com; dkim=neutral (body hash did not verify) header.i=@gmail.com header.s=20210112 header.b=gbDiPItq; spf=pass (google.com: domain of ffmpeg-devel-bounces@ffmpeg.org designates 79.124.17.100 as permitted sender) smtp.mailfrom=ffmpeg-devel-bounces@ffmpeg.org; dmarc=fail (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com Return-Path: Received: from ffbox0-bg.mplayerhq.hu (ffbox0-bg.ffmpeg.org. [79.124.17.100]) by mx.google.com with ESMTP id ho13-20020a1709070e8d00b00722ebd0d050si3847738ejc.990.2022.06.25.03.01.36; Sat, 25 Jun 2022 03:01:36 -0700 (PDT) Received-SPF: pass (google.com: domain of ffmpeg-devel-bounces@ffmpeg.org designates 79.124.17.100 as permitted sender) client-ip=79.124.17.100; Authentication-Results: mx.google.com; dkim=neutral (body hash did not verify) header.i=@gmail.com header.s=20210112 header.b=gbDiPItq; spf=pass (google.com: domain of ffmpeg-devel-bounces@ffmpeg.org designates 79.124.17.100 as permitted sender) smtp.mailfrom=ffmpeg-devel-bounces@ffmpeg.org; dmarc=fail (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com Received: from [127.0.1.1] (localhost [127.0.0.1]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTP id C2C8A68B88F; Sat, 25 Jun 2022 12:58:32 +0300 (EEST) X-Original-To: ffmpeg-devel@ffmpeg.org Delivered-To: ffmpeg-devel@ffmpeg.org Received: from mail-pl1-f179.google.com (mail-pl1-f179.google.com [209.85.214.179]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTPS id 0980268B884 for ; Sat, 25 Jun 2022 12:58:27 +0300 (EEST) Received: by mail-pl1-f179.google.com with SMTP id l6so4089311plg.11 for ; Sat, 25 Jun 2022 02:58:27 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=from:message-id:in-reply-to:references:date:subject:fcc :content-transfer-encoding:mime-version:to:cc; bh=78dtz2z6+bvIEi9EVHal5Gq+U8k1KrjZ0pGzhLnW8jc=; b=gbDiPItqQzr4wfDlOenwBV1D9gIwquZALhe9jiEDyMJyT7b97WyDCP8syvLdJqJQFI qlxMxZWhVBevUns5Q/YBZGtjrDvTWMP7JO5mNRZNowRT6uEmZhglR0UMnoGfqEXR6jU0 I3QhcBNUmF32ZGGATJ1g1pg05HvXImeSGzAzelXVHqVxafii/LnSzhbcBWT+tp19AWC6 d7BcgQCpyw06ilLKH1E/Gtn2ARV5uouB3UsWV+CyjT6VDowgvvxmdafPs7CnxVgMOyMC DHkwMgnt+dt68Mh2/VZunme/yWzhhtnHWPPJn4TbhO5ba7E+Y+8PmvvfuNlHHnw7qe4S 5Vrw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:from:message-id:in-reply-to:references:date :subject:fcc:content-transfer-encoding:mime-version:to:cc; bh=78dtz2z6+bvIEi9EVHal5Gq+U8k1KrjZ0pGzhLnW8jc=; b=asC3nO27Mp8+0R7jAUQ0E5Jh3YseDOC61tHWyZGur0y9QHvKy/hW+dSciVj4wwfdQR RaBfZcB5WHfpOTHaoU2Gz2Hf9tEIaAtFO95H9Y9YXRiUspWoOyats7/KnterAXK8kf17 uZaGP6goMdKI1HRazOHLMKFkKJK0FASX8ACJDXk7gNQh/IORNnmtA6Nq0V7H9GBftcB4 GCIhtpIDTRkgjkCvdUud50EoOSIf3YIw5S91/BCqu9VS/LlnLeFbTG0QfsySxDu+kkbw aeacMJ4fcSzF0Dx0qIqRx2fCDczarnjnlr2oH9Ma2oYawVhHZm3kHUloMeRodJV7q8Y9 7TDQ== X-Gm-Message-State: AJIora/TuckYGy1UNHpMCFyL2g1JZcldRQuOliJYAP00CfGv9PiPyBBP z00jH2AK5256FP3lRFxbUfD/Vb65B/y+mA== X-Received: by 2002:a17:902:ab96:b0:16a:6db6:2715 with SMTP id f22-20020a170902ab9600b0016a6db62715mr3714928plr.141.1656151106221; Sat, 25 Jun 2022 02:58:26 -0700 (PDT) Received: from [127.0.0.1] (master.gitmailbox.com. [34.83.118.50]) by smtp.gmail.com with ESMTPSA id m13-20020a170902e40d00b00167838b82e0sm3256340ple.205.2022.06.25.02.58.25 (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); Sat, 25 Jun 2022 02:58:25 -0700 (PDT) From: softworkz X-Google-Original-From: softworkz Message-Id: <88e8adb88992d624922165ae92f8d3347555b70d.1656151077.git.ffmpegagent@gmail.com> In-Reply-To: References: Date: Sat, 25 Jun 2022 09:57:52 +0000 Fcc: Sent MIME-Version: 1.0 To: ffmpeg-devel@ffmpeg.org Subject: [FFmpeg-devel] [PATCH v5 20/25] avfilter/subfeed: add subtitle feed filter X-BeenThere: ffmpeg-devel@ffmpeg.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: FFmpeg development discussions and patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: FFmpeg development discussions and patches Cc: Michael Niedermayer , softworkz , Andriy Gelman , Andreas Rheinhardt Errors-To: ffmpeg-devel-bounces@ffmpeg.org Sender: "ffmpeg-devel" X-TUID: 6Z0r59Pg3EFH From: softworkz Signed-off-by: softworkz --- libavfilter/Makefile | 1 + libavfilter/allfilters.c | 1 + libavfilter/sf_subfeed.c | 412 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 414 insertions(+) create mode 100644 libavfilter/sf_subfeed.c diff --git a/libavfilter/Makefile b/libavfilter/Makefile index 0f43937205..780e5333a0 100644 --- a/libavfilter/Makefile +++ b/libavfilter/Makefile @@ -583,6 +583,7 @@ OBJS-$(CONFIG_CENSOR_FILTER) += sf_textmod.o OBJS-$(CONFIG_SHOW_SPEAKER_FILTER) += sf_textmod.o OBJS-$(CONFIG_SPLITCC_FILTER) += sf_splitcc.o OBJS-$(CONFIG_STRIPSTYLES_FILTER) += sf_stripstyles.o +OBJS-$(CONFIG_SUBFEED_FILTER) += sf_subfeed.o OBJS-$(CONFIG_SUBSCALE_FILTER) += sf_subscale.o OBJS-$(CONFIG_TEXTMOD_FILTER) += sf_textmod.o diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c index 6792665730..ff56661b67 100644 --- a/libavfilter/allfilters.c +++ b/libavfilter/allfilters.c @@ -575,6 +575,7 @@ extern const AVFilter ff_sf_graphicsub2text; extern const AVFilter ff_sf_showspeaker; extern const AVFilter ff_sf_splitcc; extern const AVFilter ff_sf_stripstyles; +extern const AVFilter ff_sf_subfeed; extern const AVFilter ff_sf_subscale; extern const AVFilter ff_sf_textmod; diff --git a/libavfilter/sf_subfeed.c b/libavfilter/sf_subfeed.c new file mode 100644 index 0000000000..3227861ac0 --- /dev/null +++ b/libavfilter/sf_subfeed.c @@ -0,0 +1,412 @@ +/* + * Copyright (c) 2021 softworkz + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * @file + * subtitle filter for feeding subtitle frames into a filtergraph in a contiguous way + * + * + * also supports + * - duration fixup + * delaying a subtitle event with unknown duration and infer duration from the + * start time of the subsequent subtitle + * - scattering + * splitting a subtitle event with unknown duration into multiple ones with + * a short and fixed duration + * + */ + +#include "filters.h" +#include "libavutil/opt.h" +#include "subtitles.h" +#include "libavutil/avassert.h" + +enum SubFeedMode { + FM_REPEAT, + FM_SCATTER, + FM_FORWARD, +}; + +typedef struct SubFeedContext { + const AVClass *class; + enum AVSubtitleType format; + enum SubFeedMode mode; + + AVRational frame_rate; + int fix_durations; + int fix_overlap; + + int current_frame_isnew; + int eof; + int got_first_input; + int need_frame; + int64_t next_pts_offset; + int64_t recent_subtitle_pts; + + int64_t counter; + + /** + * Queue of frames waiting to be filtered. + */ + FFFrameQueue fifo; + +} SubFeedContext; + +static int64_t ms_to_avtb(int64_t ms) +{ + return av_rescale_q(ms, (AVRational){ 1, 1000 }, AV_TIME_BASE_Q); +} + +static int64_t avtb_to_ms(int64_t avtb) +{ + return av_rescale_q(avtb, AV_TIME_BASE_Q, (AVRational){ 1, 1000 }); +} + +static int init(AVFilterContext *ctx) +{ + SubFeedContext *s = ctx->priv; + + ff_framequeue_init(&s->fifo, NULL); + + return 0; +} + +static void uninit(AVFilterContext *ctx) +{ + SubFeedContext *s = ctx->priv; + ff_framequeue_free(&s->fifo); +} + +static int config_input(AVFilterLink *link) +{ + ////const subfeedContext *context = link->dst->priv; + + return 0; +} + +static int query_formats(AVFilterContext *ctx) +{ + AVFilterFormats *formats; + AVFilterLink *inlink0 = ctx->inputs[0]; + AVFilterLink *outlink0 = ctx->outputs[0]; + static const enum AVSubtitleType subtitle_fmts[] = { AV_SUBTITLE_FMT_BITMAP, AV_SUBTITLE_FMT_ASS, AV_SUBTITLE_FMT_NB }; + int ret; + + formats = ff_make_format_list(subtitle_fmts); + + if ((ret = ff_formats_ref(formats, &inlink0->outcfg.formats)) < 0) + return ret; + + if ((ret = ff_formats_ref(formats, &outlink0->incfg.formats)) < 0) + return ret; + + return 0; +} + +static int config_output(AVFilterLink *outlink) +{ + SubFeedContext *s = outlink->src->priv; + const AVFilterLink *inlink = outlink->src->inputs[0]; + + outlink->time_base = AV_TIME_BASE_Q; + outlink->format = inlink->format; + outlink->w = inlink->w; + outlink->h = inlink->h; + + if (s->mode == FM_FORWARD) + outlink->frame_rate = (AVRational) { 1, 0 }; + else + outlink->frame_rate = s->frame_rate; + + return 0; +} + +static int request_frame(AVFilterLink *outlink) +{ + SubFeedContext *s = outlink->src->priv; + AVFilterLink *inlink = outlink->src->inputs[0]; + int64_t last_pts = outlink->current_pts; + int64_t next_pts; + int64_t interval = ms_to_avtb((int64_t)(av_q2d(av_inv_q(outlink->frame_rate)) * 1000)); + AVFrame *out; + int status; + + if (s->mode == FM_FORWARD) + return ff_request_frame(inlink); + + s->counter++; + if (interval == 0) + interval = ms_to_avtb(200); + + status = ff_outlink_get_status(inlink); + if (status == AVERROR_EOF) + s->eof = 1; + + if (s->eof) + return AVERROR_EOF; + + if (!s->got_first_input && inlink->current_pts != AV_NOPTS_VALUE) { + + s->got_first_input = 1; + next_pts = av_rescale_q(inlink->current_pts, inlink->time_base, AV_TIME_BASE_Q); + if (next_pts < last_pts) + next_pts = last_pts + interval; + + } else if (last_pts == AV_NOPTS_VALUE) + next_pts = av_rescale_q(inlink->current_pts, inlink->time_base, AV_TIME_BASE_Q); + else + next_pts = last_pts + interval; + + if (next_pts == AV_NOPTS_VALUE) + next_pts = 0; + + if (s->next_pts_offset) { + av_log(outlink->src, AV_LOG_VERBOSE, "Subtracting next_pts_offset: %"PRId64" \n", s->next_pts_offset); + next_pts -= s->next_pts_offset; + s->next_pts_offset = 0; + } + +retry: + if (ff_framequeue_queued_frames(&s->fifo) && !s->current_frame_isnew) { + + const AVFrame *current_frame = ff_framequeue_peek(&s->fifo, 0); + const int64_t sub_end_time = current_frame->subtitle_timing.start_pts + current_frame->subtitle_timing.duration; + + if (ff_framequeue_queued_frames(&s->fifo) > 1) { + const AVFrame *next_frame = ff_framequeue_peek(&s->fifo, 1); + if (next_pts + interval > next_frame->subtitle_timing.start_pts) { + AVFrame *remove_frame = ff_framequeue_take(&s->fifo); + av_frame_free(&remove_frame); + s->current_frame_isnew = 1; + goto retry; + } + } + + if (next_pts > sub_end_time) { + AVFrame *remove_frame = ff_framequeue_take(&s->fifo); + av_frame_free(&remove_frame); + s->current_frame_isnew = 1; + goto retry; + } + } + + if (ff_framequeue_queued_frames(&s->fifo)) { + AVFrame *current_frame = ff_framequeue_peek(&s->fifo, 0); + + if (current_frame && current_frame->subtitle_timing.start_pts <= next_pts + interval) { + if (!s->current_frame_isnew) + current_frame->repeat_sub++; + + out = av_frame_clone(current_frame); + + if (!out) + return AVERROR(ENOMEM); + + if (!s->current_frame_isnew) { + out->pts = next_pts; + } else { + out->pts = out->subtitle_timing.start_pts; + + if (out->pts < next_pts) + out->pts = next_pts; + + s->next_pts_offset = (out->pts - next_pts) % interval; + } + + if (s->mode == FM_SCATTER) { + const int64_t sub_end_time = current_frame->subtitle_timing.start_pts + current_frame->subtitle_timing.duration; + + if (s->current_frame_isnew == 1 && current_frame->subtitle_timing.start_pts < out->pts) { + const int64_t diff = out->pts - current_frame->subtitle_timing.start_pts; + current_frame->subtitle_timing.duration -= diff; + } + + out->repeat_sub = 0; + out->subtitle_timing.start_pts = out->pts; + out->subtitle_timing.duration = interval; + av_assert1(out->pts >= next_pts); + av_assert1(out->pts < next_pts + interval); + av_assert1(out->pts < sub_end_time); + + if (out->pts > next_pts) + out->subtitle_timing.duration -= out->pts - next_pts; + + if (sub_end_time < next_pts + interval) { + const int64_t diff = next_pts + interval - sub_end_time; + av_assert1(diff <= out->subtitle_timing.duration); + out->subtitle_timing.duration -= diff; + } + } + + s->current_frame_isnew = 0; + s->recent_subtitle_pts = out->subtitle_timing.start_pts; + + av_log(outlink->src, AV_LOG_DEBUG, "Output1 frame pts: %"PRId64" subtitle_pts: %"PRId64" repeat_frame: %d\n", + out->pts, out->subtitle_timing.start_pts, out->repeat_sub); + + return ff_filter_frame(outlink, out); + } + } + + if (ff_framequeue_queued_frames(&s->fifo) == 0) { + status = ff_request_frame(inlink); + if (status == AVERROR_EOF) { + s->eof = 1; + return status; + } + + if (s->counter > 1 && s->counter % 2) + return 0; + } + + out = ff_get_subtitles_buffer(outlink, outlink->format); + out->pts = next_pts; + out->repeat_sub = 1; + out->subtitle_timing.start_pts = s->recent_subtitle_pts; + + av_log(outlink->src, AV_LOG_DEBUG, "Output2 frame pts: %"PRId64" subtitle_pts: %"PRId64" repeat_frame: %d\n", + out->pts, out->subtitle_timing.start_pts, out->repeat_sub); + + return ff_filter_frame(outlink, out); +} + +static int filter_frame(AVFilterLink *inlink, AVFrame *frame) +{ + AVFilterContext *ctx = inlink->dst; + SubFeedContext *s = inlink->dst->priv; + AVFilterLink *outlink = inlink->dst->outputs[0]; + const int64_t index = (int64_t)ff_framequeue_queued_frames(&s->fifo) - 1; + size_t nb_queued_frames; + int ret = 0; + + av_log(ctx, AV_LOG_VERBOSE, "frame.pts: %"PRId64" (AVTB: %"PRId64") - subtitle_timing.start_pts: %"PRId64" subtitle_timing.duration: %"PRId64" - format: %d\n", + frame->pts, av_rescale_q(frame->pts, inlink->time_base, AV_TIME_BASE_Q), frame->subtitle_timing.start_pts, frame->subtitle_timing.duration, frame->format); + + frame->pts = av_rescale_q(frame->pts, inlink->time_base, AV_TIME_BASE_Q); + + if (index < 0) { + s->current_frame_isnew = 1; + } else if (s->fix_durations || s->fix_overlap) { + AVFrame *previous_frame = ff_framequeue_peek(&s->fifo, index); + const int64_t pts_diff = frame->subtitle_timing.start_pts - previous_frame->subtitle_timing.start_pts; + nb_queued_frames = ff_framequeue_queued_frames(&s->fifo); + + if (s->fix_durations && pts_diff > 0 && previous_frame->subtitle_timing.duration > ms_to_avtb(29000)) { + av_log(ctx, AV_LOG_VERBOSE, "Previous frame (index #%"PRId64") has a duration of %"PRId64" ms, setting to %"PRId64" ms\n", + index, avtb_to_ms(previous_frame->subtitle_timing.duration), avtb_to_ms(frame->subtitle_timing.start_pts - previous_frame->subtitle_timing.start_pts)); + previous_frame->subtitle_timing.duration = frame->subtitle_timing.start_pts - previous_frame->subtitle_timing.start_pts; + } + + if (s->fix_overlap && pts_diff > 0 && previous_frame->subtitle_timing.duration > pts_diff) { + av_log(ctx, AV_LOG_VERBOSE, "Detected overlap from previous frame (index #%"PRId64") which had a duration of %"PRId64" ms, setting to the pts_diff which is %"PRId64" ms\n", + index, avtb_to_ms(previous_frame->subtitle_timing.duration), avtb_to_ms(pts_diff)); + previous_frame->subtitle_timing.duration = pts_diff; + } + + if (pts_diff <= 0) { + av_log(ctx, AV_LOG_WARNING, "The pts_diff to the previous frame (index #%"PRId64") is <= 0: %"PRId64" ms. The previous frame duration is %"PRId64" ms.\n", + index, avtb_to_ms(pts_diff), avtb_to_ms(previous_frame->subtitle_timing.duration)); + + if (s->fix_overlap) { + av_log(ctx, AV_LOG_VERBOSE, "Removing previous frame\n"); + previous_frame = ff_framequeue_take(&s->fifo); + while (nb_queued_frames > 1) { + ff_framequeue_add(&s->fifo, previous_frame); + previous_frame = ff_framequeue_take(&s->fifo); + nb_queued_frames--; + } + } + } + } + + ff_framequeue_add(&s->fifo, frame); + + nb_queued_frames = ff_framequeue_queued_frames(&s->fifo); + + if (nb_queued_frames > 3) + av_log(ctx, AV_LOG_WARNING, "frame queue count: %zu\n", nb_queued_frames); + + if (s->mode == FM_FORWARD && nb_queued_frames) { + + AVFrame *first_frame = ff_framequeue_peek(&s->fifo, 0); + + if (s->fix_overlap && nb_queued_frames < 2) { + av_log(ctx, AV_LOG_VERBOSE, "Return no frame since we have less than 2\n"); + return 0; + } + + if (s->fix_durations && first_frame->subtitle_timing.duration > ms_to_avtb(29000)) { + av_log(ctx, AV_LOG_VERBOSE, "Return no frame because first frame duration is %"PRId64" ms\n", avtb_to_ms(first_frame->subtitle_timing.duration)); + return 0; + } + + first_frame = ff_framequeue_take(&s->fifo); + return ff_filter_frame(outlink, first_frame); + } + + return ret; +} + +#define OFFSET(x) offsetof(SubFeedContext, x) +#define FLAGS (AV_OPT_FLAG_SUBTITLE_PARAM | AV_OPT_FLAG_FILTERING_PARAM) + +static const AVOption subfeed_options[] = { + { "fix_durations", "delay output and determine duration from next frame", OFFSET(fix_durations), AV_OPT_TYPE_BOOL, { .i64=1 }, 0, 1, FLAGS, NULL }, + { "fix_overlap", "delay output and adjust durations to prevent overlap", OFFSET(fix_overlap), AV_OPT_TYPE_BOOL, { .i64=0 }, 0, 1, FLAGS, NULL }, + { "mode", "set feed mode", OFFSET(mode), AV_OPT_TYPE_INT, { .i64=FM_REPEAT }, FM_REPEAT, FM_FORWARD, FLAGS, "mode" }, + { "repeat", "repeat recent while valid, send empty otherwise", 0, AV_OPT_TYPE_CONST, { .i64=FM_REPEAT }, 0, 0, FLAGS, "mode" }, + { "scatter", "subdivide subtitles into 1/framerate segments", 0, AV_OPT_TYPE_CONST, { .i64=FM_SCATTER }, 0, 0, FLAGS, "mode" }, + { "forward", "forward only (clears output framerate)", 0, AV_OPT_TYPE_CONST, { .i64=FM_FORWARD }, 0, 0, FLAGS, "mode" }, + { "rate", "output frame rate", OFFSET(frame_rate), AV_OPT_TYPE_VIDEO_RATE, { .str = "5"}, 0, INT_MAX, FLAGS, NULL },\ + { "r", "output frame rate", OFFSET(frame_rate), AV_OPT_TYPE_VIDEO_RATE, { .str = "5"}, 0, INT_MAX, FLAGS, NULL },\ + { NULL }, +}; + +AVFILTER_DEFINE_CLASS(subfeed); + +static const AVFilterPad inputs[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_SUBTITLE, + .filter_frame = filter_frame, + .config_props = config_input, + }, +}; + +static const AVFilterPad outputs[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_SUBTITLE, + .request_frame = request_frame, + .config_props = config_output, + }, +}; + +const AVFilter ff_sf_subfeed = { + .name = "subfeed", + .description = NULL_IF_CONFIG_SMALL("Control subtitle frame timing and flow in a filtergraph"), + .init = init, + .uninit = uninit, + .priv_size = sizeof(SubFeedContext), + .priv_class = &subfeed_class, + FILTER_INPUTS(inputs), + FILTER_OUTPUTS(outputs), + FILTER_QUERY_FUNC(query_formats), +};