diff mbox series

[FFmpeg-devel,v2,06/29] lavu/opt: add array options

Message ID 20240307152416.5151-1-anton@khirnov.net
State New
Headers show
Series None | expand

Commit Message

Anton Khirnov March 7, 2024, 3:24 p.m. UTC
---
 doc/APIchanges        |   3 +
 libavutil/opt.c       | 360 +++++++++++++++++++++++++++++++++++++-----
 libavutil/opt.h       |  56 ++++++-
 libavutil/tests/opt.c |  49 ++++++
 tests/ref/fate/opt    |  35 +++-
 5 files changed, 458 insertions(+), 45 deletions(-)
diff mbox series

Patch

diff --git a/doc/APIchanges b/doc/APIchanges
index 4f7612e449..6e2c851e31 100644
--- a/doc/APIchanges
+++ b/doc/APIchanges
@@ -2,6 +2,9 @@  The last version increases of all libraries were on 2024-03-07
 
 API changes, most recent first:
 
+2024-02-xx - xxxxxxxxxx - lavu 58.xx.100 - opt.h
+  Add AV_OPT_TYPE_FLAG_ARRAY and AVOptionArrayDef.
+
 2024-03-06 - xxxxxxxxxx - lavf 60.25.100 - avformat.h
   Deprecate av_fmt_ctx_get_duration_estimation_method().
   The relevant field is public and needs no getter to access.
diff --git a/libavutil/opt.c b/libavutil/opt.c
index 06d3056a37..685206f416 100644
--- a/libavutil/opt.c
+++ b/libavutil/opt.c
@@ -43,6 +43,8 @@ 
 
 #include <float.h>
 
+#define TYPE_BASE(type) ((type) & ~AV_OPT_TYPE_FLAG_ARRAY)
+
 const AVOption *av_opt_next(const void *obj, const AVOption *last)
 {
     const AVClass *class;
@@ -100,6 +102,54 @@  static int opt_is_pod(enum AVOptionType type)
     return 0;
 }
 
+static uint8_t opt_array_sep(const AVOption *o)
+{
+    const AVOptionArrayDef *d = o->default_val.arr;
+    av_assert1(o->type & AV_OPT_TYPE_FLAG_ARRAY);
+    return (d && d->sep) ? d->sep : ',';
+}
+
+static void *opt_array_pelem(const AVOption *o, void *array, unsigned idx)
+{
+    av_assert1(o->type & AV_OPT_TYPE_FLAG_ARRAY);
+    return (uint8_t *)array + idx * opt_elem_size[TYPE_BASE(o->type)];
+}
+
+static unsigned *opt_array_pcount(const void *parray)
+{
+    return (unsigned *)((const void * const *)parray + 1);
+}
+
+static void opt_free_elem(const AVOption *o, void *ptr)
+{
+    switch (TYPE_BASE(o->type)) {
+    case AV_OPT_TYPE_STRING:
+    case AV_OPT_TYPE_BINARY:
+        av_freep(ptr);
+        break;
+
+    case AV_OPT_TYPE_DICT:
+        av_dict_free((AVDictionary **)ptr);
+        break;
+
+    case AV_OPT_TYPE_CHLAYOUT:
+        av_channel_layout_uninit((AVChannelLayout *)ptr);
+        break;
+
+    default:
+        break;
+    }
+}
+
+static void opt_free_array(const AVOption *o, void *parray, unsigned *count)
+{
+    for (unsigned i = 0; i < *count; i++)
+        opt_free_elem(o, opt_array_pelem(o, *(void **)parray, i));
+
+    av_freep(parray);
+    *count = 0;
+}
+
 static int read_number(const AVOption *o, const void *dst, double *num, int *den, int64_t *intnum)
 {
     switch (o->type) {
@@ -140,14 +190,16 @@  static int read_number(const AVOption *o, const void *dst, double *num, int *den
 
 static int write_number(void *obj, const AVOption *o, void *dst, double num, int den, int64_t intnum)
 {
-    if (o->type != AV_OPT_TYPE_FLAGS &&
+    const enum AVOptionType type = TYPE_BASE(o->type);
+
+    if (type != AV_OPT_TYPE_FLAGS &&
         (!den || o->max * den < num * intnum || o->min * den > num * intnum)) {
         num = den ? num * intnum / den : (num && intnum ? INFINITY : NAN);
         av_log(obj, AV_LOG_ERROR, "Value %f for parameter '%s' out of range [%g - %g]\n",
                num, o->name, o->min, o->max);
         return AVERROR(ERANGE);
     }
-    if (o->type == AV_OPT_TYPE_FLAGS) {
+    if (type == AV_OPT_TYPE_FLAGS) {
         double d = num*intnum/den;
         if (d < -1.5 || d > 0xFFFFFFFF+0.5 || (llrint(d*256) & 255)) {
             av_log(obj, AV_LOG_ERROR,
@@ -157,7 +209,7 @@  static int write_number(void *obj, const AVOption *o, void *dst, double num, int
         }
     }
 
-    switch (o->type) {
+    switch (type) {
     case AV_OPT_TYPE_PIXEL_FMT:
         *(enum AVPixelFormat *)dst = llrint(num / den) * intnum;
         break;
@@ -271,9 +323,10 @@  static int set_string(void *obj, const AVOption *o, const char *val, uint8_t **d
 
 static int set_string_number(void *obj, void *target_obj, const AVOption *o, const char *val, void *dst)
 {
+    const enum AVOptionType type = TYPE_BASE(o->type);
     int ret = 0;
 
-    if (o->type == AV_OPT_TYPE_RATIONAL || o->type == AV_OPT_TYPE_VIDEO_RATE) {
+    if (type == AV_OPT_TYPE_RATIONAL || type == AV_OPT_TYPE_VIDEO_RATE) {
         int num, den;
         char c;
         if (sscanf(val, "%d%*1[:/]%d%c", &num, &den, &c) == 2) {
@@ -290,7 +343,7 @@  static int set_string_number(void *obj, void *target_obj, const AVOption *o, con
         double d;
         int64_t intnum = 1;
 
-        if (o->type == AV_OPT_TYPE_FLAGS) {
+        if (type == AV_OPT_TYPE_FLAGS) {
             if (*val == '+' || *val == '-')
                 cmd = *(val++);
             for (; i < sizeof(buf) - 1 && val[i] && val[i] != '+' && val[i] != '-'; i++)
@@ -346,7 +399,7 @@  static int set_string_number(void *obj, void *target_obj, const AVOption *o, con
                 }
             }
         }
-        if (o->type == AV_OPT_TYPE_FLAGS) {
+        if (type == AV_OPT_TYPE_FLAGS) {
             intnum = *(unsigned int*)dst;
             if (cmd == '+')
                 d = intnum | (int64_t)d;
@@ -531,18 +584,17 @@  static int set_string_channel_layout(void *obj, const AVOption *o,
 static int opt_set_elem(void *obj, void *target_obj, const AVOption *o,
                         const char *val, void *dst)
 {
+    const enum AVOptionType type = TYPE_BASE(o->type);
     int ret;
 
-FF_DISABLE_DEPRECATION_WARNINGS
-    if (!val && (o->type != AV_OPT_TYPE_STRING &&
-                 o->type != AV_OPT_TYPE_PIXEL_FMT && o->type != AV_OPT_TYPE_SAMPLE_FMT &&
-                 o->type != AV_OPT_TYPE_IMAGE_SIZE &&
-                 o->type != AV_OPT_TYPE_DURATION && o->type != AV_OPT_TYPE_COLOR &&
-                 o->type != AV_OPT_TYPE_BOOL))
+    if (!val && (type != AV_OPT_TYPE_STRING &&
+                 type != AV_OPT_TYPE_PIXEL_FMT && type != AV_OPT_TYPE_SAMPLE_FMT &&
+                 type != AV_OPT_TYPE_IMAGE_SIZE &&
+                 type != AV_OPT_TYPE_DURATION && type != AV_OPT_TYPE_COLOR &&
+                 type != AV_OPT_TYPE_BOOL))
         return AVERROR(EINVAL);
-FF_ENABLE_DEPRECATION_WARNINGS
 
-    switch (o->type) {
+    switch (type) {
     case AV_OPT_TYPE_BOOL:
         return set_string_bool(obj, o, val, dst);
     case AV_OPT_TYPE_STRING:
@@ -604,6 +656,85 @@  FF_ENABLE_DEPRECATION_WARNINGS
     return AVERROR(EINVAL);
 }
 
+static int opt_set_array(void *obj, void *target_obj, const AVOption *o,
+                         const char *val, void *dst)
+{
+    const AVOptionArrayDef *arr = o->default_val.arr;
+    const size_t      elem_size = opt_elem_size[TYPE_BASE(o->type)];
+    const uint8_t           sep = opt_array_sep(o);
+    uint8_t                *str = NULL;
+
+    void       *elems = NULL;
+    unsigned nb_elems = 0;
+    int ret;
+
+    if (val && *val) {
+        str = av_malloc(strlen(val) + 1);
+        if (!str)
+            return AVERROR(ENOMEM);
+    }
+
+    // split and unescape the string
+    while (val && *val) {
+        uint8_t *p = str;
+        void *tmp;
+
+        if (arr && arr->size_max && nb_elems >= arr->size_max) {
+            av_log(obj, AV_LOG_ERROR,
+                   "Cannot assign more than %u elements to array option %s\n",
+                   arr->size_max, o->name);
+            ret = AVERROR(EINVAL);
+            goto fail;
+        }
+
+        for (; *val; val++, p++) {
+            if (*val == '\\' && val[1])
+                val++;
+            else if (*val == sep) {
+                val++;
+                break;
+            }
+            *p = *val;
+        }
+        *p = 0;
+
+        tmp = av_realloc_array(elems, nb_elems + 1, elem_size);
+        if (!tmp) {
+            ret = AVERROR(ENOMEM);
+            goto fail;
+        }
+        elems = tmp;
+
+        tmp = opt_array_pelem(o, elems, nb_elems);
+        memset(tmp, 0, elem_size);
+
+        ret = opt_set_elem(obj, target_obj, o, str, tmp);
+        if (ret < 0)
+            goto fail;
+        nb_elems++;
+    }
+    av_freep(&str);
+
+    opt_free_array(o, dst, opt_array_pcount(dst));
+
+    if (arr && nb_elems < arr->size_min) {
+        av_log(obj, AV_LOG_ERROR,
+               "Cannot assign fewer than %u elements to array option %s\n",
+               arr->size_min, o->name);
+        ret = AVERROR(EINVAL);
+        goto fail;
+    }
+
+    *((void **)dst)        = elems;
+    *opt_array_pcount(dst) = nb_elems;
+
+    return 0;
+fail:
+    av_freep(&str);
+    opt_free_array(o, &elems, &nb_elems);
+    return ret;
+}
+
 int av_opt_set(void *obj, const char *name, const char *val, int search_flags)
 {
     void *dst, *target_obj;
@@ -619,7 +750,8 @@  int av_opt_set(void *obj, const char *name, const char *val, int search_flags)
 
     dst = ((uint8_t *)target_obj) + o->offset;
 
-    return opt_set_elem(obj, target_obj, o, val, dst);
+    return ((o->type & AV_OPT_TYPE_FLAG_ARRAY) ?
+            opt_set_array : opt_set_elem)(obj, target_obj, o, val, dst);
 }
 
 #define OPT_EVAL_NUMBER(name, opttype, vartype)                         \
@@ -647,7 +779,7 @@  static int set_number(void *obj, const char *name, double num, int den, int64_t
     if (!o || !target_obj)
         return AVERROR_OPTION_NOT_FOUND;
 
-    if (o->flags & AV_OPT_FLAG_READONLY)
+    if ((o->flags & AV_OPT_FLAG_READONLY) || (o->type & AV_OPT_TYPE_FLAG_ARRAY))
         return AVERROR(EINVAL);
 
     dst = ((uint8_t *)target_obj) + o->offset;
@@ -730,7 +862,8 @@  int av_opt_set_video_rate(void *obj, const char *name, AVRational val, int searc
         return AVERROR_OPTION_NOT_FOUND;
     if (o->type != AV_OPT_TYPE_VIDEO_RATE) {
         av_log(obj, AV_LOG_ERROR,
-               "The value set by option '%s' is not a video rate.\n", o->name);
+               "The value set by option '%s' is not a video rate.\n",
+               o->name);
         return AVERROR(EINVAL);
     }
     if (val.num <= 0 || val.den <= 0)
@@ -852,7 +985,7 @@  static int opt_get_elem(const AVOption *o, uint8_t **pbuf, size_t buf_len,
 {
     int ret;
 
-    switch (o->type) {
+    switch (TYPE_BASE(o->type)) {
     case AV_OPT_TYPE_BOOL:
         ret = snprintf(*pbuf, buf_len, "%s", get_bool_name(*(int *)dst));
         break;
@@ -949,6 +1082,66 @@  static int opt_get_elem(const AVOption *o, uint8_t **pbuf, size_t buf_len,
     return ret;
 }
 
+static int opt_get_array(const AVOption *o, void *dst, uint8_t **out_val)
+{
+    const unsigned count = *opt_array_pcount(dst);
+    const uint8_t    sep = opt_array_sep(o);
+
+    uint8_t *str     = NULL;
+    size_t   str_len = 0;
+    int ret;
+
+    *out_val = NULL;
+
+    for (unsigned i = 0; i < count; i++) {
+        uint8_t buf[128], *out = buf;
+        size_t out_len;
+
+        ret = opt_get_elem(o, &out, sizeof(buf),
+                           opt_array_pelem(o, *(void **)dst, i), 0);
+        if (ret < 0)
+            goto fail;
+
+        out_len = strlen(out);
+        if (out_len > SIZE_MAX / 2 - !!i ||
+            !!i + out_len * 2 > SIZE_MAX - str_len - 1) {
+            ret = AVERROR(ERANGE);
+            goto fail;
+        }
+
+        //                         terminator     escaping  separator
+        //                                ↓             ↓     ↓
+        ret = av_reallocp(&str, str_len + 1 + out_len * 2 + !!i);
+        if (ret < 0)
+            goto fail;
+
+        // add separator if needed
+        if (i)
+            str[str_len++] = sep;
+
+        // escape the element
+        for (unsigned j = 0; j < out_len; j++) {
+            uint8_t val = out[j];
+            if (val == sep || val == '\\')
+                str[str_len++] = '\\';
+            str[str_len++] = val;
+        }
+        str[str_len] = 0;
+
+fail:
+        if (out != buf)
+            av_freep(&out);
+        if (ret < 0) {
+            av_freep(&str);
+            return ret;
+        }
+    }
+
+    *out_val = str;
+
+    return 0;
+}
+
 int av_opt_get(void *obj, const char *name, int search_flags, uint8_t **out_val)
 {
     void *dst, *target_obj;
@@ -964,8 +1157,19 @@  int av_opt_get(void *obj, const char *name, int search_flags, uint8_t **out_val)
 
     dst = (uint8_t *)target_obj + o->offset;
 
-    buf[0] = 0;
+    if (o->type & AV_OPT_TYPE_FLAG_ARRAY) {
+        ret = opt_get_array(o, dst, out_val);
+        if (ret < 0)
+            return ret;
+        if (!*out_val && !(search_flags & AV_OPT_ALLOW_NULL)) {
+            *out_val = av_strdup("");
+            if (!*out_val)
+               return AVERROR(ENOMEM);
+        }
+        return 0;
+    }
 
+    buf[0] = 0;
     out = buf;
     ret = opt_get_elem(o, &out, sizeof(buf), dst, search_flags);
     if (ret < 0)
@@ -988,6 +1192,8 @@  static int get_number(void *obj, const char *name, double *num, int *den, int64_
     const AVOption *o = av_opt_find2(obj, name, NULL, 0, search_flags, &target_obj);
     if (!o || !target_obj)
         return AVERROR_OPTION_NOT_FOUND;
+    if (o->type & AV_OPT_TYPE_FLAG_ARRAY)
+        return AVERROR(EINVAL);
 
     dst = ((uint8_t *)target_obj) + o->offset;
 
@@ -1045,7 +1251,7 @@  int av_opt_get_image_size(void *obj, const char *name, int search_flags, int *w_
         return AVERROR_OPTION_NOT_FOUND;
     if (o->type != AV_OPT_TYPE_IMAGE_SIZE) {
         av_log(obj, AV_LOG_ERROR,
-               "The value for option '%s' is not an image size.\n", name);
+               "The value for option '%s' is not a image size.\n", name);
         return AVERROR(EINVAL);
     }
 
@@ -1252,11 +1458,16 @@  static void log_type(void *av_log_obj, const AVOption *o,
         [AV_OPT_TYPE_CHLAYOUT]      = "<channel_layout>",
         [AV_OPT_TYPE_BOOL]          = "<boolean>",
     };
+    const enum AVOptionType type = TYPE_BASE(o->type);
 
-    if (o->type == AV_OPT_TYPE_CONST && parent_type == AV_OPT_TYPE_INT)
+    if (o->type == AV_OPT_TYPE_CONST && TYPE_BASE(parent_type) == AV_OPT_TYPE_INT)
         av_log(av_log_obj, AV_LOG_INFO, "%-12"PRId64" ", o->default_val.i64);
-    else if (o->type < FF_ARRAY_ELEMS(desc) && desc[o->type])
-        av_log(av_log_obj, AV_LOG_INFO, "%-12s ", desc[o->type]);
+    else if (type < FF_ARRAY_ELEMS(desc) && desc[type]) {
+        if (o->type & AV_OPT_TYPE_FLAG_ARRAY)
+            av_log(av_log_obj, AV_LOG_INFO, "[%-10s]", desc[type]);
+        else
+            av_log(av_log_obj, AV_LOG_INFO, "%-12s ", desc[type]);
+    }
     else
         av_log(av_log_obj, AV_LOG_INFO, "%-12s ", "");
 }
@@ -1274,6 +1485,13 @@  static void log_default(void *obj, void *av_log_obj, const AVOption *opt)
         !opt->default_val.str)
         return;
 
+    if (opt->type & AV_OPT_TYPE_FLAG_ARRAY) {
+        const AVOptionArrayDef *arr = opt->default_val.arr;
+        if (arr && arr->def)
+            av_log(av_log_obj, AV_LOG_INFO, " (default %s)", arr->def);
+        return;
+    }
+
     av_log(av_log_obj, AV_LOG_INFO, " (default ");
     switch (opt->type) {
     case AV_OPT_TYPE_BOOL:
@@ -1434,6 +1652,21 @@  void av_opt_set_defaults2(void *s, int mask, int flags)
         if (opt->flags & AV_OPT_FLAG_READONLY)
             continue;
 
+        if (opt->type & AV_OPT_TYPE_FLAG_ARRAY) {
+            const AVOptionArrayDef *arr = opt->default_val.arr;
+            const char              sep = opt_array_sep(opt);
+
+            av_assert0(sep && sep != '\\' &&
+                       (sep < 'a' || sep > 'z') &&
+                       (sep < 'A' || sep > 'Z') &&
+                       (sep < '0' || sep > '9'));
+
+            if (arr && arr->def)
+                opt_set_array(s, s, opt, arr->def, dst);
+
+            continue;
+        }
+
         switch (opt->type) {
             case AV_OPT_TYPE_CONST:
                 /* Nothing to be done here */
@@ -1676,23 +1909,12 @@  void av_opt_free(void *obj)
 {
     const AVOption *o = NULL;
     while ((o = av_opt_next(obj, o))) {
-        switch (o->type) {
-        case AV_OPT_TYPE_STRING:
-        case AV_OPT_TYPE_BINARY:
-            av_freep((uint8_t *)obj + o->offset);
-            break;
+        void *pitem = (uint8_t *)obj + o->offset;
 
-        case AV_OPT_TYPE_DICT:
-            av_dict_free((AVDictionary **)(((uint8_t *)obj) + o->offset));
-            break;
-
-        case AV_OPT_TYPE_CHLAYOUT:
-            av_channel_layout_uninit((AVChannelLayout *)(((uint8_t *)obj) + o->offset));
-            break;
-
-        default:
-            break;
-        }
+        if (o->type & AV_OPT_TYPE_FLAG_ARRAY)
+            opt_free_array(o, pitem, opt_array_pcount(pitem));
+        else
+            opt_free_elem(o, pitem);
     }
 }
 
@@ -1794,7 +2016,9 @@  const AVClass *av_opt_child_class_iterate(const AVClass *parent, void **iter)
 void *av_opt_ptr(const AVClass *class, void *obj, const char *name)
 {
     const AVOption *opt= av_opt_find2(&class, name, NULL, 0, AV_OPT_SEARCH_FAKE_OBJ, NULL);
-    if(!opt)
+
+    // no direct access to array-type options
+    if (!opt || (opt->type & AV_OPT_TYPE_FLAG_ARRAY))
         return NULL;
     return (uint8_t*)obj + opt->offset;
 }
@@ -1844,6 +2068,40 @@  static int opt_copy_elem(void *logctx, enum AVOptionType type,
     return 0;
 }
 
+static int opt_copy_array(void *logctx, const AVOption *o,
+                          void **pdst, const void * const *psrc)
+{
+    unsigned nb_elems = *opt_array_pcount(psrc);
+    void         *dst = NULL;
+    int ret;
+
+    if (*pdst == *psrc) {
+        *pdst                   = NULL;
+        *opt_array_pcount(pdst) = 0;
+    }
+
+    opt_free_array(o, pdst, opt_array_pcount(pdst));
+
+    dst = av_calloc(nb_elems, opt_elem_size[TYPE_BASE(o->type)]);
+    if (!dst)
+        return AVERROR(ENOMEM);
+
+    for (unsigned i = 0; i < nb_elems; i++) {
+        ret = opt_copy_elem(logctx, TYPE_BASE(o->type),
+                            opt_array_pelem(o, dst, i),
+                            opt_array_pelem(o, *(void**)psrc, i));
+        if (ret < 0) {
+            opt_free_array(o, &dst, &nb_elems);
+            return ret;
+        }
+    }
+
+    *pdst                   = dst;
+    *opt_array_pcount(pdst) = nb_elems;
+
+    return 0;
+}
+
 int av_opt_copy(void *dst, const void *src)
 {
     const AVOption *o = NULL;
@@ -1861,7 +2119,9 @@  int av_opt_copy(void *dst, const void *src)
         void *field_dst = (uint8_t *)dst + o->offset;
         void *field_src = (uint8_t *)src + o->offset;
 
-        int err = opt_copy_elem(dst, o->type, field_dst, field_src);
+        int err = (o->type & AV_OPT_TYPE_FLAG_ARRAY)                 ?
+                  opt_copy_array(dst, o,       field_dst, field_src) :
+                  opt_copy_elem (dst, o->type, field_dst, field_src);
         if (err < 0)
             ret = err;
     }
@@ -1990,6 +2250,24 @@  int av_opt_is_set_to_default(void *obj, const AVOption *o)
 
     dst = ((uint8_t*)obj) + o->offset;
 
+    if (o->type & AV_OPT_TYPE_FLAG_ARRAY) {
+        const char *def = o->default_val.arr ? o->default_val.arr->def : NULL;
+        uint8_t *val;
+
+        ret = opt_get_array(o, dst, &val);
+        if (ret < 0)
+            return ret;
+
+        if (!!val != !!def)
+            ret = 0;
+        else if (val)
+            ret = !strcmp(val, def);
+
+        av_freep(&val);
+
+        return ret;
+    }
+
     switch (o->type) {
     case AV_OPT_TYPE_CONST:
         return 1;
diff --git a/libavutil/opt.h b/libavutil/opt.h
index 82bca920fb..e6013662f6 100644
--- a/libavutil/opt.h
+++ b/libavutil/opt.h
@@ -250,6 +250,17 @@  enum AVOptionType{
     AV_OPT_TYPE_COLOR,
     AV_OPT_TYPE_BOOL,
     AV_OPT_TYPE_CHLAYOUT,
+
+    /**
+     * May be combined with another regular option type to declare an array
+     * option.
+     *
+     * For array options, @ref AVOption.offset should refer to a pointer
+     * corresponding to the option type. The pointer should be immediately
+     * followed by an unsigned int that will store the number of elements in the
+     * array.
+     */
+    AV_OPT_TYPE_FLAG_ARRAY = (1 << 16),
 };
 
 /**
@@ -295,6 +306,40 @@  enum AVOptionType{
  */
 #define AV_OPT_FLAG_CHILD_CONSTS    (1 << 18)
 
+/**
+ * May be set as default_val for AV_OPT_TYPE_FLAG_ARRAY options.
+ */
+typedef struct AVOptionArrayDef {
+    /**
+     * Native access only.
+     *
+     * Default value of the option, as would be serialized by av_opt_get() (i.e.
+     * using the value of sep as the separator).
+     */
+    const char         *def;
+
+    /**
+     * Minimum number of elements in the array. When this field is non-zero, def
+     * must be non-NULL and contain at least this number of elements.
+     */
+    unsigned            size_min;
+    /**
+     * Maximum number of elements in the array, 0 when unlimited.
+     */
+    unsigned            size_max;
+
+    /**
+     * Separator between array elements in string representations of this
+     * option, used by av_opt_set() and av_opt_get(). It must be a printable
+     * ASCII character, excluding alphanumeric and the backslash. A comma is
+     * used when sep=0.
+     *
+     * The separator and the backslash must be backslash-escaped in order to
+     * appear in string representations of the option value.
+     */
+    char                sep;
+} AVOptionArrayDef;
+
 /**
  * AVOption
  */
@@ -317,8 +362,7 @@  typedef struct AVOption {
     enum AVOptionType type;
 
     /**
-     * Native access only.
-     *
+     * Native access only, except when documented otherwise.
      * the default value for scalar options
      */
     union {
@@ -327,6 +371,14 @@  typedef struct AVOption {
         const char *str;
         /* TODO those are unused now */
         AVRational q;
+
+        /**
+         * Used for AV_OPT_TYPE_FLAG_ARRAY options. May be NULL.
+         *
+         * Foreign access to some members allowed, as noted in AVOptionArrayDef
+         * documentation.
+         */
+        const AVOptionArrayDef *arr;
     } default_val;
     double min;                 ///< minimum valid value for the option
     double max;                 ///< maximum valid value for the option
diff --git a/libavutil/tests/opt.c b/libavutil/tests/opt.c
index e2582cc93d..04b4901579 100644
--- a/libavutil/tests/opt.c
+++ b/libavutil/tests/opt.c
@@ -57,6 +57,15 @@  typedef struct TestContext {
     int bool3;
     AVDictionary *dict1;
     AVDictionary *dict2;
+
+    int           **array_int;
+    unsigned     nb_array_int;
+
+    char          **array_str;
+    unsigned     nb_array_str;
+
+    AVDictionary  **array_dict;
+    unsigned     nb_array_dict;
 } TestContext;
 
 #define OFFSET(x) offsetof(TestContext, x)
@@ -65,6 +74,15 @@  typedef struct TestContext {
 #define TEST_FLAG_LAME 02
 #define TEST_FLAG_MU   04
 
+static const AVOptionArrayDef array_str = {
+    .sep         = '|',
+    .def         = "str0|str\\|1|str\\\\2",
+};
+
+static const AVOptionArrayDef array_dict = {
+    .def         = "k00=v\\\\\\\\00:k01=v\\,01,k10=v\\\\=1\\\\:0",
+};
+
 static const AVOption test_options[]= {
     {"num",        "set num",            OFFSET(num),            AV_OPT_TYPE_INT,            { .i64 = 0 },                      0,       100, 1 },
     {"toggle",     "set toggle",         OFFSET(toggle),         AV_OPT_TYPE_INT,            { .i64 = 1 },                      0,         1, 1 },
@@ -93,6 +111,10 @@  static const AVOption test_options[]= {
     {"bool3",      "set boolean value",  OFFSET(bool3),          AV_OPT_TYPE_BOOL,           { .i64 = 0 },                      0,         1, 1 },
     {"dict1",      "set dictionary value", OFFSET(dict1),        AV_OPT_TYPE_DICT,           { .str = NULL},                    0,         0, 1 },
     {"dict2",      "set dictionary value", OFFSET(dict2),        AV_OPT_TYPE_DICT,           { .str = "happy=':-)'"},           0,         0, 1 },
+    {"array_int",  "array of ints",        OFFSET(array_int),    AV_OPT_TYPE_INT | AV_OPT_TYPE_FLAG_ARRAY, .max = INT_MAX,           .flags = AV_OPT_FLAG_RUNTIME_PARAM },
+    {"array_str",  "array of strings",     OFFSET(array_str),    AV_OPT_TYPE_STRING | AV_OPT_TYPE_FLAG_ARRAY, { .arr = &array_str }, .flags = AV_OPT_FLAG_RUNTIME_PARAM },
+    // there are three levels of escaping - C string, array option, dict - so 8 backslashes are needed to get a literal one inside a dict key/val
+    {"array_dict", "array of dicts",       OFFSET(array_dict),   AV_OPT_TYPE_DICT | AV_OPT_TYPE_FLAG_ARRAY, { .arr = &array_dict },  .flags = AV_OPT_FLAG_RUNTIME_PARAM },
     { NULL },
 };
 
@@ -146,6 +168,17 @@  int main(void)
         printf("flt=%.6f\n", test_ctx.flt);
         printf("dbl=%.6f\n", test_ctx.dbl);
 
+        for (unsigned i = 0; i < test_ctx.nb_array_str; i++)
+            printf("array_str[%u]=%s\n", i, test_ctx.array_str[i]);
+
+        for (unsigned i = 0; i < test_ctx.nb_array_dict; i++) {
+            AVDictionary            *d = test_ctx.array_dict[i];
+            const AVDictionaryEntry *e = NULL;
+
+            while ((e = av_dict_iterate(d, e)))
+                printf("array_dict[%u]: %s\t%s\n", i, e->key, e->value);
+        }
+
         av_opt_show2(&test_ctx, NULL, -1, 0);
 
         av_opt_free(&test_ctx);
@@ -177,6 +210,9 @@  int main(void)
         TestContext test_ctx = { 0 };
         TestContext test2_ctx = { 0 };
         const AVOption *o = NULL;
+        char *val = NULL;
+        int ret;
+
         test_ctx.class = &test_class;
         test2_ctx.class = &test_class;
 
@@ -209,6 +245,17 @@  int main(void)
             av_free(value1);
             av_free(value2);
         }
+
+        // av_opt_set(NULL) with an array option resets it
+        ret = av_opt_set(&test_ctx, "array_dict", NULL, 0);
+        printf("av_opt_set(\"array_dict\", NULL) -> %d\n", ret);
+        printf("array_dict=%sNULL; nb_array_dict=%u\n",
+               test_ctx.array_dict ? "non-" : "", test_ctx.nb_array_dict);
+
+        // av_opt_get() on an empty array should return a NULL string
+        ret = av_opt_get(&test_ctx, "array_dict", AV_OPT_ALLOW_NULL, (uint8_t**)&val);
+        printf("av_opt_get(\"array_dict\") -> %s\n", val ? val : "NULL");
+
         av_opt_free(&test_ctx);
         av_opt_free(&test2_ctx);
     }
@@ -303,6 +350,8 @@  int main(void)
             "bool1=true",
             "bool2=auto",
             "dict1='happy=\\:-):sad=\\:-('",
+            "array_int=0,32,2147483647",
+            "array_int=2147483648", // out of range, should fail
         };
 
         test_ctx.class = &test_class;
diff --git a/tests/ref/fate/opt b/tests/ref/fate/opt
index 832f9cc8a9..f4fce1bd49 100644
--- a/tests/ref/fate/opt
+++ b/tests/ref/fate/opt
@@ -17,6 +17,12 @@  binary_size=4
 num64=1
 flt=0.333333
 dbl=0.333333
+array_str[0]=str0
+array_str[1]=str|1
+array_str[2]=str\2
+array_dict[0]: k00	v\00
+array_dict[0]: k01	v,01
+array_dict[1]: k10	v=1:0
 TestContext AVOptions:
   -num               <int>        E.......... set num (from 0 to 100) (default 0)
   -toggle            <int>        E.......... set toggle (from 0 to 1) (default 1)
@@ -45,6 +51,9 @@  TestContext AVOptions:
   -bool3             <boolean>    E.......... set boolean value (default false)
   -dict1             <dictionary> E.......... set dictionary value
   -dict2             <dictionary> E.......... set dictionary value (default "happy=':-)'")
+  -array_int         [<int>     ].........T. array of ints
+  -array_str         [<string>  ].........T. array of strings (default str0|str\|1|str\\2)
+  -array_dict        [<dictionary>].........T. array of dicts (default k00=v\\\\00:k01=v\,01,k10=v\\=1\\:0)
 
 Testing av_opt_is_set_to_default()
 name:       num default:1 error:
@@ -74,6 +83,9 @@  name:     bool2 default:0 error:
 name:     bool3 default:1 error:
 name:     dict1 default:1 error:
 name:     dict2 default:0 error:
+name: array_int default:0 error:
+name: array_str default:0 error:
+name:array_dict default:0 error:
 name:       num default:1 error:
 name:    toggle default:1 error:
 name:  rational default:1 error:
@@ -101,6 +113,9 @@  name:     bool2 default:1 error:
 name:     bool3 default:1 error:
 name:     dict1 default:1 error:
 name:     dict2 default:1 error:
+name: array_int default:0 error:
+name: array_str default:1 error:
+name:array_dict default:1 error:
 
 Testing av_opt_get/av_opt_set()
 name: num         get: 0                set: OK               get: 0                OK
@@ -127,9 +142,15 @@  name: bool2       get: true             set: OK               get: true
 name: bool3       get: false            set: OK               get: false            OK
 name: dict1       get:                  set: OK               get:                  OK
 name: dict2       get: happy=\:-)       set: OK               get: happy=\:-)       OK
+name: array_int   get:                  set: OK               get:                  OK
+name: array_str   get: str0|str\|1|str\\2 set: OK               get: str0|str\|1|str\\2 OK
+name: array_dict  get: k00=v\\\\00:k01=v\,01,k10=v\\=1\\:0 set: OK               get: k00=v\\\\00:k01=v\,01,k10=v\\=1\\:0 OK
+av_opt_set("array_dict", NULL) -> 0
+array_dict=NULL; nb_array_dict=0
+av_opt_get("array_dict") -> NULL
 
 Test av_opt_serialize()
-num=0,toggle=1,rational=1/1,string=default,escape=\\\=\,,flags=0x00000001,size=200x300,pix_fmt=0bgr,sample_fmt=s16,video_rate=25/1,duration=0.001,color=0xffc0cbff,cl=hexagonal,bin=62696E00,bin1=,bin2=,num64=1,flt=0.333333,dbl=0.333333,bool1=auto,bool2=true,bool3=false,dict1=,dict2=happy\=\\:-)
+num=0,toggle=1,rational=1/1,string=default,escape=\\\=\,,flags=0x00000001,size=200x300,pix_fmt=0bgr,sample_fmt=s16,video_rate=25/1,duration=0.001,color=0xffc0cbff,cl=hexagonal,bin=62696E00,bin1=,bin2=,num64=1,flt=0.333333,dbl=0.333333,bool1=auto,bool2=true,bool3=false,dict1=,dict2=happy\=\\:-),array_int=,array_str=str0|str\\|1|str\\\\2,array_dict=k00\=v\\\\\\\\00:k01\=v\\\,01\,k10\=v\\\\\=1\\\\:0
 Setting entry with key 'num' to value '0'
 Setting entry with key 'toggle' to value '1'
 Setting entry with key 'rational' to value '1/1'
@@ -154,7 +175,10 @@  Setting entry with key 'bool2' to value 'true'
 Setting entry with key 'bool3' to value 'false'
 Setting entry with key 'dict1' to value ''
 Setting entry with key 'dict2' to value 'happy=\:-)'
-num=0,toggle=1,rational=1/1,string=default,escape=\\\=\,,flags=0x00000001,size=200x300,pix_fmt=0bgr,sample_fmt=s16,video_rate=25/1,duration=0.001,color=0xffc0cbff,cl=hexagonal,bin=62696E00,bin1=,bin2=,num64=1,flt=0.333333,dbl=0.333333,bool1=auto,bool2=true,bool3=false,dict1=,dict2=happy\=\\:-)
+Setting entry with key 'array_int' to value ''
+Setting entry with key 'array_str' to value 'str0|str\|1|str\\2'
+Setting entry with key 'array_dict' to value 'k00=v\\\\00:k01=v\,01,k10=v\\=1\\:0'
+num=0,toggle=1,rational=1/1,string=default,escape=\\\=\,,flags=0x00000001,size=200x300,pix_fmt=0bgr,sample_fmt=s16,video_rate=25/1,duration=0.001,color=0xffc0cbff,cl=hexagonal,bin=62696E00,bin1=,bin2=,num64=1,flt=0.333333,dbl=0.333333,bool1=auto,bool2=true,bool3=false,dict1=,dict2=happy\=\\:-),array_int=,array_str=str0|str\\|1|str\\\\2,array_dict=k00\=v\\\\\\\\00:k01\=v\\\,01\,k10\=v\\\\\=1\\\\:0
 
 Testing av_set_options_string()
 Setting options string ''
@@ -378,6 +402,13 @@  OK    'bool2=auto'
 Setting options string 'dict1='happy=\:-):sad=\:-(''
 Setting entry with key 'dict1' to value 'happy=\:-):sad=\:-('
 OK    'dict1='happy=\:-):sad=\:-(''
+Setting options string 'array_int=0,32,2147483647'
+Setting entry with key 'array_int' to value '0,32,2147483647'
+OK    'array_int=0,32,2147483647'
+Setting options string 'array_int=2147483648'
+Setting entry with key 'array_int' to value '2147483648'
+Value 2147483648.000000 for parameter 'array_int' out of range [0 - 2.14748e+09]
+Error 'array_int=2147483648'
 
 Testing av_opt_set_from_string()
 Setting options string ''