diff mbox series

[FFmpeg-devel,5/5] fftools/ffmpeg: add special syntax for loading filter options from files

Message ID 20230120193132.21597-5-anton@khirnov.net
State New
Headers show
Series [FFmpeg-devel,1/5] lavfi/avfilter: export process_options() | expand

Checks

Context Check Description
andriy/make_x86 success Make finished
andriy/make_fate_x86 success Make fate finished

Commit Message

Anton Khirnov Jan. 20, 2023, 7:31 p.m. UTC
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(-)

Comments

Anton Khirnov Jan. 23, 2023, 11:34 a.m. UTC | #1
Quoting Anton Khirnov (2023-01-20 20:31:32)
> +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) {

should be s/val/data/
fixed locally
diff mbox series

Patch

diff --git a/Changelog b/Changelog
index 5c01e8365e..2d1ea9540e 100644
--- a/Changelog
+++ b/Changelog
@@ -29,6 +29,8 @@  version <next>:
 - 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 <INPUT> -vf drawtext=/text=/tmp/some_text <OUTPUT>
+@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);