diff mbox series

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

Message ID MN2PR04MB5981ACF4DCB81151911439E2BAC89@MN2PR04MB5981.namprd04.prod.outlook.com
State New
Headers show
Series [FFmpeg-devel,v3] fftools: Add option for writing detailed filtergraph information to file or stdout
Related show

Checks

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

Commit Message

Soft Works Aug. 27, 2021, 3:13 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>
---
v3: adjust commit message (add context)

 fftools/Makefile        |    2 +-
 fftools/ffmpeg.c        |   15 +
 fftools/ffmpeg.h        |    3 +
 fftools/ffmpeg_opt.c    |    9 +
 fftools/filterwriters.c | 1559 +++++++++++++++++++++++++++++++++++++++
 fftools/filterwriters.h |  205 +++++
 6 files changed, 1792 insertions(+), 1 deletion(-)
 create mode 100644 fftools/filterwriters.c
 create mode 100644 fftools/filterwriters.h

Comments

Soft Works Aug. 27, 2021, 4:22 a.m. UTC | #1
> -----Original Message-----
> From: ffmpeg-devel <ffmpeg-devel-bounces@ffmpeg.org> On Behalf Of
> Soft Works
> Sent: Friday, 27 August 2021 05:14
> To: ffmpeg-devel@ffmpeg.org
> Subject: [FFmpeg-devel] [PATCH v3] fftools: Add option for writing
> detailed filtergraph information to file or stdout
> 
> 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 submitting this updated version (fixes Fate) as I'm still convinced 
that this is a better alternative to the patch referenced above.

What remains is the issue that this patch includes code that is duplicated
from ffprobe.c

The problem is that the code from ffprobe.c is not re-usable. 

At this point I'd like to ask what is expected from me to do about it?
(for the hypothetical case that the patch would be agreeable)

Is it my responsibility to refactor the ffprobe code to make it re-usable?

- If no, what should I do?
  - Wait until somebody does this (or in this case, e.g. Nicolas' code gets merged)
    (which would mean that it's impossible to submit the patch)
  - Or can the patch with the duplicated be merged and the unification be done later?

- If yes, in which order should it be done?
  - Merge patch first and do the unification afterwards?
  - Perform the ffprobe-refactoring as part of my patch?`
  - Submit another patch first which does the ffprobe refactoring?

Thanks,
softworkz
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..11dfb54fa5
--- /dev/null
+++ b/fftools/filterwriters.c
@@ -0,0 +1,1559 @@ 
+/*
+ * Copyright (c) 2018 - softworkz
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @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..e7b58f2c13
--- /dev/null
+++ b/fftools/filterwriters.h
@@ -0,0 +1,205 @@ 
+/*
+ * Copyright (c) 2018 - softworkz
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef FFTOOLS_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 */