diff mbox series

[FFmpeg-devel,15/38] lavu/opt: add array options

Message ID 20240223143115.16521-16-anton@khirnov.net
State New
Headers show
Series [FFmpeg-devel,01/38] lavu/opt: cosmetics, change option flags to (1 << N) style | expand

Checks

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

Commit Message

Anton Khirnov Feb. 23, 2024, 1:58 p.m. UTC
AVOption.array_max_size is added before AVOption.unit to avoid
increasing sizeof(AVOption).
---
 doc/APIchanges        |   3 +
 libavutil/opt.c       | 344 ++++++++++++++++++++++++++++++++++++------
 libavutil/opt.h       |  26 ++++
 libavutil/tests/opt.c |  34 +++++
 tests/ref/fate/opt    |  23 ++-
 5 files changed, 385 insertions(+), 45 deletions(-)

Comments

Marton Balint Feb. 23, 2024, 7:05 p.m. UTC | #1
On Fri, 23 Feb 2024, Anton Khirnov wrote:

> AVOption.array_max_size is added before AVOption.unit to avoid
> increasing sizeof(AVOption).
> ---
> doc/APIchanges        |   3 +
> libavutil/opt.c       | 344 ++++++++++++++++++++++++++++++++++++------
> libavutil/opt.h       |  26 ++++
> libavutil/tests/opt.c |  34 +++++
> tests/ref/fate/opt    |  23 ++-
> 5 files changed, 385 insertions(+), 45 deletions(-)
>
[...]

> --- a/libavutil/opt.h
> +++ b/libavutil/opt.h
> @@ -288,6 +288,16 @@ enum AVOptionType{
>  */
> #define AV_OPT_FLAG_CHILD_CONSTS    (1 << 18)
> 
> +/**
> + * The option is an array.
> + *
> + * When adding array options to an object, @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.
> + */
> +#define AV_OPT_FLAG_ARRAY           (1 << 19)
> +
> /**
>  * AVOption
>  */
> @@ -313,6 +323,16 @@ typedef struct AVOption {
>     union {
>         int64_t i64;
>         double dbl;
> +
> +        /**
> +         * This member is always used for AV_OPT_FLAG_ARRAY options. When
> +         * non-NULL, the first character of the string must be the separator to
> +         * be used for (de)serializing lists to/from strings with av_opt_get(),

This is quite ugly. Also it breaks the assumption that if the user sets an 
option value to the default value of the option, than it will work. So 
let's just remove this feature for now.

Eventually I think some new struct should be introduced, e.g. 
AVOptionExtension, which can be used to specify additional option 
settings, such as array min/max size, and maybe separator. It would be a 
lot more clean and future proof than filling the holes in AVOption.

Regards,
Marton
Anton Khirnov Feb. 26, 2024, 5:14 p.m. UTC | #2
Quoting Marton Balint (2024-02-23 20:05:06)
> 
> 
> On Fri, 23 Feb 2024, Anton Khirnov wrote:
> 
> > AVOption.array_max_size is added before AVOption.unit to avoid
> > increasing sizeof(AVOption).
> > ---
> > doc/APIchanges        |   3 +
> > libavutil/opt.c       | 344 ++++++++++++++++++++++++++++++++++++------
> > libavutil/opt.h       |  26 ++++
> > libavutil/tests/opt.c |  34 +++++
> > tests/ref/fate/opt    |  23 ++-
> > 5 files changed, 385 insertions(+), 45 deletions(-)
> >
> [...]
> 
> > --- a/libavutil/opt.h
> > +++ b/libavutil/opt.h
> > @@ -288,6 +288,16 @@ enum AVOptionType{
> >  */
> > #define AV_OPT_FLAG_CHILD_CONSTS    (1 << 18)
> > 
> > +/**
> > + * The option is an array.
> > + *
> > + * When adding array options to an object, @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.
> > + */
> > +#define AV_OPT_FLAG_ARRAY           (1 << 19)
> > +
> > /**
> >  * AVOption
> >  */
> > @@ -313,6 +323,16 @@ typedef struct AVOption {
> >     union {
> >         int64_t i64;
> >         double dbl;
> > +
> > +        /**
> > +         * This member is always used for AV_OPT_FLAG_ARRAY options. When
> > +         * non-NULL, the first character of the string must be the separator to
> > +         * be used for (de)serializing lists to/from strings with av_opt_get(),
> 
> This is quite ugly. Also it breaks the assumption that if the user sets an 
> option value to the default value of the option, than it will work.

I don't follow, what assumption are you talking about?

> So let's just remove this feature for now.
> 
> Eventually I think some new struct should be introduced, e.g. 
> AVOptionExtension, which can be used to specify additional option 
> settings, such as array min/max size, and maybe separator. It would be a 
> lot more clean and future proof than filling the holes in AVOption.

I've actually considered that, but don't see a clean way of linking such
an extension with its option. We only have an int-sized hole, so can't
add a new pointer field to AVOption. I suppose there could be a new
array of extensions in AVClass, linked to options by name, but that
seems even more ugly and inefficient.
James Almer Feb. 26, 2024, 5:19 p.m. UTC | #3
On 2/26/2024 2:14 PM, Anton Khirnov wrote:
> Quoting Marton Balint (2024-02-23 20:05:06)
>>
>>
>> On Fri, 23 Feb 2024, Anton Khirnov wrote:
>>
>>> AVOption.array_max_size is added before AVOption.unit to avoid
>>> increasing sizeof(AVOption).
>>> ---
>>> doc/APIchanges        |   3 +
>>> libavutil/opt.c       | 344 ++++++++++++++++++++++++++++++++++++------
>>> libavutil/opt.h       |  26 ++++
>>> libavutil/tests/opt.c |  34 +++++
>>> tests/ref/fate/opt    |  23 ++-
>>> 5 files changed, 385 insertions(+), 45 deletions(-)
>>>
>> [...]
>>
>>> --- a/libavutil/opt.h
>>> +++ b/libavutil/opt.h
>>> @@ -288,6 +288,16 @@ enum AVOptionType{
>>>   */
>>> #define AV_OPT_FLAG_CHILD_CONSTS    (1 << 18)
>>>
>>> +/**
>>> + * The option is an array.
>>> + *
>>> + * When adding array options to an object, @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.
>>> + */
>>> +#define AV_OPT_FLAG_ARRAY           (1 << 19)
>>> +
>>> /**
>>>   * AVOption
>>>   */
>>> @@ -313,6 +323,16 @@ typedef struct AVOption {
>>>      union {
>>>          int64_t i64;
>>>          double dbl;
>>> +
>>> +        /**
>>> +         * This member is always used for AV_OPT_FLAG_ARRAY options. When
>>> +         * non-NULL, the first character of the string must be the separator to
>>> +         * be used for (de)serializing lists to/from strings with av_opt_get(),
>>
>> This is quite ugly. Also it breaks the assumption that if the user sets an
>> option value to the default value of the option, than it will work.
> 
> I don't follow, what assumption are you talking about?
> 
>> So let's just remove this feature for now.
>>
>> Eventually I think some new struct should be introduced, e.g.
>> AVOptionExtension, which can be used to specify additional option
>> settings, such as array min/max size, and maybe separator. It would be a
>> lot more clean and future proof than filling the holes in AVOption.
> 
> I've actually considered that, but don't see a clean way of linking such
> an extension with its option. We only have an int-sized hole, so can't
> add a new pointer field to AVOption.
If there's no other solution, then adding a new field is ok.
Andreas Rheinhardt Feb. 26, 2024, 5:20 p.m. UTC | #4
Anton Khirnov:
> Quoting Marton Balint (2024-02-23 20:05:06)
>>
>>
>> On Fri, 23 Feb 2024, Anton Khirnov wrote:
>>
>>> AVOption.array_max_size is added before AVOption.unit to avoid
>>> increasing sizeof(AVOption).
>>> ---
>>> doc/APIchanges        |   3 +
>>> libavutil/opt.c       | 344 ++++++++++++++++++++++++++++++++++++------
>>> libavutil/opt.h       |  26 ++++
>>> libavutil/tests/opt.c |  34 +++++
>>> tests/ref/fate/opt    |  23 ++-
>>> 5 files changed, 385 insertions(+), 45 deletions(-)
>>>
>> [...]
>>
>>> --- a/libavutil/opt.h
>>> +++ b/libavutil/opt.h
>>> @@ -288,6 +288,16 @@ enum AVOptionType{
>>>  */
>>> #define AV_OPT_FLAG_CHILD_CONSTS    (1 << 18)
>>>
>>> +/**
>>> + * The option is an array.
>>> + *
>>> + * When adding array options to an object, @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.
>>> + */
>>> +#define AV_OPT_FLAG_ARRAY           (1 << 19)
>>> +
>>> /**
>>>  * AVOption
>>>  */
>>> @@ -313,6 +323,16 @@ typedef struct AVOption {
>>>     union {
>>>         int64_t i64;
>>>         double dbl;
>>> +
>>> +        /**
>>> +         * This member is always used for AV_OPT_FLAG_ARRAY options. When
>>> +         * non-NULL, the first character of the string must be the separator to
>>> +         * be used for (de)serializing lists to/from strings with av_opt_get(),
>>
>> This is quite ugly. Also it breaks the assumption that if the user sets an 
>> option value to the default value of the option, than it will work.
> 
> I don't follow, what assumption are you talking about?
> 
>> So let's just remove this feature for now.
>>
>> Eventually I think some new struct should be introduced, e.g. 
>> AVOptionExtension, which can be used to specify additional option 
>> settings, such as array min/max size, and maybe separator. It would be a 
>> lot more clean and future proof than filling the holes in AVOption.
> 
> I've actually considered that, but don't see a clean way of linking such
> an extension with its option. We only have an int-sized hole, so can't
> add a new pointer field to AVOption. I suppose there could be a new
> array of extensions in AVClass, linked to options by name, but that
> seems even more ugly and inefficient.
> 
You could add an AVArrayOption struct and use a pointer to such a struct
as default value for an array option. Then you would not be constrained
by an int-sized hole.

- Andreas
Marton Balint Feb. 26, 2024, 7:38 p.m. UTC | #5
On Mon, 26 Feb 2024, Anton Khirnov wrote:

> Quoting Marton Balint (2024-02-23 20:05:06)
>>
>>
>> On Fri, 23 Feb 2024, Anton Khirnov wrote:
>>
>>> AVOption.array_max_size is added before AVOption.unit to avoid
>>> increasing sizeof(AVOption).
>>> ---
>>> doc/APIchanges        |   3 +
>>> libavutil/opt.c       | 344 ++++++++++++++++++++++++++++++++++++------
>>> libavutil/opt.h       |  26 ++++
>>> libavutil/tests/opt.c |  34 +++++
>>> tests/ref/fate/opt    |  23 ++-
>>> 5 files changed, 385 insertions(+), 45 deletions(-)
>>>
>> [...]
>>
>>> --- a/libavutil/opt.h
>>> +++ b/libavutil/opt.h
>>> @@ -288,6 +288,16 @@ enum AVOptionType{
>>>  */
>>> #define AV_OPT_FLAG_CHILD_CONSTS    (1 << 18)
>>>
>>> +/**
>>> + * The option is an array.
>>> + *
>>> + * When adding array options to an object, @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.
>>> + */
>>> +#define AV_OPT_FLAG_ARRAY           (1 << 19)
>>> +
>>> /**
>>>  * AVOption
>>>  */
>>> @@ -313,6 +323,16 @@ typedef struct AVOption {
>>>     union {
>>>         int64_t i64;
>>>         double dbl;
>>> +
>>> +        /**
>>> +         * This member is always used for AV_OPT_FLAG_ARRAY options. When
>>> +         * non-NULL, the first character of the string must be the separator to
>>> +         * be used for (de)serializing lists to/from strings with av_opt_get(),
>>
>> This is quite ugly. Also it breaks the assumption that if the user sets an
>> option value to the default value of the option, than it will work.
>
> I don't follow, what assumption are you talking about?

The more I think about it, it is actually a broader problem.

You are changing the semantics of existing AV_OPT_TYPE_xxx types. So 
previously an option with AV_OPT_TYPE_STRING used to have default value in 
default_val.str. After your patch, it will be either default_val.str, or 
default_val.str[1], based on if it is an array or not.

I think the API user safely assumed that if the option type is known to 
him, he will always find the default value in the relevant default_val 
field. It is actually a bigger issue for an array of AV_OPT_TYPE_INT, 
because previously to get the default value AVOption->default_val.i64 was 
used, and now .str[1] should be instead...

The way I see it, the ARRAY modifier should either be a flag/mask of the 
option type (not AVOption->flags), or it should be a entierly new type, 
AV_OPT_TYPE_ARRAY, and its base type should be stored elsewhere.

For new opt types, we can define the semantics of default_val as we want, 
so Andreas's suggestion is good to make default_val point to an 
AVArrayOptionSettings struct or something like that.

Regards,
Marton
Anton Khirnov March 3, 2024, 2:55 p.m. UTC | #6
Quoting Marton Balint (2024-02-26 20:38:46)
> The more I think about it, it is actually a broader problem.
> 
> You are changing the semantics of existing AV_OPT_TYPE_xxx types. So 
> previously an option with AV_OPT_TYPE_STRING used to have default value in 
> default_val.str. After your patch, it will be either default_val.str, or 
> default_val.str[1], based on if it is an array or not.
> 
> I think the API user safely assumed that if the option type is known to 
> him, he will always find the default value in the relevant default_val 
> field. It is actually a bigger issue for an array of AV_OPT_TYPE_INT, 
> because previously to get the default value AVOption->default_val.i64 was 
> used, and now .str[1] should be instead...

In my view the semantics of default_val (and offset) are only defined
when declaring options on your own object, not when accessing those
fields when declared by some other code. I also see no good reason for
any user to read these fields.
Diederick C. Niehorster March 3, 2024, 3:53 p.m. UTC | #7
On Sun, Mar 3, 2024 at 3:55 PM Anton Khirnov <anton@khirnov.net> wrote:

> Quoting Marton Balint (2024-02-26 20:38:46)
> > The more I think about it, it is actually a broader problem.
> >
> > You are changing the semantics of existing AV_OPT_TYPE_xxx types. So
> > previously an option with AV_OPT_TYPE_STRING used to have default value
> in
> > default_val.str. After your patch, it will be either default_val.str, or
> > default_val.str[1], based on if it is an array or not.
> >
> > I think the API user safely assumed that if the option type is known to
> > him, he will always find the default value in the relevant default_val
> > field. It is actually a bigger issue for an array of AV_OPT_TYPE_INT,
> > because previously to get the default value AVOption->default_val.i64
> was
> > used, and now .str[1] should be instead...
>
> In my view the semantics of default_val (and offset) are only defined
> when declaring options on your own object, not when accessing those
> fields when declared by some other code. I also see no good reason for
> any user to read these fields.
>

I disagree. Here an example: for a GUI using some part of ffmpeg and
wanting to give the user some control over the ffmpeg operation, it would
not be strange to be able to set options and either indicate which values
are default, or have a "reset to defaults" button. I have written such a
thing (not yet opensourced).

Also the ffmpeg CLI has the ability to print the options and their defaults
for a component. Can this still be done?
James Almer March 3, 2024, 3:57 p.m. UTC | #8
On 3/3/2024 12:53 PM, Diederick C. Niehorster wrote:
> On Sun, Mar 3, 2024 at 3:55 PM Anton Khirnov <anton@khirnov.net> wrote:
> 
>> Quoting Marton Balint (2024-02-26 20:38:46)
>>> The more I think about it, it is actually a broader problem.
>>>
>>> You are changing the semantics of existing AV_OPT_TYPE_xxx types. So
>>> previously an option with AV_OPT_TYPE_STRING used to have default value
>> in
>>> default_val.str. After your patch, it will be either default_val.str, or
>>> default_val.str[1], based on if it is an array or not.
>>>
>>> I think the API user safely assumed that if the option type is known to
>>> him, he will always find the default value in the relevant default_val
>>> field. It is actually a bigger issue for an array of AV_OPT_TYPE_INT,
>>> because previously to get the default value AVOption->default_val.i64
>> was
>>> used, and now .str[1] should be instead...
>>
>> In my view the semantics of default_val (and offset) are only defined
>> when declaring options on your own object, not when accessing those
>> fields when declared by some other code. I also see no good reason for
>> any user to read these fields.
>>
> 
> I disagree. Here an example: for a GUI using some part of ffmpeg and
> wanting to give the user some control over the ffmpeg operation, it would
> not be strange to be able to set options and either indicate which values
> are default, or have a "reset to defaults" button. I have written such a
> thing (not yet opensourced).

There's av_opt_set_defaults() to set default values, then 
av_opt_is_set_to_default() and av_opt_is_set_to_default_by_name() to 
check if an option is already set to the default value.

What Anton said is that the user has no need to access the fields 
directly when there are helpers explicitly documented for this purpose.

> 
> Also the ffmpeg CLI has the ability to print the options and their defaults
> for a component. Can this still be done?
> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org
> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
> 
> To unsubscribe, visit link above, or email
> ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".
Marton Balint March 3, 2024, 9:05 p.m. UTC | #9
On Sun, 3 Mar 2024, James Almer wrote:

> On 3/3/2024 12:53 PM, Diederick C. Niehorster wrote:
>>  On Sun, Mar 3, 2024 at 3:55 PM Anton Khirnov <anton@khirnov.net> wrote:
>>
>>>  Quoting Marton Balint (2024-02-26 20:38:46)
>>>>  The more I think about it, it is actually a broader problem.
>>>>
>>>>  You are changing the semantics of existing AV_OPT_TYPE_xxx types. So
>>>>  previously an option with AV_OPT_TYPE_STRING used to have default value
>>>  in
>>>>  default_val.str. After your patch, it will be either default_val.str, or
>>>>  default_val.str[1], based on if it is an array or not.
>>>>
>>>>  I think the API user safely assumed that if the option type is known to
>>>>  him, he will always find the default value in the relevant default_val
>>>>  field. It is actually a bigger issue for an array of AV_OPT_TYPE_INT,
>>>>  because previously to get the default value AVOption->default_val.i64
>>>  was
>>>>  used, and now .str[1] should be instead...
>>>
>>>  In my view the semantics of default_val (and offset) are only defined
>>>  when declaring options on your own object, not when accessing those
>>>  fields when declared by some other code. I also see no good reason for
>>>  any user to read these fields.
>>>
>>
>>  I disagree. Here an example: for a GUI using some part of ffmpeg and
>>  wanting to give the user some control over the ffmpeg operation, it would
>>  not be strange to be able to set options and either indicate which values
>>  are default, or have a "reset to defaults" button. I have written such a
>>  thing (not yet opensourced).
>
> There's av_opt_set_defaults() to set default values, then 
> av_opt_is_set_to_default() and av_opt_is_set_to_default_by_name() to check if 
> an option is already set to the default value.
>
> What Anton said is that the user has no need to access the fields directly 
> when there are helpers explicitly documented for this purpose.

The AVOption struct is public, and the default_val field is public too. 
There is nothing that warns the user not to access it. The fact that there 
are helper functions does not change this.

But it is not the default values that is a problem. The semantic change is 
the problem. I could have had a code which iterates through every AVOption 
of an object and prints the values with type AV_OPT_TYPE_INT. Your change 
will suddently break that, because arrays will also have the type of 
AV_OPT_TYPE_INT.

So can you please introduce a new option type for arrays?

Thanks,
Marton
diff mbox series

Patch

diff --git a/doc/APIchanges b/doc/APIchanges
index d26110b285..371fd2f465 100644
--- a/doc/APIchanges
+++ b/doc/APIchanges
@@ -2,6 +2,9 @@  The last version increases of all libraries were on 2023-02-09
 
 API changes, most recent first:
 
+2024-02-xx - xxxxxxxxxx - lavu 58.xx.100 - opt.h
+  Add AV_OPT_FLAG_ARRAY and AVOption.array_max_size.
+
 2024-02-21 - xxxxxxxxxx - lavc 60.40.100 - avcodec.h
   Deprecate AV_INPUT_BUFFER_MIN_SIZE without replacement.
 
diff --git a/libavutil/opt.c b/libavutil/opt.c
index ebc8063dc6..88236660a1 100644
--- a/libavutil/opt.c
+++ b/libavutil/opt.c
@@ -56,6 +56,77 @@  const AVOption *av_opt_next(const void *obj, const AVOption *last)
     return NULL;
 }
 
+static const size_t opt_elem_size[] = {
+    [AV_OPT_TYPE_FLAGS]         = sizeof(unsigned),
+    [AV_OPT_TYPE_INT]           = sizeof(int),
+    [AV_OPT_TYPE_INT64]         = sizeof(int64_t),
+    [AV_OPT_TYPE_UINT64]        = sizeof(uint64_t),
+    [AV_OPT_TYPE_DOUBLE]        = sizeof(double),
+    [AV_OPT_TYPE_FLOAT]         = sizeof(float),
+    [AV_OPT_TYPE_STRING]        = sizeof(char *),
+    [AV_OPT_TYPE_RATIONAL]      = sizeof(AVRational),
+    [AV_OPT_TYPE_BINARY]        = sizeof(uint8_t *),
+    [AV_OPT_TYPE_DICT]          = sizeof(AVDictionary *),
+    [AV_OPT_TYPE_IMAGE_SIZE]    = sizeof(int[2]),
+    [AV_OPT_TYPE_VIDEO_RATE]    = sizeof(AVRational),
+    [AV_OPT_TYPE_PIXEL_FMT]     = sizeof(int),
+    [AV_OPT_TYPE_SAMPLE_FMT]    = sizeof(int),
+    [AV_OPT_TYPE_DURATION]      = sizeof(int64_t),
+    [AV_OPT_TYPE_COLOR]         = sizeof(uint8_t[4]),
+#if FF_API_OLD_CHANNEL_LAYOUT
+    [AV_OPT_TYPE_CHANNEL_LAYOUT]= sizeof(uint64_t),
+#endif
+    [AV_OPT_TYPE_CHLAYOUT]      = sizeof(AVChannelLayout),
+    [AV_OPT_TYPE_BOOL]          = sizeof(int),
+};
+
+static uint8_t opt_array_sep(const AVOption *o)
+{
+    av_assert1(o->flags & AV_OPT_FLAG_ARRAY);
+    return o->default_val.str ? o->default_val.str[0] : ',';
+}
+
+static void *opt_array_pelem(const AVOption *o, void *array, unsigned idx)
+{
+    av_assert1(o->flags & AV_OPT_FLAG_ARRAY);
+    return (uint8_t *)array + idx * opt_elem_size[o->type];
+}
+
+static unsigned *opt_array_pcount(void *parray)
+{
+    return (unsigned *)((void **)parray + 1);
+}
+
+static void opt_free_elem(const AVOption *o, void *ptr)
+{
+    switch (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) {
@@ -580,6 +651,76 @@  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 size_t elem_size = opt_elem_size[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 (o->array_max_size && nb_elems >= o->array_max_size) {
+            av_log(obj, AV_LOG_ERROR,
+                   "Cannot assign more than %u elements to array option %s\n",
+                   o->array_max_size, 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));
+
+    *((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;
@@ -595,14 +736,16 @@  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->flags & AV_OPT_FLAG_ARRAY) ?
+            opt_set_array : opt_set_elem)(obj, target_obj, o, val, dst);
 }
 
 #define OPT_EVAL_NUMBER(name, opttype, vartype)                         \
 int av_opt_eval_ ## name(void *obj, const AVOption *o,                  \
                          const char *val, vartype *name ## _out)        \
 {                                                                       \
-    if (!o || o->type != opttype || o->flags & AV_OPT_FLAG_READONLY)    \
+    if (!o || o->type != opttype || o->flags & AV_OPT_FLAG_READONLY ||  \
+        o->flags & AV_OPT_FLAG_ARRAY)                                   \
         return AVERROR(EINVAL);                                         \
     return set_string_number(obj, obj, o, val, name ## _out);           \
 }
@@ -623,7 +766,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 | AV_OPT_FLAG_ARRAY))
         return AVERROR(EINVAL);
 
     dst = ((uint8_t *)target_obj) + o->offset;
@@ -656,7 +799,8 @@  int av_opt_set_bin(void *obj, const char *name, const uint8_t *val, int len, int
     if (!o || !target_obj)
         return AVERROR_OPTION_NOT_FOUND;
 
-    if (o->type != AV_OPT_TYPE_BINARY || o->flags & AV_OPT_FLAG_READONLY)
+    if (o->type != AV_OPT_TYPE_BINARY ||
+        o->flags & (AV_OPT_FLAG_READONLY | AV_OPT_FLAG_ARRAY))
         return AVERROR(EINVAL);
 
     ptr = len ? av_malloc(len) : NULL;
@@ -682,9 +826,10 @@  int av_opt_set_image_size(void *obj, const char *name, int w, int h, int search_
 
     if (!o || !target_obj)
         return AVERROR_OPTION_NOT_FOUND;
-    if (o->type != AV_OPT_TYPE_IMAGE_SIZE) {
+    if (o->type != AV_OPT_TYPE_IMAGE_SIZE ||
+        o->flags & AV_OPT_FLAG_ARRAY) {
         av_log(obj, AV_LOG_ERROR,
-               "The value set by option '%s' is not an image size.\n", o->name);
+               "The value set by option '%s' is not a scalar image size.\n", o->name);
         return AVERROR(EINVAL);
     }
     if (w<0 || h<0) {
@@ -704,9 +849,11 @@  int av_opt_set_video_rate(void *obj, const char *name, AVRational val, int searc
 
     if (!o || !target_obj)
         return AVERROR_OPTION_NOT_FOUND;
-    if (o->type != AV_OPT_TYPE_VIDEO_RATE) {
+    if (o->type != AV_OPT_TYPE_VIDEO_RATE ||
+        o->flags & AV_OPT_FLAG_ARRAY) {
         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 scalar video rate.\n",
+               o->name);
         return AVERROR(EINVAL);
     }
     if (val.num <= 0 || val.den <= 0)
@@ -724,9 +871,10 @@  static int set_format(void *obj, const char *name, int fmt, int search_flags,
 
     if (!o || !target_obj)
         return AVERROR_OPTION_NOT_FOUND;
-    if (o->type != type) {
+    if (o->type != type ||
+        o->flags & AV_OPT_FLAG_ARRAY) {
         av_log(obj, AV_LOG_ERROR,
-               "The value set by option '%s' is not a %s format", name, desc);
+               "The value set by option '%s' is not a scalar %s format", name, desc);
         return AVERROR(EINVAL);
     }
 
@@ -762,9 +910,10 @@  int av_opt_set_channel_layout(void *obj, const char *name, int64_t cl, int searc
 
     if (!o || !target_obj)
         return AVERROR_OPTION_NOT_FOUND;
-    if (o->type != AV_OPT_TYPE_CHANNEL_LAYOUT) {
+    if (o->type != AV_OPT_TYPE_CHANNEL_LAYOUT ||
+        o->flags & AV_OPT_FLAG_ARRAY) {
         av_log(obj, AV_LOG_ERROR,
-               "The value set by option '%s' is not a channel layout.\n", o->name);
+               "The value set by option '%s' is not a scalar channel layout.\n", o->name);
         return AVERROR(EINVAL);
     }
     *(int64_t *)(((uint8_t *)target_obj) + o->offset) = cl;
@@ -782,7 +931,7 @@  int av_opt_set_dict_val(void *obj, const char *name, const AVDictionary *val,
 
     if (!o || !target_obj)
         return AVERROR_OPTION_NOT_FOUND;
-    if (o->flags & AV_OPT_FLAG_READONLY)
+    if (o->flags & (AV_OPT_FLAG_READONLY | AV_OPT_FLAG_ARRAY))
         return AVERROR(EINVAL);
 
     dst = (AVDictionary **)(((uint8_t *)target_obj) + o->offset);
@@ -802,6 +951,8 @@  int av_opt_set_chlayout(void *obj, const char *name,
 
     if (!o || !target_obj)
         return AVERROR_OPTION_NOT_FOUND;
+    if (o->flags & AV_OPT_FLAG_ARRAY)
+        return AVERROR(EINVAL);
 
     dst = (AVChannelLayout*)((uint8_t*)target_obj + o->offset);
 
@@ -954,6 +1105,66 @@  FF_ENABLE_DEPRECATION_WARNINGS
     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;
@@ -969,8 +1180,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->flags & AV_OPT_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)
@@ -993,6 +1215,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->flags & AV_OPT_FLAG_ARRAY)
+        return AVERROR(EINVAL);
 
     dst = ((uint8_t *)target_obj) + o->offset;
 
@@ -1048,9 +1272,10 @@  int av_opt_get_image_size(void *obj, const char *name, int search_flags, int *w_
     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_IMAGE_SIZE) {
+    if (o->type != AV_OPT_TYPE_IMAGE_SIZE ||
+        o->flags & AV_OPT_FLAG_ARRAY) {
         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 scalar image size.\n", name);
         return AVERROR(EINVAL);
     }
 
@@ -1083,9 +1308,10 @@  static int get_format(void *obj, const char *name, int search_flags, int *out_fm
     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 != type) {
+    if (o->type != type ||
+        o->flags & AV_OPT_FLAG_ARRAY) {
         av_log(obj, AV_LOG_ERROR,
-               "The value for option '%s' is not a %s format.\n", desc, name);
+               "The value for option '%s' is not a scalar %s format.\n", desc, name);
         return AVERROR(EINVAL);
     }
 
@@ -1112,9 +1338,10 @@  int av_opt_get_channel_layout(void *obj, const char *name, int search_flags, int
     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_CHANNEL_LAYOUT) {
+    if (o->type != AV_OPT_TYPE_CHANNEL_LAYOUT ||
+        o->flags & AV_OPT_FLAG_ARRAY) {
         av_log(obj, AV_LOG_ERROR,
-               "The value for option '%s' is not a channel layout.\n", name);
+               "The value for option '%s' is not a scalar channel layout.\n", name);
         return AVERROR(EINVAL);
     }
 
@@ -1131,9 +1358,10 @@  int av_opt_get_chlayout(void *obj, const char *name, int search_flags, AVChannel
     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_CHLAYOUT) {
+    if (o->type != AV_OPT_TYPE_CHLAYOUT ||
+        o->flags & AV_OPT_FLAG_ARRAY) {
         av_log(obj, AV_LOG_ERROR,
-               "The value for option '%s' is not a channel layout.\n", name);
+               "The value for option '%s' is not a scalar channel layout.\n", name);
         return AVERROR(EINVAL);
     }
 
@@ -1149,7 +1377,8 @@  int av_opt_get_dict_val(void *obj, const char *name, int search_flags, AVDiction
 
     if (!o || !target_obj)
         return AVERROR_OPTION_NOT_FOUND;
-    if (o->type != AV_OPT_TYPE_DICT)
+    if (o->type != AV_OPT_TYPE_DICT ||
+        o->flags & AV_OPT_FLAG_ARRAY)
         return AVERROR(EINVAL);
 
     src = *(AVDictionary **)(((uint8_t *)target_obj) + o->offset);
@@ -1165,7 +1394,8 @@  int av_opt_flag_is_set(void *obj, const char *field_name, const char *flag_name)
                                         field ? field->unit : NULL, 0, 0);
     int64_t res;
 
-    if (!field || !flag || flag->type != AV_OPT_TYPE_CONST ||
+    if (!field || !(field->flags & AV_OPT_FLAG_ARRAY) ||
+        !flag || flag->type != AV_OPT_TYPE_CONST ||
         av_opt_get_int(obj, field_name, 0, &res) < 0)
         return 0;
     return res & flag->default_val.i64;
@@ -1284,8 +1514,12 @@  static void log_type(void *av_log_obj, const AVOption *o,
 
     if (o->type == AV_OPT_TYPE_CONST && 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 (o->type < FF_ARRAY_ELEMS(desc) && desc[o->type]) {
+        if (o->flags & AV_OPT_FLAG_ARRAY)
+            av_log(av_log_obj, AV_LOG_INFO, "×%-11s ", desc[o->type]);
+        else
+            av_log(av_log_obj, AV_LOG_INFO, "%-12s ", desc[o->type]);
+    }
     else
         av_log(av_log_obj, AV_LOG_INFO, "%-12s ", "");
 }
@@ -1470,6 +1704,22 @@  void av_opt_set_defaults2(void *s, int mask, int flags)
         if (opt->flags & AV_OPT_FLAG_READONLY)
             continue;
 
+        if (opt->flags & AV_OPT_FLAG_ARRAY) {
+            const char *val = opt->default_val.str;
+            if (val) {
+                const char sep = *val++;
+
+                // make sure people don't forget to set the separator correctly
+                av_assert0(sep &&
+                           (sep < 'a' || sep > 'z') &&
+                           (sep < 'A' || sep > 'Z') &&
+                           (sep < '0' || sep > '9'));
+
+                opt_set_array(s, s, opt, val, dst);
+            }
+            continue;
+        }
+
         switch (opt->type) {
             case AV_OPT_TYPE_CONST:
                 /* Nothing to be done here */
@@ -1717,23 +1967,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->flags & AV_OPT_FLAG_ARRAY)
+            opt_free_array(o, pitem, opt_array_pcount(pitem));
+        else
+            opt_free_elem(o, pitem);
     }
 }
 
@@ -1835,7 +2074,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->flags & AV_OPT_FLAG_ARRAY))
         return NULL;
     return (uint8_t*)obj + opt->offset;
 }
@@ -2067,6 +2308,23 @@  int av_opt_is_set_to_default(void *obj, const AVOption *o)
 
     dst = ((uint8_t*)obj) + o->offset;
 
+    if (o->flags & AV_OPT_FLAG_ARRAY) {
+        uint8_t *val;
+
+        ret = opt_get_array(o, dst, &val);
+        if (ret < 0)
+            return ret;
+
+        if (!!val != !!o->default_val.str)
+            ret = 0;
+        else if (val)
+            ret = !strcmp(val, o->default_val.str + 1);
+
+        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 e34b8506f8..c5678c0296 100644
--- a/libavutil/opt.h
+++ b/libavutil/opt.h
@@ -288,6 +288,16 @@  enum AVOptionType{
  */
 #define AV_OPT_FLAG_CHILD_CONSTS    (1 << 18)
 
+/**
+ * The option is an array.
+ *
+ * When adding array options to an object, @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.
+ */
+#define AV_OPT_FLAG_ARRAY           (1 << 19)
+
 /**
  * AVOption
  */
@@ -313,6 +323,16 @@  typedef struct AVOption {
     union {
         int64_t i64;
         double dbl;
+
+        /**
+         * This member is always used for AV_OPT_FLAG_ARRAY options. When
+         * non-NULL, the first character of the string must be the separator to
+         * be used for (de)serializing lists to/from strings with av_opt_get(),
+         * av_opt_set(), and similar. The separator must not conflict with valid
+         * option names or be a backslash. When the value is null, comma is used
+         * as the separator. The rest of the string is parsed as for
+         * av_opt_set().
+         */
         const char *str;
         /* TODO those are unused now */
         AVRational q;
@@ -325,6 +345,12 @@  typedef struct AVOption {
      */
     int flags;
 
+    /**
+     * For options flagged with AV_OPT_FLAG_ARRAY, this specifies the maximum
+     * number of elements that can be added to it.
+     */
+    unsigned array_max_size;
+
     /**
      * The logical unit to which the option belongs. Non-constant
      * options and corresponding named constants share the same
diff --git a/libavutil/tests/opt.c b/libavutil/tests/opt.c
index e2582cc93d..5b218b6b0a 100644
--- a/libavutil/tests/opt.c
+++ b/libavutil/tests/opt.c
@@ -57,6 +57,12 @@  typedef struct TestContext {
     int bool3;
     AVDictionary *dict1;
     AVDictionary *dict2;
+
+    char          **array_str;
+    unsigned     nb_array_str;
+
+    AVDictionary  **array_dict;
+    unsigned     nb_array_dict;
 } TestContext;
 
 #define OFFSET(x) offsetof(TestContext, x)
@@ -93,6 +99,9 @@  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_str",  "array of strings",     OFFSET(array_str),    AV_OPT_TYPE_STRING,         { .str = "|str0|str\\|1|str\\\\2" }, .flags = AV_OPT_FLAG_ARRAY },
+    // 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,           { .str = ",k00=v\\\\\\\\00:k01=v\\,01,k10=v\\\\=1\\\\:0" }, .flags = AV_OPT_FLAG_ARRAY },
     { NULL },
 };
 
@@ -146,6 +155,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 +197,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 +232,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);
     }
diff --git a/tests/ref/fate/opt b/tests/ref/fate/opt
index 832f9cc8a9..4ed632fea8 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,8 @@  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_str         ×<string>    ........... array of strings (default "|str0|str\|1|str\\2")
+  -array_dict        ×<dictionary> ........... 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 +82,8 @@  name:     bool2 default:0 error:
 name:     bool3 default:1 error:
 name:     dict1 default:1 error:
 name:     dict2 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 +111,8 @@  name:     bool2 default:1 error:
 name:     bool3 default:1 error:
 name:     dict1 default:1 error:
 name:     dict2 default:1 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 +139,14 @@  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_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_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 +171,9 @@  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_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_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 ''