diff mbox series

[FFmpeg-devel,2/5] fftools/ffmpeg: move parsing of -stats_* specifiers to lavu/parseutils

Message ID 20231211150725.46473-3-thilo.borgmann@mail.de
State New
Headers show
Series avfilter: Add fsync filter | expand

Checks

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

Commit Message

Thilo Borgmann Dec. 11, 2023, 3:07 p.m. UTC
---
 fftools/ffmpeg.h          |  31 +------
 fftools/ffmpeg_enc.c      |   3 +-
 fftools/ffmpeg_mux_init.c | 152 +++-----------------------------
 libavutil/parseutils.c    | 176 ++++++++++++++++++++++++++++++++++++++
 libavutil/parseutils.h    | 102 ++++++++++++++++++++++
 libavutil/version.h       |   2 +-
 6 files changed, 296 insertions(+), 170 deletions(-)

Comments

Anton Khirnov Dec. 13, 2023, noon UTC | #1
Quoting Thilo Borgmann via ffmpeg-devel (2023-12-11 16:07:22)
> ---
>  fftools/ffmpeg.h          |  31 +------
>  fftools/ffmpeg_enc.c      |   3 +-
>  fftools/ffmpeg_mux_init.c | 152 +++-----------------------------
>  libavutil/parseutils.c    | 176 ++++++++++++++++++++++++++++++++++++++
>  libavutil/parseutils.h    | 102 ++++++++++++++++++++++
>  libavutil/version.h       |   2 +-
>  6 files changed, 296 insertions(+), 170 deletions(-)

Absolutely not.

This is application code and does not belong in the libraries.
Thilo Borgmann Dec. 13, 2023, 12:05 p.m. UTC | #2
Am 13.12.23 um 13:00 schrieb Anton Khirnov:
> Quoting Thilo Borgmann via ffmpeg-devel (2023-12-11 16:07:22)
>> ---
>>   fftools/ffmpeg.h          |  31 +------
>>   fftools/ffmpeg_enc.c      |   3 +-
>>   fftools/ffmpeg_mux_init.c | 152 +++-----------------------------
>>   libavutil/parseutils.c    | 176 ++++++++++++++++++++++++++++++++++++++
>>   libavutil/parseutils.h    | 102 ++++++++++++++++++++++
>>   libavutil/version.h       |   2 +-
>>   6 files changed, 296 insertions(+), 170 deletions(-)
> 
> Absolutely not.
> 
> This is application code and does not belong in the libraries.

How else do we not have a redundant copy of all that and make sure that -stats_* options and the filter understand the same {..} directives?

-Thilo
Anton Khirnov Dec. 13, 2023, 12:08 p.m. UTC | #3
Quoting Thilo Borgmann via ffmpeg-devel (2023-12-13 13:05:35)
> Am 13.12.23 um 13:00 schrieb Anton Khirnov:
> > Quoting Thilo Borgmann via ffmpeg-devel (2023-12-11 16:07:22)
> >> ---
> >>   fftools/ffmpeg.h          |  31 +------
> >>   fftools/ffmpeg_enc.c      |   3 +-
> >>   fftools/ffmpeg_mux_init.c | 152 +++-----------------------------
> >>   libavutil/parseutils.c    | 176 ++++++++++++++++++++++++++++++++++++++
> >>   libavutil/parseutils.h    | 102 ++++++++++++++++++++++
> >>   libavutil/version.h       |   2 +-
> >>   6 files changed, 296 insertions(+), 170 deletions(-)
> > 
> > Absolutely not.
> > 
> > This is application code and does not belong in the libraries.
> 
> How else do we not have a redundant copy of all that and make sure that -stats_* options and the filter understand the same {..} directives?

Why does that filter need to understand the same directives? No other
filter does.
Nicolas George Dec. 13, 2023, 12:10 p.m. UTC | #4
Thilo Borgmann via ffmpeg-devel (12023-12-11):
> ---
>  fftools/ffmpeg.h          |  31 +------
>  fftools/ffmpeg_enc.c      |   3 +-
>  fftools/ffmpeg_mux_init.c | 152 +++-----------------------------
>  libavutil/parseutils.c    | 176 ++++++++++++++++++++++++++++++++++++++
>  libavutil/parseutils.h    | 102 ++++++++++++++++++++++
>  libavutil/version.h       |   2 +-
>  6 files changed, 296 insertions(+), 170 deletions(-)

LGTM

Regards,
Thilo Borgmann Dec. 13, 2023, 12:15 p.m. UTC | #5
Am 13.12.23 um 13:08 schrieb Anton Khirnov:
> Quoting Thilo Borgmann via ffmpeg-devel (2023-12-13 13:05:35)
>> Am 13.12.23 um 13:00 schrieb Anton Khirnov:
>>> Quoting Thilo Borgmann via ffmpeg-devel (2023-12-11 16:07:22)
>>>> ---
>>>>    fftools/ffmpeg.h          |  31 +------
>>>>    fftools/ffmpeg_enc.c      |   3 +-
>>>>    fftools/ffmpeg_mux_init.c | 152 +++-----------------------------
>>>>    libavutil/parseutils.c    | 176 ++++++++++++++++++++++++++++++++++++++
>>>>    libavutil/parseutils.h    | 102 ++++++++++++++++++++++
>>>>    libavutil/version.h       |   2 +-
>>>>    6 files changed, 296 insertions(+), 170 deletions(-)
>>>
>>> Absolutely not.
>>>
>>> This is application code and does not belong in the libraries.
>>
>> How else do we not have a redundant copy of all that and make sure that -stats_* options and the filter understand the same {..} directives?
> 
> Why does that filter need to understand the same directives? No other
> filter does.

Because it is meant to use the file(s) the -stats_* option writes out. The most convenient and most error resilient way is to use the very same format string for -stats_* option as well as for the filter.

Otherwise it could be a 'usual' scanf-format, but then the user has to translate it from one format into the other - without making mistakes.
But that would also mean to update the filter (if someone realizes it) if the option ever changes.

-Thilo
Anton Khirnov Dec. 13, 2023, 12:39 p.m. UTC | #6
Quoting Thilo Borgmann via ffmpeg-devel (2023-12-13 13:15:27)
> Am 13.12.23 um 13:08 schrieb Anton Khirnov:
> > Quoting Thilo Borgmann via ffmpeg-devel (2023-12-13 13:05:35)
> >> Am 13.12.23 um 13:00 schrieb Anton Khirnov:
> >>> Quoting Thilo Borgmann via ffmpeg-devel (2023-12-11 16:07:22)
> >>>> ---
> >>>>    fftools/ffmpeg.h          |  31 +------
> >>>>    fftools/ffmpeg_enc.c      |   3 +-
> >>>>    fftools/ffmpeg_mux_init.c | 152 +++-----------------------------
> >>>>    libavutil/parseutils.c    | 176 ++++++++++++++++++++++++++++++++++++++
> >>>>    libavutil/parseutils.h    | 102 ++++++++++++++++++++++
> >>>>    libavutil/version.h       |   2 +-
> >>>>    6 files changed, 296 insertions(+), 170 deletions(-)
> >>>
> >>> Absolutely not.
> >>>
> >>> This is application code and does not belong in the libraries.
> >>
> >> How else do we not have a redundant copy of all that and make sure that -stats_* options and the filter understand the same {..} directives?
> > 
> > Why does that filter need to understand the same directives? No other
> > filter does.
> 
> Because it is meant to use the file(s) the -stats_* option writes out. The most convenient and most error resilient way is to use the very same format string for -stats_* option as well as for the filter.
> 
> Otherwise it could be a 'usual' scanf-format, but then the user has to translate it from one format into the other - without making mistakes.
> But that would also mean to update the filter (if someone realizes it) if the option ever changes.

Why does it need a dynamic format at all? Just postulate a fixed format
for its input like every other filter and none of this complexity is
needed.
Thilo Borgmann Dec. 13, 2023, 12:50 p.m. UTC | #7
Am 13.12.23 um 13:39 schrieb Anton Khirnov:
> Quoting Thilo Borgmann via ffmpeg-devel (2023-12-13 13:15:27)
>> Am 13.12.23 um 13:08 schrieb Anton Khirnov:
>>> Quoting Thilo Borgmann via ffmpeg-devel (2023-12-13 13:05:35)
>>>> Am 13.12.23 um 13:00 schrieb Anton Khirnov:
>>>>> Quoting Thilo Borgmann via ffmpeg-devel (2023-12-11 16:07:22)
>>>>>> ---
>>>>>>     fftools/ffmpeg.h          |  31 +------
>>>>>>     fftools/ffmpeg_enc.c      |   3 +-
>>>>>>     fftools/ffmpeg_mux_init.c | 152 +++-----------------------------
>>>>>>     libavutil/parseutils.c    | 176 ++++++++++++++++++++++++++++++++++++++
>>>>>>     libavutil/parseutils.h    | 102 ++++++++++++++++++++++
>>>>>>     libavutil/version.h       |   2 +-
>>>>>>     6 files changed, 296 insertions(+), 170 deletions(-)
>>>>>
>>>>> Absolutely not.
>>>>>
>>>>> This is application code and does not belong in the libraries.
>>>>
>>>> How else do we not have a redundant copy of all that and make sure that -stats_* options and the filter understand the same {..} directives?
>>>
>>> Why does that filter need to understand the same directives? No other
>>> filter does.
>>
>> Because it is meant to use the file(s) the -stats_* option writes out. The most convenient and most error resilient way is to use the very same format string for -stats_* option as well as for the filter.
>>
>> Otherwise it could be a 'usual' scanf-format, but then the user has to translate it from one format into the other - without making mistakes.
>> But that would also mean to update the filter (if someone realizes it) if the option ever changes.
> 
> Why does it need a dynamic format at all? Just postulate a fixed format
> for its input like every other filter and none of this complexity is
> needed.

That would unnecessarily limit the user of the -stats_* option to a specific format if the user also wants to use this filter later.
Either sacrificing the other info the user wanted to process from the file created or having to run the same command (with distinct format strings for the -stats_* option) twice. Or do the conversion from his extensive format into the fixed format manually - without errors.

-Thilo
Anton Khirnov Dec. 13, 2023, 4:28 p.m. UTC | #8
Quoting Thilo Borgmann via ffmpeg-devel (2023-12-13 13:50:09)
> Am 13.12.23 um 13:39 schrieb Anton Khirnov:
> > Quoting Thilo Borgmann via ffmpeg-devel (2023-12-13 13:15:27)
> >> Am 13.12.23 um 13:08 schrieb Anton Khirnov:
> >>> Quoting Thilo Borgmann via ffmpeg-devel (2023-12-13 13:05:35)
> >>>> Am 13.12.23 um 13:00 schrieb Anton Khirnov:
> >>>>> Quoting Thilo Borgmann via ffmpeg-devel (2023-12-11 16:07:22)
> >>>>>> ---
> >>>>>>     fftools/ffmpeg.h          |  31 +------
> >>>>>>     fftools/ffmpeg_enc.c      |   3 +-
> >>>>>>     fftools/ffmpeg_mux_init.c | 152 +++-----------------------------
> >>>>>>     libavutil/parseutils.c    | 176 ++++++++++++++++++++++++++++++++++++++
> >>>>>>     libavutil/parseutils.h    | 102 ++++++++++++++++++++++
> >>>>>>     libavutil/version.h       |   2 +-
> >>>>>>     6 files changed, 296 insertions(+), 170 deletions(-)
> >>>>>
> >>>>> Absolutely not.
> >>>>>
> >>>>> This is application code and does not belong in the libraries.
> >>>>
> >>>> How else do we not have a redundant copy of all that and make sure that -stats_* options and the filter understand the same {..} directives?
> >>>
> >>> Why does that filter need to understand the same directives? No other
> >>> filter does.
> >>
> >> Because it is meant to use the file(s) the -stats_* option writes out. The most convenient and most error resilient way is to use the very same format string for -stats_* option as well as for the filter.
> >>
> >> Otherwise it could be a 'usual' scanf-format, but then the user has to translate it from one format into the other - without making mistakes.
> >> But that would also mean to update the filter (if someone realizes it) if the option ever changes.
> > 
> > Why does it need a dynamic format at all? Just postulate a fixed format
> > for its input like every other filter and none of this complexity is
> > needed.
> 
> That would unnecessarily limit the user of the -stats_* option to a
> specific format if the user also wants to use this filter later.
> Either sacrificing the other info the user wanted to process from the
> file created or having to run the same command (with distinct format
> strings for the -stats_* option) twice. Or do the conversion from his
> extensive format into the fixed format manually - without errors.

It is bad practice to design library features around the needs and
limitations of a single specific caller.

Many of the directives supported by -stats* make sense only in the
context of the ffmpeg CLI, and we may want to add many more in the
future. We definitely do not want to hardcode them into libavutil public
API.

If the problem is limiting ffmpeg CLI users to a specific stats format,
then you could extend these options to allow writing multiple stats
files with different formats.
Thilo Borgmann Dec. 13, 2023, 6:17 p.m. UTC | #9
Am 13.12.23 um 17:28 schrieb Anton Khirnov:
> Quoting Thilo Borgmann via ffmpeg-devel (2023-12-13 13:50:09)
>> Am 13.12.23 um 13:39 schrieb Anton Khirnov:
>>> Quoting Thilo Borgmann via ffmpeg-devel (2023-12-13 13:15:27)
>>>> Am 13.12.23 um 13:08 schrieb Anton Khirnov:
>>>>> Quoting Thilo Borgmann via ffmpeg-devel (2023-12-13 13:05:35)
>>>>>> Am 13.12.23 um 13:00 schrieb Anton Khirnov:
>>>>>>> Quoting Thilo Borgmann via ffmpeg-devel (2023-12-11 16:07:22)
>>>>>>>> ---
>>>>>>>>      fftools/ffmpeg.h          |  31 +------
>>>>>>>>      fftools/ffmpeg_enc.c      |   3 +-
>>>>>>>>      fftools/ffmpeg_mux_init.c | 152 +++-----------------------------
>>>>>>>>      libavutil/parseutils.c    | 176 ++++++++++++++++++++++++++++++++++++++
>>>>>>>>      libavutil/parseutils.h    | 102 ++++++++++++++++++++++
>>>>>>>>      libavutil/version.h       |   2 +-
>>>>>>>>      6 files changed, 296 insertions(+), 170 deletions(-)
>>>>>>>
>>>>>>> Absolutely not.
>>>>>>>
>>>>>>> This is application code and does not belong in the libraries.
>>>>>>
>>>>>> How else do we not have a redundant copy of all that and make sure that -stats_* options and the filter understand the same {..} directives?
>>>>>
>>>>> Why does that filter need to understand the same directives? No other
>>>>> filter does.
>>>>
>>>> Because it is meant to use the file(s) the -stats_* option writes out. The most convenient and most error resilient way is to use the very same format string for -stats_* option as well as for the filter.
>>>>
>>>> Otherwise it could be a 'usual' scanf-format, but then the user has to translate it from one format into the other - without making mistakes.
>>>> But that would also mean to update the filter (if someone realizes it) if the option ever changes.
>>>
>>> Why does it need a dynamic format at all? Just postulate a fixed format
>>> for its input like every other filter and none of this complexity is
>>> needed.
>>
>> That would unnecessarily limit the user of the -stats_* option to a
>> specific format if the user also wants to use this filter later.
>> Either sacrificing the other info the user wanted to process from the
>> file created or having to run the same command (with distinct format
>> strings for the -stats_* option) twice. Or do the conversion from his
>> extensive format into the fixed format manually - without errors.
> 
> It is bad practice to design library features around the needs and
> limitations of a single specific caller.

The callers here would be the CLI and this filter.
Very much like for av_parse_ratio().
In contrast to av_get_known_color_name() which actually has just one specific caller, the CLI.

Other parsing functions have more callers, some including the CLI.
And it makes only sense if we offer the same format anywhere for reading/writing the same thing.

This approach follows what we already do.


> Many of the directives supported by -stats* make sense only in the
> context of the ffmpeg CLI, and we may want to add many more in the
> future. We definitely do not want to hardcode them into libavutil public
> API.

At least three of them are already useful for this filter outside of the CLI.
Others might be useful for other/new uses cases as well and all of them would be useful if you'd want e.g. overlay them on top of the video.


> If the problem is limiting ffmpeg CLI users to a specific stats format,
> then you could extend these options to allow writing multiple stats
> files with different formats.

This would allow for the same use-case without moving the code to libavutil for the price of a less versatile filter.
And some non-standard behaviour in the CLI where multiple equal options override each other, usually.
I find this even less preferable than a fixed forced format.

-Thilo
Anton Khirnov Dec. 14, 2023, 5:23 a.m. UTC | #10
Quoting Thilo Borgmann via ffmpeg-devel (2023-12-13 19:17:04)
> Am 13.12.23 um 17:28 schrieb Anton Khirnov:
> > It is bad practice to design library features around the needs and
> > limitations of a single specific caller.
> 
> The callers here would be the CLI and this filter.

First, public APIs should be designed for general classes of use cases,
not specific callers.

Second, you just said above that the reason you are moving this code
into libavutil is specifically because ffmpeg CLI currently behaves in a
certain way. This is the opposite of proper design, AKA an ad-hoc hack.

> Very much like for av_parse_ratio().
> In contrast to av_get_known_color_name() which actually has just one specific caller, the CLI.

No, not at all like any of these. A ratio or a color are generic
concepts that are not tied to a specific use. By contrast this code is
very much speficic to current ffmpeg CLI processing model. I know this,
because I wrote it.

> Other parsing functions have more callers, some including the CLI.
> And it makes only sense if we offer the same format anywhere for reading/writing the same thing.
> 
> This approach follows what we already do.

It most certainly does not. It _might_ make slightly more sense if it
were a generic pattern decoupled from specific properties being parsed
and used across many filters and/or other modules. But that's now what
is happening here.

> > Many of the directives supported by -stats* make sense only in the
> > context of the ffmpeg CLI, and we may want to add many more in the
> > future. We definitely do not want to hardcode them into libavutil public
> > API.
> 
> At least three of them are already useful for this filter outside of
> the CLI.

Out of 18.

> Others might be useful for other/new uses cases as well and
> all of them would be useful if you'd want e.g. overlay them on top of
> the video.

"Someone might want to use it somehow" is generically true of any
definable value at all, and so does not work as an argument for why
these specific values should be particularly useful to users of this
specific API.
Thilo Borgmann Dec. 14, 2023, 10:34 a.m. UTC | #11
Am 14.12.23 um 06:23 schrieb Anton Khirnov:
> Quoting Thilo Borgmann via ffmpeg-devel (2023-12-13 19:17:04)
>> Am 13.12.23 um 17:28 schrieb Anton Khirnov:
>>> It is bad practice to design library features around the needs and
>>> limitations of a single specific caller.
>>
>> The callers here would be the CLI and this filter.
> 
> First, public APIs should be designed for general classes of use cases,
> not specific callers.

Parsing a "...{var0}...{varN}..." string for its variables appears quite generic to me.
IMO, especially for libavutil, you think a bit too ideological here.

A) How about putting it under avpriv_ then?
B) Not filibustering about lib design, nor contradictory support of this patch, but use a fixed format. IMHO just sad for the user and a drawback for us not being able to support the same format strings in the CLI & filters without redundant code.

-Thilo
Anton Khirnov Dec. 14, 2023, 5:51 p.m. UTC | #12
Quoting Thilo Borgmann via ffmpeg-devel (2023-12-14 11:34:11)
> Am 14.12.23 um 06:23 schrieb Anton Khirnov:
> > Quoting Thilo Borgmann via ffmpeg-devel (2023-12-13 19:17:04)
> >> Am 13.12.23 um 17:28 schrieb Anton Khirnov:
> >>> It is bad practice to design library features around the needs and
> >>> limitations of a single specific caller.
> >>
> >> The callers here would be the CLI and this filter.
> > 
> > First, public APIs should be designed for general classes of use cases,
> > not specific callers.
> 
> Parsing a "...{var0}...{varN}..." string for its variables appears quite generic to me.
> IMO, especially for libavutil, you think a bit too ideological here.

You're not parsing generic variables though, but a predefined list of
highly specific ones.

> A) How about putting it under avpriv_ then?

1) fftools are not allowed to use avpriv functions.
2) It does not solve any of the problems I pointed out.

> B) Not filibustering

https://en.wiktionary.org/wiki/filibuster
2. (US politics) A tactic (such as giving long, often irrelevant
   speeches) employed to delay the proceedings of, or the making of a
   decision by, a legislative body,

Are you seriously calling my comments irrelevant stalling?

> about lib design, nor contradictory support of
> this patch, but use a fixed format. IMHO just sad
> for the user

This is an appeal to emotion along the lines of "THINK OF THE CHILDREN".
It is a fallacious argument that has no place in a technical discussion.
Thilo Borgmann Dec. 14, 2023, 6:42 p.m. UTC | #13
Am 14.12.23 um 18:51 schrieb Anton Khirnov:
> Quoting Thilo Borgmann via ffmpeg-devel (2023-12-14 11:34:11)
>> Am 14.12.23 um 06:23 schrieb Anton Khirnov:
>>> Quoting Thilo Borgmann via ffmpeg-devel (2023-12-13 19:17:04)
>>>> Am 13.12.23 um 17:28 schrieb Anton Khirnov:
>>>>> It is bad practice to design library features around the needs and
>>>>> limitations of a single specific caller.
>>>>
>>>> The callers here would be the CLI and this filter.
>>>
>>> First, public APIs should be designed for general classes of use cases,
>>> not specific callers.
>>
>> Parsing a "...{var0}...{varN}..." string for its variables appears quite generic to me.
>> IMO, especially for libavutil, you think a bit too ideological here.
> 
> You're not parsing generic variables though, but a predefined list of
> highly specific ones.
> 
>> A) How about putting it under avpriv_ then?
> 
> 1) fftools are not allowed to use avpriv functions.
> 2) It does not solve any of the problems I pointed out.
> 
>> B) Not filibustering
> 
> https://en.wiktionary.org/wiki/filibuster
> 2. (US politics) A tactic (such as giving long, often irrelevant
>     speeches) employed to delay the proceedings of, or the making of a
>     decision by, a legislative body,
> 
> Are you seriously calling my comments irrelevant stalling?

No, sorry, I just mean endless discussing of this disagreement.

Going to send a fixed format version.

-Thilo
diff mbox series

Patch

diff --git a/fftools/ffmpeg.h b/fftools/ffmpeg.h
index 1f11a2f002..cb4d90c7b2 100644
--- a/fftools/ffmpeg.h
+++ b/fftools/ffmpeg.h
@@ -42,6 +42,7 @@ 
 #include "libavutil/eval.h"
 #include "libavutil/fifo.h"
 #include "libavutil/hwcontext.h"
+#include "libavutil/parseutils.h"
 #include "libavutil/pixfmt.h"
 #include "libavutil/rational.h"
 #include "libavutil/thread.h"
@@ -437,36 +438,8 @@  enum forced_keyframes_const {
 #define ABORT_ON_FLAG_EMPTY_OUTPUT        (1 <<  0)
 #define ABORT_ON_FLAG_EMPTY_OUTPUT_STREAM (1 <<  1)
 
-enum EncStatsType {
-    ENC_STATS_LITERAL = 0,
-    ENC_STATS_FILE_IDX,
-    ENC_STATS_STREAM_IDX,
-    ENC_STATS_FRAME_NUM,
-    ENC_STATS_FRAME_NUM_IN,
-    ENC_STATS_TIMEBASE,
-    ENC_STATS_TIMEBASE_IN,
-    ENC_STATS_PTS,
-    ENC_STATS_PTS_TIME,
-    ENC_STATS_PTS_IN,
-    ENC_STATS_PTS_TIME_IN,
-    ENC_STATS_DTS,
-    ENC_STATS_DTS_TIME,
-    ENC_STATS_SAMPLE_NUM,
-    ENC_STATS_NB_SAMPLES,
-    ENC_STATS_PKT_SIZE,
-    ENC_STATS_BITRATE,
-    ENC_STATS_AVG_BITRATE,
-};
-
-typedef struct EncStatsComponent {
-    enum EncStatsType type;
-
-    uint8_t *str;
-    size_t   str_len;
-} EncStatsComponent;
-
 typedef struct EncStats {
-    EncStatsComponent  *components;
+    AVEncStatsComponent  *components;
     int              nb_components;
 
     AVIOContext        *io;
diff --git a/fftools/ffmpeg_enc.c b/fftools/ffmpeg_enc.c
index fa4539664f..a499bc0c81 100644
--- a/fftools/ffmpeg_enc.c
+++ b/fftools/ffmpeg_enc.c
@@ -30,6 +30,7 @@ 
 #include "libavutil/frame.h"
 #include "libavutil/intreadwrite.h"
 #include "libavutil/log.h"
+#include "libavutil/parseutils.h"
 #include "libavutil/pixdesc.h"
 #include "libavutil/rational.h"
 #include "libavutil/timestamp.h"
@@ -499,7 +500,7 @@  void enc_stats_write(OutputStream *ost, EncStats *es,
     }
 
     for (size_t i = 0; i < es->nb_components; i++) {
-        const EncStatsComponent *c = &es->components[i];
+        const AVEncStatsComponent *c = &es->components[i];
 
         switch (c->type) {
         case ENC_STATS_LITERAL:         avio_write (io, c->str,     c->str_len);                    continue;
diff --git a/fftools/ffmpeg_mux_init.c b/fftools/ffmpeg_mux_init.c
index 6c473a8f09..6acdf92c2c 100644
--- a/fftools/ffmpeg_mux_init.c
+++ b/fftools/ffmpeg_mux_init.c
@@ -242,152 +242,26 @@  void of_enc_stats_close(void)
     nb_enc_stats_files = 0;
 }
 
-static int unescape(char **pdst, size_t *dst_len,
-                    const char **pstr, char delim)
-{
-    const char *str = *pstr;
-    char *dst;
-    size_t len, idx;
-
-    *pdst = NULL;
-
-    len = strlen(str);
-    if (!len)
-        return 0;
-
-    dst = av_malloc(len + 1);
-    if (!dst)
-        return AVERROR(ENOMEM);
-
-    for (idx = 0; *str; idx++, str++) {
-        if (str[0] == '\\' && str[1])
-            str++;
-        else if (*str == delim)
-            break;
-
-        dst[idx] = *str;
-    }
-    if (!idx) {
-        av_freep(&dst);
-        return 0;
-    }
-
-    dst[idx] = 0;
-
-    *pdst    = dst;
-    *dst_len = idx;
-    *pstr    = str;
-
-    return 0;
-}
-
 static int enc_stats_init(OutputStream *ost, EncStats *es, int pre,
                           const char *path, const char *fmt_spec)
 {
-    static const struct {
-        enum EncStatsType  type;
-        const char        *str;
-        int                pre_only:1;
-        int                post_only:1;
-        int                need_input_data:1;
-    } fmt_specs[] = {
-        { ENC_STATS_FILE_IDX,       "fidx"                      },
-        { ENC_STATS_STREAM_IDX,     "sidx"                      },
-        { ENC_STATS_FRAME_NUM,      "n"                         },
-        { ENC_STATS_FRAME_NUM_IN,   "ni",       0, 0, 1         },
-        { ENC_STATS_TIMEBASE,       "tb"                        },
-        { ENC_STATS_TIMEBASE_IN,    "tbi",      0, 0, 1         },
-        { ENC_STATS_PTS,            "pts"                       },
-        { ENC_STATS_PTS_TIME,       "t"                         },
-        { ENC_STATS_PTS_IN,         "ptsi",     0, 0, 1         },
-        { ENC_STATS_PTS_TIME_IN,    "ti",       0, 0, 1         },
-        { ENC_STATS_DTS,            "dts",      0, 1            },
-        { ENC_STATS_DTS_TIME,       "dt",       0, 1            },
-        { ENC_STATS_SAMPLE_NUM,     "sn",       1               },
-        { ENC_STATS_NB_SAMPLES,     "samp",     1               },
-        { ENC_STATS_PKT_SIZE,       "size",     0, 1            },
-        { ENC_STATS_BITRATE,        "br",       0, 1            },
-        { ENC_STATS_AVG_BITRATE,    "abr",      0, 1            },
-    };
-    const char *next = fmt_spec;
-
     int ret;
 
-    while (*next) {
-        EncStatsComponent *c;
-        char *val;
-        size_t val_len;
-
-        // get the sequence up until next opening brace
-        ret = unescape(&val, &val_len, &next, '{');
-        if (ret < 0)
-            return ret;
-
-        if (val) {
-            ret = GROW_ARRAY(es->components, es->nb_components);
-            if (ret < 0) {
-                av_freep(&val);
-                return ret;
-            }
-
-            c          = &es->components[es->nb_components - 1];
-            c->type    = ENC_STATS_LITERAL;
-            c->str     = val;
-            c->str_len = val_len;
-        }
-
-        if (!*next)
-            break;
-        next++;
-
-        // get the part inside braces
-        ret = unescape(&val, &val_len, &next, '}');
-        if (ret < 0)
-            return ret;
-
-        if (!val) {
-            av_log(NULL, AV_LOG_ERROR,
-                   "Empty formatting directive in: %s\n", fmt_spec);
-            return AVERROR(EINVAL);
-        }
-
-        if (!*next) {
-            av_log(NULL, AV_LOG_ERROR,
-                   "Missing closing brace in: %s\n", fmt_spec);
-            ret = AVERROR(EINVAL);
-            goto fail;
-        }
-        next++;
-
-        ret = GROW_ARRAY(es->components, es->nb_components);
-        if (ret < 0)
-            goto fail;
-
-        c = &es->components[es->nb_components - 1];
-
-        for (size_t i = 0; i < FF_ARRAY_ELEMS(fmt_specs); i++) {
-            if (!strcmp(val, fmt_specs[i].str)) {
-                c->type = fmt_specs[i].type;
-                c->str  = val;
-                c->str_len = val_len;
-                break;
-            }
-        }
-
-        if (!c->type) {
-            av_log(NULL, AV_LOG_ERROR, "Invalid format directive: %s\n", val);
-            ret = AVERROR(EINVAL);
-            goto fail;
-        }
-    }
+    ret = av_parse_enc_stats_components(&es->components, &es->nb_components, fmt_spec);
+    if (ret < 0)
+        goto fail;
 
     for (int j = 0; j < es->nb_components; j++) {
-        EncStatsComponent *c = &es->components[j];
+        AVEncStatsComponent *c = &es->components[j];
         char *val = c->str;
 
-        for (size_t i = 0; i < FF_ARRAY_ELEMS(fmt_specs); i++) {
-            if (!strcmp(val, fmt_specs[i].str)) {
-                if ((pre && fmt_specs[i].post_only) || (!pre && fmt_specs[i].pre_only)) {
+        for (size_t i = 0; i < av_get_nb_stats_fmt_specs(); i++) {
+            AVEncStatsFormatSpecifier f;
+            if (av_get_stats_fmt_spec(&f, i))
+                goto fail;
+
+            if (!strcmp(val, f.str)) {
+                if ((pre && f.post_only) || (!pre && f.pre_only)) {
                     av_log(NULL, AV_LOG_ERROR,
                            "Format directive '%s' may only be used %s-encoding\n",
                            val, pre ? "post" : "pre");
@@ -395,7 +269,7 @@  static int enc_stats_init(OutputStream *ost, EncStats *es, int pre,
                     goto fail;
                 }
 
-                if (fmt_specs[i].need_input_data && !ost->ist) {
+                if (f.need_input_data && !ost->ist) {
                     av_log(ost, AV_LOG_WARNING,
                            "Format directive '%s' is unavailable, because "
                            "this output stream has no associated input stream\n",
@@ -410,7 +284,7 @@  static int enc_stats_init(OutputStream *ost, EncStats *es, int pre,
     ret = enc_stats_get_file(&es->io, path);
 fail:
     for (int j = 0; j < es->nb_components; j++) {
-        EncStatsComponent *c = &es->components[j];
+        AVEncStatsComponent *c = &es->components[j];
         if (c->type != ENC_STATS_LITERAL) {
             av_freep(&c->str);
         }
diff --git a/libavutil/parseutils.c b/libavutil/parseutils.c
index 94e88e0a79..796811aa6e 100644
--- a/libavutil/parseutils.c
+++ b/libavutil/parseutils.c
@@ -788,3 +788,179 @@  int av_find_info_tag(char *arg, int arg_size, const char *tag1, const char *info
     }
     return 0;
 }
+
+static const AVEncStatsFormatSpecifier fmt_specs[] = {
+    { ENC_STATS_FILE_IDX,       "fidx"                      },
+    { ENC_STATS_STREAM_IDX,     "sidx"                      },
+    { ENC_STATS_FRAME_NUM,      "n"                         },
+    { ENC_STATS_FRAME_NUM_IN,   "ni",       0, 0, 1         },
+    { ENC_STATS_TIMEBASE,       "tb"                        },
+    { ENC_STATS_TIMEBASE_IN,    "tbi",      0, 0, 1         },
+    { ENC_STATS_PTS,            "pts"                       },
+    { ENC_STATS_PTS_TIME,       "t"                         },
+    { ENC_STATS_PTS_IN,         "ptsi",     0, 0, 1         },
+    { ENC_STATS_PTS_TIME_IN,    "ti",       0, 0, 1         },
+    { ENC_STATS_DTS,            "dts",      0, 1            },
+    { ENC_STATS_DTS_TIME,       "dt",       0, 1            },
+    { ENC_STATS_SAMPLE_NUM,     "sn",       1               },
+    { ENC_STATS_NB_SAMPLES,     "samp",     1               },
+    { ENC_STATS_PKT_SIZE,       "size",     0, 1            },
+    { ENC_STATS_BITRATE,        "br",       0, 1            },
+    { ENC_STATS_AVG_BITRATE,    "abr",      0, 1            },
+};
+
+size_t av_get_nb_stats_fmt_specs(void)
+{
+    return sizeof(fmt_specs) / sizeof(*fmt_specs);
+}
+
+int av_get_stats_fmt_spec(AVEncStatsFormatSpecifier *fmt_spec, int idx)
+{
+    if (!fmt_spec || idx >= av_get_nb_stats_fmt_specs())
+        return AVERROR(EINVAL);
+
+    *fmt_spec = fmt_specs[idx];
+    return 0;
+}
+
+static int unescape(char **pdst, size_t *dst_len,
+                    const char **pstr, char delim)
+{
+    const char *str = *pstr;
+    char *dst;
+    size_t len, idx;
+
+    *pdst = NULL;
+
+    len = strlen(str);
+    if (!len)
+        return 0;
+
+    dst = av_malloc(len + 1);
+    if (!dst)
+        return AVERROR(ENOMEM);
+
+    for (idx = 0; *str; idx++, str++) {
+        if (str[0] == '\\' && str[1])
+            str++;
+        else if (*str == delim)
+            break;
+
+        dst[idx] = *str;
+    }
+    if (!idx) {
+        av_freep(&dst);
+        return 0;
+    }
+
+    dst[idx] = 0;
+
+    *pdst    = dst;
+    *dst_len = idx;
+    *pstr    = str;
+
+    return 0;
+}
+
+static int grow_array(void **array, int elem_size, int *size, int new_size)
+{
+    if (new_size >= INT_MAX / elem_size) {
+        av_log(NULL, AV_LOG_ERROR, "Array too big.\n");
+        return AVERROR(ERANGE);
+    }
+    if (*size < new_size) {
+        uint8_t *tmp = av_realloc_array(*array, new_size, elem_size);
+        if (!tmp)
+            return AVERROR(ENOMEM);
+        memset(tmp + *size*elem_size, 0, (new_size-*size) * elem_size);
+        *size = new_size;
+        *array = tmp;
+        return 0;
+    }
+    return 0;
+}
+
+#define GROW_ARRAY(array, nb_elems)\
+    grow_array((void**)&array, sizeof(*array), &nb_elems, nb_elems + 1)
+
+int av_parse_enc_stats_components(AVEncStatsComponent **components, int *nb_components, const char *fmt_spec)
+{
+    const char *next = fmt_spec;
+    int ret;
+
+    while (*next) {
+        AVEncStatsComponent *c;
+        char *val;
+        size_t val_len;
+
+        // get the sequence up until next opening brace
+        ret = unescape(&val, &val_len, &next, '{');
+        if (ret < 0)
+            return ret;
+
+        if (val) {
+            ret = GROW_ARRAY(*components, *nb_components);
+            if (ret < 0) {
+                av_freep(&val);
+                return ret;
+            }
+
+            c          = &((*components)[*nb_components - 1]);
+            c->type    = ENC_STATS_LITERAL;
+            c->str     = val;
+            c->str_len = val_len;
+        }
+
+        if (!*next)
+            break;
+        next++;
+
+        // get the part inside braces
+        ret = unescape(&val, &val_len, &next, '}');
+        if (ret < 0)
+            return ret;
+
+        if (!val) {
+            av_log(NULL, AV_LOG_ERROR,
+                   "Empty formatting directive in: %s\n", fmt_spec);
+            return AVERROR(EINVAL);
+        }
+
+        if (!*next) {
+            av_log(NULL, AV_LOG_ERROR,
+                   "Missing closing brace in: %s\n", fmt_spec);
+            ret = AVERROR(EINVAL);
+            goto fail;
+        }
+        next++;
+
+        ret = GROW_ARRAY(*components, *nb_components);
+        if (ret < 0)
+            goto fail;
+
+        c = &(*components)[*nb_components - 1];
+
+        for (int i = 0; i < FF_ARRAY_ELEMS(fmt_specs); i++) {
+            if (!strcmp(val, fmt_specs[i].str)) {
+                c->type    = fmt_specs[i].type;
+                c->str     = val;
+                c->str_len = val_len;
+                break;
+            }
+        }
+
+        if (!c->type) {
+            av_log(NULL, AV_LOG_ERROR, "Invalid format directive: %s\n", val);
+            ret = AVERROR(EINVAL);
+            goto fail;
+        }
+
+        continue;
+fail:
+        av_freep(&val);
+        if (ret < 0)
+            return ret;
+    }
+
+    return 0;
+}
diff --git a/libavutil/parseutils.h b/libavutil/parseutils.h
index dad5c2775b..d546d77de0 100644
--- a/libavutil/parseutils.h
+++ b/libavutil/parseutils.h
@@ -22,6 +22,7 @@ 
 #include <time.h>
 
 #include "rational.h"
+#include "mem.h"
 
 /**
  * @file
@@ -194,4 +195,105 @@  char *av_small_strptime(const char *p, const char *fmt, struct tm *dt);
  */
 time_t av_timegm(struct tm *tm);
 
+typedef enum AVEncStatsType {
+    ENC_STATS_LITERAL = 0,
+    ENC_STATS_FILE_IDX,
+    ENC_STATS_STREAM_IDX,
+    ENC_STATS_FRAME_NUM,
+    ENC_STATS_FRAME_NUM_IN,
+    ENC_STATS_TIMEBASE,
+    ENC_STATS_TIMEBASE_IN,
+    ENC_STATS_PTS,
+    ENC_STATS_PTS_TIME,
+    ENC_STATS_PTS_IN,
+    ENC_STATS_PTS_TIME_IN,
+    ENC_STATS_DTS,
+    ENC_STATS_DTS_TIME,
+    ENC_STATS_SAMPLE_NUM,
+    ENC_STATS_NB_SAMPLES,
+    ENC_STATS_PKT_SIZE,
+    ENC_STATS_BITRATE,
+    ENC_STATS_AVG_BITRATE,
+} AVEncStatsType;
+
+/**
+ * Structure describing an encoding stats component.
+ */
+typedef struct AVEncStatsComponent {
+    /**
+     * The type of this component.
+     */
+    AVEncStatsType type;
+    /**
+     * The string representation of this component.
+     */
+    uint8_t *str;
+    /**
+     * The length of the string.
+     */
+    size_t   str_len;
+} AVEncStatsComponent;
+
+/**
+ * Structure describing an encoding stats format specifier.
+ */
+typedef struct AVEncStatsFormatSpecifier {
+    /**
+     * The type of this format specifier.
+     */
+    enum AVEncStatsType  type;
+    /**
+     * The string representation of this format specifier.
+     */
+    const char          *str;
+    /**
+     * Flag if this format specifier is only valid before encoding.
+     */
+    int                  pre_only:1;
+    /**
+     * Flag if this format specifier is only valid after encoding.
+     */
+    int                  post_only:1;
+    /**
+     * Flag if this format specifier requires an associated input
+     * stream for it to be processed.
+     */
+    int                  need_input_data:1;
+} AVEncStatsFormatSpecifier;
+
+/**
+ * Get the number of available enc stats format specifiers.
+ */
+size_t av_get_nb_stats_fmt_specs(void);
+
+/**
+ * Get a copy of an enc stats format specifier.
+ *
+ * @param *fmt_spec Destination for the copy of the format specifier.
+ *                  Has to be previously allocated.
+ *
+ * @param idx       Index to the table of format specifiers.
+ *
+ * @return          Return 0 on success, a negative value corresponding
+ *                  to an AVERROR code otherwise.
+ */
+int av_get_stats_fmt_spec(AVEncStatsFormatSpecifier *fmt_spec, int idx);
+
+/**
+ * Parse an enc stats format string into an array of AVEncStatsComponent.
+ *
+ * @param components Pointer to the array of AVEncStatsComponent to store
+ *                   the parsed elements. The arrary will be reallocated
+ *                   in the process if any elements are found.
+ *
+ * @param nb_components Pointer to the number of components in the array.
+ *
+ * @param fmt_spec   The format string to parse for components.
+ *
+ * @return          Return 0 on success, a negative value corresponding
+ *                  to an AVERROR code otherwise.
+ *
+ */
+int av_parse_enc_stats_components(AVEncStatsComponent **components, int *nb_components, const char *fmt_spec);
+
 #endif /* AVUTIL_PARSEUTILS_H */
diff --git a/libavutil/version.h b/libavutil/version.h
index c5fa7c3692..0684996bf2 100644
--- a/libavutil/version.h
+++ b/libavutil/version.h
@@ -79,7 +79,7 @@ 
  */
 
 #define LIBAVUTIL_VERSION_MAJOR  58
-#define LIBAVUTIL_VERSION_MINOR  32
+#define LIBAVUTIL_VERSION_MINOR  33
 #define LIBAVUTIL_VERSION_MICRO 100
 
 #define LIBAVUTIL_VERSION_INT   AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \