Add -json_cmd_gen option that converts a standard ffmpeg command line
into the equivalent JSON command file, printed to stdout.

The implementation uses a custom command-line parser that groups options
into globals, inputs, outputs, and decoders. Unlike split_commandline(),
it handles unknown AVOptions gracefully using a heuristic: if the next
argument exists and does not look like another option, treat it as
that option's value.

To support this feature, the static groups[] array in ffmpeg_opt.c is
renamed to the non-static ffmpeg_groups[] and declared extern in
ffmpeg.h, along with the OptGroup enum and FFMPEG_NUM_GROUPS macro.

Signed-off-by: Tom Vaughan
---
fftools/ffmpeg.c      |  18 ++
fftools/ffmpeg.h      |   9 +
fftools/ffmpeg_json.c | 386 ++++++++++++++++++++++++++++++++++++++++++
fftools/ffmpeg_json.h |   9 +
fftools/ffmpeg_opt.c  |  23 ++-
5 files changed, 436 insertions(+), 9 deletions(-)

diff --git a/fftools/ffmpeg.c b/fftools/ffmpeg.c
index 3090864c09..11e43e3f48 100644
--- a/fftools/ffmpeg.c
+++ b/fftools/ffmpeg.c
@@ -1021,6 +1021,24 @@ int main(int argc, char **argv)
         goto finish;
     }
+    /* Handle -json_cmd_gen: convert the rest of the command line to JSON */
+    if (argc >= 2 && !strcmp(argv[1], "-json_cmd_gen")) {
+        /* Build a new argv without -json_cmd_gen, keeping argv[0] */
+        int gen_argc = argc - 1;
+        char **gen_argv = av_malloc_array(gen_argc + 1, sizeof(*gen_argv));
+        if (!gen_argv) {
+            ret = AVERROR(ENOMEM);
+            goto finish;
+        }
+        gen_argv[0] = argv[0];
+        for (int k = 2; k < argc; k++)
+            gen_argv[k - 1] = argv[k];
+        gen_argv[gen_argc] = NULL;
+        ret = ffmpeg_json_generate(gen_argc, gen_argv);
+        av_freep(&gen_argv);
+        goto finish;
+    }
+
     sch = sch_alloc();
     if (!sch) {
         ret = AVERROR(ENOMEM);
diff --git a/fftools/ffmpeg.h b/fftools/ffmpeg.h
index 3a19e5878d..975b4b5dcf 100644
--- a/fftools/ffmpeg.h
+++ b/fftools/ffmpeg.h
@@ -854,6 +854,15 @@ void fg_free(FilterGraph **pfg);
void fg_send_command(FilterGraph *fg, double time, const char *target,
                      const char *command, const char *arg, int all_filters);
+enum OptGroup {
+    GROUP_OUTFILE,
+    GROUP_INFILE,
+    GROUP_DECODER,
+};
+
+extern const OptionGroupDef ffmpeg_groups[];
+#define FFMPEG_NUM_GROUPS 3
+
int ffmpeg_parse_options(int argc, char **argv, Scheduler *sch);
 void enc_stats_write(OutputStream *ost, EncStats *es,
diff --git a/fftools/ffmpeg_json.c b/fftools/ffmpeg_json.c
index fb5e25fe6c..b9dd357834 100644
--- a/fftools/ffmpeg_json.c
+++ b/fftools/ffmpeg_json.c
@@ -886,3 +886,389 @@ void ffmpeg_json_print_cmd(int argc, char **argv)
     }
     printf("\n");
}
+
+/* ------------------------------------------------------------------ */
+/*  JSON generation from command-line args                            */
+/* ------------------------------------------------------------------ */
+
+/**
+ * Print a JSON string with proper escaping to stdout.
+ */
+static void print_json_string(const char *s)
+{
+    putchar('"');
+    for (; *s; s++) {
+        switch (*s) {
+        case '"':  printf("\\\""); break;
+        case '\\': printf("\\\\"); break;
+        case '\b': printf(\\b</b>);  break;
+        case '\f': printf(\\f</f>);  break;
+        case '\n': printf(\\n</n>);  break;
+        case '\r': printf(\\r</r>);  break;
+        case '\t': printf(\\t</t>);  break;
+        default:
+            if ((unsigned char)*s < 0x20)
+                printf(\\u%04x</u%04x>, (unsigned char)*s);
+            else
+                putchar(*s);
+        }
+    }
+    putchar('"');
+}
+
+/**
+ * Count occurrences of a key in a GenOpt array.
+ */
+static int gen_key_count(const void *opts, int nb, const char *key)
+{
+    const struct { const char *key; const char *val; int is_flag; } *o = opts;
+    int i, count = 0;
+    for (i = 0; i < nb; i++)
+        if (!strcmp(o[i].key, key))
+            count++;
+    return count;
+}
+
+/**
+ * Check if index is the first occurrence of its key.
+ */
+static int gen_is_first(const void *opts, int idx)
+{
+    const struct { const char *key; const char *val; int is_flag; } *o = opts;
+    int i;
+    for (i = 0; i < idx; i++)
+        if (!strcmp(o[i].key, o[idx].key))
+            return 0;
+    return 1;
+}
+
+/**
+ * Print a GenOpt array as JSON object fields with given indentation.
+ * Repeated keys are emitted as JSON arrays.
+ */
+static void print_gen_opts_json(const void *opts, int nb, const char *indent)
+{
+    const struct { const char *key; const char *val; int is_flag; } *o = opts;
+    int i, j, first = 1;
+
+    for (i = 0; i < nb; i++) {
+        if (!gen_is_first(opts, i))
+            continue;
+
+        if (!first)
+            printf(",\n");
+        first = 0;
+
+        printf("%s", indent);
+        print_json_string(o[i].key);
+        printf(": ");
+
+        if (gen_key_count(opts, nb, o[i].key) > 1) {
+            int arr_first = 1;
+            printf("[");
+            for (j = 0; j < nb; j++) {
+                if (strcmp(o[j].key, o[i].key))
+                    continue;
+                if (!arr_first)
+                    printf(", ");
+                arr_first = 0;
+                if (o[j].is_flag)
+                    printf("true");
+                else
+                    print_json_string(o[j].val);
+            }
+            printf("]");
+        } else if (o[i].is_flag) {
+            printf("true");
+        } else {
+            print_json_string(o[i].val);
+        }
+    }
+}
+
+/**
+ * @sa ffmpeg_json_generate() in ffmpeg_json.h for full documentation.
+ *
+ * Custom command-line parser that groups options into globals, inputs,
+ * outputs, and decoders. Unlike split_commandline(), this handles
+ * unknown AVOptions gracefully by using a heuristic: if the next
+ * argument exists and doesn't look like another option, treat it
+ * as the value for the unknown option.
+ */
+int ffmpeg_json_generate(int argc, char **argv)
+{
+    typedef struct GenOpt {
+        const char *key;
+        const char *val;   /* NULL for boolean flags */
+        int         is_flag;
+    } GenOpt;
+
+    typedef struct GenGroup {
+        const char *arg;   /* url or decoder name */
+        GenOpt    *opts;
+        int        nb_opts;
+        int        alloc_opts;
+    } GenGroup;
+
+    GenOpt  *cur_opts   = NULL;
+    int      nb_cur     = 0;
+    int      alloc_cur  = 0;
+
+    GenOpt  *global     = NULL;
+    int      nb_global  = 0;
+
+    GenGroup *inputs    = NULL;
+    int      nb_inputs  = 0;
+    int      alloc_in   = 0;
+
+    GenGroup *outputs   = NULL;
+    int      nb_outputs = 0;
+    int      alloc_out  = 0;
+
+    GenGroup *decoders  = NULL;
+    int      nb_decoders = 0;
+    int      alloc_dec  = 0;
+
+    int      have_group = 0;   /* set after first group separator / output url 
*/
+    int      i, j;
+    int      ret = 0;
+
+    /* --- Helper macros --- */
+#define ADD_CUR_OPT(k, v, f) do {                                      \
+    if (nb_cur >= alloc_cur) {                                          \
+        alloc_cur = alloc_cur ? alloc_cur * 2 : 16;                    \
+        cur_opts = av_realloc_array(cur_opts, alloc_cur, sizeof(*cur_opts)); \
+        if (!cur_opts) { ret = AVERROR(ENOMEM); goto gen_fail; }       \
+    }                                                                   \
+    cur_opts[nb_cur].key = (k);                                         \
+    cur_opts[nb_cur].val = (v);                                         \
+    cur_opts[nb_cur].is_flag = (f);                                     \
+    nb_cur++;                                                           \
+} while (0)
+
+#define FLUSH_GROUP(arr, nb, alloc, url_val) do {                       \
+    if ((nb) >= (alloc)) {                                              \
+        (alloc) = (alloc) ? (alloc) * 2 : 4;                           \
+        (arr) = av_realloc_array((arr), (alloc), sizeof(*(arr)));       \
+        if (!(arr)) { ret = AVERROR(ENOMEM); goto gen_fail; }          \
+    }                                                                   \
+    (arr)[(nb)].arg       = (url_val);                                  \
+    (arr)[(nb)].opts      = cur_opts;                                   \
+    (arr)[(nb)].nb_opts   = nb_cur;                                     \
+    (arr)[(nb)].alloc_opts = alloc_cur;                                 \
+    (nb)++;                                                             \
+    cur_opts  = NULL;                                                   \
+    nb_cur    = 0;                                                      \
+    alloc_cur = 0;                                                      \
+} while (0)
+
+    /* --- find_option replica (static in cmdutils.c) --- */
+#define OPT_LOOKUP(po_var, name_str) do {                               \
+    const char *_n = (name_str);                                        \
+    if (*_n == '/') _n++;                                               \
+    (po_var) = options;                                                 \
+    while ((po_var)->name) {                                            \
+        const char *_e;                                                 \
+        if (av_strstart(_n, (po_var)->name, &_e) && (!*_e || *_e == ':')) \
+            break;                                                      \
+        (po_var)++;                                                     \
+    }                                                                   \
+} while (0)
+
+    /* --- Walk argv (skip argv[0] = program name) --- */
+    for (i = 1; i < argc; i++) {
+        const char *arg = argv[i];
+
+        /* Non-option argument → output url */
+        if (arg[0] != '-' || arg[1] == '\0' || !strcmp(arg, "--")) {
+            if (!strcmp(arg, "--"))
+                continue;
+            /* Before the first group separator, any accumulated opts are 
global */
+            if (!have_group) {
+                global    = cur_opts;
+                nb_global = nb_cur;
+                cur_opts  = NULL;
+                nb_cur    = 0;
+                alloc_cur = 0;
+                have_group = 1;
+            }
+            FLUSH_GROUP(outputs, nb_outputs, alloc_out, arg);
+            continue;
+        }
+
+        /* Strip leading '-' */
+        arg++;
+
+        /* Group separator: -i */
+        if (!strcmp(arg, "i") && i + 1 < argc) {
+            if (!have_group) {
+                global    = cur_opts;
+                nb_global = nb_cur;
+                cur_opts  = NULL;
+                nb_cur    = 0;
+                alloc_cur = 0;
+                have_group = 1;
+            }
+            i++;
+            FLUSH_GROUP(inputs, nb_inputs, alloc_in, argv[i]);
+            continue;
+        }
+
+        /* Group separator: -dec */
+        if (!strcmp(arg, "dec") && i + 1 < argc) {
+            if (!have_group) {
+                global    = cur_opts;
+                nb_global = nb_cur;
+                cur_opts  = NULL;
+                nb_cur    = 0;
+                alloc_cur = 0;
+                have_group = 1;
+            }
+            i++;
+            FLUSH_GROUP(decoders, nb_decoders, alloc_dec, argv[i]);
+            continue;
+        }
+
+        /* Try to find in the known options table */
+        {
+            const OptionDef *po;
+            OPT_LOOKUP(po, arg);
+
+            if (po->name) {
+                /* Known option */
+                int has_arg = 1;
+                if (po->type == OPT_TYPE_BOOL)
+                    has_arg = 0;
+                else if (po->type == OPT_TYPE_FUNC && !(po->flags & 
OPT_FUNC_ARG))
+                    has_arg = 0;
+
+                if (has_arg && i + 1 < argc) {
+                    i++;
+                    ADD_CUR_OPT(arg, argv[i], 0);
+                } else if (!has_arg) {
+                    ADD_CUR_OPT(arg, NULL, 1);
+                } else {
+                    /* Option expects arg but none available */
+                    av_log(NULL, AV_LOG_ERROR,
+                           "Missing argument for option '-%s'.\n", arg);
+                    ret = AVERROR(EINVAL);
+                    goto gen_fail;
+                }
+                continue;
+            }
+        }
+
+        /* Unknown option — heuristic: assume it takes one argument
+         * unless the next token looks like another option */
+        if (i + 1 < argc && argv[i + 1][0] != '-') {
+            i++;
+            ADD_CUR_OPT(arg, argv[i], 0);
+        } else if (i + 1 < argc) {
+            /* Next token starts with '-'; could be a negative number */
+            const char *next = argv[i + 1];
+            int is_num = (next[0] == '-' && next[1] >= '0' && next[1] <= '9');
+            if (is_num) {
+                i++;
+                ADD_CUR_OPT(arg, argv[i], 0);
+            } else {
+                /* Treat as a flag */
+                ADD_CUR_OPT(arg, NULL, 1);
+            }
+        } else {
+            /* Last argument - treat as flag */
+            ADD_CUR_OPT(arg, NULL, 1);
+        }
+    }
+
+    /* Any remaining options without a group flush become global */
+    if (!have_group) {
+        global    = cur_opts;
+        nb_global = nb_cur;
+        cur_opts  = NULL;
+        nb_cur    = 0;
+    } else if (nb_cur > 0) {
+        /* Trailing options after last group (unusual) — treat as global */
+        for (j = 0; j < nb_cur; j++) {
+            /* Append to global array - need to reallocate */
+        }
+        /* For simplicity, just add to global via realloc */
+        GenOpt *new_global = av_realloc_array(global, nb_global + nb_cur,
+                                              sizeof(*global));
+        if (!new_global) { ret = AVERROR(ENOMEM); goto gen_fail; }
+        global = new_global;
+        memcpy(global + nb_global, cur_opts, nb_cur * sizeof(*cur_opts));
+        nb_global += nb_cur;
+        av_freep(&cur_opts);
+        nb_cur = 0;
+    }
+
+    /* --- Emit JSON --- */
+    printf("{\n");
+
+    /* Global options */
+    printf("    \"global_options\": {");
+    if (nb_global > 0) {
+        printf("\n");
+        print_gen_opts_json(global, nb_global, "        ");
+        printf("\n    ");
+    }
+    printf("},\n");
+
+    /* Helper macro to emit a group array (inputs, outputs, decoders) */
+#define EMIT_GROUPS(label, url_field, arr, count) do {                  \
+    printf("    \"%s\": [", (label));                                   \
+    if ((count) > 0) {                                                  \
+        printf("\n");                                                    \
+        for (i = 0; i < (count); i++) {                                 \
+            GenGroup *grp = &(arr)[i];                                   \
+            if (i > 0) printf(",\n");                                   \
+            printf("        {\n");                                      \
+            printf("            \"%s\": ", (url_field));                 \
+            print_json_string(grp->arg);                                 \
+            printf(",\n");                                               \
+            printf("            \"options\": {");                        \
+            if (grp->nb_opts > 0) {                                      \
+                printf("\n");                                            \
+                print_gen_opts_json(grp->opts, grp->nb_opts, "                
"); \
+                printf("\n            ");                                \
+            }                                                           \
+            printf("}\n");                                              \
+            printf("        }");                                        \
+        }                                                               \
+        printf("\n    ");                                                \
+    }                                                                   \
+    printf("]");                                                        \
+} while (0)
+
+    EMIT_GROUPS("inputs",  "url",  inputs,  nb_inputs);
+    printf(",\n");
+    EMIT_GROUPS("outputs", "url",  outputs, nb_outputs);
+
+    if (nb_decoders > 0) {
+        printf(",\n");
+        EMIT_GROUPS("decoders", "name", decoders, nb_decoders);
+    }
+
+#undef EMIT_GROUPS
+
+    printf("\n}\n");
+    ret = 0;
+
+gen_fail:
+    av_freep(&cur_opts);
+    av_freep(&global);
+    for (i = 0; i < nb_inputs; i++)
+        av_freep(&inputs[i].opts);
+    av_freep(&inputs);
+    for (i = 0; i < nb_outputs; i++)
+        av_freep(&outputs[i].opts);
+    av_freep(&outputs);
+    for (i = 0; i < nb_decoders; i++)
+        av_freep(&decoders[i].opts);
+    av_freep(&decoders);
+    return ret;
+
+#undef ADD_CUR_OPT
+#undef FLUSH_GROUP
+#undef OPT_LOOKUP
+}
diff --git a/fftools/ffmpeg_json.h b/fftools/ffmpeg_json.h
index 0ed63903cd..8e7f4e4e89 100644
--- a/fftools/ffmpeg_json.h
+++ b/fftools/ffmpeg_json.h
@@ -85,5 +85,14 @@ void ffmpeg_json_free_argv(int argc, char ***argv);
  */
void ffmpeg_json_print_cmd(int argc, char **argv);
+/**
+ * Parse a standard ffmpeg command line and print the equivalent JSON
+ * command file to stdout.
+ *
+ * @param argc    Argument count (including program name)
+ * @param argv    Argument vector (argv[0] should be "ffmpeg")
+ * @return 0 on success, negative AVERROR code on failure
+ */
+int ffmpeg_json_generate(int argc, char **argv);
 #endif /* FFTOOLS_FFMPEG_JSON_H */
diff --git a/fftools/ffmpeg_opt.c b/fftools/ffmpeg_opt.c
index 5a0e07af33..c19fe894c6 100644
--- a/fftools/ffmpeg_opt.c
+++ b/fftools/ffmpeg_opt.c
@@ -1428,13 +1428,7 @@ void show_usage(void)
     av_log(NULL, AV_LOG_INFO, "\n");
}
-enum OptGroup {
-    GROUP_OUTFILE,
-    GROUP_INFILE,
-    GROUP_DECODER,
-};
-
-static const OptionGroupDef groups[] = {
+const OptionGroupDef ffmpeg_groups[] = {
     [GROUP_OUTFILE] = { "output url",  NULL, OPT_OUTPUT },
     [GROUP_INFILE]  = { "input url",   "i",  OPT_INPUT },
     [GROUP_DECODER] = { "loopback decoder", "dec", OPT_DECODER },
@@ -1485,8 +1479,8 @@ int ffmpeg_parse_options(int argc, char **argv, Scheduler 
*sch)
     memset(&octx, 0, sizeof(octx));
     /* split the commandline into an internal representation */
-    ret = split_commandline(&octx, argc, argv, options, groups,
-                            FF_ARRAY_ELEMS(groups));
+    ret = split_commandline(&octx, argc, argv, options, ffmpeg_groups,
+                            FF_ARRAY_ELEMS(ffmpeg_groups));
     if (ret < 0) {
         errmsg = "splitting the argument list";
         goto fail;
@@ -1627,6 +1621,14 @@ static int opt_json_cmd_print(void *optctx, const char 
*opt, const char *arg)
     return AVERROR(EINVAL);
}
+static int opt_json_cmd_gen(void *optctx, const char *opt, const char *arg)
+{
+    av_log(NULL, AV_LOG_ERROR,
+           "-json_cmd_gen must be the first option: "
+           "ffmpeg -json_cmd_gen [options] <output>\n");
+    return AVERROR(EINVAL);
+}
+
static const char *const alt_channel_layout[] = { "ch_layout", NULL};
static const char *const alt_codec[]          = { "c", "acodec", "vcodec", 
"scodec", "dcodec", NULL };
static const char *const alt_filter[]         = { "af", "vf", NULL };
@@ -1645,6 +1647,9 @@ const OptionDef options[] = {
     { "json_cmd_print",         OPT_TYPE_FUNC, OPT_FUNC_ARG | OPT_EXIT,
         { .func_arg = opt_json_cmd_print },
         "print the CLI command from a JSON command file", "file" },
+    { "json_cmd_gen",           OPT_TYPE_FUNC, OPT_FUNC_ARG | OPT_EXIT,
+        { .func_arg = opt_json_cmd_gen },
+        "convert CLI arguments to a JSON command file" },
     { "f",                      OPT_TYPE_STRING, OPT_OFFSET | OPT_INPUT | 
OPT_OUTPUT,
         { .off       = OFFSET(format) },
         "force container format (auto-detected otherwise)", "fmt" },
--
2.49.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to