I’ve developed a set of patches that enable FFmpeg to be optionally given a
single
JSON containing all commands and parameters.
This…
* makes building FFMPEG commands programmatically easier and more reliable
* avoids difficulties that come with very long FFMPEG command-line strings
* avoids the requirement to escape slashes and other common characters
(particularly useful when passing urls or presigned urls)
* makes it easier to save and modify ffmpeg “recipes” (taking a template,
modifying one or more parameters, saving as a new ffmpeg command)
The only slight caveat is that filter complexes must still be defined as a
string.
I believe that this design is independent of any particular command-line
parameters, and so it shouldn’t break when
commands or parameters are added or changed (so, it shouldn’t be a maintenance
headache).
There is also a json_cmd_gen command that converts a FFMPEG command-line string
into the equivalent JSON,
and a json_cmd_print command that converts a FFMPEG JSON into the equivalent
command-line string.
Add -json_cmd option that reads ffmpeg parameters from a structured
JSON file. The JSON format supports global options, input/output file
specifications with per-file options, and loopback decoder definitions.
The implementation includes a self-contained JSON parser (no external
dependencies) and an argv builder that translates JSON key-value pairs
into the equivalent command-line arguments.
Option values support strings, booleans (true for flags, false to skip),
numbers, arrays (for repeated options like -map), and null (skip).
Keys starting with "/" use FFmpeg's file-loading syntax.
Signed-off-by: Tom Vaughan
---
fftools/Makefile | 1 +
fftools/ffmpeg.c | 14 +
fftools/ffmpeg_json.c | 831 ++++++++++++++++++++++++++++++++++++++++++
fftools/ffmpeg_json.h | 76 ++++
fftools/ffmpeg_opt.c | 12 +
5 files changed, 934 insertions(+)
create mode 100644 fftools/ffmpeg_json.c
create mode 100644 fftools/ffmpeg_json.h
diff --git a/fftools/Makefile b/fftools/Makefile
index 01b16fa8f4..e6eea056eb 100644
--- a/fftools/Makefile
+++ b/fftools/Makefile
@@ -17,6 +17,7 @@ OBJS-ffmpeg += \
fftools/ffmpeg_enc.o \
fftools/ffmpeg_filter.o \
fftools/ffmpeg_hw.o \
+ fftools/ffmpeg_json.o \
fftools/ffmpeg_mux.o \
fftools/ffmpeg_mux_init.o \
fftools/ffmpeg_opt.o \
diff --git a/fftools/ffmpeg.c b/fftools/ffmpeg.c
index b394243f59..5dcd641700 100644
--- a/fftools/ffmpeg.c
+++ b/fftools/ffmpeg.c
@@ -82,6 +82,7 @@
#include "compat/android/binder.h"
#endif
#include "ffmpeg.h"
+#include "ffmpeg_json.h"
#include "ffmpeg_sched.h"
#include "ffmpeg_utils.h"
#include "graph/graphprint.h"
@@ -984,6 +985,8 @@ int main(int argc, char **argv)
int ret;
BenchmarkTimeStamps ti;
+ int json_argc = 0;
+ char **json_argv = NULL;
init_dynload();
@@ -999,6 +1002,15 @@ int main(int argc, char **argv)
show_banner(argc, argv, options);
+ /* Handle -json_cmd <file>: parse JSON file into argc/argv */
+ if (argc == 3 && !strcmp(argv[1], "-json_cmd")) {
+ ret = ffmpeg_json_parse_file(argv[2], &json_argc, &json_argv);
+ if (ret < 0)
+ goto finish;
+ argc = json_argc;
+ argv = json_argv;
+ }
+
sch = sch_alloc();
if (!sch) {
ret = AVERROR(ENOMEM);
@@ -1051,6 +1063,8 @@ finish:
sch_free(&sch);
+ ffmpeg_json_free_argv(json_argc, &json_argv);
+
av_log(NULL, AV_LOG_VERBOSE, "\n");
av_log(NULL, AV_LOG_VERBOSE, "Exiting with exit code %d\n", ret);
diff --git a/fftools/ffmpeg_json.c b/fftools/ffmpeg_json.c
new file mode 100644
index 0000000000..3fdc1884e5
--- /dev/null
+++ b/fftools/ffmpeg_json.c
@@ -0,0 +1,831 @@
+/*
+ * JSON command file support for FFmpeg
+ *
+ * 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
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "libavutil/avstring.h"
+#include "libavutil/avutil.h"
+#include "libavutil/error.h"
+#include "libavutil/log.h"
+#include "libavutil/mem.h"
+
+#include "cmdutils.h"
+#include "ffmpeg.h"
+#include "ffmpeg_json.h"
+
+/* ------------------------------------------------------------------ */
+/* Minimal JSON parser */
+/* ------------------------------------------------------------------ */
+
+/** JSON value type tag. */
+typedef enum {
+ JSON_NULL,
+ JSON_BOOL,
+ JSON_NUMBER,
+ JSON_STRING,
+ JSON_ARRAY,
+ JSON_OBJECT,
+} JsonType;
+
+typedef struct JsonValue JsonValue;
+
+/** Single key/value pair in a JSON object. */
+typedef struct JsonObjectEntry {
+ char *key; /**< property name */
+ JsonValue *value; /**< property value */
+} JsonObjectEntry;
+
+/** Parsed JSON value node. */
+struct JsonValue {
+ JsonType type; /**< discriminator tag */
+ union {
+ int bool_val; /**< JSON_BOOL */
+ double num_val; /**< JSON_NUMBER */
+ char *str_val; /**< JSON_STRING */
+ struct { /**< JSON_ARRAY */
+ JsonValue **items; /**< array elements */
+ int nb_items; /**< element count */
+ } arr;
+ struct { /**< JSON_OBJECT */
+ JsonObjectEntry *entries; /**< key/value pairs */
+ int nb_entries; /**< pair count */
+ } obj;
+ } u;
+};
+
+/* Forward declarations */
+static JsonValue *json_parse_value(const char **p);
+static void json_free(JsonValue *v);
+
+static void skip_whitespace(const char **p)
+{
+ while (**p == ' ' || **p == '\t' || **p == '\n' || **p == '\r')
+ (*p)++;
+}
+
+static JsonValue *json_alloc(JsonType type)
+{
+ JsonValue *v = av_mallocz(sizeof(*v));
+ if (v)
+ v->type = type;
+ return v;
+}
+
+/**
+ * Parse a JSON string literal.
+ * The opening '"' must be the current character at @p *p.
+ *
+ * @param p pointer to the current parse position (advanced past
+ * the closing quote on success)
+ * @return heap-allocated string, or NULL on error
+ */
+static char *json_parse_string_raw(const char **p)
+{
+ const char *s;
+ char *out, *dst;
+ size_t len;
+
+ if (**p != '"')
+ return NULL;
+ (*p)++; /* skip opening quote */
+ s = *p;
+
+ /* First pass: find the closing quote and measure output length */
+ len = 0;
+ while (*s && *s != '"') {
+ if (*s == '\\') {
+ s++;
+ if (!*s)
+ return NULL;
+ if (*s == 'u') {
+ /* \uXXXX – simplified: just store as-is in UTF-8 later */
+ if (!s[1] || !s[2] || !s[3] || !s[4])
+ return NULL;
+ s += 4;
+ len += 4; /* conservative upper bound for UTF-8 */
+ }
+ }
+ len++;
+ s++;
+ }
+ if (*s != '"')
+ return NULL;
+
+ out = av_malloc(len + 1);
+ if (!out)
+ return NULL;
+
+ dst = out;
+ while (**p != '"') {
+ if (**p == '\\') {
+ (*p)++;
+ switch (**p) {
+ case '"': *dst++ = '"'; break;
+ case '\\': *dst++ = '\\'; break;
+ case '/': *dst++ = '/'; break;
+ case 'b': *dst++ = '\b'; break;
+ case 'f': *dst++ = '\f'; break;
+ case 'n': *dst++ = '\n'; break;
+ case 'r': *dst++ = '\r'; break;
+ case 't': *dst++ = '\t'; break;
+ case 'u': {
+ /* Simplified \uXXXX: decode to UTF-8 */
+ unsigned cp = 0;
+ int i;
+ for (i = 0; i < 4; i++) {
+ (*p)++;
+ cp <<= 4;
+ if (**p >= '0' && **p <= '9') cp |= **p - '0';
+ else if (**p >= 'a' && **p <= 'f') cp |= **p - 'a' + 10;
+ else if (**p >= 'A' && **p <= 'F') cp |= **p - 'A' + 10;
+ else { av_free(out); return NULL; }
+ }
+ if (cp < 0x80) {
+ *dst++ = (char)cp;
+ } else if (cp < 0x800) {
+ *dst++ = (char)(0xC0 | (cp >> 6));
+ *dst++ = (char)(0x80 | (cp & 0x3F));
+ } else {
+ *dst++ = (char)(0xE0 | (cp >> 12));
+ *dst++ = (char)(0x80 | ((cp >> 6) & 0x3F));
+ *dst++ = (char)(0x80 | (cp & 0x3F));
+ }
+ break;
+ }
+ default:
+ av_free(out);
+ return NULL;
+ }
+ } else {
+ *dst++ = **p;
+ }
+ (*p)++;
+ }
+ *dst = '\0';
+ (*p)++; /* skip closing quote */
+ return out;
+}
+
+/** Parse a JSON string and wrap it in a JsonValue. */
+static JsonValue *json_parse_string(const char **p)
+{
+ JsonValue *v;
+ char *s = json_parse_string_raw(p);
+ if (!s)
+ return NULL;
+ v = json_alloc(JSON_STRING);
+ if (!v) {
+ av_free(s);
+ return NULL;
+ }
+ v->u.str_val = s;
+ return v;
+}
+
+/** Parse a JSON number literal and return it as a JsonValue. */
+static JsonValue *json_parse_number(const char **p)
+{
+ JsonValue *v;
+ char *end;
+ double d;
+
+ d = strtod(*p, &end);
+ if (end == *p)
+ return NULL;
+ v = json_alloc(JSON_NUMBER);
+ if (!v)
+ return NULL;
+ v->u.num_val = d;
+ *p = end;
+ return v;
+}
+
+/** Parse a JSON array and return it as a JsonValue. */
+static JsonValue *json_parse_array(const char **p)
+{
+ JsonValue *v = json_alloc(JSON_ARRAY);
+ if (!v)
+ return NULL;
+
+ (*p)++; /* skip '[' */
+ skip_whitespace(p);
+
+ if (**p == ']') {
+ (*p)++;
+ return v;
+ }
+
+ for (;;) {
+ JsonValue *item;
+ JsonValue **new_items;
+
+ skip_whitespace(p);
+ item = json_parse_value(p);
+ if (!item)
+ goto fail;
+
+ new_items = av_realloc_array(v->u.arr.items,
+ v->u.arr.nb_items + 1,
+ sizeof(*v->u.arr.items));
+ if (!new_items) {
+ json_free(item);
+ goto fail;
+ }
+ v->u.arr.items = new_items;
+ v->u.arr.items[v->u.arr.nb_items++] = item;
+
+ skip_whitespace(p);
+ if (**p == ']') {
+ (*p)++;
+ return v;
+ }
+ if (**p != ',')
+ goto fail;
+ (*p)++;
+ }
+
+fail:
+ json_free(v);
+ return NULL;
+}
+
+/** Parse a JSON object and return it as a JsonValue. */
+static JsonValue *json_parse_object(const char **p)
+{
+ JsonValue *v = json_alloc(JSON_OBJECT);
+ if (!v)
+ return NULL;
+
+ (*p)++; /* skip '{' */
+ skip_whitespace(p);
+
+ if (**p == '}') {
+ (*p)++;
+ return v;
+ }
+
+ for (;;) {
+ char *key;
+ JsonValue *val;
+ JsonObjectEntry *new_entries;
+
+ skip_whitespace(p);
+ key = json_parse_string_raw(p);
+ if (!key)
+ goto fail;
+
+ skip_whitespace(p);
+ if (**p != ':') {
+ av_free(key);
+ goto fail;
+ }
+ (*p)++;
+
+ skip_whitespace(p);
+ val = json_parse_value(p);
+ if (!val) {
+ av_free(key);
+ goto fail;
+ }
+
+ new_entries = av_realloc_array(v->u.obj.entries,
+ v->u.obj.nb_entries + 1,
+ sizeof(*v->u.obj.entries));
+ if (!new_entries) {
+ av_free(key);
+ json_free(val);
+ goto fail;
+ }
+ v->u.obj.entries = new_entries;
+ v->u.obj.entries[v->u.obj.nb_entries].key = key;
+ v->u.obj.entries[v->u.obj.nb_entries].value = val;
+ v->u.obj.nb_entries++;
+
+ skip_whitespace(p);
+ if (**p == '}') {
+ (*p)++;
+ return v;
+ }
+ if (**p != ',')
+ goto fail;
+ (*p)++;
+ }
+
+fail:
+ json_free(v);
+ return NULL;
+}
+
+/** Dispatch to the appropriate parser based on the next character. */
+static JsonValue *json_parse_value(const char **p)
+{
+ skip_whitespace(p);
+
+ switch (**p) {
+ case '"':
+ return json_parse_string(p);
+ case '{':
+ return json_parse_object(p);
+ case '[':
+ return json_parse_array(p);
+ case 't':
+ if (strncmp(*p, "true", 4) == 0) {
+ JsonValue *v = json_alloc(JSON_BOOL);
+ if (v)
+ v->u.bool_val = 1;
+ *p += 4;
+ return v;
+ }
+ return NULL;
+ case 'f':
+ if (strncmp(*p, "false", 5) == 0) {
+ JsonValue *v = json_alloc(JSON_BOOL);
+ if (v)
+ v->u.bool_val = 0;
+ *p += 5;
+ return v;
+ }
+ return NULL;
+ case 'n':
+ if (strncmp(*p, "null", 4) == 0) {
+ JsonValue *v = json_alloc(JSON_NULL);
+ *p += 4;
+ return v;
+ }
+ return NULL;
+ default:
+ if (**p == '-' || (**p >= '0' && **p <= '9'))
+ return json_parse_number(p);
+ return NULL;
+ }
+}
+
+/** Recursively free a parsed JSON value tree. */
+static void json_free(JsonValue *v)
+{
+ int i;
+ if (!v)
+ return;
+ switch (v->type) {
+ case JSON_STRING:
+ av_free(v->u.str_val);
+ break;
+ case JSON_ARRAY:
+ for (i = 0; i < v->u.arr.nb_items; i++)
+ json_free(v->u.arr.items[i]);
+ av_free(v->u.arr.items);
+ break;
+ case JSON_OBJECT:
+ for (i = 0; i < v->u.obj.nb_entries; i++) {
+ av_free(v->u.obj.entries[i].key);
+ json_free(v->u.obj.entries[i].value);
+ }
+ av_free(v->u.obj.entries);
+ break;
+ default:
+ break;
+ }
+ av_free(v);
+}
+
+/** Look up a key in a JSON object, returning the value or NULL. */
+static JsonValue *json_object_get(const JsonValue *obj, const char *key)
+{
+ int i;
+ if (!obj || obj->type != JSON_OBJECT)
+ return NULL;
+ for (i = 0; i < obj->u.obj.nb_entries; i++) {
+ if (strcmp(obj->u.obj.entries[i].key, key) == 0)
+ return obj->u.obj.entries[i].value;
+ }
+ return NULL;
+}
+
+/* ------------------------------------------------------------------ */
+/* argv builder */
+/* ------------------------------------------------------------------ */
+
+/** Growable argument vector used while building argc/argv. */
+typedef struct ArgvBuilder {
+ char **args; /**< argument strings */
+ int nb_args; /**< current argument count */
+ int capacity; /**< allocated slots in @p args */
+} ArgvBuilder;
+
+/** Append a single argument string to the builder. */
+static int argv_add(ArgvBuilder *b, const char *arg)
+{
+ char **new_args;
+
+ if (b->nb_args >= b->capacity) {
+ int new_cap = b->capacity ? b->capacity * 2 : 32;
+ new_args = av_realloc_array(b->args, new_cap, sizeof(*b->args));
+ if (!new_args)
+ return AVERROR(ENOMEM);
+ b->args = new_args;
+ b->capacity = new_cap;
+ }
+ b->args[b->nb_args] = av_strdup(arg);
+ if (!b->args[b->nb_args])
+ return AVERROR(ENOMEM);
+ b->nb_args++;
+ return 0;
+}
+
+/** Free all strings and reset the builder to empty. */
+static void argv_builder_free(ArgvBuilder *b)
+{
+ int i;
+ for (i = 0; i < b->nb_args; i++)
+ av_free(b->args[i]);
+ av_free(b->args);
+ memset(b, 0, sizeof(*b));
+}
+
+/**
+ * Add a JSON value as a CLI argument string to the argv builder.
+ * Handles strings of any length (no fixed buffer for JSON_STRING).
+ */
+static int argv_add_json_value(ArgvBuilder *b, const JsonValue *v)
+{
+ char buf[64];
+ switch (v->type) {
+ case JSON_STRING:
+ return argv_add(b, v->u.str_val);
+ case JSON_NUMBER:
+ /* Print integers without decimal point where possible */
+ if (v->u.num_val == (int64_t)v->u.num_val)
+ snprintf(buf, sizeof(buf), "%" PRId64, (int64_t)v->u.num_val);
+ else
+ snprintf(buf, sizeof(buf), "%g", v->u.num_val);
+ return argv_add(b, buf);
+ case JSON_BOOL:
+ snprintf(buf, sizeof(buf), "%d", v->u.bool_val);
+ return argv_add(b, buf);
+ default:
+ return AVERROR(EINVAL);
+ }
+}
+
+/**
+ * Build an option key string for the argv builder.
+ *
+ * Keys starting with "/" use FFmpeg's file-loading syntax: -/option filename
+ * reads the option value from a file. In JSON this is represented as:
+ * "/filter_complex": "graph.txt" => -/filter_complex graph.txt
+ *
+ * All other keys are emitted as -key.
+ */
+static int argv_add_option_key(ArgvBuilder *b, const char *key)
+{
+ char buf[256];
+ if (key[0] == '/') {
+ /* File-loading syntax: -/option */
+ snprintf(buf, sizeof(buf), "-%s", key);
+ } else {
+ snprintf(buf, sizeof(buf), "-%s", key);
+ }
+ return argv_add(b, buf);
+}
+
+/**
+ * Emit the options from an object into the argv builder.
+ *
+ * Each key becomes "-key" and each value becomes the argument:
+ * "c:v": "libx264" => -c:v libx264
+ * "y": true => -y
+ * "an": false => (skipped; use "an": true to disable audio)
+ * "map": ["0:v","0:a"] => -map 0:v -map 0:a
+ * "/filter_complex": "f.txt" => -/filter_complex f.txt (load from file)
+ */
+static int emit_options(ArgvBuilder *b, const JsonValue *opts)
+{
+ int i, j, ret;
+
+ if (!opts)
+ return 0;
+ if (opts->type != JSON_OBJECT) {
+ av_log(NULL, AV_LOG_ERROR,
+ "JSON: \"options\" must be an object.\n");
+ return AVERROR(EINVAL);
+ }
+
+ for (i = 0; i < opts->u.obj.nb_entries; i++) {
+ const char *key = opts->u.obj.entries[i].key;
+ const JsonValue *val = opts->u.obj.entries[i].value;
+
+ if (val->type == JSON_BOOL) {
+ if (val->u.bool_val) {
+ /* -flag */
+ ret = argv_add_option_key(b, key);
+ if (ret < 0) return ret;
+ }
+ /* false booleans are intentionally skipped (no-op) */
+ continue;
+ }
+
+ if (val->type == JSON_ARRAY) {
+ /* Repeated option: -key elem0 -key elem1 ... */
+ for (j = 0; j < val->u.arr.nb_items; j++) {
+ ret = argv_add_option_key(b, key);
+ if (ret < 0) return ret;
+
+ ret = argv_add_json_value(b, val->u.arr.items[j]);
+ if (ret < 0) {
+ av_log(NULL, AV_LOG_ERROR,
+ "JSON: unsupported type in array for option
\"%s\".\n", key);
+ return ret;
+ }
+ }
+ continue;
+ }
+
+ if (val->type == JSON_NULL) {
+ continue;
+ }
+
+ /* Scalar: -key value */
+ ret = argv_add_option_key(b, key);
+ if (ret < 0) return ret;
+
+ ret = argv_add_json_value(b, val);
+ if (ret < 0) {
+ av_log(NULL, AV_LOG_ERROR,
+ "JSON: unsupported value type for option \"%s\".\n", key);
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* Public API */
+/* ------------------------------------------------------------------ */
+
+/** @sa ffmpeg_json_parse_file() in ffmpeg_json.h for full documentation. */
+int ffmpeg_json_parse_file(const char *filename, int *out_argc, char
***out_argv)
+{
+ FILE *f = NULL;
+ long fsize;
+ char *data = NULL;
+ const char *p;
+ JsonValue *root = NULL;
+ JsonValue *global_opts, *inputs, *outputs, *decoders;
+ ArgvBuilder b = { 0 };
+ int ret, i;
+
+ *out_argc = 0;
+ *out_argv = NULL;
+
+ /* Read file contents */
+ f = fopen(filename, "rb");
+ if (!f) {
+ av_log(NULL, AV_LOG_ERROR,
+ "JSON: cannot open file '%s': %s\n", filename, strerror(errno));
+ return AVERROR(errno);
+ }
+
+ if (fseek(f, 0, SEEK_END) < 0) {
+ ret = AVERROR(errno);
+ goto fail;
+ }
+ fsize = ftell(f);
+ if (fsize < 0 || fsize > 100 * 1024 * 1024) { /* 100 MB sanity limit */
+ av_log(NULL, AV_LOG_ERROR,
+ "JSON: file '%s' is too large or unreadable.\n", filename);
+ ret = AVERROR(EINVAL);
+ goto fail;
+ }
+ if (fseek(f, 0, SEEK_SET) < 0) {
+ ret = AVERROR(errno);
+ goto fail;
+ }
+
+ data = av_malloc(fsize + 1);
+ if (!data) {
+ ret = AVERROR(ENOMEM);
+ goto fail;
+ }
+ if (fread(data, 1, fsize, f) != (size_t)fsize) {
+ av_log(NULL, AV_LOG_ERROR, "JSON: failed to read '%s'.\n", filename);
+ ret = AVERROR(EIO);
+ goto fail;
+ }
+ data[fsize] = '\0';
+ fclose(f);
+ f = NULL;
+
+ /* Parse JSON */
+ p = data;
+ root = json_parse_value(&p);
+ if (!root || root->type != JSON_OBJECT) {
+ av_log(NULL, AV_LOG_ERROR,
+ "JSON: failed to parse '%s' – root must be an object.\n",
filename);
+ ret = AVERROR(EINVAL);
+ goto fail;
+ }
+
+ /* Verify there are no trailing non-whitespace characters */
+ skip_whitespace(&p);
+ if (*p != '\0') {
+ av_log(NULL, AV_LOG_ERROR,
+ "JSON: trailing content after root object in '%s'.\n",
filename);
+ ret = AVERROR(EINVAL);
+ goto fail;
+ }
+
+ /* Build argv: start with program name */
+ ret = argv_add(&b, "ffmpeg");
+ if (ret < 0) goto fail;
+
+ /* 1. Global options */
+ global_opts = json_object_get(root, "global_options");
+ if (global_opts) {
+ ret = emit_options(&b, global_opts);
+ if (ret < 0) goto fail;
+ }
+
+ /* 2. Input files: options then -i url */
+ inputs = json_object_get(root, "inputs");
+ if (inputs) {
+ if (inputs->type != JSON_ARRAY) {
+ av_log(NULL, AV_LOG_ERROR, "JSON: \"inputs\" must be an array.\n");
+ ret = AVERROR(EINVAL);
+ goto fail;
+ }
+ for (i = 0; i < inputs->u.arr.nb_items; i++) {
+ JsonValue *input = inputs->u.arr.items[i];
+ JsonValue *url, *opts;
+
+ if (input->type != JSON_OBJECT) {
+ av_log(NULL, AV_LOG_ERROR,
+ "JSON: each input must be an object.\n");
+ ret = AVERROR(EINVAL);
+ goto fail;
+ }
+
+ url = json_object_get(input, "url");
+ if (!url || url->type != JSON_STRING) {
+ av_log(NULL, AV_LOG_ERROR,
+ "JSON: input %d missing \"url\" string.\n", i);
+ ret = AVERROR(EINVAL);
+ goto fail;
+ }
+
+ /* Emit input options before -i */
+ opts = json_object_get(input, "options");
+ ret = emit_options(&b, opts);
+ if (ret < 0) goto fail;
+
+ ret = argv_add(&b, "-i");
+ if (ret < 0) goto fail;
+ ret = argv_add(&b, url->u.str_val);
+ if (ret < 0) goto fail;
+ }
+ }
+
+ /* 3. Output files: options then url */
+ outputs = json_object_get(root, "outputs");
+ if (outputs) {
+ if (outputs->type != JSON_ARRAY) {
+ av_log(NULL, AV_LOG_ERROR, "JSON: \"outputs\" must be an
array.\n");
+ ret = AVERROR(EINVAL);
+ goto fail;
+ }
+ for (i = 0; i < outputs->u.arr.nb_items; i++) {
+ JsonValue *output = outputs->u.arr.items[i];
+ JsonValue *url, *opts;
+
+ if (output->type != JSON_OBJECT) {
+ av_log(NULL, AV_LOG_ERROR,
+ "JSON: each output must be an object.\n");
+ ret = AVERROR(EINVAL);
+ goto fail;
+ }
+
+ url = json_object_get(output, "url");
+ if (!url || url->type != JSON_STRING) {
+ av_log(NULL, AV_LOG_ERROR,
+ "JSON: output %d missing \"url\" string.\n", i);
+ ret = AVERROR(EINVAL);
+ goto fail;
+ }
+
+ /* Emit output options before the output url */
+ opts = json_object_get(output, "options");
+ ret = emit_options(&b, opts);
+ if (ret < 0) goto fail;
+
+ ret = argv_add(&b, url->u.str_val);
+ if (ret < 0) goto fail;
+ }
+ }
+
+ /* 4. Loopback decoders: options then -dec name */
+ decoders = json_object_get(root, "decoders");
+ if (decoders) {
+ if (decoders->type != JSON_ARRAY) {
+ av_log(NULL, AV_LOG_ERROR, "JSON: \"decoders\" must be an
array.\n");
+ ret = AVERROR(EINVAL);
+ goto fail;
+ }
+ for (i = 0; i < decoders->u.arr.nb_items; i++) {
+ JsonValue *decoder = decoders->u.arr.items[i];
+ JsonValue *name, *opts;
+
+ if (decoder->type != JSON_OBJECT) {
+ av_log(NULL, AV_LOG_ERROR,
+ "JSON: each decoder must be an object.\n");
+ ret = AVERROR(EINVAL);
+ goto fail;
+ }
+
+ name = json_object_get(decoder, "name");
+ if (!name || name->type != JSON_STRING) {
+ av_log(NULL, AV_LOG_ERROR,
+ "JSON: decoder %d missing \"name\" string.\n", i);
+ ret = AVERROR(EINVAL);
+ goto fail;
+ }
+
+ /* Emit decoder options before -dec */
+ opts = json_object_get(decoder, "options");
+ ret = emit_options(&b, opts);
+ if (ret < 0) goto fail;
+
+ ret = argv_add(&b, "-dec");
+ if (ret < 0) goto fail;
+ ret = argv_add(&b, name->u.str_val);
+ if (ret < 0) goto fail;
+ }
+ }
+
+ /* Add NULL sentinel for execv-style usage */
+ if (b.nb_args >= b.capacity) {
+ char **new_args = av_realloc_array(b.args, b.nb_args + 1,
sizeof(*b.args));
+ if (!new_args) {
+ ret = AVERROR(ENOMEM);
+ goto fail;
+ }
+ b.args = new_args;
+ }
+ b.args[b.nb_args] = NULL;
+
+ *out_argc = b.nb_args;
+ *out_argv = b.args;
+
+ /* Log the constructed command line */
+ {
+ int k;
+ av_log(NULL, AV_LOG_INFO, "JSON: constructed command line:\n ");
+ for (k = 0; k < b.nb_args; k++)
+ av_log(NULL, AV_LOG_INFO, " %s", b.args[k]);
+ av_log(NULL, AV_LOG_INFO, "\n");
+ }
+
+ json_free(root);
+ av_free(data);
+ return 0;
+
+fail:
+ if (f)
+ fclose(f);
+ json_free(root);
+ av_free(data);
+ argv_builder_free(&b);
+ return ret;
+}
+
+/** @sa ffmpeg_json_free_argv() in ffmpeg_json.h for full documentation. */
+void ffmpeg_json_free_argv(int argc, char ***argv)
+{
+ int i;
+ if (!argv || !*argv)
+ return;
+ for (i = 0; i < argc; i++)
+ av_free((*argv)[i]);
+ av_free(*argv);
+ *argv = NULL;
+}
diff --git a/fftools/ffmpeg_json.h b/fftools/ffmpeg_json.h
new file mode 100644
index 0000000000..d509f142d7
--- /dev/null
+++ b/fftools/ffmpeg_json.h
@@ -0,0 +1,76 @@
+/*
+ * JSON command file support for FFmpeg
+ *
+ * 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
+ */
+
+#ifndef FFTOOLS_FFMPEG_JSON_H
+#define FFTOOLS_FFMPEG_JSON_H
+
+/**
+ * Parse a JSON command file and convert it to an argc/argv pair
+ * compatible with ffmpeg_parse_options().
+ *
+ * The JSON file should have the following structure:
+ * {
+ * "global_options": { "option_name": "value", ... },
+ * "inputs": [
+ * { "url": "input_file", "options": { "option_name": "value", ... } },
+ * ...
+ * ],
+ * "outputs": [
+ * { "url": "output_file", "options": { "option_name": "value", ... } },
+ * ...
+ * ],
+ * "decoders": [
+ * { "name": "decoder_name", "options": { ... } },
+ * ...
+ * ]
+ * }
+ *
+ * Option values can be:
+ * - string: added as -option value (no length limit)
+ * - true: added as -option (flag)
+ * - false: skipped (no-op); to negate a boolean, use the "no"-prefixed
+ * name with true, e.g. "noautorotate": true
+ * - number: converted to string and added as -option value
+ * - array: option is repeated for each element, e.g. "map": ["0:v","0:a"]
+ * becomes -map 0:v -map 0:a
+ * - null: skipped
+ *
+ * Keys starting with "/" use FFmpeg's file-loading syntax:
+ * "/filter_complex": "graph.txt" => -/filter_complex graph.txt
+ *
+ * All fields except the root object are optional.
+ *
+ * @param filename Path to the JSON file
+ * @param[out] argc Number of arguments in the constructed argv
+ * @param[out] argv Constructed argument vector (caller must free with
+ * ffmpeg_json_free_argv())
+ * @return 0 on success, negative AVERROR code on failure
+ */
+int ffmpeg_json_parse_file(const char *filename, int *argc, char ***argv);
+
+/**
+ * Free an argv array previously allocated by ffmpeg_json_parse_file().
+ *
+ * @param argc Number of arguments
+ * @param argv Pointer to the argument vector (set to NULL on return)
+ */
+void ffmpeg_json_free_argv(int argc, char ***argv);
+
+#endif /* FFTOOLS_FFMPEG_JSON_H */
diff --git a/fftools/ffmpeg_opt.c b/fftools/ffmpeg_opt.c
index 48e6816c19..abdc81988c 100644
--- a/fftools/ffmpeg_opt.c
+++ b/fftools/ffmpeg_opt.c
@@ -1610,6 +1610,15 @@ static int opt_adrift_threshold(void *optctx, const char
*opt, const char *arg)
}
#endif
+static int opt_json_cmd(void *optctx, const char *opt, const char *arg)
+{
+ av_log(NULL, AV_LOG_ERROR,
+ "-json_cmd requires exactly one argument: the path to a JSON file\n"
+ "containing ffmpeg commands and parameters. No other options are
allowed.\n"
+ "Usage: ffmpeg -json_cmd <file.json>\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 };
@@ -1622,6 +1631,9 @@ static const char *const alt_tag[] = { "atag",
"vtag", "stag", NULL }
const OptionDef options[] = {
/* main options */
CMDUTILS_COMMON_OPTIONS
+ { "json_cmd", OPT_TYPE_FUNC, OPT_FUNC_ARG | OPT_EXIT,
+ { .func_arg = opt_json_cmd },
+ "read options from a JSON command file", "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]