diff mbox series

[FFmpeg-devel,RFC] Add option for writing detailed filtergraph information to file or stdout

Message ID MN2PR04MB59819AD807350499BD5E94F5BAC79@MN2PR04MB5981.namprd04.prod.outlook.com
State Superseded, archived
Headers show
Series [FFmpeg-devel,RFC] Add option for writing detailed filtergraph information to file or stdout | expand

Checks

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

Commit Message

Soft Works Aug. 26, 2021, 7:12 a.m. UTC
This is a more complete and more flexible alternative to the recently submitted
patch "fftools: add -lavfi_dump option"
https://patchwork.ffmpeg.org/project/ffmpeg/patch/20210823094504.100789-2-george@nsup.org/

I'm tagging this patch as 'RFC'. There's no need to tell that this is duplicating
code from ffprobe. It does, because that's the most efficient way for the code to
live at my side (unaffected and safe from regressions by upstream changes).

Should it be considered for merging into main, it will likely make sense to merge
and or re-use ffprobe output writers.

The key benefits of this implementation are:

- Other than graph2dot and other means for printing filter graphs, this is
  outputting:
  - all graphs with runtime state
    (including auto-inserted filters)
  - each graph with its inputs and outputs
  - all filters with their in- and output pads
  - all connections between all input- and output pads
  - for each connection:
    - the runtime-negotiated format and media type
    - the hw context
    - if video hw context, both: hw pixfmt + sw pixfmt
- Output can either be printed to stdout or written to specified file
- Output is machine-readable
- Can output in all the same formats like ffprobe (default, compact, flat,
  ini, json, xml)

Signed-off-by: softworkz <softworkz@hotmail.com>
---
 fftools/Makefile        |    2 +-
 fftools/ffmpeg.c        |   15 +
 fftools/ffmpeg.h        |    3 +
 fftools/ffmpeg_opt.c    |    9 +
 fftools/filterwriters.c | 1547 +++++++++++++++++++++++++++++++++++++++
 fftools/filterwriters.h |  193 +++++
 libavcodec/allcodecs.c  |    1 -
 7 files changed, 1768 insertions(+), 2 deletions(-)
 create mode 100644 fftools/filterwriters.c
 create mode 100644 fftools/filterwriters.h

Comments

Nicolas George Aug. 26, 2021, 7:34 a.m. UTC | #1
Soft Works (12021-08-26):
> This is a more complete and more flexible alternative to the recently submitted
> patch "fftools: add -lavfi_dump option"
> https://patchwork.ffmpeg.org/project/ffmpeg/patch/20210823094504.100789-2-george@nsup.org/
> 
> I'm tagging this patch as 'RFC'. There's no need to tell that this is duplicating
> code from ffprobe. It does, because that's the most efficient way for the code to
> live at my side (unaffected and safe from regressions by upstream changes).
> 
> Should it be considered for merging into main, it will likely make sense to merge
> and or re-use ffprobe output writers.
> 
> The key benefits of this implementation are:
> 
> - Other than graph2dot and other means for printing filter graphs, this is
>   outputting:
>   - all graphs with runtime state
>     (including auto-inserted filters)
>   - each graph with its inputs and outputs
>   - all filters with their in- and output pads
>   - all connections between all input- and output pads
>   - for each connection:
>     - the runtime-negotiated format and media type
>     - the hw context
>     - if video hw context, both: hw pixfmt + sw pixfmt
> - Output can either be printed to stdout or written to specified file
> - Output is machine-readable
> - Can output in all the same formats like ffprobe (default, compact, flat,
>   ini, json, xml)

What is the practical purpose of this feature?
Paul B Mahol Aug. 26, 2021, 7:36 a.m. UTC | #2
Unacceptable, NACK.
Soft Works Aug. 26, 2021, 7:37 a.m. UTC | #3
> -----Original Message-----
> From: ffmpeg-devel <ffmpeg-devel-bounces@ffmpeg.org> On Behalf Of
> Nicolas George
> Sent: Thursday, 26 August 2021 09:34
> To: FFmpeg development discussions and patches <ffmpeg-
> devel@ffmpeg.org>
> Subject: Re: [FFmpeg-devel] [PATCH] [RFC] Add option for writing
> detailed filtergraph information to file or stdout
> 
> Soft Works (12021-08-26):
> > This is a more complete and more flexible alternative to the
> recently submitted
> > patch "fftools: add -lavfi_dump option"
> >
> https://patchwork.ffmpeg.org/project/ffmpeg/patch/20210823094504.1007
> 89-2-george@nsup.org/
> >
> > I'm tagging this patch as 'RFC'. There's no need to tell that this
> is duplicating
> > code from ffprobe. It does, because that's the most efficient way
> for the code to
> > live at my side (unaffected and safe from regressions by upstream
> changes).
> >
> > Should it be considered for merging into main, it will likely make
> sense to merge
> > and or re-use ffprobe output writers.
> >
> > The key benefits of this implementation are:
> >
> > - Other than graph2dot and other means for printing filter graphs,
> this is
> >   outputting:
> >   - all graphs with runtime state
> >     (including auto-inserted filters)
> >   - each graph with its inputs and outputs
> >   - all filters with their in- and output pads
> >   - all connections between all input- and output pads
> >   - for each connection:
> >     - the runtime-negotiated format and media type
> >     - the hw context
> >     - if video hw context, both: hw pixfmt + sw pixfmt
> > - Output can either be printed to stdout or written to specified
> file
> > - Output is machine-readable
> > - Can output in all the same formats like ffprobe (default,
> compact, flat,
> >   ini, json, xml)
> 
> What is the practical purpose of this feature?

The purpose is to get a precise and detailed view of the filtergraphs
at runtime including all connections with their negotiated formats and 
media types.

sw
Nicolas George Aug. 26, 2021, 7:40 a.m. UTC | #4
Soft Works (12021-08-26):
> The purpose is to get a precise and detailed view of the filtergraphs
> at runtime including all connections with their negotiated formats and 
> media types.

I asked the purpose, you are rephrasing what the patch does.
Soft Works Aug. 26, 2021, 7:58 a.m. UTC | #5
> -----Original Message-----
> From: ffmpeg-devel <ffmpeg-devel-bounces@ffmpeg.org> On Behalf Of
> Nicolas George
> Sent: Thursday, 26 August 2021 09:40
> To: FFmpeg development discussions and patches <ffmpeg-
> devel@ffmpeg.org>
> Subject: Re: [FFmpeg-devel] [PATCH] [RFC] Add option for writing
> detailed filtergraph information to file or stdout
> 
> Soft Works (12021-08-26):
> > The purpose is to get a precise and detailed view of the
> filtergraphs
> > at runtime including all connections with their negotiated formats
> and
> > media types.
> 
> I asked the purpose, you are rephrasing what the patch does.

Okay, then let's go back a few years and I'll tell you about my
original motivation. 

I often did not understand why filters couldn't connect to each other
or when they did - how they actually connected and which format was 
actually used between them.
In many cases, this wasn't clear from the log output.

I've also seen that the format filter got inserted quite often, but 
I wanted to know exactly where it gets inserted and from which
to which format it converts in each case.

Another aspect that I needed to understand precisely was the use
of formats in case of hw acceleration (sw pixel format); there
was no way to see what was being used and negotiated.

We are covering a wide range of platforms and hardware accelerations
and the requirements are complex: deinterlacing, scaling, color 
conversion, tone mapping, overlay of graphical and textual subtitles, 
etc. and all that in arbitrary combinations with almost all
ffmpeg-supported hwas. All that needs to work as good as possible
on all older and newer hardware, which is not a trivial task.

I'm using the output that this patch is creating either for diagnosing,
either by manual review or in combination with another tool
that allows to visualize those filtergraphs like I had shown last year:

https://github.com/softworkz/ffmpegfiltergraphs/issues/1

Does that answer your question?

Kind regards,
softworkz
Nicolas George Aug. 26, 2021, 8:04 a.m. UTC | #6
Soft Works (12021-08-26):
> Does that answer your question?

Yes. It is almost exactly what I had guessed. And therefore my answer is
what I expected it do be: that does not justify thousands, or even
hundreds, of lines of code in the core library and tools.
Soft Works Aug. 26, 2021, 8:11 a.m. UTC | #7
> -----Original Message-----
> From: ffmpeg-devel <ffmpeg-devel-bounces@ffmpeg.org> On Behalf Of
> Nicolas George
> Sent: Thursday, 26 August 2021 10:05
> To: FFmpeg development discussions and patches <ffmpeg-
> devel@ffmpeg.org>
> Subject: Re: [FFmpeg-devel] [PATCH] [RFC] Add option for writing
> detailed filtergraph information to file or stdout
> 
> Soft Works (12021-08-26):
> > Does that answer your question?
> 
> Yes. It is almost exactly what I had guessed. And therefore my answer
> is
> what I expected it do be: that does not justify thousands, or even
> hundreds, of lines of code in the core library and tools.

Did you actually read the patch message? I just didn't want to waste 
time for merging the code with the ffprobe writers.
The question is whether we could agree about the actual output which is 
just line 1185-1518 in filterwriters.c. 
All the rest is just duplicated.

softworkz
Soft Works Aug. 26, 2021, 8:58 a.m. UTC | #8
> -----Original Message-----
> From: ffmpeg-devel <ffmpeg-devel-bounces@ffmpeg.org> On Behalf Of
> Nicolas George
> Sent: Thursday, 26 August 2021 10:05
> To: FFmpeg development discussions and patches <ffmpeg-
> devel@ffmpeg.org>
> Subject: Re: [FFmpeg-devel] [PATCH] [RFC] Add option for writing
> detailed filtergraph information to file or stdout
> 
> Soft Works (12021-08-26):
> > Does that answer your question?
> 
> Yes. It is almost exactly what I had guessed. And therefore my answer
> is
> what I expected it do be: that does not justify thousands, or even
> hundreds, of lines of code in the core library and tools.

To be clear: I didn't submit this to end up in a LOC count discussion.
My point is that there's much more relevant and important information
that a filtergraph-information feature should provide than what your
proposed patch is outputting.

It doesn't make sense to me to add once another half-baked graph-output
functionality that provides just a fraction of the relevant information,
consisting of just two or three values that a developer is currently 
interested in (and then praising it for its low LOC count).

A new feature for outputting filtergraph information should be complete,
comprehensive and provide machine readable output.
It doesn't have to be my code, but it should provide similar information
(ideally even more).

softworkz
Nicolas George Aug. 26, 2021, 9:03 a.m. UTC | #9
Soft Works (12021-08-26):
> To be clear: I didn't submit this to end up in a LOC count discussion.

Good.

> My point is that there's much more relevant and important information
> that a filtergraph-information feature should provide than what your
> proposed patch is outputting.

Which one?

> comprehensive and provide machine readable output.

You are wrong on the "machine readable" point, especially if it means
adding a lot of code in the library and more importantly ensuring
long-term compatibility. What you want to print is internal information.
It is useful for debugging and testing purposes but should not be
accessed by regular tools.
Soft Works Aug. 26, 2021, 9:20 a.m. UTC | #10
> -----Original Message-----
> From: ffmpeg-devel <ffmpeg-devel-bounces@ffmpeg.org> On Behalf Of
> Nicolas George
> Sent: Thursday, 26 August 2021 11:03
> To: FFmpeg development discussions and patches <ffmpeg-
> devel@ffmpeg.org>
> Subject: Re: [FFmpeg-devel] [PATCH] [RFC] Add option for writing
> detailed filtergraph information to file or stdout
> 
> Soft Works (12021-08-26):
> > To be clear: I didn't submit this to end up in a LOC count
> discussion.
> 
> Good.
> 
> > My point is that there's much more relevant and important
> information
> > that a filtergraph-information feature should provide than what
> your
> > proposed patch is outputting.
> 
> Which one?

All of what my patch is outputting (and ideally even more).

Please see here for an example:
https://gist.github.com/softworkz/08cd593cebdfb5da3bbe1f6e5683320e

> > comprehensive and provide machine readable output.
> 
> You are wrong on the "machine readable" point, 

No - you are wrong! I do not "want" to print that. I have that in 
place for years and I'm using this information regularly for diagnosing
user issues. 

> more importantly ensuring long-term compatibility. 

When you don't change it, it won't break. As simple as that.

> What you want to print is internal information.

How do you come to that idea? When I specify a filter graph
and perform format conversions, I specify the formats to which 
I want to convert between filters.

And now you want to tell me that the actual formats that have 
been negotiated between filters are "internal"? Like an implementation
detail?
No. Those are not implementation details. I set up a filtergraph
with a specific intention and formats for the individual connections
and that means that knowing about the actually negotiated formats
cannot be considered "internal".

softworkz










> It is useful for debugging and testing purposes but should not be
> accessed by regular tools.
Soft Works Aug. 26, 2021, 10:30 a.m. UTC | #11
> -----Original Message-----
> From: ffmpeg-devel <ffmpeg-devel-bounces@ffmpeg.org> On Behalf Of
> Nicolas George
> Sent: Thursday, 26 August 2021 10:05
> To: FFmpeg development discussions and patches <ffmpeg-
> devel@ffmpeg.org>
> Subject: Re: [FFmpeg-devel] [PATCH] [RFC] Add option for writing
> detailed filtergraph information to file or stdout
> 
> Soft Works (12021-08-26):
> > Does that answer your question?
> 
> Yes. It is almost exactly what I had guessed. And therefore my answer
> is
> what I expected it do be: that does not justify thousands, or even
> hundreds, of lines of code in the core library and tools.

I'm getting confused.

While I've been going through your recent submissions, I noticed your
proposals from April: 

- lavu: new AVWriter API
- lavu: add a JSON writer API
- et. al.

There's no doubt for me that this proposal is a good thing, especially
when it would be able to replace the ffprobe writers eventually.
(I have once another project where I just duplicated the ffprobe writers
because they weren't easily reusable).
So, nice work, I really appreciate that.


What makes me wonder a bit:

- I had written in the commit message that the output writers would need 
  to be merged with the ffprobe writers

- But you didn’t tell that you already got the solution "in your pocket"

- Instead you criticized my patch for its LOC count, even though that is 
  just made up from the output writers code duplication...


I'm confused...

softworkz
Nicolas George Aug. 26, 2021, 11:11 a.m. UTC | #12
Soft Works (12021-08-26):
> - Instead you criticized my patch for its LOC count, even though that is 
>   just made up from the output writers code duplication...

You are mistaken here too. Unlike what you seem to think, I factored
your saying that the writers would have to be de-duplicated in my
accounting of the amount of code required.

The feature you propose is of dubious usefulness, if not harmful, it
would be only acceptable if it took very very little code.

This is the end of the discussion from my side until you start to take
maintenance and general usefulness into consideration in your
discussion.
Soft Works Aug. 26, 2021, 11:41 a.m. UTC | #13
> -----Original Message-----
> From: ffmpeg-devel <ffmpeg-devel-bounces@ffmpeg.org> On Behalf Of
> Nicolas George
> Sent: Thursday, 26 August 2021 13:12
> To: FFmpeg development discussions and patches <ffmpeg-
> devel@ffmpeg.org>
> Subject: Re: [FFmpeg-devel] [PATCH] [RFC] Add option for writing
> detailed filtergraph information to file or stdout
> 
> Soft Works (12021-08-26):
> > - Instead you criticized my patch for its LOC count, even though
> that is
> >   just made up from the output writers code duplication...
> 
> You are mistaken here too. Unlike what you seem to think, I factored
> your saying that the writers would have to be de-duplicated in my
> accounting of the amount of code required.
> 
> The feature you propose is of dubious usefulness

??

> if not harmful, 

Seriously?


> would be only acceptable if it took very very little code.

I'm sure I could squeeze it into a few weird one-liners, LOL

> This is the end of the discussion from my side until you start to
> take
> maintenance and general usefulness into consideration in your
> discussion.

I do take "general usefulness" into account. That's actually my
primary motivation which originated from the fact that I find the 
patch that you have submitted not much useful in that form because
it targets only a very specific and limited niche that you're 
intending to use it for. While I agree that it is useful for your
specific task, that change is not of "general usefulness".
Instead of setting up and raising demands upon your patch, I 
have submitted an alternative that I'm considering more
useful for general use than yours; without going into details,
simply because it provides a lot more important information
that is lacking from your approach.

softworkz
Soft Works Aug. 26, 2021, 10:30 p.m. UTC | #14
> -----Original Message-----
> From: ffmpeg-devel <ffmpeg-devel-bounces@ffmpeg.org> On Behalf Of
> Paul B Mahol
> Sent: Thursday, 26 August 2021 09:36
> To: FFmpeg development discussions and patches <ffmpeg-
> devel@ffmpeg.org>
> Subject: Re: [FFmpeg-devel] [PATCH] [RFC] Add option for writing
> detailed filtergraph information to file or stdout
> 
> Unacceptable, NACK.

Sounds like your new hit single?

I prefer classics though, like:

- NACK around the clock
- We will NACK you!
- NACKING on heaven's door..

:-)
diff mbox series

Patch

diff --git a/fftools/Makefile b/fftools/Makefile
index 5234932ab0..384b340018 100644
--- a/fftools/Makefile
+++ b/fftools/Makefile
@@ -9,7 +9,7 @@  AVBASENAMES  = ffmpeg ffplay ffprobe
 ALLAVPROGS   = $(AVBASENAMES:%=%$(PROGSSUF)$(EXESUF))
 ALLAVPROGS_G = $(AVBASENAMES:%=%$(PROGSSUF)_g$(EXESUF))
 
-OBJS-ffmpeg                        += fftools/ffmpeg_opt.o fftools/ffmpeg_filter.o fftools/ffmpeg_hw.o
+OBJS-ffmpeg                        += fftools/ffmpeg_opt.o fftools/ffmpeg_filter.o fftools/ffmpeg_hw.o fftools/filterwriters.o
 ifndef CONFIG_VIDEOTOOLBOX
 OBJS-ffmpeg-$(CONFIG_VDA)          += fftools/ffmpeg_videotoolbox.o
 endif
diff --git a/fftools/ffmpeg.c b/fftools/ffmpeg.c
index b0ce7c7c32..7ffdc5250c 100644
--- a/fftools/ffmpeg.c
+++ b/fftools/ffmpeg.c
@@ -103,6 +103,7 @@ 
 
 #include "ffmpeg.h"
 #include "cmdutils.h"
+#include "filterwriters.h"
 
 #include "libavutil/avassert.h"
 
@@ -139,6 +140,7 @@  static int64_t decode_error_stat[2];
 static unsigned nb_output_dumped = 0;
 
 static int want_sdp = 1;
+static int filtergraphs_printed = 0;
 
 static BenchmarkTimeStamps current_time;
 AVIOContext *progress_avio = NULL;
@@ -512,10 +514,23 @@  static int decode_interrupt_cb(void *ctx)
 
 const AVIOInterruptCB int_cb = { decode_interrupt_cb, NULL };
 
+static void check_print_graphs(void)
+{
+    if (filtergraphs_printed)
+        return;
+
+    if (print_graphs || print_graphs_file)
+        print_filtergraphs(filtergraphs, nb_filtergraphs);
+
+    filtergraphs_printed = 1;
+}
+
 static void ffmpeg_cleanup(int ret)
 {
     int i, j;
 
+    check_print_graphs();
+
     if (do_benchmark) {
         int maxrss = getmaxrss() / 1024;
         av_log(NULL, AV_LOG_INFO, "bench: maxrss=%ikB\n", maxrss);
diff --git a/fftools/ffmpeg.h b/fftools/ffmpeg.h
index d2dd7ca092..1697f42028 100644
--- a/fftools/ffmpeg.h
+++ b/fftools/ffmpeg.h
@@ -636,6 +636,9 @@  extern char *videotoolbox_pixfmt;
 extern int filter_nbthreads;
 extern int filter_complex_nbthreads;
 extern int vstats_version;
+extern int print_graphs;
+extern char* print_graphs_file;
+extern char* print_graphs_format;
 extern int auto_conversion_filters;
 
 extern const AVIOInterruptCB int_cb;
diff --git a/fftools/ffmpeg_opt.c b/fftools/ffmpeg_opt.c
index 428934a3d8..6ffded5f9d 100644
--- a/fftools/ffmpeg_opt.c
+++ b/fftools/ffmpeg_opt.c
@@ -172,6 +172,9 @@  float max_error_rate  = 2.0/3;
 int filter_nbthreads = 0;
 int filter_complex_nbthreads = 0;
 int vstats_version = 2;
+int print_graphs = 0;
+char* print_graphs_file = NULL;
+char* print_graphs_format = NULL;
 int auto_conversion_filters = 1;
 int64_t stats_period = 500000;
 
@@ -3634,6 +3637,12 @@  const OptionDef options[] = {
         "create a complex filtergraph", "graph_description" },
     { "filter_complex_script", HAS_ARG | OPT_EXPERT,                 { .func_arg = opt_filter_complex_script },
         "read complex filtergraph description from a file", "filename" },
+    { "print_graphs",   OPT_BOOL,                                    { &print_graphs },
+        "prints filtergraph details to stderr" },
+    { "print_graphs_file", HAS_ARG | OPT_STRING,                     { &print_graphs_file },
+        "writes graph details to a file", "filename" },
+    { "print_graphs_format", OPT_STRING | HAS_ARG,                   { &print_graphs_format },
+      "set the output printing format (available formats are: default, compact, csv, flat, ini, json, xml)", "format" },
     { "auto_conversion_filters", OPT_BOOL | OPT_EXPERT,              { &auto_conversion_filters },
         "enable automatic conversion filters globally" },
     { "stats",          OPT_BOOL,                                    { &print_stats },
diff --git a/fftools/filterwriters.c b/fftools/filterwriters.c
new file mode 100644
index 0000000000..070e201833
--- /dev/null
+++ b/fftools/filterwriters.c
@@ -0,0 +1,1547 @@ 
+/*
+ * Copyright (C) 2018 - softworkz for Emby Llc. 
+ * All rights reserved.
+ *
+ * This code is not yet published under any license.
+ *
+ */
+
+/**
+ * @file
+ * output writers for filtergraph details
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include "libavutil/avassert.h"
+#include "libavutil/avstring.h"
+#include "libavutil/mastering_display_metadata.h"
+#include "libavutil/opt.h"
+#include "libavutil/pixdesc.h"
+#include "libavutil/stereo3d.h"
+#include "libavutil/dict.h"
+#include "libavutil/intreadwrite.h"
+#include "libavutil/libm.h"
+#include "libavutil/common.h"
+#include "filterwriters.h"
+#include "libavfilter/avfilter.h"
+#include "libavfilter/internal.h"
+#include "libavutil/buffer.h"
+#include "libavutil/hwcontext.h"
+
+
+static const char *writer_get_name(void *p)
+{
+    WriterContext *wctx = p;
+    return wctx->writer->name;
+}
+
+#define OFFSET(x) offsetof(WriterContext, x)
+
+static const AVOption writer_options[] = {
+    { "string_validation", "set string validation mode",
+      OFFSET(string_validation), AV_OPT_TYPE_INT, {.i64=WRITER_STRING_VALIDATION_REPLACE}, 0, WRITER_STRING_VALIDATION_NB-1, .unit = "sv" },
+    { "sv", "set string validation mode",
+      OFFSET(string_validation), AV_OPT_TYPE_INT, {.i64=WRITER_STRING_VALIDATION_REPLACE}, 0, WRITER_STRING_VALIDATION_NB-1, .unit = "sv" },
+    { "ignore",  NULL, 0, AV_OPT_TYPE_CONST, {.i64 = WRITER_STRING_VALIDATION_IGNORE},  .unit = "sv" },
+    { "replace", NULL, 0, AV_OPT_TYPE_CONST, {.i64 = WRITER_STRING_VALIDATION_REPLACE}, .unit = "sv" },
+    { "fail",    NULL, 0, AV_OPT_TYPE_CONST, {.i64 = WRITER_STRING_VALIDATION_FAIL},    .unit = "sv" },
+    { "string_validation_replacement", "set string validation replacement string", OFFSET(string_validation_replacement), AV_OPT_TYPE_STRING, {.str=""}},
+    { "svr", "set string validation replacement string", OFFSET(string_validation_replacement), AV_OPT_TYPE_STRING, {.str="\xEF\xBF\xBD"}},
+    { NULL }
+};
+
+static void *writer_child_next(void *obj, void *prev)
+{
+    WriterContext *ctx = obj;
+    if (!prev && ctx->writer && ctx->writer->priv_class && ctx->priv)
+        return ctx->priv;
+    return NULL;
+}
+
+static const AVClass writer_class = {
+    .class_name = "Writer",
+    .item_name  = writer_get_name,
+    .option     = writer_options,
+    .version    = LIBAVUTIL_VERSION_INT,
+    .child_next = writer_child_next,
+};
+
+void writer_close(WriterContext **wctx)
+{
+    int i;
+
+    if (!*wctx)
+        return;
+
+    if ((*wctx)->writer->uninit)
+        (*wctx)->writer->uninit(*wctx);
+    for (i = 0; i < SECTION_MAX_NB_LEVELS; i++)
+        av_bprint_finalize(&(*wctx)->section_pbuf[i], NULL);
+    av_bprint_finalize(&(*wctx)->bpBuf, NULL);
+    if ((*wctx)->writer->priv_class)
+        av_opt_free((*wctx)->priv);
+    av_freep(&((*wctx)->priv));
+    av_opt_free(*wctx);
+    av_freep(wctx);
+}
+
+static void bprint_bytes(AVBPrint *bp, const uint8_t *ubuf, size_t ubuf_size)
+{
+    int i;
+    av_bprintf(bp, "0X");
+    for (i = 0; i < ubuf_size; i++)
+        av_bprintf(bp, "%02X", ubuf[i]);
+}
+
+int writer_open(WriterContext **wctx, const Writer *writer, const char *args,
+                       const struct section *sections, int nb_sections)
+{
+    int i, ret = 0;
+
+    if (!(*wctx = av_mallocz(sizeof(WriterContext)))) {
+        ret = AVERROR(ENOMEM);
+        goto fail;
+    }
+
+    if (!((*wctx)->priv = av_mallocz(writer->priv_size))) {
+        ret = AVERROR(ENOMEM);
+        goto fail;
+    }
+
+    (*wctx)->class = &writer_class;
+    (*wctx)->writer = writer;
+    (*wctx)->level = -1;
+    (*wctx)->sections = sections;
+    (*wctx)->nb_sections = nb_sections;
+
+    av_opt_set_defaults(*wctx);
+
+    if (writer->priv_class) {
+        void *priv_ctx = (*wctx)->priv;
+        *((const AVClass **)priv_ctx) = writer->priv_class;
+        av_opt_set_defaults(priv_ctx);
+    }
+
+    /* convert options to dictionary */
+    if (args) {
+        AVDictionary *opts = NULL;
+        AVDictionaryEntry *opt = NULL;
+
+        if ((ret = av_dict_parse_string(&opts, args, "=", ":", 0)) < 0) {
+            av_log(*wctx, AV_LOG_ERROR, "Failed to parse option string '%s' provided to writer context\n", args);
+            av_dict_free(&opts);
+            goto fail;
+        }
+
+        while ((opt = av_dict_get(opts, "", opt, AV_DICT_IGNORE_SUFFIX))) {
+            if ((ret = av_opt_set(*wctx, opt->key, opt->value, AV_OPT_SEARCH_CHILDREN)) < 0) {
+                av_log(*wctx, AV_LOG_ERROR, "Failed to set option '%s' with value '%s' provided to writer context\n",
+                       opt->key, opt->value);
+                av_dict_free(&opts);
+                goto fail;
+            }
+        }
+
+        av_dict_free(&opts);
+    }
+
+    /* validate replace string */
+    {
+        const uint8_t *p = (*wctx)->string_validation_replacement;
+        const uint8_t *endp = p + strlen(p);
+        while (*p) {
+            const uint8_t *p0 = p;
+            int32_t code;
+            ret = av_utf8_decode(&code, &p, endp, (*wctx)->string_validation_utf8_flags);
+            if (ret < 0) {
+                AVBPrint bp;
+                av_bprint_init(&bp, 0, AV_BPRINT_SIZE_AUTOMATIC);
+                bprint_bytes(&bp, p0, p-p0),
+                    av_log(wctx, AV_LOG_ERROR,
+                           "Invalid UTF8 sequence %s found in string validation replace '%s'\n",
+                           bp.str, (*wctx)->string_validation_replacement);
+                return ret;
+            }
+        }
+    }
+
+    for (i = 0; i < SECTION_MAX_NB_LEVELS; i++)
+        av_bprint_init(&(*wctx)->section_pbuf[i], 1, AV_BPRINT_SIZE_UNLIMITED);
+
+    av_bprint_init(&(*wctx)->bpBuf, 500000, AV_BPRINT_SIZE_UNLIMITED);
+
+    if ((*wctx)->writer->init)
+        ret = (*wctx)->writer->init(*wctx);
+    if (ret < 0)
+        goto fail;
+
+    return 0;
+
+fail:
+    writer_close(wctx);
+    return ret;
+}
+
+void writer_print_section_header(WriterContext *wctx, int section_id)
+{
+    //int parent_section_id;
+    wctx->level++;
+    av_assert0(wctx->level < SECTION_MAX_NB_LEVELS);
+    //parent_section_id = wctx->level ?
+    //    (wctx->section[wctx->level-1])->id : SECTION_ID_NONE;
+
+    wctx->nb_item[wctx->level] = 0;
+    wctx->section[wctx->level] = &wctx->sections[section_id];
+
+    if (wctx->writer->print_section_header)
+        wctx->writer->print_section_header(wctx);
+}
+
+void writer_print_section_footer(WriterContext *wctx)
+{
+    //int section_id = wctx->section[wctx->level]->id;
+    int parent_section_id = wctx->level ?
+        wctx->section[wctx->level-1]->id : SECTION_ID_NONE;
+
+    if (parent_section_id != SECTION_ID_NONE)
+        wctx->nb_item[wctx->level-1]++;
+
+    if (wctx->writer->print_section_footer)
+        wctx->writer->print_section_footer(wctx);
+    wctx->level--;
+}
+
+static inline int validate_string(WriterContext *wctx, char **dstp, const char *src)
+{
+    const uint8_t *p, *endp;
+    AVBPrint dstbuf;
+    int invalid_chars_nb = 0, ret = 0;
+
+    av_bprint_init(&dstbuf, 0, AV_BPRINT_SIZE_UNLIMITED);
+
+    endp = src + strlen(src);
+    for (p = (uint8_t *)src; *p;) {
+        uint32_t code;
+        int invalid = 0;
+        const uint8_t *p0 = p;
+
+        if (av_utf8_decode(&code, &p, endp, wctx->string_validation_utf8_flags) < 0) {
+            AVBPrint bp;
+            av_bprint_init(&bp, 0, AV_BPRINT_SIZE_AUTOMATIC);
+            bprint_bytes(&bp, p0, p-p0);
+            av_log(wctx, AV_LOG_DEBUG,
+                   "Invalid UTF-8 sequence %s found in string '%s'\n", bp.str, src);
+            invalid = 1;
+        }
+
+        if (invalid) {
+            invalid_chars_nb++;
+
+            switch (wctx->string_validation) {
+            case WRITER_STRING_VALIDATION_FAIL:
+                av_log(wctx, AV_LOG_ERROR,
+                       "Invalid UTF-8 sequence found in string '%s'\n", src);
+                ret = AVERROR_INVALIDDATA;
+                goto end;
+
+            case WRITER_STRING_VALIDATION_REPLACE:
+                av_bprintf(&dstbuf, "%s", wctx->string_validation_replacement);
+                break;
+            }
+        }
+
+        if (!invalid || wctx->string_validation == WRITER_STRING_VALIDATION_IGNORE)
+            av_bprint_append_data(&dstbuf, p0, p-p0);
+    }
+
+    if (invalid_chars_nb && wctx->string_validation == WRITER_STRING_VALIDATION_REPLACE) {
+        av_log(wctx, AV_LOG_WARNING,
+               "%d invalid UTF-8 sequence(s) found in string '%s', replaced with '%s'\n",
+               invalid_chars_nb, src, wctx->string_validation_replacement);
+    }
+
+end:
+    av_bprint_finalize(&dstbuf, dstp);
+    return ret;
+}
+
+int writer_print_string(WriterContext *wctx, const char *key, const char *val, int flags)
+{
+    const struct section *section = wctx->section[wctx->level];
+    int ret = 0;
+
+    if ((flags & PRINT_STRING_OPT)
+        && !(wctx->writer->flags & WRITER_FLAG_DISPLAY_OPTIONAL_FIELDS))
+        return 0;
+
+    if (val == NULL)
+        return 0;
+
+    if (flags & PRINT_STRING_VALIDATE) {
+        char *key1 = NULL, *val1 = NULL;
+        ret = validate_string(wctx, &key1, key);
+        if (ret < 0) goto end;
+        ret = validate_string(wctx, &val1, val);
+        if (ret < 0) goto end;
+        wctx->writer->print_string(wctx, key1, val1);
+    end:
+        if (ret < 0) {
+            av_log(wctx, AV_LOG_ERROR,
+                    "Invalid key=value string combination %s=%s in section %s\n",
+                    key, val, section->unique_name);
+        }
+        av_free(key1);
+        av_free(val1);
+    } else {
+        wctx->writer->print_string(wctx, key, val);
+    }
+
+    wctx->nb_item[wctx->level]++;
+
+    return ret;
+}
+
+void writer_print_integer(WriterContext *wctx, const char *key, long long int val)
+{
+    //const struct section *section = wctx->section[wctx->level];
+
+    wctx->writer->print_integer(wctx, key, val);
+    wctx->nb_item[wctx->level]++;
+}
+
+void writer_print_rational(WriterContext *wctx, const char *key, AVRational q, char sep)
+{
+    AVBPrint buf;
+    av_bprint_init(&buf, 0, AV_BPRINT_SIZE_AUTOMATIC);
+    av_bprintf(&buf, "%d%c%d", q.num, sep, q.den);
+    writer_print_string(wctx, key, buf.str, 0);
+}
+
+static const Writer *registered_writers[MAX_REGISTERED_WRITERS_NB + 1];
+
+static int writer_register(const Writer *writer)
+{
+    static int next_registered_writer_idx = 0;
+
+    if (next_registered_writer_idx == MAX_REGISTERED_WRITERS_NB)
+        return AVERROR(ENOMEM);
+
+    registered_writers[next_registered_writer_idx++] = writer;
+    return 0;
+}
+
+const Writer *writer_get_by_name(const char *name)
+{
+    int i;
+
+    for (i = 0; registered_writers[i]; i++)
+        if (!strcmp(registered_writers[i]->name, name))
+            return registered_writers[i];
+
+    return NULL;
+}
+
+/* WRITERS */
+
+#define DEFINE_WRITER_CLASS(name)                   \
+static const char *name##_get_name(void *ctx)       \
+{                                                   \
+    return #name ;                                  \
+}                                                   \
+static const AVClass name##_class = {               \
+    .class_name = #name,                            \
+    .item_name  = name##_get_name,                  \
+    .option     = name##_options                    \
+}
+
+/* Default output */
+
+typedef struct DefaultContext {
+    const AVClass *class;
+    int nokey;
+    int noprint_wrappers;
+    int nested_section[SECTION_MAX_NB_LEVELS];
+} DefaultContext;
+
+#undef OFFSET
+#define OFFSET(x) offsetof(DefaultContext, x)
+
+static const AVOption default_options[] = {
+    { "noprint_wrappers", "do not print headers and footers", OFFSET(noprint_wrappers), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1 },
+    { "nw",               "do not print headers and footers", OFFSET(noprint_wrappers), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1 },
+    { "nokey",          "force no key printing",     OFFSET(nokey),          AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1 },
+    { "nk",             "force no key printing",     OFFSET(nokey),          AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1 },
+    {NULL},
+};
+
+DEFINE_WRITER_CLASS(default);
+
+/* lame uppercasing routine, assumes the string is lower case ASCII */
+static inline char *upcase_string(char *dst, size_t dst_size, const char *src)
+{
+    int i;
+    for (i = 0; src[i] && i < dst_size-1; i++)
+        dst[i] = av_toupper(src[i]);
+    dst[i] = 0;
+    return dst;
+}
+
+static void default_print_section_header(WriterContext *wctx)
+{
+    DefaultContext *def = wctx->priv;
+    char buf[32];
+    const struct section *section = wctx->section[wctx->level];
+    const struct section *parent_section = wctx->level ?
+        wctx->section[wctx->level-1] : NULL;
+
+    av_bprint_clear(&wctx->section_pbuf[wctx->level]);
+    if (parent_section &&
+        !(parent_section->flags & (SECTION_FLAG_IS_WRAPPER|SECTION_FLAG_IS_ARRAY))) {
+        def->nested_section[wctx->level] = 1;
+        av_bprintf(&wctx->section_pbuf[wctx->level], "%s%s:",
+                   wctx->section_pbuf[wctx->level-1].str,
+                   upcase_string(buf, sizeof(buf),
+                                 av_x_if_null(section->element_name, section->name)));
+    }
+
+    if (def->noprint_wrappers || def->nested_section[wctx->level])
+        return;
+
+    if (!(section->flags & (SECTION_FLAG_IS_WRAPPER|SECTION_FLAG_IS_ARRAY)))
+        av_bprintf(&wctx->bpBuf, "[%s]\n", upcase_string(buf, sizeof(buf), section->name));
+}
+
+static void default_print_section_footer(WriterContext *wctx)
+{
+    DefaultContext *def = wctx->priv;
+    const struct section *section = wctx->section[wctx->level];
+    char buf[32];
+
+    if (def->noprint_wrappers || def->nested_section[wctx->level])
+        return;
+
+    if (!(section->flags & (SECTION_FLAG_IS_WRAPPER|SECTION_FLAG_IS_ARRAY)))
+        av_bprintf(&wctx->bpBuf, "[/%s]\n", upcase_string(buf, sizeof(buf), section->name));
+}
+
+static void default_print_str(WriterContext *wctx, const char *key, const char *value)
+{
+    DefaultContext *def = wctx->priv;
+
+    if (!def->nokey)
+        av_bprintf(&wctx->bpBuf, "%s%s=", wctx->section_pbuf[wctx->level].str, key);
+    av_bprintf(&wctx->bpBuf, "%s\n", value);
+}
+
+static void default_print_int(WriterContext *wctx, const char *key, long long int value)
+{
+    DefaultContext *def = wctx->priv;
+
+    if (!def->nokey)
+        av_bprintf(&wctx->bpBuf, "%s%s=", wctx->section_pbuf[wctx->level].str, key);
+    av_bprintf(&wctx->bpBuf, "%lld\n", value);
+}
+
+static const Writer default_writer = {
+    .name                  = "default",
+    .priv_size             = sizeof(DefaultContext),
+    .print_section_header  = default_print_section_header,
+    .print_section_footer  = default_print_section_footer,
+    .print_integer         = default_print_int,
+    .print_string          = default_print_str,
+    .flags = WRITER_FLAG_DISPLAY_OPTIONAL_FIELDS,
+    .priv_class            = &default_class,
+};
+
+/* Compact output */
+
+/**
+ * Apply C-language-like string escaping.
+ */
+static const char *c_escape_str(AVBPrint *dst, const char *src, const char sep, void *log_ctx)
+{
+    const char *p;
+
+    for (p = src; *p; p++) {
+        switch (*p) {
+        case '\b': av_bprintf(dst, "%s", "\\b");  break;
+        case '\f': av_bprintf(dst, "%s", "\\f");  break;
+        case '\n': av_bprintf(dst, "%s", "\\n");  break;
+        case '\r': av_bprintf(dst, "%s", "\\r");  break;
+        case '\\': av_bprintf(dst, "%s", "\\\\"); break;
+        default:
+            if (*p == sep)
+                av_bprint_chars(dst, '\\', 1);
+            av_bprint_chars(dst, *p, 1);
+        }
+    }
+    return dst->str;
+}
+
+/**
+ * Quote fields containing special characters, check RFC4180.
+ */
+static const char *csv_escape_str(AVBPrint *dst, const char *src, const char sep, void *log_ctx)
+{
+    char meta_chars[] = { sep, '"', '\n', '\r', '\0' };
+    int needs_quoting = !!src[strcspn(src, meta_chars)];
+
+    if (needs_quoting)
+        av_bprint_chars(dst, '"', 1);
+
+    for (; *src; src++) {
+        if (*src == '"')
+            av_bprint_chars(dst, '"', 1);
+        av_bprint_chars(dst, *src, 1);
+    }
+    if (needs_quoting)
+        av_bprint_chars(dst, '"', 1);
+    return dst->str;
+}
+
+static const char *none_escape_str(AVBPrint *dst, const char *src, const char sep, void *log_ctx)
+{
+    return src;
+}
+
+typedef struct CompactContext {
+    const AVClass *class;
+    char *item_sep_str;
+    char item_sep;
+    int nokey;
+    int print_section;
+    char *escape_mode_str;
+    const char * (*escape_str)(AVBPrint *dst, const char *src, const char sep, void *log_ctx);
+    int nested_section[SECTION_MAX_NB_LEVELS];
+    int has_nested_elems[SECTION_MAX_NB_LEVELS];
+    int terminate_line[SECTION_MAX_NB_LEVELS];
+} CompactContext;
+
+#undef OFFSET
+#define OFFSET(x) offsetof(CompactContext, x)
+
+static const AVOption compact_options[]= {
+    {"item_sep", "set item separator",    OFFSET(item_sep_str),    AV_OPT_TYPE_STRING, {.str="|"},  0, 0 },
+    {"s",        "set item separator",    OFFSET(item_sep_str),    AV_OPT_TYPE_STRING, {.str="|"},  0, 0 },
+    {"nokey",    "force no key printing", OFFSET(nokey),           AV_OPT_TYPE_BOOL,   {.i64=0},    0,        1        },
+    {"nk",       "force no key printing", OFFSET(nokey),           AV_OPT_TYPE_BOOL,   {.i64=0},    0,        1        },
+    {"escape",   "set escape mode",       OFFSET(escape_mode_str), AV_OPT_TYPE_STRING, {.str="c"},  0, 0 },
+    {"e",        "set escape mode",       OFFSET(escape_mode_str), AV_OPT_TYPE_STRING, {.str="c"},  0, 0 },
+    {"print_section", "print section name", OFFSET(print_section), AV_OPT_TYPE_BOOL,   {.i64=1},    0,        1        },
+    {"p",             "print section name", OFFSET(print_section), AV_OPT_TYPE_BOOL,   {.i64=1},    0,        1        },
+    {NULL},
+};
+
+DEFINE_WRITER_CLASS(compact);
+
+static av_cold int compact_init(WriterContext *wctx)
+{
+    CompactContext *compact = wctx->priv;
+
+    if (strlen(compact->item_sep_str) != 1) {
+        av_log(wctx, AV_LOG_ERROR, "Item separator '%s' specified, but must contain a single character\n",
+               compact->item_sep_str);
+        return AVERROR(EINVAL);
+    }
+    compact->item_sep = compact->item_sep_str[0];
+
+    if      (!strcmp(compact->escape_mode_str, "none")) compact->escape_str = none_escape_str;
+    else if (!strcmp(compact->escape_mode_str, "c"   )) compact->escape_str = c_escape_str;
+    else if (!strcmp(compact->escape_mode_str, "csv" )) compact->escape_str = csv_escape_str;
+    else {
+        av_log(wctx, AV_LOG_ERROR, "Unknown escape mode '%s'\n", compact->escape_mode_str);
+        return AVERROR(EINVAL);
+    }
+
+    return 0;
+}
+
+static void compact_print_section_header(WriterContext *wctx)
+{
+    CompactContext *compact = wctx->priv;
+    const struct section *section = wctx->section[wctx->level];
+    const struct section *parent_section = wctx->level ?
+        wctx->section[wctx->level-1] : NULL;
+    compact->terminate_line[wctx->level] = 1;
+    compact->has_nested_elems[wctx->level] = 0;
+
+    av_bprint_clear(&wctx->section_pbuf[wctx->level]);
+    if (!(section->flags & SECTION_FLAG_IS_ARRAY) && parent_section &&
+        !(parent_section->flags & (SECTION_FLAG_IS_WRAPPER|SECTION_FLAG_IS_ARRAY))) {
+        compact->nested_section[wctx->level] = 1;
+        compact->has_nested_elems[wctx->level-1] = 1;
+        av_bprintf(&wctx->section_pbuf[wctx->level], "%s%s:",
+                   wctx->section_pbuf[wctx->level-1].str,
+                   (char *)av_x_if_null(section->element_name, section->name));
+        wctx->nb_item[wctx->level] = wctx->nb_item[wctx->level-1];
+    } else {
+        if (parent_section && compact->has_nested_elems[wctx->level-1] &&
+            (section->flags & SECTION_FLAG_IS_ARRAY)) {
+            compact->terminate_line[wctx->level-1] = 0;
+            printf("\n");
+        }
+        if (compact->print_section &&
+            !(section->flags & (SECTION_FLAG_IS_WRAPPER|SECTION_FLAG_IS_ARRAY)))
+            printf("%s%c", section->name, compact->item_sep);
+    }
+}
+
+static void compact_print_section_footer(WriterContext *wctx)
+{
+    CompactContext *compact = wctx->priv;
+
+    if (!compact->nested_section[wctx->level] &&
+        compact->terminate_line[wctx->level] &&
+        !(wctx->section[wctx->level]->flags & (SECTION_FLAG_IS_WRAPPER|SECTION_FLAG_IS_ARRAY)))
+        printf("\n");
+}
+
+static void compact_print_str(WriterContext *wctx, const char *key, const char *value)
+{
+    CompactContext *compact = wctx->priv;
+    AVBPrint buf;
+
+    if (wctx->nb_item[wctx->level]) printf("%c", compact->item_sep);
+    if (!compact->nokey)
+        printf("%s%s=", wctx->section_pbuf[wctx->level].str, key);
+    av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
+    printf("%s", compact->escape_str(&buf, value, compact->item_sep, wctx));
+    av_bprint_finalize(&buf, NULL);
+}
+
+static void compact_print_int(WriterContext *wctx, const char *key, long long int value)
+{
+    CompactContext *compact = wctx->priv;
+
+    if (wctx->nb_item[wctx->level]) printf("%c", compact->item_sep);
+    if (!compact->nokey)
+        printf("%s%s=", wctx->section_pbuf[wctx->level].str, key);
+    printf("%lld", value);
+}
+
+static const Writer compact_writer = {
+    .name                 = "compact",
+    .priv_size            = sizeof(CompactContext),
+    .init                 = compact_init,
+    .print_section_header = compact_print_section_header,
+    .print_section_footer = compact_print_section_footer,
+    .print_integer        = compact_print_int,
+    .print_string         = compact_print_str,
+    .flags = WRITER_FLAG_DISPLAY_OPTIONAL_FIELDS,
+    .priv_class           = &compact_class,
+};
+
+/* CSV output */
+
+#undef OFFSET
+#define OFFSET(x) offsetof(CompactContext, x)
+
+static const AVOption csv_options[] = {
+    {"item_sep", "set item separator",    OFFSET(item_sep_str),    AV_OPT_TYPE_STRING, {.str=","},  0, 0 },
+    {"s",        "set item separator",    OFFSET(item_sep_str),    AV_OPT_TYPE_STRING, {.str=","},  0, 0 },
+    {"nokey",    "force no key printing", OFFSET(nokey),           AV_OPT_TYPE_BOOL,   {.i64=1},    0,        1        },
+    {"nk",       "force no key printing", OFFSET(nokey),           AV_OPT_TYPE_BOOL,   {.i64=1},    0,        1        },
+    {"escape",   "set escape mode",       OFFSET(escape_mode_str), AV_OPT_TYPE_STRING, {.str="csv"}, 0, 0 },
+    {"e",        "set escape mode",       OFFSET(escape_mode_str), AV_OPT_TYPE_STRING, {.str="csv"}, 0, 0 },
+    {"print_section", "print section name", OFFSET(print_section), AV_OPT_TYPE_BOOL,   {.i64=1},    0,        1        },
+    {"p",             "print section name", OFFSET(print_section), AV_OPT_TYPE_BOOL,   {.i64=1},    0,        1        },
+    {NULL},
+};
+
+DEFINE_WRITER_CLASS(csv);
+
+static const Writer csv_writer = {
+    .name                 = "csv",
+    .priv_size            = sizeof(CompactContext),
+    .init                 = compact_init,
+    .print_section_header = compact_print_section_header,
+    .print_section_footer = compact_print_section_footer,
+    .print_integer        = compact_print_int,
+    .print_string         = compact_print_str,
+    .flags = WRITER_FLAG_DISPLAY_OPTIONAL_FIELDS,
+    .priv_class           = &csv_class,
+};
+
+/* Flat output */
+
+typedef struct FlatContext {
+    const AVClass *class;
+    const char *sep_str;
+    char sep;
+    int hierarchical;
+} FlatContext;
+
+#undef OFFSET
+#define OFFSET(x) offsetof(FlatContext, x)
+
+static const AVOption flat_options[]= {
+    {"sep_char", "set separator",    OFFSET(sep_str),    AV_OPT_TYPE_STRING, {.str="."},  0, 0 },
+    {"s",        "set separator",    OFFSET(sep_str),    AV_OPT_TYPE_STRING, {.str="."},  0, 0 },
+    {"hierarchical", "specify if the section specification should be hierarchical", OFFSET(hierarchical), AV_OPT_TYPE_BOOL, {.i64=1}, 0, 1 },
+    {"h",            "specify if the section specification should be hierarchical", OFFSET(hierarchical), AV_OPT_TYPE_BOOL, {.i64=1}, 0, 1 },
+    {NULL},
+};
+
+DEFINE_WRITER_CLASS(flat);
+
+static av_cold int flat_init(WriterContext *wctx)
+{
+    FlatContext *flat = wctx->priv;
+
+    if (strlen(flat->sep_str) != 1) {
+        av_log(wctx, AV_LOG_ERROR, "Item separator '%s' specified, but must contain a single character\n",
+               flat->sep_str);
+        return AVERROR(EINVAL);
+    }
+    flat->sep = flat->sep_str[0];
+
+    return 0;
+}
+
+static const char *flat_escape_key_str(AVBPrint *dst, const char *src, const char sep)
+{
+    const char *p;
+
+    for (p = src; *p; p++) {
+        if (!((*p >= '0' && *p <= '9') ||
+              (*p >= 'a' && *p <= 'z') ||
+              (*p >= 'A' && *p <= 'Z')))
+            av_bprint_chars(dst, '_', 1);
+        else
+            av_bprint_chars(dst, *p, 1);
+    }
+    return dst->str;
+}
+
+static const char *flat_escape_value_str(AVBPrint *dst, const char *src)
+{
+    const char *p;
+
+    for (p = src; *p; p++) {
+        switch (*p) {
+        case '\n': av_bprintf(dst, "%s", "\\n");  break;
+        case '\r': av_bprintf(dst, "%s", "\\r");  break;
+        case '\\': av_bprintf(dst, "%s", "\\\\"); break;
+        case '"':  av_bprintf(dst, "%s", "\\\""); break;
+        case '`':  av_bprintf(dst, "%s", "\\`");  break;
+        case '$':  av_bprintf(dst, "%s", "\\$");  break;
+        default:   av_bprint_chars(dst, *p, 1);   break;
+        }
+    }
+    return dst->str;
+}
+
+static void flat_print_section_header(WriterContext *wctx)
+{
+    FlatContext *flat = wctx->priv;
+    AVBPrint *buf = &wctx->section_pbuf[wctx->level];
+    const struct section *section = wctx->section[wctx->level];
+    const struct section *parent_section = wctx->level ?
+        wctx->section[wctx->level-1] : NULL;
+
+    /* build section header */
+    av_bprint_clear(buf);
+    if (!parent_section)
+        return;
+    av_bprintf(buf, "%s", wctx->section_pbuf[wctx->level-1].str);
+
+    if (flat->hierarchical ||
+        !(section->flags & (SECTION_FLAG_IS_ARRAY|SECTION_FLAG_IS_WRAPPER))) {
+        av_bprintf(buf, "%s%s", wctx->section[wctx->level]->name, flat->sep_str);
+    }
+}
+
+static void flat_print_int(WriterContext *wctx, const char *key, long long int value)
+{
+    printf("%s%s=%lld\n", wctx->section_pbuf[wctx->level].str, key, value);
+}
+
+static void flat_print_str(WriterContext *wctx, const char *key, const char *value)
+{
+    FlatContext *flat = wctx->priv;
+    AVBPrint buf;
+
+    printf("%s", wctx->section_pbuf[wctx->level].str);
+    av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
+    printf("%s=", flat_escape_key_str(&buf, key, flat->sep));
+    av_bprint_clear(&buf);
+    printf("\"%s\"\n", flat_escape_value_str(&buf, value));
+    av_bprint_finalize(&buf, NULL);
+}
+
+static const Writer flat_writer = {
+    .name                  = "flat",
+    .priv_size             = sizeof(FlatContext),
+    .init                  = flat_init,
+    .print_section_header  = flat_print_section_header,
+    .print_integer         = flat_print_int,
+    .print_string          = flat_print_str,
+    .flags = WRITER_FLAG_DISPLAY_OPTIONAL_FIELDS|WRITER_FLAG_PUT_PACKETS_AND_FRAMES_IN_SAME_CHAPTER,
+    .priv_class            = &flat_class,
+};
+
+/* INI format output */
+
+typedef struct INIContext {
+    const AVClass *class;
+    int hierarchical;
+} INIContext;
+
+#undef OFFSET
+#define OFFSET(x) offsetof(INIContext, x)
+
+static const AVOption ini_options[] = {
+    {"hierarchical", "specify if the section specification should be hierarchical", OFFSET(hierarchical), AV_OPT_TYPE_BOOL, {.i64=1}, 0, 1 },
+    {"h",            "specify if the section specification should be hierarchical", OFFSET(hierarchical), AV_OPT_TYPE_BOOL, {.i64=1}, 0, 1 },
+    {NULL},
+};
+
+DEFINE_WRITER_CLASS(ini);
+
+static char *ini_escape_str(AVBPrint *dst, const char *src)
+{
+    int i = 0;
+    char c = 0;
+
+    while (c = src[i++]) {
+        switch (c) {
+        case '\b': av_bprintf(dst, "%s", "\\b"); break;
+        case '\f': av_bprintf(dst, "%s", "\\f"); break;
+        case '\n': av_bprintf(dst, "%s", "\\n"); break;
+        case '\r': av_bprintf(dst, "%s", "\\r"); break;
+        case '\t': av_bprintf(dst, "%s", "\\t"); break;
+        case '\\':
+        case '#' :
+        case '=' :
+        case ':' : av_bprint_chars(dst, '\\', 1);
+        default:
+            if ((unsigned char)c < 32)
+                av_bprintf(dst, "\\x00%02x", c & 0xff);
+            else
+                av_bprint_chars(dst, c, 1);
+            break;
+        }
+    }
+    return dst->str;
+}
+
+static void ini_print_section_header(WriterContext *wctx)
+{
+    INIContext *ini = wctx->priv;
+    AVBPrint *buf = &wctx->section_pbuf[wctx->level];
+    const struct section *section = wctx->section[wctx->level];
+    const struct section *parent_section = wctx->level ?
+        wctx->section[wctx->level-1] : NULL;
+
+    av_bprint_clear(buf);
+    if (!parent_section) {
+        printf("# ffprobe output\n\n");
+        return;
+    }
+
+    if (wctx->nb_item[wctx->level-1])
+        printf("\n");
+
+    av_bprintf(buf, "%s", wctx->section_pbuf[wctx->level-1].str);
+    if (ini->hierarchical ||
+        !(section->flags & (SECTION_FLAG_IS_ARRAY|SECTION_FLAG_IS_WRAPPER))) {
+        av_bprintf(buf, "%s%s", buf->str[0] ? "." : "", wctx->section[wctx->level]->name);
+    }
+
+    if (!(section->flags & (SECTION_FLAG_IS_ARRAY|SECTION_FLAG_IS_WRAPPER)))
+        printf("[%s]\n", buf->str);
+}
+
+static void ini_print_str(WriterContext *wctx, const char *key, const char *value)
+{
+    AVBPrint buf;
+
+    av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
+    printf("%s=", ini_escape_str(&buf, key));
+    av_bprint_clear(&buf);
+    printf("%s\n", ini_escape_str(&buf, value));
+    av_bprint_finalize(&buf, NULL);
+}
+
+static void ini_print_int(WriterContext *wctx, const char *key, long long int value)
+{
+    printf("%s=%lld\n", key, value);
+}
+
+static const Writer ini_writer = {
+    .name                  = "ini",
+    .priv_size             = sizeof(INIContext),
+    .print_section_header  = ini_print_section_header,
+    .print_integer         = ini_print_int,
+    .print_string          = ini_print_str,
+    .flags = WRITER_FLAG_DISPLAY_OPTIONAL_FIELDS|WRITER_FLAG_PUT_PACKETS_AND_FRAMES_IN_SAME_CHAPTER,
+    .priv_class            = &ini_class,
+};
+
+/* JSON output */
+
+typedef struct JSONContext {
+    const AVClass *class;
+    int indent_level;
+    int compact;
+    const char *item_sep, *item_start_end;
+} JSONContext;
+
+#undef OFFSET
+#define OFFSET(x) offsetof(JSONContext, x)
+
+static const AVOption json_options[]= {
+    { "compact", "enable compact output", OFFSET(compact), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1 },
+    { "c",       "enable compact output", OFFSET(compact), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1 },
+    { NULL }
+};
+
+DEFINE_WRITER_CLASS(json);
+
+static av_cold int json_init(WriterContext *wctx)
+{
+    JSONContext *json = wctx->priv;
+
+    json->item_sep       = json->compact ? ", " : ",\n";
+    json->item_start_end = json->compact ? " "  : "\n";
+
+    return 0;
+}
+
+static const char *json_escape_str(AVBPrint *dst, const char *src, void *log_ctx)
+{
+    static const char json_escape[] = {'"', '\\', '\b', '\f', '\n', '\r', '\t', 0};
+    static const char json_subst[]  = {'"', '\\',  'b',  'f',  'n',  'r',  't', 0};
+    const char *p;
+
+    for (p = src; *p; p++) {
+        char *s = strchr(json_escape, *p);
+        if (s) {
+            av_bprint_chars(dst, '\\', 1);
+            av_bprint_chars(dst, json_subst[s - json_escape], 1);
+        } else if ((unsigned char)*p < 32) {
+            av_bprintf(dst, "\\u00%02x", *p & 0xff);
+        } else {
+            av_bprint_chars(dst, *p, 1);
+        }
+    }
+    return dst->str;
+}
+
+#define JSON_INDENT() av_bprintf(&wctx->bpBuf, "%*c", json->indent_level * 4, ' ')
+
+static void json_print_section_header(WriterContext *wctx)
+{
+    JSONContext *json = wctx->priv;
+    AVBPrint buf;
+    const struct section *section = wctx->section[wctx->level];
+    const struct section *parent_section = wctx->level ?
+        wctx->section[wctx->level-1] : NULL;
+
+    if (wctx->level && wctx->nb_item[wctx->level-1])
+        av_bprintf(&wctx->bpBuf, ",\n");
+
+    if (section->flags & SECTION_FLAG_IS_WRAPPER) {
+        av_bprintf(&wctx->bpBuf, "{\n");
+        json->indent_level++;
+    } else {
+        av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
+        json_escape_str(&buf, section->name, wctx);
+        JSON_INDENT();
+
+        json->indent_level++;
+        if (section->flags & SECTION_FLAG_IS_ARRAY) {
+            av_bprintf(&wctx->bpBuf, "\"%s\": [\n", buf.str);
+        } else if (parent_section && !(parent_section->flags & SECTION_FLAG_IS_ARRAY)) {
+            av_bprintf(&wctx->bpBuf, "\"%s\": {%s", buf.str, json->item_start_end);
+        } else {
+            av_bprintf(&wctx->bpBuf, "{%s", json->item_start_end);
+        }
+        av_bprint_finalize(&buf, NULL);
+    }
+}
+
+static void json_print_section_footer(WriterContext *wctx)
+{
+    JSONContext *json = wctx->priv;
+    const struct section *section = wctx->section[wctx->level];
+
+    if (wctx->level == 0) {
+        json->indent_level--;
+        av_bprintf(&wctx->bpBuf, "\n}\n");
+    } else if (section->flags & SECTION_FLAG_IS_ARRAY) {
+        av_bprintf(&wctx->bpBuf, "\n");
+        json->indent_level--;
+        JSON_INDENT();
+        av_bprintf(&wctx->bpBuf, "]");
+    } else {
+        av_bprintf(&wctx->bpBuf, "%s", json->item_start_end);
+        json->indent_level--;
+        if (!json->compact)
+            JSON_INDENT();
+        av_bprintf(&wctx->bpBuf, "}");
+    }
+}
+
+static inline void json_print_item_str(WriterContext *wctx,
+                                       const char *key, const char *value)
+{
+    AVBPrint buf;
+
+    av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
+    av_bprintf(&wctx->bpBuf, "\"%s\":", json_escape_str(&buf, key,   wctx));
+    av_bprint_clear(&buf);
+    av_bprintf(&wctx->bpBuf, " \"%s\"", json_escape_str(&buf, value, wctx));
+    av_bprint_finalize(&buf, NULL);
+}
+
+static void json_print_str(WriterContext *wctx, const char *key, const char *value)
+{
+    JSONContext *json = wctx->priv;
+
+    if (wctx->nb_item[wctx->level])
+        av_bprintf(&wctx->bpBuf, "%s", json->item_sep);
+    if (!json->compact)
+        JSON_INDENT();
+    json_print_item_str(wctx, key, value);
+}
+
+static void json_print_int(WriterContext *wctx, const char *key, long long int value)
+{
+    JSONContext *json = wctx->priv;
+    AVBPrint buf;
+
+    if (wctx->nb_item[wctx->level])
+        av_bprintf(&wctx->bpBuf, "%s", json->item_sep);
+    if (!json->compact)
+        JSON_INDENT();
+
+    av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
+    av_bprintf(&wctx->bpBuf, "\"%s\": %lld", json_escape_str(&buf, key, wctx), value);
+    av_bprint_finalize(&buf, NULL);
+}
+
+static const Writer json_writer = {
+    .name                 = "json",
+    .priv_size            = sizeof(JSONContext),
+    .init                 = json_init,
+    .print_section_header = json_print_section_header,
+    .print_section_footer = json_print_section_footer,
+    .print_integer        = json_print_int,
+    .print_string         = json_print_str,
+    .flags = WRITER_FLAG_PUT_PACKETS_AND_FRAMES_IN_SAME_CHAPTER,
+    .priv_class           = &json_class,
+};
+
+
+/* XML output */
+
+typedef struct XMLContext {
+    const AVClass *class;
+    int within_tag;
+    int indent_level;
+    int fully_qualified;
+    int xsd_strict;
+} XMLContext;
+
+#undef OFFSET
+#define OFFSET(x) offsetof(XMLContext, x)
+
+static const AVOption xml_options[] = {
+    {"fully_qualified", "specify if the output should be fully qualified", OFFSET(fully_qualified), AV_OPT_TYPE_BOOL, {.i64=0},  0, 1 },
+    {"q",               "specify if the output should be fully qualified", OFFSET(fully_qualified), AV_OPT_TYPE_BOOL, {.i64=0},  0, 1 },
+    {"xsd_strict",      "ensure that the output is XSD compliant",         OFFSET(xsd_strict),      AV_OPT_TYPE_BOOL, {.i64=0},  0, 1 },
+    {"x",               "ensure that the output is XSD compliant",         OFFSET(xsd_strict),      AV_OPT_TYPE_BOOL, {.i64=0},  0, 1 },
+    {NULL},
+};
+
+DEFINE_WRITER_CLASS(xml);
+
+static av_cold int xml_init(WriterContext *wctx)
+{
+    XMLContext *xml = wctx->priv;
+
+    if (xml->xsd_strict) {
+        xml->fully_qualified = 1;
+    }
+
+    return 0;
+}
+
+#define XML_INDENT() printf("%*c", xml->indent_level * 4, ' ')
+
+static void xml_print_section_header(WriterContext *wctx)
+{
+    XMLContext *xml = wctx->priv;
+    const struct section *section = wctx->section[wctx->level];
+    const struct section *parent_section = wctx->level ?
+        wctx->section[wctx->level-1] : NULL;
+
+    if (wctx->level == 0) {
+        const char *qual = " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
+            "xmlns:ffprobe=\"http://www.ffmpeg.org/schema/ffprobe\" "
+            "xsi:schemaLocation=\"http://www.ffmpeg.org/schema/ffprobe ffprobe.xsd\"";
+
+        printf("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
+        printf("<%sffprobe%s>\n",
+               xml->fully_qualified ? "ffprobe:" : "",
+               xml->fully_qualified ? qual : "");
+        return;
+    }
+
+    if (xml->within_tag) {
+        xml->within_tag = 0;
+        printf(">\n");
+    }
+    if (section->flags & SECTION_FLAG_HAS_VARIABLE_FIELDS) {
+        xml->indent_level++;
+    } else {
+        if (parent_section && (parent_section->flags & SECTION_FLAG_IS_WRAPPER) &&
+            wctx->level && wctx->nb_item[wctx->level-1])
+            printf("\n");
+        xml->indent_level++;
+
+        if (section->flags & SECTION_FLAG_IS_ARRAY) {
+            XML_INDENT(); printf("<%s>\n", section->name);
+        } else {
+            XML_INDENT(); printf("<%s ", section->name);
+            xml->within_tag = 1;
+        }
+    }
+}
+
+static void xml_print_section_footer(WriterContext *wctx)
+{
+    XMLContext *xml = wctx->priv;
+    const struct section *section = wctx->section[wctx->level];
+
+    if (wctx->level == 0) {
+        printf("</%sffprobe>\n", xml->fully_qualified ? "ffprobe:" : "");
+    } else if (xml->within_tag) {
+        xml->within_tag = 0;
+        printf("/>\n");
+        xml->indent_level--;
+    } else if (section->flags & SECTION_FLAG_HAS_VARIABLE_FIELDS) {
+        xml->indent_level--;
+    } else {
+        XML_INDENT(); printf("</%s>\n", section->name);
+        xml->indent_level--;
+    }
+}
+
+static void xml_print_str(WriterContext *wctx, const char *key, const char *value)
+{
+    AVBPrint buf;
+    XMLContext *xml = wctx->priv;
+    const struct section *section = wctx->section[wctx->level];
+
+    av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
+
+    if (section->flags & SECTION_FLAG_HAS_VARIABLE_FIELDS) {
+        XML_INDENT();
+        av_bprint_escape(&buf, key, NULL,
+                         AV_ESCAPE_MODE_XML, AV_ESCAPE_FLAG_XML_DOUBLE_QUOTES);
+        printf("<%s key=\"%s\"",
+               section->element_name, buf.str);
+        av_bprint_clear(&buf);
+
+        av_bprint_escape(&buf, value, NULL,
+                         AV_ESCAPE_MODE_XML, AV_ESCAPE_FLAG_XML_DOUBLE_QUOTES);
+        printf(" value=\"%s\"/>\n", buf.str);
+    } else {
+        if (wctx->nb_item[wctx->level])
+            printf(" ");
+
+        av_bprint_escape(&buf, value, NULL,
+                         AV_ESCAPE_MODE_XML, AV_ESCAPE_FLAG_XML_DOUBLE_QUOTES);
+        printf("%s=\"%s\"", key, buf.str);
+    }
+
+    av_bprint_finalize(&buf, NULL);
+}
+
+static void xml_print_int(WriterContext *wctx, const char *key, long long int value)
+{
+    if (wctx->nb_item[wctx->level])
+        printf(" ");
+    printf("%s=\"%lld\"", key, value);
+}
+
+static Writer xml_writer = {
+    .name                 = "xml",
+    .priv_size            = sizeof(XMLContext),
+    .init                 = xml_init,
+    .print_section_header = xml_print_section_header,
+    .print_section_footer = xml_print_section_footer,
+    .print_integer        = xml_print_int,
+    .print_string         = xml_print_str,
+    .flags = WRITER_FLAG_PUT_PACKETS_AND_FRAMES_IN_SAME_CHAPTER,
+    .priv_class           = &xml_class,
+};
+
+static void print_hwdevicecontext(WriterContext *w, AVHWDeviceContext *hw_device_context)
+{
+    writer_print_section_header(w, SECTION_ID_HWDEViCECONTEXT);
+
+    print_int("HasHwDeviceContext", 1);
+    print_str("DeviceType", av_hwdevice_get_type_name(hw_device_context->type));
+
+    writer_print_section_footer(w); // SECTION_ID_HWDEViCECONTEXT
+}
+
+static void print_hwframescontext(WriterContext *w, AVHWFramesContext *hw_frames_context)
+{
+    const AVPixFmtDescriptor* pixdescHw = NULL;
+    const AVPixFmtDescriptor* pixdescSw = NULL;
+
+    writer_print_section_header(w, SECTION_ID_HWFRAMESCONTEXT);
+
+    print_int("HasHwFramesContext", 1);
+    print_str("HardwareFormat", av_hwdevice_get_type_name(hw_frames_context->format));
+    print_str("SoftwareFormat", av_hwdevice_get_type_name(hw_frames_context->sw_format));
+
+    pixdescHw = av_pix_fmt_desc_get(hw_frames_context->format);
+    if (pixdescHw != NULL) {
+        print_str("HwPixelFormat", pixdescHw->name);
+        print_str("HwPixelFormatAlias", pixdescHw->alias);
+    }
+    
+    pixdescSw = av_pix_fmt_desc_get(hw_frames_context->sw_format);
+    if (pixdescSw != NULL) {
+        print_str("SwPixelFormat", pixdescSw->name);
+        print_str("SwPixelFormatAlias", pixdescSw->alias);
+    }
+    
+    print_int("Width", hw_frames_context->width);
+    print_int("Height", hw_frames_context->height);
+
+    print_hwdevicecontext(w, hw_frames_context->device_ctx);
+    
+    writer_print_section_footer(w); // SECTION_ID_HWFRAMESCONTEXT
+}
+
+static void print_link(WriterContext *w, AVFilterLink *link)
+{
+    char layoutString[64];
+
+    switch (link->type) {
+        case AVMEDIA_TYPE_VIDEO:
+            print_str("Format",  av_x_if_null(av_get_pix_fmt_name(link->format), "?"));
+            print_int("Width", link->w);
+            print_int("Height", link->h);
+            print_q("SAR", link->sample_aspect_ratio, ':');
+            break;
+
+        case AVMEDIA_TYPE_AUDIO:
+            av_get_channel_layout_string(layoutString, sizeof(layoutString), link->channels, link->channel_layout);
+            print_str("ChannelString", layoutString);
+            print_int("Channels", link->channels);
+            print_int("ChannelLayout", link->channel_layout);
+            print_int("SampleRate", link->sample_rate);
+            break;
+    }
+
+    if (link->hw_frames_ctx != NULL && link->hw_frames_ctx->buffer != NULL)
+        print_hwframescontext(w, (AVHWFramesContext*)link->hw_frames_ctx->data);
+}
+
+static void print_filter(WriterContext *w, AVFilterContext* filter)
+{
+    writer_print_section_header(w, SECTION_ID_FILTER);
+
+    print_str("Name", filter->name);
+
+    if (filter->filter != NULL) {
+        print_str("Name2", filter->filter->name);
+        print_str("Description", filter->filter->description);
+    }
+
+    if (filter->hw_device_ctx != NULL) {
+        AVHWDeviceContext* decCtx = (AVHWDeviceContext*)filter->hw_device_ctx->data;
+        print_hwdevicecontext(w, decCtx);
+    }
+
+    writer_print_section_header(w, SECTION_ID_INPUTS);
+
+    for (int i = 0; i < filter->nb_inputs; i++) {
+        AVFilterLink *link = filter->inputs[i];
+        writer_print_section_header(w, SECTION_ID_INPUT);
+
+        print_str("SourceName", link->src->name);
+        print_str("SourcePadName", link->srcpad->name);
+        print_str("DestPadName", link->dstpad->name);
+
+        print_link(w, link);
+
+        writer_print_section_footer(w); // SECTION_ID_INPUT
+    }
+
+    writer_print_section_footer(w); // SECTION_ID_INPUTS
+
+    // --------------------------------------------------
+    
+    writer_print_section_header(w, SECTION_ID_OUTPUTS);
+
+    for (int i = 0; i < filter->nb_outputs; i++) {
+        AVFilterLink *link = filter->outputs[i];
+        writer_print_section_header(w, SECTION_ID_OUTPUT);
+
+        print_str("DestName", link->dst->name);
+        print_str("DestPadName", link->dstpad->name);
+        print_str("SourceName", link->src->name);
+
+        print_link(w, link);
+
+        writer_print_section_footer(w); // SECTION_ID_OUTPUT
+    }
+
+    writer_print_section_footer(w); // SECTION_ID_OUTPUTS
+
+    writer_print_section_footer(w); // SECTION_ID_FILTER
+}
+
+static void print_filtergraph_single(WriterContext *w, FilterGraph* fg)
+{
+    char layoutString[64];
+
+    writer_print_section_header(w, SECTION_ID_FILTERGRAPH);
+
+    print_int("GraphIndex", fg->index);
+    print_str("Description", fg->graph_desc);
+
+    writer_print_section_header(w, SECTION_ID_INPUTS);
+
+    for (int i = 0; i < fg->nb_inputs; i++) {
+        enum AVMediaType mediaType = AVMEDIA_TYPE_UNKNOWN;
+        InputFilter *ifilter = fg->inputs[i];
+
+        writer_print_section_header(w, SECTION_ID_INPUT);
+
+        print_str("Name1", ifilter->name);
+
+        if (ifilter->filter != NULL) {
+            print_str("Name2", ifilter->filter->name);
+            print_str("Name3", ifilter->filter->filter->name);
+            print_str("Description", ifilter->filter->filter->description);
+
+            if (ifilter->filter->nb_outputs > 0)
+                mediaType = ifilter->filter->outputs[0]->type;
+        }
+
+        print_str("MediaType", av_get_media_type_string(mediaType));
+        print_int("MediaTypeId", mediaType);
+
+        switch (ifilter->type) {
+        case AVMEDIA_TYPE_VIDEO:
+        case AVMEDIA_TYPE_SUBTITLE:
+            print_str("Format",  av_x_if_null(av_get_pix_fmt_name(ifilter->format), "?"));
+            print_int("Width", ifilter->width);
+            print_int("Height", ifilter->height);
+            print_q("SAR", ifilter->sample_aspect_ratio, ':');
+            break;
+        case AVMEDIA_TYPE_AUDIO:
+
+            av_get_channel_layout_string(layoutString, sizeof(layoutString), ifilter->channels, ifilter->channel_layout);
+            print_str("ChannelString", layoutString);
+            print_int("Channels", ifilter->channels);
+            print_int("ChannelLayout", ifilter->channel_layout);
+            print_int("SampleRate", ifilter->sample_rate);
+            break;
+        case AVMEDIA_TYPE_ATTACHMENT:
+        case AVMEDIA_TYPE_DATA:
+            break;
+        }
+
+        if (ifilter->hw_frames_ctx != NULL)
+            print_hwframescontext(w, (AVHWFramesContext*)ifilter->hw_frames_ctx->data);
+        else if (ifilter->filter != NULL && ifilter->filter->hw_device_ctx != NULL) {
+            AVHWDeviceContext* devCtx = (AVHWDeviceContext*)ifilter->filter->hw_device_ctx->data;
+            print_hwdevicecontext(w, devCtx);
+        }
+
+        writer_print_section_footer(w); // SECTION_ID_INPUT
+    }
+
+    writer_print_section_footer(w); // SECTION_ID_INPUTS
+
+
+    writer_print_section_header(w, SECTION_ID_OUTPUTS);
+
+    for (int i = 0; i < fg->nb_outputs; i++) {
+        OutputFilter *ofilter = fg->outputs[i];
+        enum AVMediaType mediaType = AVMEDIA_TYPE_UNKNOWN;
+
+        writer_print_section_header(w, SECTION_ID_OUTPUT);
+        print_str("Name1", ofilter->name);
+
+        if (ofilter->filter != NULL) {
+            print_str("Name2", ofilter->filter->name);
+            print_str("Name3", ofilter->filter->filter->name);
+            print_str("Description", ofilter->filter->filter->description);
+
+            if (ofilter->filter->nb_inputs > 0)
+                mediaType = ofilter->filter->inputs[0]->type;
+        }
+
+        print_str("MediaType", av_get_media_type_string(mediaType));
+        print_int("MediaTypeId", mediaType);
+
+        switch (ofilter->type) {
+        case AVMEDIA_TYPE_VIDEO:
+        case AVMEDIA_TYPE_SUBTITLE:
+            print_str("Format",  av_x_if_null(av_get_pix_fmt_name(ofilter->format), "?"));
+            print_int("Width", ofilter->width);
+            print_int("Height", ofilter->height);
+            break;
+        case AVMEDIA_TYPE_AUDIO:
+
+            ////av_get_channel_layout_string(layoutString, sizeof(layoutString), ofilter->ost->, ofilter->channel_layout);
+            ////print_str("ChannelString", layoutString);
+            ////print_int("Channels", ofilter->channels);
+            print_int("ChannelLayout", ofilter->channel_layout);
+            print_int("SampleRate", ofilter->sample_rate);
+            break;
+        case AVMEDIA_TYPE_ATTACHMENT:
+        case AVMEDIA_TYPE_DATA:
+            break;
+        }
+
+        if (ofilter->filter != NULL && ofilter->filter->hw_device_ctx != NULL) {
+            AVHWDeviceContext* devCtx = (AVHWDeviceContext*)ofilter->filter->hw_device_ctx->data;
+            print_hwdevicecontext(w, devCtx);
+        }
+
+        writer_print_section_footer(w); // SECTION_ID_OUTPUT
+    }
+
+    writer_print_section_footer(w); // SECTION_ID_OUTPUTS
+
+
+    writer_print_section_header(w, SECTION_ID_FILTERS);
+
+    if (fg->graph != NULL) {
+        for (int i = 0; i < fg->graph->nb_filters; i++) {
+            AVFilterContext *filter = fg->graph->filters[i];
+            writer_print_section_header(w, SECTION_ID_FILTER);
+
+            print_filter(w, filter);
+
+            writer_print_section_footer(w); // SECTION_ID_FILTER
+        }
+    }
+
+    writer_print_section_footer(w); // SECTION_ID_FILTERS
+
+
+    writer_print_section_footer(w); // SECTION_ID_FILTERGRAPH
+}
+
+int print_filtergraphs(FilterGraph **graphs, int nb_graphs)
+{
+    const Writer *writer;
+    WriterContext *w;
+    char *buf;
+    char *w_name, *w_args;
+    int ret;
+
+    writer_register_all();
+
+    if (!print_graphs_format)
+        print_graphs_format = av_strdup("default");
+    if (!print_graphs_format) {
+        return AVERROR(ENOMEM);
+    }
+
+    w_name = av_strtok(print_graphs_format, "=", &buf);
+    if (!w_name) {
+        av_log(NULL, AV_LOG_ERROR, "No name specified for the filter graph output format\n");
+        return AVERROR(EINVAL);
+    }
+    w_args = buf;
+
+    writer = writer_get_by_name(w_name);
+    if (writer == NULL) {
+        av_log(NULL, AV_LOG_ERROR, "Unknown filter graph  output format with name '%s'\n", w_name);
+        return AVERROR(EINVAL);
+    }
+
+    ////if (print_graphs_file) {
+
+    ////    graph_file = freopen(print_graphs_file, "w", stdout);
+    ////    if (!graph_file) {
+    ////        int ret = AVERROR(errno);
+
+    ////        av_log(NULL, AV_LOG_ERROR, "Failed to open graph output file \"%s\": %s\n",
+    ////               print_graphs_file, strerror(errno));
+
+    ////        return ret;
+    ////    }
+    ////}
+
+    if ((ret = writer_open(&w, writer, w_args, sections, FF_ARRAY_ELEMS(sections))) >= 0) {
+        writer_print_section_header(w, SECTION_ID_ROOT);
+
+        writer_print_section_header(w, SECTION_ID_FILTERGRAPHS);
+
+        for (int i = 0; i < nb_graphs; i++)
+            print_filtergraph_single(w, graphs[i]);
+
+        writer_print_section_footer(w); // SECTION_ID_FILTERGRAPHS
+
+        writer_print_section_footer(w); // SECTION_ID_ROOT
+
+        if (print_graphs_file) {
+
+            AVIOContext *avio = NULL;
+
+            ret = avio_open2(&avio, print_graphs_file, AVIO_FLAG_WRITE, NULL, NULL);
+            if (ret < 0) {
+                av_log(NULL, AV_LOG_ERROR, "Failed to open graph output file, \"%s\": %s\n",
+                       print_graphs_file, av_err2str(ret));
+                return ret;
+            }
+
+            avio_write(avio, w->bpBuf.str, FFMIN(w->bpBuf.len, w->bpBuf.size - 1));
+            avio_flush(avio);
+
+            if ((ret = avio_closep(&avio)) < 0)
+                av_log(NULL, AV_LOG_ERROR, "Error closing graph output file, loss of information possible: %s\n", av_err2str(ret));
+        }
+
+        if (print_graphs)
+            av_log(NULL, AV_LOG_INFO, "%s    %c", w->bpBuf.str, '\n');
+
+        writer_close(&w);
+    }
+
+    ////if (graph_file) {
+
+    ////    if (fclose(graph_file))
+    ////        av_log(NULL, AV_LOG_ERROR,
+    ////            "Error closing filter report file, loss of information possible: %s\n",
+    ////            av_err2str(AVERROR(errno)));
+    ////    graph_file = NULL;
+    ////}
+
+    return 0;
+}
+
+void writer_register_all(void)
+{
+    static int initialized;
+
+    if (initialized)
+        return;
+    initialized = 1;
+
+    writer_register(&default_writer);
+    writer_register(&compact_writer);
+    writer_register(&csv_writer);
+    writer_register(&flat_writer);
+    writer_register(&ini_writer);
+    writer_register(&json_writer);
+    writer_register(&xml_writer);
+}
diff --git a/fftools/filterwriters.h b/fftools/filterwriters.h
new file mode 100644
index 0000000000..8edf9f3e82
--- /dev/null
+++ b/fftools/filterwriters.h
@@ -0,0 +1,193 @@ 
+/*
+ * Copyright (C) 2018 - softworkz for Emby Llc. 
+ * All rights reserved.
+ *
+ * This code is not yet published under any license.
+ *
+ */
+
+#ifndef FFTOOLS_FILTERWRITERS_H
+#define FFTOOLS_FILTERWRITERS_H
+
+#include <stdint.h>
+
+#include "config.h"
+#include "ffmpeg.h"
+#include "libavutil/avutil.h"
+#include "libavutil/bprint.h"
+
+#define SECTION_MAX_NB_CHILDREN 11
+#define PRINT_STRING_OPT      1
+#define PRINT_STRING_VALIDATE 2
+#define MAX_REGISTERED_WRITERS_NB 64
+
+struct section {
+    int id;             ///< unique id identifying a section
+    const char *name;
+
+#define SECTION_FLAG_IS_WRAPPER      1 ///< the section only contains other sections, but has no data at its own level
+#define SECTION_FLAG_IS_ARRAY        2 ///< the section contains an array of elements of the same type
+#define SECTION_FLAG_HAS_VARIABLE_FIELDS 4 ///< the section may contain a variable number of fields with variable keys.
+                                           ///  For these sections the element_name field is mandatory.
+    int flags;
+    int children_ids[SECTION_MAX_NB_CHILDREN+1]; ///< list of children section IDS, terminated by -1
+    const char *element_name; ///< name of the contained element, if provided
+    const char *unique_name;  ///< unique section name, in case the name is ambiguous
+};
+
+typedef enum {
+    SECTION_ID_NONE = -1,
+    SECTION_ID_ROOT,
+    SECTION_ID_PROGRAM_VERSION,
+    SECTION_ID_FILTERGRAPHS,
+    SECTION_ID_FILTERGRAPH,
+    SECTION_ID_INPUTS,
+    SECTION_ID_INPUT,
+    SECTION_ID_OUTPUTS,
+    SECTION_ID_OUTPUT,
+    SECTION_ID_FILTERS,
+    SECTION_ID_FILTER,
+    SECTION_ID_HWDEViCECONTEXT,
+    SECTION_ID_HWFRAMESCONTEXT,
+    SECTION_ID_ERROR,
+    SECTION_ID_LOG,
+    SECTION_ID_LOGS,
+
+} SectionID;
+
+static const struct section sections[] = {
+    [SECTION_ID_ROOT] =               { SECTION_ID_ROOT, "GraphDescription", SECTION_FLAG_IS_WRAPPER,
+                                      { SECTION_ID_ERROR, SECTION_ID_PROGRAM_VERSION, SECTION_ID_FILTERGRAPHS, SECTION_ID_LOGS, -1} },
+    [SECTION_ID_PROGRAM_VERSION] =    { SECTION_ID_PROGRAM_VERSION, "ProgramVersion", 0, { -1 } },
+
+    [SECTION_ID_FILTERGRAPHS] =       { SECTION_ID_FILTERGRAPHS, "Graphs", SECTION_FLAG_IS_ARRAY, { SECTION_ID_FILTERGRAPH, -1 } },
+    [SECTION_ID_FILTERGRAPH] =        { SECTION_ID_FILTERGRAPH, "Graph", 0, { SECTION_ID_INPUTS, SECTION_ID_OUTPUTS, SECTION_ID_FILTERS, -1 },  },
+
+    [SECTION_ID_INPUTS] =             { SECTION_ID_INPUTS, "Inputs", SECTION_FLAG_IS_ARRAY, { SECTION_ID_INPUT, SECTION_ID_ERROR, -1 } },
+    [SECTION_ID_INPUT] =              { SECTION_ID_INPUT, "Input", 0, { SECTION_ID_HWFRAMESCONTEXT, SECTION_ID_ERROR, -1 },  },
+
+    [SECTION_ID_OUTPUTS] =            { SECTION_ID_OUTPUTS, "Outputs", SECTION_FLAG_IS_ARRAY, { SECTION_ID_OUTPUT, SECTION_ID_ERROR, -1 } },
+    [SECTION_ID_OUTPUT] =             { SECTION_ID_OUTPUT, "Output", 0, { SECTION_ID_HWFRAMESCONTEXT, SECTION_ID_ERROR, -1 },  },
+
+    [SECTION_ID_FILTERS] =            { SECTION_ID_FILTERS, "Filters", SECTION_FLAG_IS_ARRAY, { SECTION_ID_FILTER, SECTION_ID_ERROR, -1 } },
+    [SECTION_ID_FILTER] =             { SECTION_ID_FILTER, "Filter", 0, { SECTION_ID_HWDEViCECONTEXT, SECTION_ID_ERROR, -1 },  },
+
+    [SECTION_ID_HWDEViCECONTEXT] =    { SECTION_ID_HWDEViCECONTEXT, "HwDeviceContext", 0, { SECTION_ID_ERROR, -1 },  },
+    [SECTION_ID_HWFRAMESCONTEXT] =    { SECTION_ID_HWFRAMESCONTEXT, "HwFramesContext", 0, { SECTION_ID_ERROR, -1 },  },
+
+    [SECTION_ID_ERROR] =              { SECTION_ID_ERROR, "Error", 0, { -1 } },
+    [SECTION_ID_LOGS] =               { SECTION_ID_LOGS, "Log", SECTION_FLAG_IS_ARRAY, { SECTION_ID_LOG, -1 } },
+    [SECTION_ID_LOG] =                { SECTION_ID_LOG, "LogEntry", 0, { -1 },  },
+};
+
+struct unit_value {
+    union { double d; long long int i; } val;
+    const char *unit;
+};
+
+static const char unit_second_str[]         = "s"    ;
+static const char unit_hertz_str[]          = "Hz"   ;
+static const char unit_byte_str[]           = "byte" ;
+static const char unit_bit_per_second_str[] = "bit/s";
+
+/* WRITERS API */
+
+typedef struct WriterContext WriterContext;
+
+#define WRITER_FLAG_DISPLAY_OPTIONAL_FIELDS 1
+#define WRITER_FLAG_PUT_PACKETS_AND_FRAMES_IN_SAME_CHAPTER 2
+
+typedef enum {
+    WRITER_STRING_VALIDATION_FAIL,
+    WRITER_STRING_VALIDATION_REPLACE,
+    WRITER_STRING_VALIDATION_IGNORE,
+    WRITER_STRING_VALIDATION_NB
+} StringValidation;
+
+typedef struct Writer {
+    const AVClass *priv_class;      ///< private class of the writer, if any
+    int priv_size;                  ///< private size for the writer context
+    const char *name;
+
+    int  (*init)  (WriterContext *wctx);
+    void (*uninit)(WriterContext *wctx);
+
+    void (*print_section_header)(WriterContext *wctx);
+    void (*print_section_footer)(WriterContext *wctx);
+    void (*print_integer)       (WriterContext *wctx, const char *, long long int);
+    void (*print_rational)      (WriterContext *wctx, AVRational *q, char *sep);
+    void (*print_string)        (WriterContext *wctx, const char *, const char *);
+    int flags;                  ///< a combination or WRITER_FLAG_*
+} Writer;
+
+#define SECTION_MAX_NB_LEVELS 10
+
+struct WriterContext {
+    const AVClass *class;           ///< class of the writer
+    const Writer *writer;           ///< the Writer of which this is an instance
+    char *name;                     ///< name of this writer instance
+    void *priv;                     ///< private data for use by the filter
+
+    const struct section *sections; ///< array containing all sections
+    int nb_sections;                ///< number of sections
+
+    int level;                      ///< current level, starting from 0
+
+    /** number of the item printed in the given section, starting from 0 */
+    unsigned int nb_item[SECTION_MAX_NB_LEVELS];
+
+    /** section per each level */
+    const struct section *section[SECTION_MAX_NB_LEVELS];
+    AVBPrint section_pbuf[SECTION_MAX_NB_LEVELS]; ///< generic print buffer dedicated to each section,
+                                                  ///  used by various writers
+    AVBPrint bpBuf;
+    unsigned int nb_section_packet; ///< number of the packet section in case we are in "packets_and_frames" section
+    unsigned int nb_section_frame;  ///< number of the frame  section in case we are in "packets_and_frames" section
+    unsigned int nb_section_packet_frame; ///< nb_section_packet or nb_section_frame according if is_packets_and_frames
+
+    int string_validation;
+    char *string_validation_replacement;
+    unsigned int string_validation_utf8_flags;
+};
+
+#define print_fmt(k, f, ...) do {              \
+    av_bprint_clear(&pbuf);                    \
+    av_bprintf(&pbuf, f, __VA_ARGS__);         \
+    writer_print_string(w, k, pbuf.str, 0);    \
+} while (0)
+
+#define print_int(k, v)         writer_print_integer(w, k, v)
+#define print_q(k, v, s)        writer_print_rational(w, k, v, s)
+#define print_guid(k, v)        writer_print_guid(w, k, v)
+#define print_str(k, v)         writer_print_string(w, k, v, 0)
+#define print_str_opt(k, v)     writer_print_string(w, k, v, PRINT_STRING_OPT)
+#define print_str_validate(k, v) writer_print_string(w, k, v, PRINT_STRING_VALIDATE)
+#define print_time(k, v, tb)    writer_print_time(w, k, v, tb, 0)
+#define print_ts(k, v)          writer_print_ts(w, k, v, 0)
+#define print_duration_time(k, v, tb) writer_print_time(w, k, v, tb, 1)
+#define print_duration_ts(k, v)       writer_print_ts(w, k, v, 1)
+#define print_val(k, v, u) do {                                     \
+    struct unit_value uv;                                           \
+    uv.val.i = v;                                                   \
+    uv.unit = u;                                                    \
+    writer_print_string(w, k, value_string(val_str, sizeof(val_str), uv), 0); \
+} while (0)
+
+void writer_register_all(void);
+int print_filtergraphs(FilterGraph **filtergraphs, int nb_filtergraphs);
+
+const Writer *writer_get_by_name(const char *name);
+
+int writer_open(WriterContext **wctx, const Writer *writer, const char *args,
+                       const struct section *sections, int nb_sections);
+
+void writer_print_section_header(WriterContext *wctx, int section_id);
+void writer_print_section_footer(WriterContext *wctx);
+
+int writer_print_string(WriterContext *wctx, const char *key, const char *val, int flags);
+void writer_print_integer(WriterContext *wctx, const char *key, long long int val);
+void writer_print_rational(WriterContext *wctx, const char *key, AVRational q, char sep);
+
+void writer_close(WriterContext **wctx);
+
+#endif /* FFTOOLS_FILTERWRITERS_H */
diff --git a/libavcodec/allcodecs.c b/libavcodec/allcodecs.c
index c087b91148..b457f1767f 100644
--- a/libavcodec/allcodecs.c
+++ b/libavcodec/allcodecs.c
@@ -797,7 +797,6 @@  extern const AVCodec ff_h263_v4l2m2m_encoder;
 extern const AVCodec ff_libaom_av1_decoder;
 /* hwaccel hooks only, so prefer external decoders */
 extern const AVCodec ff_av1_decoder;
-extern const AVCodec ff_av1_cuvid_decoder;
 extern const AVCodec ff_av1_qsv_decoder;
 extern const AVCodec ff_libopenh264_encoder;
 extern const AVCodec ff_libopenh264_decoder;