From patchwork Fri Jan 20 19:31:32 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Anton Khirnov X-Patchwork-Id: 40082 Delivered-To: ffmpegpatchwork2@gmail.com Received: by 2002:a05:6a20:3ca3:b0:b9:1511:ac2c with SMTP id b35csp1432341pzj; Fri, 20 Jan 2023 11:32:58 -0800 (PST) X-Google-Smtp-Source: AMrXdXv69RIbE+acJgCKCjBSxIPWRgiQHXu/Vl8N60IBkm10fo7C+62ZkWxr5uAH5imxh/og29ly X-Received: by 2002:a05:6402:548f:b0:49c:1fe4:9efc with SMTP id fg15-20020a056402548f00b0049c1fe49efcmr17272349edb.40.1674243177920; Fri, 20 Jan 2023 11:32:57 -0800 (PST) ARC-Seal: i=1; a=rsa-sha256; t=1674243177; cv=none; d=google.com; s=arc-20160816; b=UE0R2aJ+HnAEDlNsbx+BOd7LA9ZMY+LpueNGNvCh0lf2sMq0qLMWc6YETZ2pRFOnbv VZCc5ivgGAOSCNB8em/UhtlHFJBVHjQGDUZE4LUwcmEDvSzWzGAj8t3SS8mlTSm5Vli6 YLX4G8/JE4iHGhNIBeObx9hcTzngSD+EyKTU7q6LExHtaOZFm2YT2+Ru04TZgx62gDBe QylrvCEnkHe7Y+epX6A0wfSos6FcOrR+p3lu7q1AA8u60p9cOEGerrAOft6MzzQZS23N tEGKG8rvIYOC17iJG7i1djOcswtE1ao9bLNL7aeRv0AGkCGgLj0wd+OIqczCMzgkafCc qdIg== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816; h=sender:errors-to:content-transfer-encoding:reply-to:list-subscribe :list-help:list-post:list-archive:list-unsubscribe:list-id :precedence:subject:mime-version:references:in-reply-to:message-id :date:to:from:delivered-to; bh=37QTbnpEwLa4epZAkl9YUQ0EnNTQyfwIEL4HmTW9ibY=; b=QTOsdCVMT34Gfq812NljDIldGb0dgg0bBLf056vfED4F7L/r0FyLXZPmRyYpuo/5YO KYwq/M7D+l7Xth/0lVw1Sf3agJOizBXwfckJuC59cCw/ciAAhVQQzx9m9maYCBrXPG/m YyHwTcHMSNpRPOAbJO3OLsVEEQcGmCwcWYVWtYVrh+aCvLn73h/P+ziRpWxRbRxh3Fv9 OllDgh2h1Cw//YjRcuyI9rGVbk6flL21uhd3VElyhXO3k0207T/D19PH+nvLy2J0uQMD U7mYFwH8VK23UhbZOcrd7PANsYk52pcojiB3bRyY7uvZ34jxJiOlkPeH8ily1wNphYLH 1oIw== ARC-Authentication-Results: i=1; mx.google.com; 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 Return-Path: Received: from ffbox0-bg.mplayerhq.hu (ffbox0-bg.ffmpeg.org. [79.124.17.100]) by mx.google.com with ESMTP id l19-20020a056402125300b0049deaf9d2absi19912672edw.126.2023.01.20.11.32.41; Fri, 20 Jan 2023 11:32:57 -0800 (PST) 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; 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 Received: from [127.0.1.1] (localhost [127.0.0.1]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTP id E08A568BD35; Fri, 20 Jan 2023 21:32:08 +0200 (EET) X-Original-To: ffmpeg-devel@ffmpeg.org Delivered-To: ffmpeg-devel@ffmpeg.org Received: from mail0.khirnov.net (red.khirnov.net [176.97.15.12]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTPS id B117E68BCDC for ; Fri, 20 Jan 2023 21:31:59 +0200 (EET) Received: from localhost (localhost [IPv6:::1]) by mail0.khirnov.net (Postfix) with ESMTP id 758AB240591 for ; Fri, 20 Jan 2023 20:31:59 +0100 (CET) Received: from mail0.khirnov.net ([IPv6:::1]) by localhost (mail0.khirnov.net [IPv6:::1]) (amavisd-new, port 10024) with ESMTP id QDbixY-Vx1Kp for ; Fri, 20 Jan 2023 20:31:58 +0100 (CET) Received: from libav.khirnov.net (libav.khirnov.net [IPv6:2a00:c500:561:201::7]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256 client-signature RSA-PSS (2048 bits) client-digest SHA256) (Client CN "libav.khirnov.net", Issuer "smtp.khirnov.net SMTP CA" (verified OK)) by mail0.khirnov.net (Postfix) with ESMTPS id B0CC42404F5 for ; Fri, 20 Jan 2023 20:31:56 +0100 (CET) Received: from libav.khirnov.net (libav.khirnov.net [IPv6:::1]) by libav.khirnov.net (Postfix) with ESMTP id D5ED83A0354 for ; Fri, 20 Jan 2023 20:31:48 +0100 (CET) From: Anton Khirnov To: ffmpeg-devel@ffmpeg.org Date: Fri, 20 Jan 2023 20:31:32 +0100 Message-Id: <20230120193132.21597-5-anton@khirnov.net> X-Mailer: git-send-email 2.35.1 In-Reply-To: <20230120193132.21597-1-anton@khirnov.net> References: <20230120193132.21597-1-anton@khirnov.net> MIME-Version: 1.0 Subject: [FFmpeg-devel] [PATCH 5/5] fftools/ffmpeg: add special syntax for loading filter options from files 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 Errors-To: ffmpeg-devel-bounces@ffmpeg.org Sender: "ffmpeg-devel" X-TUID: XFDcbvRZE2yv Many filters accept user-provided data that is cumbersome to provide as text strings - e.g. binary files or very long text. For that reason such filters typically provide a option whose value is the path from which the filter loads the actual data. However, filters doing their own IO internally is a layering violation that the callers may not expect, and is thus best avoided. With the recently introduced graph segment parsing API, loading option values from files can now be handled by the caller. This commit makes use of the new API in ffmpeg CLI. Any option name in the filtergraph syntax can now be prefixed with a slash '/'. This will cause ffmpeg to interpret the value as the path to load the actual value from. --- Changelog | 2 + doc/filters.texi | 11 +++ fftools/ffmpeg_filter.c | 154 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 165 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index 5c01e8365e..2d1ea9540e 100644 --- a/Changelog +++ b/Changelog @@ -29,6 +29,8 @@ version : - corr video filter - adrc audio filter - afdelaysrc audio filter +- filtergraph syntax in ffmpeg CLI now supports passing file contents + as option values, by prefixing option name with '/' version 5.1: diff --git a/doc/filters.texi b/doc/filters.texi index be70a2396b..7ff9948239 100644 --- a/doc/filters.texi +++ b/doc/filters.texi @@ -171,6 +171,17 @@ within the quoted text; otherwise the argument string is considered terminated when the next special character (belonging to the set @samp{[]=;,}) is encountered. +A special syntax implemented in the @command{ffmpeg} CLI tool allows loading +option values from files. This is done be prepending a slash '/' to the option +name, then the supplied value is interpreted as a path from which the actual +value is loaded. E.g. +@example +ffmpeg -i -vf drawtext=/text=/tmp/some_text +@end example +will load the text to be drawn from @file{/tmp/some_text}. API users wishing to +implement a similar feature should use the @code{avfilter_graph_segment_*()} +functions together with custom IO code. + The name and arguments of the filter are optionally preceded and followed by a list of link labels. A link label allows one to name a link and associate it to a filter output diff --git a/fftools/ffmpeg_filter.c b/fftools/ffmpeg_filter.c index 7eb656dbe5..3f79133f75 100644 --- a/fftools/ffmpeg_filter.c +++ b/fftools/ffmpeg_filter.c @@ -314,6 +314,156 @@ static void init_input_filter(FilterGraph *fg, AVFilterInOut *in) ist->filters[ist->nb_filters - 1] = ifilter; } +static int read_binary(const char *path, uint8_t **data, int *len) +{ + AVIOContext *io = NULL; + int64_t fsize; + int ret; + + *data = NULL; + *len = 0; + + ret = avio_open2(&io, path, AVIO_FLAG_READ, &int_cb, NULL); + if (ret < 0) { + av_log(NULL, AV_LOG_ERROR, "Cannot open file '%s': %s\n", + path, av_err2str(ret)); + return ret; + } + + fsize = avio_size(io); + if (fsize < 0 || fsize > INT_MAX) { + av_log(NULL, AV_LOG_ERROR, "Cannot obtain size of file %s\n", path); + ret = AVERROR(EIO); + goto fail; + } + + *data = av_malloc(fsize); + if (!*data) { + ret = AVERROR(ENOMEM); + goto fail; + } + + ret = avio_read(io, *data, fsize); + if (ret != fsize) { + av_log(NULL, AV_LOG_ERROR, "Error reading file %s\n", path); + ret = ret < 0 ? ret : AVERROR(EIO); + goto fail; + } + + *len = fsize; + + return 0; +fail: + avio_close(io); + av_freep(data); + *len = 0; + return ret; +} + +static int filter_opt_apply(AVFilterContext *f, const char *key, const char *val) +{ + const AVOption *o; + int ret; + + ret = av_opt_set(f, key, val, AV_OPT_SEARCH_CHILDREN); + if (ret >= 0) + return 0; + + if (ret == AVERROR_OPTION_NOT_FOUND && key[0] == '/') + o = av_opt_find(f, key + 1, NULL, 0, AV_OPT_SEARCH_CHILDREN); + if (!o) + goto err_apply; + + // key is a valid option name prefixed with '/' + // interpret value as a path from which to load the actual option value + key++; + + if (o->type == AV_OPT_TYPE_BINARY) { + uint8_t *data; + int len; + + ret = read_binary(val, &data, &len); + if (ret < 0) + goto err_load; + + ret = av_opt_set_bin(f, key, data, len, AV_OPT_SEARCH_CHILDREN); + av_freep(&data); + } else { + char *data = file_read(val); + if (!val) { + ret = AVERROR(EIO); + goto err_load; + } + + ret = av_opt_set(f, key, data, AV_OPT_SEARCH_CHILDREN); + av_freep(&data); + } + if (ret < 0) + goto err_apply; + + return 0; + +err_apply: + av_log(NULL, AV_LOG_ERROR, + "Error applying option '%s' to filter '%s': %s\n", + key, f->filter->name, av_err2str(ret)); + return ret; +err_load: + av_log(NULL, AV_LOG_ERROR, + "Error loading value for option '%s' from file '%s'\n", + key, val); + return ret; +} + +static int graph_opts_apply(AVFilterGraphSegment *seg) +{ + for (size_t i = 0; i < seg->nb_chains; i++) { + AVFilterChain *ch = seg->chains[i]; + + for (size_t j = 0; j < ch->nb_filters; j++) { + AVFilterParams *p = ch->filters[j]; + const AVDictionaryEntry *e = NULL; + + av_assert0(p->filter); + + while ((e = av_dict_iterate(p->opts, e))) { + int ret = filter_opt_apply(p->filter, e->key, e->value); + if (ret < 0) + return ret; + } + + av_dict_free(&p->opts); + } + } + + return 0; +} + +static int graph_parse(AVFilterGraph *graph, const char *desc, + AVFilterInOut **inputs, AVFilterInOut **outputs) +{ + AVFilterGraphSegment *seg; + int ret; + + ret = avfilter_graph_segment_parse(graph, desc, 0, &seg); + if (ret < 0) + return ret; + + ret = avfilter_graph_segment_create_filters(seg, 0); + if (ret < 0) + goto fail; + + ret = graph_opts_apply(seg); + if (ret < 0) + goto fail; + + ret = avfilter_graph_segment_apply(seg, 0, inputs, outputs); + +fail: + avfilter_graph_segment_free(&seg); + return ret; +} + int init_complex_filtergraph(FilterGraph *fg) { AVFilterInOut *inputs, *outputs, *cur; @@ -327,7 +477,7 @@ int init_complex_filtergraph(FilterGraph *fg) return AVERROR(ENOMEM); graph->nb_threads = 1; - ret = avfilter_graph_parse2(graph, fg->graph_desc, &inputs, &outputs); + ret = graph_parse(graph, fg->graph_desc, &inputs, &outputs); if (ret < 0) goto fail; @@ -1004,7 +1154,7 @@ int configure_filtergraph(FilterGraph *fg) fg->graph->nb_threads = filter_complex_nbthreads; } - if ((ret = avfilter_graph_parse2(fg->graph, graph_desc, &inputs, &outputs)) < 0) + if ((ret = graph_parse(fg->graph, graph_desc, &inputs, &outputs)) < 0) goto fail; ret = hw_device_setup_for_filter(fg);