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]