History

fftools/graphprint: Add execution graph printing

The key benefits are: - Different to other graph printing methods, 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 - Use the same output implementation as ffprobe, supporting multiple formats

softworkz committed 1 year ago

Commit 45926bc1 parent 50fcc0c12 files · +1922 −2Browse files

Unified · Split

12 files
  1. doc/ffmpeg.texi +10 −0
  2. fftools/Makefile +19 −1
  3. fftools/ffmpeg.c +4 −0
  4. fftools/ffmpeg.h +3 −0
  5. fftools/ffmpeg_filter.c +5 −0
  6. fftools/ffmpeg_opt.c +13 −0
  7. fftools/graph/graphprint.c +1107 −0
  8. fftools/graph/graphprint.h +30 −0
  9. fftools/textformat/avtextformat.c +3 −1
  10. fftools/textformat/avtextformat.h +29 −0
  11. fftools/textformat/tf_mermaid.c +658 −0
  12. fftools/textformat/tf_mermaid.h +41 −0
doc/ffmpeg.texi+10−0
@@ -1394,6 +1394,16 @@ It is on by default, to explicitly disable it you need to specify @code{-nostats · 1393 unmodified lines
13941394 @item -stats_period @var{time} (@emph{global})
13951395 Set period at which encoding progress/statistics are updated. Default is 0.5 seconds.
13961396
1397+@item -print_graphs (@emph{global})
1398+Prints execution graph details to stderr in the format set via -print_graphs_format.
1399+
1400+@item -print_graphs_file @var{filename} (@emph{global})
1401+Writes execution graph details to the specified file in the format set via -print_graphs_format.
1402+
1403+@item -print_graphs_format @var{format} (@emph{global})
1404+Sets the output format (available formats are: default, compact, csv, flat, ini, json, xml, mermaid, mermaidhtml)
1405+The default format is json.
1406+
13971407 @item -progress @var{url} (@emph{global})
13981408 Send program-friendly progress information to @var{url}.
13991409
fftools/Makefile+19−1
@@ -9,6 +9,8 @@ AVBASENAMES = ffmpeg ffplay ffprobe · 8 unmodified lines
99 ALLAVPROGS = $(AVBASENAMES:%=%$(PROGSSUF)$(EXESUF))
1010 ALLAVPROGS_G = $(AVBASENAMES:%=%$(PROGSSUF)_g$(EXESUF))
1111
12+include $(SRC_PATH)/fftools/resources/Makefile
13+
1214 OBJS-ffmpeg += \
1315 fftools/ffmpeg_dec.o \
1416 fftools/ffmpeg_demux.o \
@@ -19,8 +21,21 @@ OBJS-ffmpeg += \ · 4 unmodified lines
1921 fftools/ffmpeg_mux_init.o \
2022 fftools/ffmpeg_opt.o \
2123 fftools/ffmpeg_sched.o \
24+ fftools/graph/graphprint.o \
2225 fftools/sync_queue.o \
2326 fftools/thread_queue.o \
27+ fftools/textformat/avtextformat.o \
28+ fftools/textformat/tf_compact.o \
29+ fftools/textformat/tf_default.o \
30+ fftools/textformat/tf_flat.o \
31+ fftools/textformat/tf_ini.o \
32+ fftools/textformat/tf_json.o \
33+ fftools/textformat/tf_mermaid.o \
34+ fftools/textformat/tf_xml.o \
35+ fftools/textformat/tw_avio.o \
36+ fftools/textformat/tw_buffer.o \
37+ fftools/textformat/tw_stdout.o \
38+ $(OBJS-resman) \
2439
2540 OBJS-ffprobe += \
2641 fftools/textformat/avtextformat.o \
@@ -29,10 +44,12 @@ OBJS-ffprobe += \ · 2 unmodified lines
2944 fftools/textformat/tf_flat.o \
3045 fftools/textformat/tf_ini.o \
3146 fftools/textformat/tf_json.o \
47+ fftools/textformat/tf_mermaid.o \
3248 fftools/textformat/tf_xml.o \
3349 fftools/textformat/tw_avio.o \
3450 fftools/textformat/tw_buffer.o \
3551 fftools/textformat/tw_stdout.o \
52+ $(OBJS-resman) \
3653
3754 OBJS-ffplay += fftools/ffplay_renderer.o
3855
@@ -42,7 +59,7 @@ ifdef HAVE_GNU_WINDRES · 3 unmodified lines
4259 OBJS-$(1) += fftools/fftoolsres.o
4360 endif
4461 $(1)$(PROGSSUF)_g$(EXESUF): $$(OBJS-$(1))
45−$$(OBJS-$(1)): | fftools fftools/textformat fftools/resources
62+$$(OBJS-$(1)): | fftools fftools/textformat fftools/resources fftools/graph
4663 $$(OBJS-$(1)): CFLAGS += $(CFLAGS-$(1))
4764 $(1)$(PROGSSUF)_g$(EXESUF): LDFLAGS += $(LDFLAGS-$(1))
4865 $(1)$(PROGSSUF)_g$(EXESUF): FF_EXTRALIBS += $(EXTRALIBS-$(1))
@@ -57,6 +74,7 @@ fftools/ffprobe.o fftools/cmdutils.o: libavutil/ffversion.h | fftools · 8 unmodified lines
5774 OUTDIRS += fftools
5875 OUTDIRS += fftools/textformat
5976 OUTDIRS += fftools/resources
77+OUTDIRS += fftools/graph
6078
6179 ifdef AVPROGS
6280 install: install-progs install-data
fftools/ffmpeg.c+4−0
@@ -81,6 +81,7 @@ · 80 unmodified lines
8181 #include "ffmpeg.h"
8282 #include "ffmpeg_sched.h"
8383 #include "ffmpeg_utils.h"
84+#include "graph/graphprint.h"
8485
8586 const char program_name[] = "ffmpeg";
8687 const int program_birth_year = 2000;
@@ -308,6 +309,9 @@ const AVIOInterruptCB int_cb = { decode_interrupt_cb, NULL }; · 221 unmodified lines
308309
309310 static void ffmpeg_cleanup(int ret)
310311 {
312+ if (print_graphs || print_graphs_file)
313+ print_filtergraphs(filtergraphs, nb_filtergraphs, input_files, nb_input_files, output_files, nb_output_files);
314+
311315 if (do_benchmark) {
312316 int64_t maxrss = getmaxrss() / 1024;
313317 av_log(NULL, AV_LOG_INFO, "bench: maxrss=%"PRId64"KiB\n", maxrss);
fftools/ffmpeg.h+3−0
@@ -717,6 +717,9 @@ extern float max_error_rate; · 716 unmodified lines
717717 extern char *filter_nbthreads;
718718 extern int filter_complex_nbthreads;
719719 extern int vstats_version;
720+extern int print_graphs;
721+extern char *print_graphs_file;
722+extern char *print_graphs_format;
720723 extern int auto_conversion_filters;
721724
722725 extern const AVIOInterruptCB int_cb;
fftools/ffmpeg_filter.c+5−0
@@ -22,6 +22,7 @@ · 21 unmodified lines
2222
2323 #include "ffmpeg.h"
2424 #include "ffmpeg_filter.h"
25+#include "graph/graphprint.h"
2526
2627 #include "libavfilter/avfilter.h"
2728 #include "libavfilter/buffersink.h"
@@ -2983,6 +2984,10 @@ read_frames: · 2955 unmodified lines
29832984 }
29842985
29852986 finish:
2987+
2988+ if (print_graphs || print_graphs_file)
2989+ print_filtergraph(fg, fgt.graph);
2990+
29862991 // EOF is normal termination
29872992 if (ret == AVERROR_EOF)
29882993 ret = 0;
fftools/ffmpeg_opt.c+13−0
@@ -47,6 +47,7 @@ · 46 unmodified lines
4747 #include "libavutil/opt.h"
4848 #include "libavutil/parseutils.h"
4949 #include "libavutil/stereo3d.h"
50+#include "graph/graphprint.h"
5051
5152 HWDevice *filter_hw_device;
5253
@@ -75,6 +76,9 @@ float max_error_rate = 2.0/3; · 22 unmodified lines
7576 char *filter_nbthreads;
7677 int filter_complex_nbthreads = 0;
7778 int vstats_version = 2;
79+int print_graphs = 0;
80+char *print_graphs_file = NULL;
81+char *print_graphs_format = NULL;
7882 int auto_conversion_filters = 1;
7983 int64_t stats_period = 500000;
8084
@@ -1735,6 +1739,15 @@ const OptionDef options[] = { · 1654 unmodified lines
17351739 { .func_arg = opt_filter_complex_script },
17361740 "deprecated, use -/filter_complex instead", "filename" },
17371741 #endif
1742+ { "print_graphs", OPT_TYPE_BOOL, 0,
1743+ { &print_graphs },
1744+ "print execution graph data to stderr" },
1745+ { "print_graphs_file", OPT_TYPE_STRING, 0,
1746+ { &print_graphs_file },
1747+ "write execution graph data to the specified file", "filename" },
1748+ { "print_graphs_format", OPT_TYPE_STRING, 0,
1749+ { &print_graphs_format },
1750+ "set the output printing format (available formats are: default, compact, csv, flat, ini, json, xml, mermaid, mermaidhtml)", "format" },
17381751 { "auto_conversion_filters", OPT_TYPE_BOOL, OPT_EXPERT,
17391752 { &auto_conversion_filters },
17401753 "enable automatic conversion filters globally" },
fftools/graph/graphprint.c+1107−0
@@ -0,0 +1,1107 @@
1+/*
2+ * Copyright (c) 2018-2025 - softworkz
3+ *
4+ * This file is part of FFmpeg.
5+ *
6+ * FFmpeg is free software; you can redistribute it and/or
7+ * modify it under the terms of the GNU Lesser General Public
8+ * License as published by the Free Software Foundation; either
9+ * version 2.1 of the License, or (at your option) any later version.
10+ *
11+ * FFmpeg is distributed in the hope that it will be useful,
12+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+ * Lesser General Public License for more details.
15+ *
16+ * You should have received a copy of the GNU Lesser General Public
17+ * License along with FFmpeg; if not, write to the Free Software
18+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19+ */
20+
21+/**
22+ * @file
23+ * output writers for filtergraph details
24+ */
25+
26+#include <string.h>
27+#include <stdatomic.h>
28+
29+#include "graphprint.h"
30+
31+#include "fftools/ffmpeg_filter.h"
32+#include "fftools/ffmpeg_mux.h"
33+
34+#include "libavutil/avassert.h"
35+#include "libavutil/avstring.h"
36+#include "libavutil/pixdesc.h"
37+#include "libavutil/dict.h"
38+#include "libavutil/common.h"
39+#include "libavfilter/avfilter.h"
40+#include "libavutil/buffer.h"
41+#include "libavutil/hwcontext.h"
42+#include "fftools/textformat/avtextformat.h"
43+#include "fftools/textformat/tf_mermaid.h"
44+#include "fftools/resources/resman.h"
45+
46+typedef enum {
47+ SECTION_ID_ROOT,
48+ SECTION_ID_FILTERGRAPHS,
49+ SECTION_ID_FILTERGRAPH,
50+ SECTION_ID_GRAPH_INPUTS,
51+ SECTION_ID_GRAPH_INPUT,
52+ SECTION_ID_GRAPH_OUTPUTS,
53+ SECTION_ID_GRAPH_OUTPUT,
54+ SECTION_ID_FILTERS,
55+ SECTION_ID_FILTER,
56+ SECTION_ID_FILTER_INPUTS,
57+ SECTION_ID_FILTER_INPUT,
58+ SECTION_ID_FILTER_OUTPUTS,
59+ SECTION_ID_FILTER_OUTPUT,
60+ SECTION_ID_HWFRAMESCONTEXT,
61+ SECTION_ID_INPUTFILES,
62+ SECTION_ID_INPUTFILE,
63+ SECTION_ID_INPUTSTREAMS,
64+ SECTION_ID_INPUTSTREAM,
65+ SECTION_ID_OUTPUTFILES,
66+ SECTION_ID_OUTPUTFILE,
67+ SECTION_ID_OUTPUTSTREAMS,
68+ SECTION_ID_OUTPUTSTREAM,
69+ SECTION_ID_STREAMLINKS,
70+ SECTION_ID_STREAMLINK,
71+ SECTION_ID_DECODERS,
72+ SECTION_ID_DECODER,
73+ SECTION_ID_ENCODERS,
74+ SECTION_ID_ENCODER,
75+} SectionID;
76+
77+static struct AVTextFormatSection sections[] = {
78+ [SECTION_ID_ROOT] = { SECTION_ID_ROOT, "root", AV_TEXTFORMAT_SECTION_FLAG_IS_WRAPPER, { SECTION_ID_FILTERGRAPHS, SECTION_ID_INPUTFILES, SECTION_ID_OUTPUTFILES, SECTION_ID_DECODERS, SECTION_ID_ENCODERS, SECTION_ID_STREAMLINKS, -1 } },
79+
80+ [SECTION_ID_FILTERGRAPHS] = { SECTION_ID_FILTERGRAPHS, "graphs", AV_TEXTFORMAT_SECTION_FLAG_IS_ARRAY, { SECTION_ID_FILTERGRAPH, -1 } },
81+ [SECTION_ID_FILTERGRAPH] = { SECTION_ID_FILTERGRAPH, "graph", AV_TEXTFORMAT_SECTION_FLAG_HAS_VARIABLE_FIELDS, { SECTION_ID_GRAPH_INPUTS, SECTION_ID_GRAPH_OUTPUTS, SECTION_ID_FILTERS, -1 }, .element_name = "graph_info" },
82+
83+ [SECTION_ID_GRAPH_INPUTS] = { SECTION_ID_GRAPH_INPUTS, "graph_inputs", AV_TEXTFORMAT_SECTION_FLAG_IS_ARRAY, { SECTION_ID_GRAPH_INPUT, -1 }, .id_key = "id" },
84+ [SECTION_ID_GRAPH_INPUT] = { SECTION_ID_GRAPH_INPUT, "graph_input", 0, { -1 }, .id_key = "filter_id" },
85+
86+ [SECTION_ID_GRAPH_OUTPUTS] = { SECTION_ID_GRAPH_OUTPUTS, "graph_outputs", AV_TEXTFORMAT_SECTION_FLAG_IS_ARRAY, { SECTION_ID_GRAPH_OUTPUT, -1 }, .id_key = "id" },
87+ [SECTION_ID_GRAPH_OUTPUT] = { SECTION_ID_GRAPH_OUTPUT, "graph_output", 0, { -1 }, .id_key = "filter_id" },
88+
89+ [SECTION_ID_FILTERS] = { SECTION_ID_FILTERS, "filters", AV_TEXTFORMAT_SECTION_FLAG_IS_ARRAY | AV_TEXTFORMAT_SECTION_FLAG_IS_SUBGRAPH, { SECTION_ID_FILTER, -1 }, .id_key = "graph_id" },
90+ [SECTION_ID_FILTER] = { SECTION_ID_FILTER, "filter", AV_TEXTFORMAT_SECTION_FLAG_IS_SHAPE | AV_TEXTFORMAT_SECTION_PRINT_TAGS, { SECTION_ID_FILTER_INPUTS, SECTION_ID_FILTER_OUTPUTS, -1 }, .id_key = "filter_id" },
91+
92+ [SECTION_ID_FILTER_INPUTS] = { SECTION_ID_FILTER_INPUTS, "filter_inputs", AV_TEXTFORMAT_SECTION_FLAG_IS_ARRAY, { SECTION_ID_FILTER_INPUT, -1 } },
93+ [SECTION_ID_FILTER_INPUT] = { SECTION_ID_FILTER_INPUT, "filter_input", AV_TEXTFORMAT_SECTION_FLAG_HAS_LINKS, { SECTION_ID_HWFRAMESCONTEXT, -1 }, .id_key = "filter_id", .src_id_key = "source_filter_id", .dest_id_key = "filter_id" },
94+
95+ [SECTION_ID_FILTER_OUTPUTS] = { SECTION_ID_FILTER_OUTPUTS, "filter_outputs", AV_TEXTFORMAT_SECTION_FLAG_IS_ARRAY, { SECTION_ID_FILTER_OUTPUT, -1 } },
96+ [SECTION_ID_FILTER_OUTPUT] = { SECTION_ID_FILTER_OUTPUT, "filter_output", AV_TEXTFORMAT_SECTION_FLAG_HAS_LINKS, { SECTION_ID_HWFRAMESCONTEXT, -1 }, .id_key = "filter_id", .src_id_key = "filter_id", .dest_id_key = "dest_filter_id" },
97+
98+ [SECTION_ID_HWFRAMESCONTEXT] = { SECTION_ID_HWFRAMESCONTEXT, "hw_frames_context", 0, { -1 }, },
99+
100+ [SECTION_ID_INPUTFILES] = { SECTION_ID_INPUTFILES, "inputfiles", AV_TEXTFORMAT_SECTION_FLAG_IS_ARRAY | AV_TEXTFORMAT_SECTION_FLAG_IS_SUBGRAPH, { SECTION_ID_INPUTFILE, -1 }, .id_key = "id" },
101+ [SECTION_ID_INPUTFILE] = { SECTION_ID_INPUTFILE, "inputfile", AV_TEXTFORMAT_SECTION_FLAG_IS_SUBGRAPH, { SECTION_ID_INPUTSTREAMS, -1 }, .id_key = "id" },
102+
103+ [SECTION_ID_INPUTSTREAMS] = { SECTION_ID_INPUTSTREAMS, "inputstreams", AV_TEXTFORMAT_SECTION_FLAG_IS_ARRAY | AV_TEXTFORMAT_SECTION_FLAG_IS_SUBGRAPH, { SECTION_ID_INPUTSTREAM, -1 }, .id_key = "id" },
104+ [SECTION_ID_INPUTSTREAM] = { SECTION_ID_INPUTSTREAM, "inputstream", AV_TEXTFORMAT_SECTION_FLAG_IS_SHAPE | AV_TEXTFORMAT_SECTION_PRINT_TAGS, { -1 }, .id_key = "id" },
105+
106+ [SECTION_ID_OUTPUTFILES] = { SECTION_ID_OUTPUTFILES, "outputfiles", AV_TEXTFORMAT_SECTION_FLAG_IS_ARRAY | AV_TEXTFORMAT_SECTION_FLAG_IS_SUBGRAPH, { SECTION_ID_OUTPUTFILE, -1 }, .id_key = "id" },
107+ [SECTION_ID_OUTPUTFILE] = { SECTION_ID_OUTPUTFILE, "outputfile", AV_TEXTFORMAT_SECTION_FLAG_IS_SUBGRAPH, { SECTION_ID_OUTPUTSTREAMS, -1 }, .id_key = "id" },
108+
109+ [SECTION_ID_OUTPUTSTREAMS] = { SECTION_ID_OUTPUTSTREAMS, "outputstreams", AV_TEXTFORMAT_SECTION_FLAG_IS_ARRAY | AV_TEXTFORMAT_SECTION_FLAG_IS_SUBGRAPH, { SECTION_ID_OUTPUTSTREAM, -1 }, .id_key = "id" },
110+ [SECTION_ID_OUTPUTSTREAM] = { SECTION_ID_OUTPUTSTREAM, "outputstream", AV_TEXTFORMAT_SECTION_FLAG_IS_SHAPE | AV_TEXTFORMAT_SECTION_PRINT_TAGS, { -1 }, .id_key = "id", },
111+
112+ [SECTION_ID_STREAMLINKS] = { SECTION_ID_STREAMLINKS, "streamlinks", AV_TEXTFORMAT_SECTION_FLAG_IS_ARRAY, { SECTION_ID_STREAMLINK, -1 } },
113+ [SECTION_ID_STREAMLINK] = { SECTION_ID_STREAMLINK, "streamlink", AV_TEXTFORMAT_SECTION_FLAG_HAS_LINKS, { -1 }, .src_id_key = "source_stream_id", .dest_id_key = "dest_stream_id" },
114+
115+ [SECTION_ID_DECODERS] = { SECTION_ID_DECODERS, "decoders", AV_TEXTFORMAT_SECTION_FLAG_IS_ARRAY | AV_TEXTFORMAT_SECTION_FLAG_IS_SUBGRAPH, { SECTION_ID_DECODER, -1 } },
116+ [SECTION_ID_DECODER] = { SECTION_ID_DECODER, "decoder", AV_TEXTFORMAT_SECTION_FLAG_IS_SHAPE | AV_TEXTFORMAT_SECTION_PRINT_TAGS | AV_TEXTFORMAT_SECTION_FLAG_HAS_LINKS, { -1 }, .id_key = "id", .src_id_key = "source_id", .dest_id_key = "id" },
117+
118+ [SECTION_ID_ENCODERS] = { SECTION_ID_ENCODERS, "encoders", AV_TEXTFORMAT_SECTION_FLAG_IS_ARRAY | AV_TEXTFORMAT_SECTION_FLAG_IS_SUBGRAPH, { SECTION_ID_ENCODER, -1 } },
119+ [SECTION_ID_ENCODER] = { SECTION_ID_ENCODER, "encoder", AV_TEXTFORMAT_SECTION_FLAG_IS_SHAPE | AV_TEXTFORMAT_SECTION_PRINT_TAGS | AV_TEXTFORMAT_SECTION_FLAG_HAS_LINKS, { -1 }, .id_key = "id", .src_id_key = "id", .dest_id_key = "dest_id" },
120+};
121+
122+typedef struct GraphPrintContext {
123+ AVTextFormatContext *tfc;
124+ AVTextWriterContext *wctx;
125+ AVDiagramConfig diagram_config;
126+
127+ int id_prefix_num;
128+ int is_diagram;
129+ int opt_flags;
130+ int skip_buffer_filters;
131+ AVBPrint pbuf;
132+
133+} GraphPrintContext;
134+
135+/* Text Format API Shortcuts */
136+#define print_id(k, v) print_sanizied_id(gpc, k, v, 0)
137+#define print_id_noprefix(k, v) print_sanizied_id(gpc, k, v, 1)
138+#define print_int(k, v) avtext_print_integer(tfc, k, v, 0)
139+#define print_int_opt(k, v) avtext_print_integer(tfc, k, v, gpc->opt_flags)
140+#define print_q(k, v, s) avtext_print_rational(tfc, k, v, s)
141+#define print_str(k, v) avtext_print_string(tfc, k, v, 0)
142+#define print_str_opt(k, v) avtext_print_string(tfc, k, v, gpc->opt_flags)
143+#define print_val(k, v, u) avtext_print_unit_int(tfc, k, v, u)
144+
145+#define print_fmt(k, f, ...) do { \
146+ av_bprint_clear(&gpc->pbuf); \
147+ av_bprintf(&gpc->pbuf, f, __VA_ARGS__); \
148+ avtext_print_string(tfc, k, gpc->pbuf.str, 0); \
149+} while (0)
150+
151+#define print_fmt_opt(k, f, ...) do { \
152+ av_bprint_clear(&gpc->pbuf); \
153+ av_bprintf(&gpc->pbuf, f, __VA_ARGS__); \
154+ avtext_print_string(tfc, k, gpc->pbuf.str, gpc->opt_flags); \
155+} while (0)
156+
157+
158+static atomic_int prefix_num = 0;
159+
160+static inline char *upcase_string(char *dst, size_t dst_size, const char *src)
161+{
162+ unsigned i;
163+ for (i = 0; src[i] && i < dst_size - 1; i++)
164+ dst[i] = (char)av_toupper(src[i]);
165+ dst[i] = 0;
166+ return dst;
167+}
168+
169+static char *get_extension(const char *url)
170+{
171+ const char *dot = NULL;
172+ const char *sep = NULL;
173+ const char *end;
174+
175+ if (!url)
176+ return NULL;
177+
178+ /* Stop at the first query ('?') or fragment ('#') delimiter so they
179+ * are not considered part of the path. */
180+ end = strpbrk(url, "?#");
181+ if (!end)
182+ end = url + strlen(url);
183+
184+ /* Scan the path component only. */
185+ for (const char *p = url; p < end; p++) {
186+ if (*p == '.')
187+ dot = p;
188+ else if (*p == '/' || *p == '\\')
189+ sep = p;
190+ }
191+
192+ /* Validate that we have a proper extension. */
193+ if (dot && dot != url && (!sep || dot > sep + 1) && (dot + 1) < end) {
194+ /* Use FFmpeg helper to duplicate the substring. */
195+ return av_strndup(dot + 1, end - (dot + 1));
196+ }
197+
198+ return NULL;
199+}
200+
201+static void print_hwdevicecontext(const GraphPrintContext *gpc, const AVHWDeviceContext *hw_device_context)
202+{
203+ AVTextFormatContext *tfc = gpc->tfc;
204+
205+ if (!hw_device_context)
206+ return;
207+
208+ print_int_opt("has_hw_device_context", 1);
209+ print_str_opt("hw_device_type", av_hwdevice_get_type_name(hw_device_context->type));
210+}
211+
212+static void print_hwframescontext(const GraphPrintContext *gpc, const AVHWFramesContext *hw_frames_context)
213+{
214+ AVTextFormatContext *tfc = gpc->tfc;
215+ const AVPixFmtDescriptor *pix_desc_hw;
216+ const AVPixFmtDescriptor *pix_desc_sw;
217+
218+ if (!hw_frames_context || !hw_frames_context->device_ctx)
219+ return;
220+
221+ avtext_print_section_header(tfc, NULL, SECTION_ID_HWFRAMESCONTEXT);
222+
223+ print_int_opt("has_hw_frames_context", 1);
224+ print_str("hw_device_type", av_hwdevice_get_type_name(hw_frames_context->device_ctx->type));
225+
226+ pix_desc_hw = av_pix_fmt_desc_get(hw_frames_context->format);
227+ if (pix_desc_hw) {
228+ print_str("hw_pixel_format", pix_desc_hw->name);
229+ if (pix_desc_hw->alias)
230+ print_str_opt("hw_pixel_format_alias", pix_desc_hw->alias);
231+ }
232+
233+ pix_desc_sw = av_pix_fmt_desc_get(hw_frames_context->sw_format);
234+ if (pix_desc_sw) {
235+ print_str("sw_pixel_format", pix_desc_sw->name);
236+ if (pix_desc_sw->alias)
237+ print_str_opt("sw_pixel_format_alias", pix_desc_sw->alias);
238+ }
239+
240+ print_int_opt("width", hw_frames_context->width);
241+ print_int_opt("height", hw_frames_context->height);
242+ print_int_opt("initial_pool_size", hw_frames_context->initial_pool_size);
243+
244+ avtext_print_section_footer(tfc); // SECTION_ID_HWFRAMESCONTEXT
245+}
246+
247+static void print_link(GraphPrintContext *gpc, AVFilterLink *link)
248+{
249+ AVTextFormatContext *tfc = gpc->tfc;
250+ AVBufferRef *hw_frames_ctx;
251+ char layout_string[64];
252+
253+ if (!link)
254+ return;
255+
256+ hw_frames_ctx = avfilter_link_get_hw_frames_ctx(link);
257+
258+ print_str_opt("media_type", av_get_media_type_string(link->type));
259+
260+ switch (link->type) {
261+ case AVMEDIA_TYPE_VIDEO:
262+
263+ if (hw_frames_ctx && hw_frames_ctx->data) {
264+ AVHWFramesContext * hwfctx = (AVHWFramesContext *)hw_frames_ctx->data;
265+ const AVPixFmtDescriptor *pix_desc_hw = av_pix_fmt_desc_get(hwfctx->format);
266+ const AVPixFmtDescriptor *pix_desc_sw = av_pix_fmt_desc_get(hwfctx->sw_format);
267+ if (pix_desc_hw && pix_desc_sw)
268+ print_fmt("format", "%s | %s", pix_desc_hw->name, pix_desc_sw->name);
269+ } else {
270+ print_str("format", av_x_if_null(av_get_pix_fmt_name(link->format), "?"));
271+ }
272+
273+ if (link->w && link->h) {
274+ if (tfc->show_value_unit) {
275+ print_fmt("size", "%dx%d", link->w, link->h);
276+ } else {
277+ print_int("width", link->w);
278+ print_int("height", link->h);
279+ }
280+ }
281+
282+ print_q("sar", link->sample_aspect_ratio, ':');
283+
284+ if (link->color_range != AVCOL_RANGE_UNSPECIFIED)
285+ print_str_opt("color_range", av_color_range_name(link->color_range));
286+
287+ if (link->colorspace != AVCOL_SPC_UNSPECIFIED)
288+ print_str("color_space", av_color_space_name(link->colorspace));
289+ break;
290+
291+ case AVMEDIA_TYPE_SUBTITLE:
292+ ////print_str("format", av_x_if_null(av_get_subtitle_fmt_name(link->format), "?"));
293+
294+ if (link->w && link->h) {
295+ if (tfc->show_value_unit) {
296+ print_fmt("size", "%dx%d", link->w, link->h);
297+ } else {
298+ print_int("width", link->w);
299+ print_int("height", link->h);
300+ }
301+ }
302+
303+ break;
304+
305+ case AVMEDIA_TYPE_AUDIO:
306+ av_channel_layout_describe(&link->ch_layout, layout_string, sizeof(layout_string));
307+ print_str("channel_layout", layout_string);
308+ print_val("channels", link->ch_layout.nb_channels, "ch");
309+ if (tfc->show_value_unit)
310+ print_fmt("sample_rate", "%d.1 kHz", link->sample_rate / 1000);
311+ else
312+ print_val("sample_rate", link->sample_rate, "Hz");
313+
314+ break;
315+ }
316+
317+ print_fmt_opt("sample_rate", "%d/%d", link->time_base.num, link->time_base.den);
318+
319+ if (hw_frames_ctx && hw_frames_ctx->data)
320+ print_hwframescontext(gpc, (AVHWFramesContext *)hw_frames_ctx->data);
321+}
322+
323+static char sanitize_char(const char c)
324+{
325+ if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
326+ return c;
327+ return '_';
328+}
329+
330+static void print_sanizied_id(const GraphPrintContext *gpc, const char *key, const char *id_str, int skip_prefix)
331+{
332+ AVTextFormatContext *tfc = gpc->tfc;
333+ AVBPrint buf;
334+
335+ if (!key || !id_str)
336+ return;
337+
338+ av_bprint_init(&buf, 0, AV_BPRINT_SIZE_UNLIMITED);
339+
340+ if (!skip_prefix)
341+ av_bprintf(&buf, "G%d_", gpc->id_prefix_num);
342+
343+ // sanizize section id
344+ for (const char *p = id_str; *p; p++)
345+ av_bprint_chars(&buf, sanitize_char(*p), 1);
346+
347+ print_str(key, buf.str);
348+
349+ av_bprint_finalize(&buf, NULL);
350+}
351+
352+static void print_section_header_id(const GraphPrintContext *gpc, int section_id, const char *id_str, int skip_prefix)
353+{
354+ AVTextFormatContext *tfc = gpc->tfc;
355+ AVTextFormatSectionContext sec_ctx = { 0 };
356+ AVBPrint buf;
357+
358+ if (!id_str)
359+ return;
360+
361+ av_bprint_init(&buf, 0, AV_BPRINT_SIZE_UNLIMITED);
362+
363+ if (!skip_prefix)
364+ av_bprintf(&buf, "G%d_", gpc->id_prefix_num);
365+
366+ // sanizize section id
367+ for (const char *p = id_str; *p; p++)
368+ av_bprint_chars(&buf, sanitize_char(*p), 1);
369+
370+ sec_ctx.context_id = buf.str;
371+
372+ avtext_print_section_header(tfc, &sec_ctx, section_id);
373+
374+ av_bprint_finalize(&buf, NULL);
375+}
376+
377+static const char *get_filterpad_name(const AVFilterPad *pad)
378+{
379+ return pad ? avfilter_pad_get_name(pad, 0) : "pad";
380+}
381+
382+static void print_filter(GraphPrintContext *gpc, const AVFilterContext *filter, AVDictionary *input_map, AVDictionary *output_map)
383+{
384+ AVTextFormatContext *tfc = gpc->tfc;
385+ AVTextFormatSectionContext sec_ctx = { 0 };
386+
387+ print_section_header_id(gpc, SECTION_ID_FILTER, filter->name, 0);
388+
389+ ////print_id("filter_id", filter->name);
390+
391+ if (filter->filter) {
392+ print_str("filter_name", filter->filter->name);
393+ print_str_opt("description", filter->filter->description);
394+ print_int_opt("nb_inputs", filter->nb_inputs);
395+ print_int_opt("nb_outputs", filter->nb_outputs);
396+ }
397+
398+ if (filter->hw_device_ctx) {
399+ AVHWDeviceContext *device_context = (AVHWDeviceContext *)filter->hw_device_ctx->data;
400+ print_hwdevicecontext(gpc, device_context);
401+ if (filter->extra_hw_frames > 0)
402+ print_int("extra_hw_frames", filter->extra_hw_frames);
403+ }
404+
405+ avtext_print_section_header(tfc, NULL, SECTION_ID_FILTER_INPUTS);
406+
407+ for (unsigned i = 0; i < filter->nb_inputs; i++) {
408+ AVDictionaryEntry *dic_entry;
409+ AVFilterLink *link = filter->inputs[i];
410+
411+ sec_ctx.context_type = av_get_media_type_string(link->type);
412+ avtext_print_section_header(tfc, &sec_ctx, SECTION_ID_FILTER_INPUT);
413+ sec_ctx.context_type = NULL;
414+
415+ print_int_opt("input_index", i);
416+ print_str_opt("pad_name", get_filterpad_name(link->dstpad));;
417+
418+ dic_entry = av_dict_get(input_map, link->src->name, NULL, 0);
419+ if (dic_entry) {
420+ char buf[256];
421+ (void)snprintf(buf, sizeof(buf), "in_%s", dic_entry->value);
422+ print_id_noprefix("source_filter_id", buf);
423+ } else {
424+ print_id("source_filter_id", link->src->name);
425+ }
426+
427+ print_str_opt("source_pad_name", get_filterpad_name(link->srcpad));
428+ print_id("filter_id", filter->name);
429+
430+ print_link(gpc, link);
431+
432+ avtext_print_section_footer(tfc); // SECTION_ID_FILTER_INPUT
433+ }
434+
435+ avtext_print_section_footer(tfc); // SECTION_ID_FILTER_INPUTS
436+
437+ avtext_print_section_header(tfc, NULL, SECTION_ID_FILTER_OUTPUTS);
438+
439+ for (unsigned i = 0; i < filter->nb_outputs; i++) {
440+ AVDictionaryEntry *dic_entry;
441+ AVFilterLink *link = filter->outputs[i];
442+ char buf[256];
443+
444+ sec_ctx.context_type = av_get_media_type_string(link->type);
445+ avtext_print_section_header(tfc, &sec_ctx, SECTION_ID_FILTER_OUTPUT);
446+ sec_ctx.context_type = NULL;
447+
448+ dic_entry = av_dict_get(output_map, link->dst->name, NULL, 0);
449+ if (dic_entry) {
450+ (void)snprintf(buf, sizeof(buf), "out_%s", dic_entry->value);
451+ print_id_noprefix("dest_filter_id", buf);
452+ } else {
453+ print_id("dest_filter_id", link->dst->name);
454+ }
455+
456+ print_int_opt("output_index", i);
457+ print_str_opt("pad_name", get_filterpad_name(link->srcpad));
458+ ////print_id("dest_filter_id", link->dst->name);
459+ print_str_opt("dest_pad_name", get_filterpad_name(link->dstpad));
460+ print_id("filter_id", filter->name);
461+
462+ print_link(gpc, link);
463+
464+ avtext_print_section_footer(tfc); // SECTION_ID_FILTER_OUTPUT
465+ }
466+
467+ avtext_print_section_footer(tfc); // SECTION_ID_FILTER_OUTPUTS
468+
469+ avtext_print_section_footer(tfc); // SECTION_ID_FILTER
470+}
471+
472+static void init_sections(void)
473+{
474+ for (unsigned i = 0; i < FF_ARRAY_ELEMS(sections); i++)
475+ sections[i].show_all_entries = 1;
476+}
477+
478+static void print_filtergraph_single(GraphPrintContext *gpc, FilterGraph *fg, AVFilterGraph *graph)
479+{
480+ AVTextFormatContext *tfc = gpc->tfc;
481+ FilterGraphPriv *fgp = fgp_from_fg(fg);
482+ AVDictionary *input_map = NULL;
483+ AVDictionary *output_map = NULL;
484+
485+ print_int("graph_index", fg->index);
486+ print_fmt("name", "Graph %d.%d", gpc->id_prefix_num, fg->index);
487+ print_fmt("id", "Graph_%d_%d", gpc->id_prefix_num, fg->index);
488+ print_str("description", fgp->graph_desc);
489+
490+ print_section_header_id(gpc, SECTION_ID_GRAPH_INPUTS, "Input_File", 0);
491+
492+ for (int i = 0; i < fg->nb_inputs; i++) {
493+ InputFilterPriv *ifilter = ifp_from_ifilter(fg->inputs[i]);
494+ enum AVMediaType media_type = ifilter->type;
495+
496+ avtext_print_section_header(tfc, NULL, SECTION_ID_GRAPH_INPUT);
497+
498+ print_int("input_index", ifilter->index);
499+
500+ if (ifilter->linklabel)
501+ print_str("link_label", (const char*)ifilter->linklabel);
502+
503+ if (ifilter->filter) {
504+ print_id("filter_id", ifilter->filter->name);
505+ print_str("filter_name", ifilter->filter->filter->name);
506+ }
507+
508+ if (ifilter->linklabel && ifilter->filter)
509+ av_dict_set(&input_map, ifilter->filter->name, (const char *)ifilter->linklabel, 0);
510+ else if (ifilter->opts.name && ifilter->filter)
511+ av_dict_set(&input_map, ifilter->filter->name, (const char *)ifilter->opts.name, 0);
512+
513+ print_str("media_type", av_get_media_type_string(media_type));
514+
515+ avtext_print_section_footer(tfc); // SECTION_ID_GRAPH_INPUT
516+ }
517+
518+ avtext_print_section_footer(tfc); // SECTION_ID_GRAPH_INPUTS
519+
520+ print_section_header_id(gpc, SECTION_ID_GRAPH_OUTPUTS, "Output_File", 0);
521+
522+ for (int i = 0; i < fg->nb_outputs; i++) {
523+ OutputFilterPriv *ofilter = ofp_from_ofilter(fg->outputs[i]);
524+
525+ avtext_print_section_header(tfc, NULL, SECTION_ID_GRAPH_OUTPUT);
526+
527+ print_int("output_index", ofilter->index);
528+
529+ print_str("name", ofilter->name);
530+
531+ if (fg->outputs[i]->linklabel)
532+ print_str("link_label", (const char*)fg->outputs[i]->linklabel);
533+
534+ if (ofilter->filter) {
535+ print_id("filter_id", ofilter->filter->name);
536+ print_str("filter_name", ofilter->filter->filter->name);
537+ }
538+
539+ if (ofilter->name && ofilter->filter)
540+ av_dict_set(&output_map, ofilter->filter->name, ofilter->name, 0);
541+
542+
543+ print_str("media_type", av_get_media_type_string(fg->outputs[i]->type));
544+
545+ avtext_print_section_footer(tfc); // SECTION_ID_GRAPH_OUTPUT
546+ }
547+
548+ avtext_print_section_footer(tfc); // SECTION_ID_GRAPH_OUTPUTS
549+
550+ if (graph) {
551+ AVTextFormatSectionContext sec_ctx = { 0 };
552+
553+ sec_ctx.context_id = av_asprintf("Graph_%d_%d", gpc->id_prefix_num, fg->index);
554+
555+ avtext_print_section_header(tfc, &sec_ctx, SECTION_ID_FILTERS);
556+
557+ if (gpc->is_diagram) {
558+ print_fmt("name", "Graph %d.%d", gpc->id_prefix_num, fg->index);
559+ print_str("description", fgp->graph_desc);
560+ print_str("id", sec_ctx.context_id);
561+ }
562+
563+ av_freep(&sec_ctx.context_id);
564+
565+ for (unsigned i = 0; i < graph->nb_filters; i++) {
566+ AVFilterContext *filter = graph->filters[i];
567+
568+ if (gpc->skip_buffer_filters) {
569+ if (av_dict_get(input_map, filter->name, NULL, 0))
570+ continue;
571+ if (av_dict_get(output_map, filter->name, NULL, 0))
572+ continue;
573+ }
574+
575+ sec_ctx.context_id = filter->name;
576+
577+ print_filter(gpc, filter, input_map, output_map);
578+ }
579+
580+ avtext_print_section_footer(tfc); // SECTION_ID_FILTERS
581+ }
582+
583+ // Clean up dictionaries
584+ av_dict_free(&input_map);
585+ av_dict_free(&output_map);
586+}
587+
588+static int print_streams(GraphPrintContext *gpc, InputFile **ifiles, int nb_ifiles, OutputFile **ofiles, int nb_ofiles)
589+{
590+ AVTextFormatContext *tfc = gpc->tfc;
591+ AVBPrint buf;
592+ AVTextFormatSectionContext sec_ctx = { 0 };
593+
594+ av_bprint_init(&buf, 0, AV_BPRINT_SIZE_UNLIMITED);
595+
596+ print_section_header_id(gpc, SECTION_ID_INPUTFILES, "Inputs", 0);
597+
598+ for (int n = nb_ifiles - 1; n >= 0; n--) {
599+ InputFile *ifi = ifiles[n];
600+ AVFormatContext *fc = ifi->ctx;
601+
602+ sec_ctx.context_id = av_asprintf("Input_%d", n);
603+ avtext_print_section_header(tfc, &sec_ctx, SECTION_ID_INPUTFILE);
604+ av_freep(&sec_ctx.context_id);
605+
606+ print_fmt("index", "%d", ifi->index);
607+
608+ if (fc) {
609+ print_str("demuxer_name", fc->iformat->name);
610+ if (fc->url) {
611+ char *extension = get_extension(fc->url);
612+ if (extension) {
613+ print_str("file_extension", extension);
614+ av_freep(&extension);
615+ }
616+ print_str("url", fc->url);
617+ }
618+ }
619+
620+ sec_ctx.context_id = av_asprintf("InputStreams_%d", n);
621+
622+ avtext_print_section_header(tfc, &sec_ctx, SECTION_ID_INPUTSTREAMS);
623+
624+ av_freep(&sec_ctx.context_id);
625+
626+ for (int i = 0; i < ifi->nb_streams; i++) {
627+ InputStream *ist = ifi->streams[i];
628+ const AVCodecDescriptor *codec_desc;
629+
630+ if (!ist || !ist->par)
631+ continue;
632+
633+ codec_desc = avcodec_descriptor_get(ist->par->codec_id);
634+
635+ sec_ctx.context_id = av_asprintf("r_in_%d_%d", n, i);
636+
637+ sec_ctx.context_type = av_get_media_type_string(ist->par->codec_type);
638+
639+ avtext_print_section_header(tfc, &sec_ctx, SECTION_ID_INPUTSTREAM);
640+ av_freep(&sec_ctx.context_id);
641+ sec_ctx.context_type = NULL;
642+
643+ av_bprint_clear(&buf);
644+
645+ print_fmt("id", "r_in_%d_%d", n, i);
646+
647+ if (codec_desc && codec_desc->name) {
648+ ////av_bprintf(&buf, "%s", upcase_string(char_buf, sizeof(char_buf), codec_desc->long_name));
649+ av_bprintf(&buf, "%s", codec_desc->long_name);
650+ } else if (ist->dec) {
651+ char char_buf[256];
652+ av_bprintf(&buf, "%s", upcase_string(char_buf, sizeof(char_buf), ist->dec->name));
653+ } else if (ist->par->codec_type == AVMEDIA_TYPE_ATTACHMENT) {
654+ av_bprintf(&buf, "%s", "Attachment");
655+ } else if (ist->par->codec_type == AVMEDIA_TYPE_DATA) {
656+ av_bprintf(&buf, "%s", "Data");
657+ }
658+
659+ print_fmt("name", "%s", buf.str);
660+ print_fmt("index", "%d", ist->index);
661+
662+ if (ist->dec)
663+ print_str_opt("media_type", av_get_media_type_string(ist->par->codec_type));
664+
665+ avtext_print_section_footer(tfc); // SECTION_ID_INPUTSTREAM
666+ }
667+
668+ avtext_print_section_footer(tfc); // SECTION_ID_INPUTSTREAMS
669+ avtext_print_section_footer(tfc); // SECTION_ID_INPUTFILE
670+ }
671+
672+ avtext_print_section_footer(tfc); // SECTION_ID_INPUTFILES
673+
674+
675+ print_section_header_id(gpc, SECTION_ID_DECODERS, "Decoders", 0);
676+
677+ for (int n = 0; n < nb_ifiles; n++) {
678+ InputFile *ifi = ifiles[n];
679+
680+ for (int i = 0; i < ifi->nb_streams; i++) {
681+ InputStream *ist = ifi->streams[i];
682+
683+ if (!ist->decoder)
684+ continue;
685+
686+ sec_ctx.context_id = av_asprintf("in_%d_%d", n, i);
687+ sec_ctx.context_type = av_get_media_type_string(ist->par->codec_type);
688+ sec_ctx.context_flags = 2;
689+
690+ avtext_print_section_header(tfc, &sec_ctx, SECTION_ID_DECODER);
691+ av_freep(&sec_ctx.context_id);
692+ sec_ctx.context_type = NULL;
693+ sec_ctx.context_flags = 0;
694+
695+ av_bprint_clear(&buf);
696+
697+ print_fmt("source_id", "r_in_%d_%d", n, i);
698+ print_fmt("id", "in_%d_%d", n, i);
699+
700+ ////av_bprintf(&buf, "%s", upcase_string(char_buf, sizeof(char_buf), ist->dec->name));
701+ print_fmt("name", "%s", ist->dec->name);
702+
703+ print_str_opt("media_type", av_get_media_type_string(ist->par->codec_type));
704+
705+ avtext_print_section_footer(tfc); // SECTION_ID_DECODER
706+ }
707+ }
708+
709+ avtext_print_section_footer(tfc); // SECTION_ID_DECODERS
710+
711+
712+ print_section_header_id(gpc, SECTION_ID_ENCODERS, "Encoders", 0);
713+
714+ for (int n = 0; n < nb_ofiles; n++) {
715+ OutputFile *of = ofiles[n];
716+
717+ for (int i = 0; i < of->nb_streams; i++) {
718+ OutputStream *ost = of->streams[i];
719+ ////const AVCodecDescriptor *codec_desc;
720+
721+ if (!ost || !ost->st || !ost->st->codecpar || !ost->enc)
722+ continue;
723+
724+ ////codec_desc = avcodec_descriptor_get(ost->st->codecpar->codec_id);
725+
726+ sec_ctx.context_id = av_asprintf("out__%d_%d", n, i);
727+ sec_ctx.context_type = av_get_media_type_string(ost->type);
728+ sec_ctx.context_flags = 2;
729+
730+ avtext_print_section_header(tfc, &sec_ctx, SECTION_ID_ENCODER);
731+ av_freep(&sec_ctx.context_id);
732+ sec_ctx.context_type = NULL;
733+ sec_ctx.context_flags = 0;
734+
735+ av_bprint_clear(&buf);
736+
737+ print_fmt("id", "out__%d_%d", n, i);
738+ print_fmt("dest_id", "r_out__%d_%d", n, i);
739+
740+ print_fmt("name", "%s", ost->enc->enc_ctx->av_class->item_name(ost->enc->enc_ctx));
741+
742+ print_str_opt("media_type", av_get_media_type_string(ost->type));
743+
744+ avtext_print_section_footer(tfc); // SECTION_ID_ENCODER
745+ }
746+ }
747+
748+ avtext_print_section_footer(tfc); // SECTION_ID_ENCODERS
749+
750+
751+ print_section_header_id(gpc, SECTION_ID_OUTPUTFILES, "Outputs", 0);
752+
753+ for (int n = nb_ofiles - 1; n >= 0; n--) {
754+ OutputFile *of = ofiles[n];
755+ Muxer *muxer = (Muxer *)of;
756+
757+ if (!muxer->fc)
758+ continue;
759+
760+ sec_ctx.context_id = av_asprintf("Output_%d", n);
761+
762+ avtext_print_section_header(tfc, &sec_ctx, SECTION_ID_OUTPUTFILE);
763+
764+ av_freep(&sec_ctx.context_id);
765+
766+ ////print_str_opt("index", av_get_media_type_string(of->index));
767+ print_fmt("index", "%d", of->index);
768+ ////print_str("url", of->url);
769+ print_str("muxer_name", muxer->fc->oformat->name);
770+ if (of->url) {
771+ char *extension = get_extension(of->url);
772+ if (extension) {
773+ print_str("file_extension", extension);
774+ av_freep(&extension);
775+ }
776+ print_str("url", of->url);
777+ }
778+
779+ sec_ctx.context_id = av_asprintf("OutputStreams_%d", n);
780+
781+ avtext_print_section_header(tfc, &sec_ctx, SECTION_ID_OUTPUTSTREAMS);
782+
783+ for (int i = 0; i < of->nb_streams; i++) {
784+ OutputStream *ost = of->streams[i];
785+ const AVCodecDescriptor *codec_desc = avcodec_descriptor_get(ost->st->codecpar->codec_id);
786+
787+ sec_ctx.context_id = av_asprintf("r_out__%d_%d", n, i);
788+ sec_ctx.context_type = av_get_media_type_string(ost->type);
789+ avtext_print_section_header(tfc, &sec_ctx, SECTION_ID_OUTPUTSTREAM);
790+ av_freep(&sec_ctx.context_id);
791+ sec_ctx.context_type = NULL;
792+
793+ av_bprint_clear(&buf);
794+
795+ print_fmt("id", "r_out__%d_%d", n, i);
796+
797+ if (codec_desc && codec_desc->name) {
798+ av_bprintf(&buf, "%s", codec_desc->long_name);
799+ } else {
800+ av_bprintf(&buf, "%s", "unknown");
801+ }
802+
803+ print_fmt("name", "%s", buf.str);
804+ print_fmt("index", "%d", ost->index);
805+
806+ print_str_opt("media_type", av_get_media_type_string(ost->type));
807+
808+ avtext_print_section_footer(tfc); // SECTION_ID_OUTPUTSTREAM
809+ }
810+
811+ avtext_print_section_footer(tfc); // SECTION_ID_OUTPUTSTREAMS
812+ avtext_print_section_footer(tfc); // SECTION_ID_OUTPUTFILE
813+ }
814+
815+ avtext_print_section_footer(tfc); // SECTION_ID_OUTPUTFILES
816+
817+
818+ avtext_print_section_header(tfc, NULL, SECTION_ID_STREAMLINKS);
819+
820+ for (int n = 0; n < nb_ofiles; n++) {
821+ OutputFile *of = ofiles[n];
822+
823+ for (int i = 0; i < of->nb_streams; i++) {
824+ OutputStream *ost = of->streams[i];
825+
826+ if (ost->ist && !ost->filter) {
827+ sec_ctx.context_type = av_get_media_type_string(ost->type);
828+ avtext_print_section_header(tfc, &sec_ctx, SECTION_ID_STREAMLINK);
829+ sec_ctx.context_type = NULL;
830+
831+ if (ost->enc) {
832+ print_fmt("dest_stream_id", "out__%d_%d", n, i);
833+ print_fmt("source_stream_id", "in_%d_%d", ost->ist->file->index, ost->ist->index);
834+ print_str("operation", "Transcode");
835+ } else {
836+ print_fmt("dest_stream_id", "r_out__%d_%d", n, i);
837+ print_fmt("source_stream_id", "r_in_%d_%d", ost->ist->file->index, ost->ist->index);
838+ print_str("operation", "Stream Copy");
839+ }
840+
841+ print_str_opt("media_type", av_get_media_type_string(ost->type));
842+
843+ avtext_print_section_footer(tfc); // SECTION_ID_STREAMLINK
844+ }
845+ }
846+ }
847+
848+ avtext_print_section_footer(tfc); // SECTION_ID_STREAMLINKS
849+
850+ av_bprint_finalize(&buf, NULL);
851+ return 0;
852+}
853+
854+
855+static void uninit_graphprint(GraphPrintContext *gpc)
856+{
857+ if (gpc->tfc)
858+ avtext_context_close(&gpc->tfc);
859+
860+ if (gpc->wctx)
861+ avtextwriter_context_close(&gpc->wctx);
862+
863+ // Finalize the print buffer if it was initialized
864+ av_bprint_finalize(&gpc->pbuf, NULL);
865+}
866+
867+static int init_graphprint(GraphPrintContext **pgpc, AVBPrint *target_buf)
868+{
869+ const AVTextFormatter *text_formatter;
870+ AVTextFormatContext *tfc = NULL;
871+ AVTextWriterContext *wctx = NULL;
872+ GraphPrintContext *gpc = NULL;
873+ char *w_args = NULL;
874+ char *w_name;
875+ int ret;
876+
877+ init_sections();
878+ *pgpc = NULL;
879+
880+ av_bprint_init(target_buf, 0, AV_BPRINT_SIZE_UNLIMITED);
881+
882+ if (!print_graphs_format)
883+ print_graphs_format = av_strdup("json");
884+ if (!print_graphs_format) {
885+ ret = AVERROR(ENOMEM);
886+ goto fail;
887+ }
888+
889+ w_name = av_strtok(print_graphs_format, "=", &w_args);
890+ if (!w_name) {
891+ av_log(NULL, AV_LOG_ERROR, "No name specified for the filter graph output format\n");
892+ ret = AVERROR(EINVAL);
893+ goto fail;
894+ }
895+
896+ text_formatter = avtext_get_formatter_by_name(w_name);
897+ if (!text_formatter) {
898+ av_log(NULL, AV_LOG_ERROR, "Unknown filter graph output format with name '%s'\n", w_name);
899+ ret = AVERROR(EINVAL);
900+ goto fail;
901+ }
902+
903+ ret = avtextwriter_create_buffer(&wctx, target_buf);
904+ if (ret < 0) {
905+ av_log(NULL, AV_LOG_ERROR, "avtextwriter_create_buffer failed. Error code %d\n", ret);
906+ ret = AVERROR(EINVAL);
907+ goto fail;
908+ }
909+
910+ AVTextFormatOptions tf_options = { .show_optional_fields = -1 };
911+ ret = avtext_context_open(&tfc, text_formatter, wctx, w_args, sections, FF_ARRAY_ELEMS(sections), tf_options, NULL);
912+ if (ret < 0) {
913+ goto fail;
914+ }
915+
916+ gpc = av_mallocz(sizeof(GraphPrintContext));
917+ if (!gpc) {
918+ ret = AVERROR(ENOMEM);
919+ goto fail;
920+ }
921+
922+ gpc->wctx = wctx;
923+ gpc->tfc = tfc;
924+ av_bprint_init(&gpc->pbuf, 0, AV_BPRINT_SIZE_UNLIMITED);
925+
926+ gpc->id_prefix_num = atomic_fetch_add(&prefix_num, 1);
927+ gpc->is_diagram = !!(tfc->formatter->flags & AV_TEXTFORMAT_FLAG_IS_DIAGRAM_FORMATTER);
928+ if (gpc->is_diagram) {
929+ tfc->show_value_unit = 1;
930+ tfc->show_optional_fields = -1;
931+ gpc->opt_flags = AV_TEXTFORMAT_PRINT_STRING_OPTIONAL;
932+ gpc->skip_buffer_filters = 1;
933+ ////} else {
934+ //// gpc->opt_flags = AV_TEXTFORMAT_PRINT_STRING_OPTIONAL;
935+ }
936+
937+ if (!strcmp(text_formatter->name, "mermaid") || !strcmp(text_formatter->name, "mermaidhtml")) {
938+ gpc->diagram_config.diagram_css = ff_resman_get_string(FF_RESOURCE_GRAPH_CSS);
939+
940+ if (!strcmp(text_formatter->name, "mermaidhtml"))
941+ gpc->diagram_config.html_template = ff_resman_get_string(FF_RESOURCE_GRAPH_HTML);
942+
943+ av_diagram_init(tfc, &gpc->diagram_config);
944+ }
945+
946+ *pgpc = gpc;
947+
948+ return 0;
949+
950+fail:
951+ if (tfc)
952+ avtext_context_close(&tfc);
953+ if (wctx && !tfc) // Only free wctx if tfc didn't take ownership of it
954+ avtextwriter_context_close(&wctx);
955+ av_freep(&gpc);
956+
957+ return ret;
958+}
959+
960+
961+int print_filtergraph(FilterGraph *fg, AVFilterGraph *graph)
962+{
963+ GraphPrintContext *gpc = NULL;
964+ AVTextFormatContext *tfc;
965+ FilterGraphPriv *fgp = fgp_from_fg(fg);
966+ AVBPrint *target_buf = &fgp->graph_print_buf;
967+ int ret;
968+
969+ if (!fg || !fgp) {
970+ av_log(NULL, AV_LOG_ERROR, "Invalid filter graph provided\n");
971+ return AVERROR(EINVAL);
972+ }
973+
974+ if (target_buf->len)
975+ av_bprint_finalize(target_buf, NULL);
976+
977+ ret = init_graphprint(&gpc, target_buf);
978+ if (ret)
979+ return ret;
980+
981+ if (!gpc) {
982+ av_log(NULL, AV_LOG_ERROR, "Failed to initialize graph print context\n");
983+ return AVERROR(ENOMEM);
984+ }
985+
986+ tfc = gpc->tfc;
987+
988+ // Due to the threading model each graph needs to print itself into a buffer
989+ // from its own thread. The actual printing happens short before cleanup in ffmpeg.c
990+ // where all graphs are assembled together. To make this work, we need to put the
991+ // formatting context into the same state like it would be when printing all at once,
992+ // so here we print the section headers and clear the buffer to get into the right state.
993+ avtext_print_section_header(tfc, NULL, SECTION_ID_ROOT);
994+ avtext_print_section_header(tfc, NULL, SECTION_ID_FILTERGRAPHS);
995+ avtext_print_section_header(tfc, NULL, SECTION_ID_FILTERGRAPH);
996+
997+ av_bprint_clear(target_buf);
998+
999+ print_filtergraph_single(gpc, fg, graph);
1000+
1001+ if (gpc->is_diagram) {
1002+ avtext_print_section_footer(tfc); // SECTION_ID_FILTERGRAPH
1003+ avtext_print_section_footer(tfc); // SECTION_ID_FILTERGRAPHS
1004+ }
1005+
1006+ uninit_graphprint(gpc);
1007+
1008+ return 0;
1009+}
1010+
1011+static int print_filtergraphs_priv(FilterGraph **graphs, int nb_graphs, InputFile **ifiles, int nb_ifiles, OutputFile **ofiles, int nb_ofiles)
1012+{
1013+ GraphPrintContext *gpc = NULL;
1014+ AVTextFormatContext *tfc;
1015+ AVBPrint target_buf;
1016+ int ret;
1017+
1018+ ret = init_graphprint(&gpc, &target_buf);
1019+ if (ret)
1020+ goto cleanup;
1021+
1022+ if (!gpc) {
1023+ ret = AVERROR(ENOMEM);
1024+ goto cleanup;
1025+ }
1026+
1027+ tfc = gpc->tfc;
1028+
1029+ avtext_print_section_header(tfc, NULL, SECTION_ID_ROOT);
1030+ avtext_print_section_header(tfc, NULL, SECTION_ID_FILTERGRAPHS);
1031+
1032+ for (int i = 0; i < nb_graphs; i++) {
1033+ FilterGraphPriv *fgp = fgp_from_fg(graphs[i]);
1034+ AVBPrint *graph_buf = &fgp->graph_print_buf;
1035+
1036+ if (graph_buf->len > 0) {
1037+ avtext_print_section_header(tfc, NULL, SECTION_ID_FILTERGRAPH);
1038+ av_bprint_append_data(&target_buf, graph_buf->str, graph_buf->len);
1039+ av_bprint_finalize(graph_buf, NULL);
1040+ avtext_print_section_footer(tfc); // SECTION_ID_FILTERGRAPH
1041+ }
1042+ }
1043+
1044+ for (int n = 0; n < nb_ofiles; n++) {
1045+ OutputFile *of = ofiles[n];
1046+
1047+ for (int i = 0; i < of->nb_streams; i++) {
1048+ OutputStream *ost = of->streams[i];
1049+
1050+ if (ost->fg_simple) {
1051+ FilterGraphPriv *fgp = fgp_from_fg(ost->fg_simple);
1052+ AVBPrint *graph_buf = &fgp->graph_print_buf;
1053+
1054+ if (graph_buf->len > 0) {
1055+ avtext_print_section_header(tfc, NULL, SECTION_ID_FILTERGRAPH);
1056+ av_bprint_append_data(&target_buf, graph_buf->str, graph_buf->len);
1057+ av_bprint_finalize(graph_buf, NULL);
1058+ avtext_print_section_footer(tfc); // SECTION_ID_FILTERGRAPH
1059+ }
1060+ }
1061+ }
1062+ }
1063+
1064+ avtext_print_section_footer(tfc); // SECTION_ID_FILTERGRAPHS
1065+
1066+ print_streams(gpc, ifiles, nb_ifiles, ofiles, nb_ofiles);
1067+
1068+ avtext_print_section_footer(tfc); // SECTION_ID_ROOT
1069+
1070+ if (print_graphs_file) {
1071+ AVIOContext *avio = NULL;
1072+
1073+ if (!strcmp(print_graphs_file, "-")) {
1074+ printf("%s", target_buf.str);
1075+ } else {
1076+ ret = avio_open2(&avio, print_graphs_file, AVIO_FLAG_WRITE, NULL, NULL);
1077+ if (ret < 0) {
1078+ av_log(NULL, AV_LOG_ERROR, "Failed to open graph output file, \"%s\": %s\n", print_graphs_file, av_err2str(ret));
1079+ goto cleanup;
1080+ }
1081+
1082+ avio_write(avio, (const unsigned char *)target_buf.str, FFMIN(target_buf.len, target_buf.size - 1));
1083+ avio_flush(avio);
1084+
1085+ if ((ret = avio_closep(&avio)) < 0)
1086+ av_log(NULL, AV_LOG_ERROR, "Error closing graph output file, loss of information possible: %s\n", av_err2str(ret));
1087+ }
1088+ }
1089+
1090+ if (print_graphs)
1091+ av_log(NULL, AV_LOG_INFO, "%s %c", target_buf.str, '\n');
1092+
1093+cleanup:
1094+ // Properly clean up resources
1095+ if (gpc)
1096+ uninit_graphprint(gpc);
1097+
1098+ // Ensure the target buffer is properly finalized
1099+ av_bprint_finalize(&target_buf, NULL);
1100+
1101+ return ret;
1102+}
1103+
1104+int print_filtergraphs(FilterGraph **graphs, int nb_graphs, InputFile **ifiles, int nb_ifiles, OutputFile **ofiles, int nb_ofiles)
1105+{
1106+ return print_filtergraphs_priv(graphs, nb_graphs, ifiles, nb_ifiles, ofiles, nb_ofiles);
1107+}
fftools/graph/graphprint.h+30−0
@@ -0,0 +1,30 @@
1+/*
2+ * Copyright (c) 2018-2025 - softworkz
3+ *
4+ * This file is part of FFmpeg.
5+ *
6+ * FFmpeg is free software; you can redistribute it and/or
7+ * modify it under the terms of the GNU Lesser General Public
8+ * License as published by the Free Software Foundation; either
9+ * version 2.1 of the License, or (at your option) any later version.
10+ *
11+ * FFmpeg is distributed in the hope that it will be useful,
12+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+ * Lesser General Public License for more details.
15+ *
16+ * You should have received a copy of the GNU Lesser General Public
17+ * License along with FFmpeg; if not, write to the Free Software
18+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19+ */
20+
21+#ifndef FFTOOLS_GRAPH_GRAPHPRINT_H
22+#define FFTOOLS_GRAPH_GRAPHPRINT_H
23+
24+#include "fftools/ffmpeg.h"
25+
26+int print_filtergraphs(FilterGraph **graphs, int nb_graphs, InputFile **ifiles, int nb_ifiles, OutputFile **ofiles, int nb_ofiles);
27+
28+int print_filtergraph(FilterGraph *fg, AVFilterGraph *graph);
29+
30+#endif /* FFTOOLS_GRAPH_GRAPHPRINT_H */
fftools/textformat/avtextformat.c+3−1
@@ -677,7 +677,7 @@ fail: · 676 unmodified lines
677677 return ret;
678678 }
679679
680−static const AVTextFormatter *registered_formatters[7 + 1];
680+static const AVTextFormatter *registered_formatters[9 + 1];
681681
682682 static void formatters_register_all(void)
683683 {
@@ -694,6 +694,8 @@ static void formatters_register_all(void) · 10 unmodified lines
694694 registered_formatters[4] = &avtextformatter_ini;
695695 registered_formatters[5] = &avtextformatter_json;
696696 registered_formatters[6] = &avtextformatter_xml;
697+ registered_formatters[7] = &avtextformatter_mermaid;
698+ registered_formatters[8] = &avtextformatter_mermaidhtml;
697699 }
698700
699701 const AVTextFormatter *avtext_get_formatter_by_name(const char *name)
fftools/textformat/avtextformat.h+29−0
@@ -31,6 +31,12 @@ · 30 unmodified lines
3131
3232 #define SECTION_MAX_NB_CHILDREN 11
3333
34+typedef struct AVTextFormatSectionContext {
35+ char *context_id;
36+ const char *context_type;
37+ int context_flags;
38+} AVTextFormatSectionContext;
39+
3440
3541 typedef struct AVTextFormatSection {
3642 int id; ///< unique id identifying a section
@@ -42,6 +48,10 @@ typedef struct AVTextFormatSection { · 5 unmodified lines
4248 /// For these sections the element_name field is mandatory.
4349 #define AV_TEXTFORMAT_SECTION_FLAG_HAS_TYPE 8 ///< the section contains a type to distinguish multiple nested elements
4450 #define AV_TEXTFORMAT_SECTION_FLAG_NUMBERING_BY_TYPE 16 ///< the items in this array section should be numbered individually by type
51+#define AV_TEXTFORMAT_SECTION_FLAG_IS_SHAPE 32 ///< ...
52+#define AV_TEXTFORMAT_SECTION_FLAG_HAS_LINKS 64 ///< ...
53+#define AV_TEXTFORMAT_SECTION_PRINT_TAGS 128 ///< ...
54+#define AV_TEXTFORMAT_SECTION_FLAG_IS_SUBGRAPH 256 ///< ...
4555
4656 int flags;
4757 const int children_ids[SECTION_MAX_NB_CHILDREN + 1]; ///< list of children section IDS, terminated by -1
@@ -50,12 +60,17 @@ typedef struct AVTextFormatSection { · 2 unmodified lines
5060 AVDictionary *entries_to_show;
5161 const char *(*get_type)(const void *data); ///< function returning a type if defined, must be defined when SECTION_FLAG_HAS_TYPE is defined
5262 int show_all_entries;
63+ const char *id_key; ///< name of the key to be used as the id
64+ const char *src_id_key; ///< name of the key to be used as the source id for diagram connections
65+ const char *dest_id_key; ///< name of the key to be used as the target id for diagram connections
66+ const char *linktype_key; ///< name of the key to be used as the link type for diagram connections (AVTextFormatLinkType)
5367 } AVTextFormatSection;
5468
5569 typedef struct AVTextFormatContext AVTextFormatContext;
5670
5771 #define AV_TEXTFORMAT_FLAG_SUPPORTS_OPTIONAL_FIELDS 1
5872 #define AV_TEXTFORMAT_FLAG_SUPPORTS_MIXED_ARRAY_CONTENT 2
73+#define AV_TEXTFORMAT_FLAG_IS_DIAGRAM_FORMATTER 4
5974
6075 typedef enum {
6176 AV_TEXTFORMAT_STRING_VALIDATION_FAIL,
@@ -64,6 +79,18 @@ typedef enum { · 2 unmodified lines
6479 AV_TEXTFORMAT_STRING_VALIDATION_NB
6580 } StringValidation;
6681
82+typedef enum {
83+ AV_TEXTFORMAT_LINKTYPE_SRCDEST,
84+ AV_TEXTFORMAT_LINKTYPE_DESTSRC,
85+ AV_TEXTFORMAT_LINKTYPE_BIDIR,
86+ AV_TEXTFORMAT_LINKTYPE_NONDIR,
87+ AV_TEXTFORMAT_LINKTYPE_HIDDEN,
88+ AV_TEXTFORMAT_LINKTYPE_ONETOMANY = AV_TEXTFORMAT_LINKTYPE_SRCDEST,
89+ AV_TEXTFORMAT_LINKTYPE_MANYTOONE = AV_TEXTFORMAT_LINKTYPE_DESTSRC,
90+ AV_TEXTFORMAT_LINKTYPE_ONETOONE = AV_TEXTFORMAT_LINKTYPE_BIDIR,
91+ AV_TEXTFORMAT_LINKTYPE_MANYTOMANY = AV_TEXTFORMAT_LINKTYPE_NONDIR,
92+} AVTextFormatLinkType;
93+
6794 typedef struct AVTextFormatter {
6895 const AVClass *priv_class; ///< private class of the formatter, if any
6996 int priv_size; ///< private size for the formatter context
@@ -166,5 +193,7 @@ extern const AVTextFormatter avtextformatter_flat; · 96 unmodified lines
166193 extern const AVTextFormatter avtextformatter_ini;
167194 extern const AVTextFormatter avtextformatter_json;
168195 extern const AVTextFormatter avtextformatter_xml;
196+extern const AVTextFormatter avtextformatter_mermaid;
197+extern const AVTextFormatter avtextformatter_mermaidhtml;
169198
170199 #endif /* FFTOOLS_TEXTFORMAT_AVTEXTFORMAT_H */
fftools/textformat/tf_mermaid.c+658−0
@@ -0,0 +1,658 @@
1+/*
2+ * Copyright (c) The FFmpeg developers
3+ *
4+ * This file is part of FFmpeg.
5+ *
6+ * FFmpeg is free software; you can redistribute it and/or
7+ * modify it under the terms of the GNU Lesser General Public
8+ * License as published by the Free Software Foundation; either
9+ * version 2.1 of the License, or (at your option) any later version.
10+ *
11+ * FFmpeg is distributed in the hope that it will be useful,
12+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+ * Lesser General Public License for more details.
15+ *
16+ * You should have received a copy of the GNU Lesser General Public
17+ * License along with FFmpeg; if not, write to the Free Software
18+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19+ */
20+
21+#include <limits.h>
22+#include <stdarg.h>
23+#include <stdint.h>
24+#include <stdio.h>
25+#include <string.h>
26+
27+#include "avtextformat.h"
28+#include "tf_internal.h"
29+#include "tf_mermaid.h"
30+#include <libavutil/mem.h>
31+#include <libavutil/avassert.h>
32+#include <libavutil/bprint.h>
33+#include <libavutil/opt.h>
34+
35+
36+static const char *init_directive = ""
37+ "%%{init: {"
38+ "\"theme\": \"base\","
39+ "\"curve\": \"monotoneX\","
40+ "\"rankSpacing\": 10,"
41+ "\"nodeSpacing\": 10,"
42+ "\"themeCSS\": \"__###__\","
43+ "\"fontFamily\": \"Roboto,Segoe UI,sans-serif\","
44+ "\"themeVariables\": { "
45+ "\"clusterBkg\": \"white\", "
46+ "\"primaryBorderColor\": \"gray\", "
47+ "\"lineColor\": \"gray\", "
48+ "\"secondaryTextColor\": \"gray\", "
49+ "\"tertiaryBorderColor\": \"gray\", "
50+ "\"primaryTextColor\": \"#666\", "
51+ "\"secondaryTextColor\": \"red\" "
52+ "},"
53+ "\"flowchart\": { "
54+ "\"subGraphTitleMargin\": { \"top\": -15, \"bottom\": 20 }, "
55+ "\"diagramPadding\": 20, "
56+ "\"curve\": \"monotoneX\" "
57+ "}"
58+ " }}%%\n\n";
59+
60+static const char* init_directive_er = ""
61+ "%%{init: {"
62+ "\"theme\": \"base\","
63+ "\"layout\": \"elk\","
64+ "\"curve\": \"monotoneX\","
65+ "\"rankSpacing\": 65,"
66+ "\"nodeSpacing\": 60,"
67+ "\"themeCSS\": \"__###__\","
68+ "\"fontFamily\": \"Roboto,Segoe UI,sans-serif\","
69+ "\"themeVariables\": { "
70+ "\"clusterBkg\": \"white\", "
71+ "\"primaryBorderColor\": \"gray\", "
72+ "\"lineColor\": \"gray\", "
73+ "\"secondaryTextColor\": \"gray\", "
74+ "\"tertiaryBorderColor\": \"gray\", "
75+ "\"primaryTextColor\": \"#666\", "
76+ "\"secondaryTextColor\": \"red\" "
77+ "},"
78+ "\"er\": { "
79+ "\"diagramPadding\": 12, "
80+ "\"entityPadding\": 4, "
81+ "\"minEntityWidth\": 150, "
82+ "\"minEntityHeight\": 20, "
83+ "\"curve\": \"monotoneX\" "
84+ "}"
85+ " }}%%\n\n";
86+
87+static const char *theme_css_er = ""
88+
89+ // Variables
90+ ".root { "
91+ "--ff-colvideo: #6eaa7b; "
92+ "--ff-colaudio: #477fb3; "
93+ "--ff-colsubtitle: #ad76ab; "
94+ "--ff-coltext: #666; "
95+ "} "
96+ " g.nodes g.node.default rect.basic.label-container, "
97+ " g.nodes g.node.default path { "
98+ " rx: 1; "
99+ " ry: 1; "
100+ " stroke-width: 1px !important; "
101+ " stroke: #e9e9e9 !important; "
102+ " fill: url(#ff-filtergradient) !important; "
103+ " filter: drop-shadow(0px 0px 5.5px rgba(0, 0, 0, 0.05)); "
104+ " fill: white !important; "
105+ " } "
106+ " "
107+ " .relationshipLine { "
108+ " stroke: gray; "
109+ " stroke-width: 1; "
110+ " fill: none; "
111+ " filter: drop-shadow(0px 0px 3px rgba(0, 0, 0, 0.2)); "
112+ " } "
113+ " "
114+ " g.node.default g.label.name foreignObject > div > span > p, "
115+ " g.nodes g.node.default g.label:not(.attribute-name, .attribute-keys, .attribute-type, .attribute-comment) foreignObject > div > span > p { "
116+ " font-size: 0.95rem; "
117+ " font-weight: 500; "
118+ " text-transform: uppercase; "
119+ " min-width: 5.5rem; "
120+ " margin-bottom: 0.5rem; "
121+ " "
122+ " } "
123+ " "
124+ " .edgePaths path { "
125+ " marker-end: none; "
126+ " marker-start: none; "
127+ " "
128+ "} ";
129+
130+
131+/* Mermaid Graph output */
132+
133+typedef struct MermaidContext {
134+ const AVClass *class;
135+ AVDiagramConfig *diagram_config;
136+ int subgraph_count;
137+ int within_tag;
138+ int indent_level;
139+ int create_html;
140+
141+ // Options
142+ int enable_link_colors; // Requires Mermaid 11.5
143+
144+ struct section_data {
145+ const char *section_id;
146+ const char *section_type;
147+ const char *src_id;
148+ const char *dest_id;
149+ AVTextFormatLinkType link_type;
150+ int current_is_textblock;
151+ int current_is_stadium;
152+ int subgraph_start_incomplete;
153+ } section_data[SECTION_MAX_NB_LEVELS];
154+
155+ unsigned nb_link_captions[SECTION_MAX_NB_LEVELS]; ///< generic print buffer dedicated to each section,
156+ AVBPrint section_pbuf[SECTION_MAX_NB_LEVELS]; ///< generic print buffer dedicated to each section,
157+ AVBPrint link_buf; ///< print buffer for writing diagram links
158+ AVDictionary *link_dict;
159+} MermaidContext;
160+
161+#undef OFFSET
162+#define OFFSET(x) offsetof(MermaidContext, x)
163+
164+static const AVOption mermaid_options[] = {
165+ { "link_coloring", "enable colored links (requires Mermaid >= 11.5)", OFFSET(enable_link_colors), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1 },
166+ ////{"diagram_css", "CSS for the diagram", OFFSET(diagram_css), AV_OPT_TYPE_STRING, {.i64=0}, 0, 1 },
167+ ////{"html_template", "Template HTML", OFFSET(html_template), AV_OPT_TYPE_STRING, {.i64=0}, 0, 1 },
168+ { NULL },
169+};
170+
171+DEFINE_FORMATTER_CLASS(mermaid);
172+
173+void av_diagram_init(AVTextFormatContext *tfc, AVDiagramConfig *diagram_config)
174+{
175+ MermaidContext *mmc = tfc->priv;
176+ mmc->diagram_config = diagram_config;
177+}
178+
179+static av_cold int has_link_pair(const AVTextFormatContext *tfc, const char *src, const char *dest)
180+{
181+ MermaidContext *mmc = tfc->priv;
182+ AVBPrint buf;
183+
184+ av_bprint_init(&buf, 0, AV_BPRINT_SIZE_UNLIMITED);
185+ av_bprintf(&buf, "%s--%s", src, dest);
186+
187+ if (mmc->link_dict && av_dict_get(mmc->link_dict, buf.str, NULL, 0))
188+ return 1;
189+
190+ av_dict_set(&mmc->link_dict, buf.str, buf.str, 0);
191+
192+ return 0;
193+}
194+
195+static av_cold int mermaid_init(AVTextFormatContext *tfc)
196+{
197+ MermaidContext *mmc = tfc->priv;
198+
199+ av_bprint_init(&mmc->link_buf, 0, AV_BPRINT_SIZE_UNLIMITED);
200+
201+ ////mmc->enable_link_colors = 1; // Requires Mermaid 11.5
202+ return 0;
203+}
204+
205+static av_cold int mermaid_init_html(AVTextFormatContext *tfc)
206+{
207+ MermaidContext *mmc = tfc->priv;
208+
209+ int ret = mermaid_init(tfc);
210+
211+ if (ret < 0)
212+ return ret;
213+
214+ mmc->create_html = 1;
215+
216+ return 0;
217+}
218+
219+#define MM_INDENT() writer_printf(tfc, "%*c", mmc->indent_level * 2, ' ')
220+
221+static void mermaid_print_section_header(AVTextFormatContext *tfc, const void *data)
222+{
223+ const AVTextFormatSection *section = tf_get_section(tfc, tfc->level);
224+ const AVTextFormatSection *parent_section = tf_get_parent_section(tfc, tfc->level);
225+
226+ if (!section)
227+ return;
228+ AVBPrint *buf = &tfc->section_pbuf[tfc->level];
229+ MermaidContext *mmc = tfc->priv;
230+ const AVTextFormatSectionContext *sec_ctx = data;
231+
232+ if (tfc->level == 0) {
233+ char *directive;
234+ AVBPrint css_buf;
235+ const char *diag_directive = mmc->diagram_config->diagram_type == AV_DIAGRAMTYPE_ENTITYRELATIONSHIP ? init_directive_er : init_directive;
236+ char *single_line_css = av_strireplace(mmc->diagram_config->diagram_css, "\n", " ");
237+ (void)theme_css_er;
238+ ////char *single_line_css = av_strireplace(theme_css_er, "\n", " ");
239+ av_bprint_init(&css_buf, 0, AV_BPRINT_SIZE_UNLIMITED);
240+ av_bprint_escape(&css_buf, single_line_css, "'\\", AV_ESCAPE_MODE_BACKSLASH, AV_ESCAPE_FLAG_STRICT);
241+ av_freep(&single_line_css);
242+
243+ directive = av_strireplace(diag_directive, "__###__", css_buf.str);
244+ if (mmc->create_html) {
245+ uint64_t length;
246+ char *token_pos = av_stristr(mmc->diagram_config->html_template, "__###__");
247+ if (!token_pos) {
248+ av_log(tfc, AV_LOG_ERROR, "Unable to locate the required token (__###__) in the html template.");
249+ return;
250+ }
251+
252+ length = token_pos - mmc->diagram_config->html_template;
253+ for (uint64_t i = 0; i < length; i++)
254+ writer_w8(tfc, mmc->diagram_config->html_template[i]);
255+ }
256+
257+ writer_put_str(tfc, directive);
258+ switch (mmc->diagram_config->diagram_type) {
259+ case AV_DIAGRAMTYPE_GRAPH:
260+ writer_put_str(tfc, "flowchart LR\n");
261+ ////writer_put_str(tfc, " gradient_def@{ shape: text, label: \"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1\" height=\"1\"><defs><linearGradient id=\"ff-filtergradient\" x1=\"0%\" y1=\"0%\" x2=\"0%\" y2=\"100%\"><stop offset=\"0%\" style=\"stop-color:hsla(0, 0%, 30%, 0.02);\"/><stop offset=\"50%\" style=\"stop-color:hsla(0, 0%, 30%, 0);\"/><stop offset=\"100%\" style=\"stop-color:hsla(0, 0%, 30%, 0.05);\"/></linearGradient></defs></svg>\" }\n");
262+ writer_put_str(tfc, " gradient_def@{ shape: text, label: \"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1\" height=\"1\"><defs><linearGradient id=\"ff-filtergradient\" x1=\"0%\" y1=\"0%\" x2=\"0%\" y2=\"100%\"><stop offset=\"0%\" style=\"stop-color:hsl(0, 0%, 98.6%); \"/><stop offset=\"50%\" style=\"stop-color:hsl(0, 0%, 100%); \"/><stop offset=\"100%\" style=\"stop-color:hsl(0, 0%, 96.5%); \"/></linearGradient><radialGradient id=\"ff-radgradient\" cx=\"50%\" cy=\"50%\" r=\"100%\" fx=\"45%\" fy=\"40%\"><stop offset=\"25%\" stop-color=\"hsl(0, 0%, 100%)\" /><stop offset=\"100%\" stop-color=\"hsl(0, 0%, 96%)\" /></radialGradient></defs></svg>\" }\n");
263+ break;
264+ case AV_DIAGRAMTYPE_ENTITYRELATIONSHIP:
265+ writer_put_str(tfc, "erDiagram\n");
266+ break;
267+ }
268+
269+ return;
270+ }
271+
272+ if (parent_section && parent_section->flags & AV_TEXTFORMAT_SECTION_FLAG_IS_SUBGRAPH) {
273+
274+ struct section_data parent_sec_data = mmc->section_data[tfc->level - 1];
275+ AVBPrint *parent_buf = &tfc->section_pbuf[tfc->level - 1];
276+
277+ if (parent_sec_data.subgraph_start_incomplete) {
278+
279+ if (parent_buf->len > 0)
280+ writer_printf(tfc, "%s", parent_buf->str);
281+
282+ writer_put_str(tfc, "</div>\"]\n");
283+
284+ mmc->section_data[tfc->level - 1].subgraph_start_incomplete = 0;
285+ }
286+ }
287+
288+ av_freep(&mmc->section_data[tfc->level].section_id);
289+ av_freep(&mmc->section_data[tfc->level].section_type);
290+ av_freep(&mmc->section_data[tfc->level].src_id);
291+ av_freep(&mmc->section_data[tfc->level].dest_id);
292+ mmc->section_data[tfc->level].current_is_textblock = 0;
293+ mmc->section_data[tfc->level].current_is_stadium = 0;
294+ mmc->section_data[tfc->level].subgraph_start_incomplete = 0;
295+ mmc->section_data[tfc->level].link_type = AV_TEXTFORMAT_LINKTYPE_SRCDEST;
296+
297+ // NOTE: av_strdup() allocations aren't checked
298+ if (section->flags & AV_TEXTFORMAT_SECTION_FLAG_IS_SUBGRAPH) {
299+
300+ av_bprint_clear(buf);
301+ writer_put_str(tfc, "\n");
302+
303+ mmc->indent_level++;
304+
305+ if (sec_ctx->context_id) {
306+ MM_INDENT();
307+ writer_printf(tfc, "subgraph %s[\"<div class=\"ff-%s\">", sec_ctx->context_id, section->name);
308+ } else {
309+ av_log(tfc, AV_LOG_ERROR, "Unable to write subgraph start. Missing id field. Section: %s", section->name);
310+ }
311+
312+ mmc->section_data[tfc->level].subgraph_start_incomplete = 1;
313+ mmc->section_data[tfc->level].section_id = av_strdup(sec_ctx->context_id);
314+ }
315+
316+ if (section->flags & AV_TEXTFORMAT_SECTION_FLAG_IS_SHAPE) {
317+
318+ av_bprint_clear(buf);
319+ writer_put_str(tfc, "\n");
320+
321+ mmc->indent_level++;
322+
323+ if (sec_ctx->context_id) {
324+
325+ mmc->section_data[tfc->level].section_id = av_strdup(sec_ctx->context_id);
326+
327+ switch (mmc->diagram_config->diagram_type) {
328+ case AV_DIAGRAMTYPE_GRAPH:
329+ if (sec_ctx->context_flags & 1) {
330+
331+ MM_INDENT();
332+ writer_printf(tfc, "%s@{ shape: text, label: \"", sec_ctx->context_id);
333+ mmc->section_data[tfc->level].current_is_textblock = 1;
334+ } else if (sec_ctx->context_flags & 2) {
335+
336+ MM_INDENT();
337+ writer_printf(tfc, "%s([\"", sec_ctx->context_id);
338+ mmc->section_data[tfc->level].current_is_stadium = 1;
339+ } else {
340+ MM_INDENT();
341+ writer_printf(tfc, "%s(\"", sec_ctx->context_id);
342+ }
343+
344+ break;
345+ case AV_DIAGRAMTYPE_ENTITYRELATIONSHIP:
346+ MM_INDENT();
347+ writer_printf(tfc, "%s {\n", sec_ctx->context_id);
348+ break;
349+ }
350+
351+ } else {
352+ av_log(tfc, AV_LOG_ERROR, "Unable to write shape start. Missing id field. Section: %s", section->name);
353+ }
354+
355+ mmc->section_data[tfc->level].section_id = av_strdup(sec_ctx->context_id);
356+ }
357+
358+
359+ if (section->flags & AV_TEXTFORMAT_SECTION_PRINT_TAGS) {
360+
361+ if (sec_ctx && sec_ctx->context_type)
362+ writer_printf(tfc, "<div class=\"ff-%s %s\">", section->name, sec_ctx->context_type);
363+ else
364+ writer_printf(tfc, "<div class=\"ff-%s\">", section->name);
365+ }
366+
367+
368+ if (section->flags & AV_TEXTFORMAT_SECTION_FLAG_HAS_LINKS) {
369+
370+ av_bprint_clear(buf);
371+ mmc->nb_link_captions[tfc->level] = 0;
372+
373+ if (sec_ctx && sec_ctx->context_type)
374+ mmc->section_data[tfc->level].section_type = av_strdup(sec_ctx->context_type);
375+
376+ ////if (section->flags & AV_TEXTFORMAT_SECTION_FLAG_HAS_TYPE) {
377+ //// AVBPrint buf;
378+ //// av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
379+ //// av_bprint_escape(&buf, section->get_type(data), NULL,
380+ //// AV_ESCAPE_MODE_XML, AV_ESCAPE_FLAG_XML_DOUBLE_QUOTES);
381+ //// writer_printf(tfc, " type=\"%s\"", buf.str);
382+ }
383+}
384+
385+static void mermaid_print_section_footer(AVTextFormatContext *tfc)
386+{
387+ MermaidContext *mmc = tfc->priv;
388+ const AVTextFormatSection *section = tf_get_section(tfc, tfc->level);
389+
390+ if (!section)
391+ return;
392+ AVBPrint *buf = &tfc->section_pbuf[tfc->level];
393+ struct section_data sec_data = mmc->section_data[tfc->level];
394+
395+ if (section->flags & AV_TEXTFORMAT_SECTION_PRINT_TAGS)
396+ writer_put_str(tfc, "</div>");
397+
398+ if (section->flags & AV_TEXTFORMAT_SECTION_FLAG_IS_SHAPE) {
399+
400+ switch (mmc->diagram_config->diagram_type) {
401+ case AV_DIAGRAMTYPE_GRAPH:
402+
403+ if (sec_data.current_is_textblock) {
404+ writer_printf(tfc, "\"}\n", section->name);
405+
406+ if (sec_data.section_id) {
407+ MM_INDENT();
408+ writer_put_str(tfc, "class ");
409+ writer_put_str(tfc, sec_data.section_id);
410+ writer_put_str(tfc, " ff-");
411+ writer_put_str(tfc, section->name);
412+ writer_put_str(tfc, "\n");
413+ }
414+ } else if (sec_data.current_is_stadium) {
415+ writer_printf(tfc, "\"]):::ff-%s\n", section->name);
416+ } else {
417+ writer_printf(tfc, "\"):::ff-%s\n", section->name);
418+ }
419+
420+ break;
421+ case AV_DIAGRAMTYPE_ENTITYRELATIONSHIP:
422+ MM_INDENT();
423+ writer_put_str(tfc, "}\n\n");
424+ break;
425+ }
426+
427+ mmc->indent_level--;
428+
429+ } else if ((section->flags & AV_TEXTFORMAT_SECTION_FLAG_IS_SUBGRAPH)) {
430+
431+ MM_INDENT();
432+ writer_put_str(tfc, "end\n");
433+
434+ if (sec_data.section_id) {
435+ MM_INDENT();
436+ writer_put_str(tfc, "class ");
437+ writer_put_str(tfc, sec_data.section_id);
438+ writer_put_str(tfc, " ff-");
439+ writer_put_str(tfc, section->name);
440+ writer_put_str(tfc, "\n");
441+ }
442+
443+ mmc->indent_level--;
444+ }
445+
446+ if ((section->flags & AV_TEXTFORMAT_SECTION_FLAG_HAS_LINKS))
447+ if (sec_data.src_id && sec_data.dest_id
448+ && !has_link_pair(tfc, sec_data.src_id, sec_data.dest_id))
449+ switch (mmc->diagram_config->diagram_type) {
450+ case AV_DIAGRAMTYPE_GRAPH:
451+
452+ if (sec_data.section_type && mmc->enable_link_colors)
453+ av_bprintf(&mmc->link_buf, "\n %s %s-%s-%s@==", sec_data.src_id, sec_data.section_type, sec_data.src_id, sec_data.dest_id);
454+ else
455+ av_bprintf(&mmc->link_buf, "\n %s ==", sec_data.src_id);
456+
457+ if (buf->len > 0) {
458+ av_bprintf(&mmc->link_buf, " \"%s", buf->str);
459+
460+ for (unsigned i = 0; i < mmc->nb_link_captions[tfc->level]; i++)
461+ av_bprintf(&mmc->link_buf, "<br>&nbsp;");
462+
463+ av_bprintf(&mmc->link_buf, "\" ==");
464+ }
465+
466+ av_bprintf(&mmc->link_buf, "> %s", sec_data.dest_id);
467+
468+ break;
469+ case AV_DIAGRAMTYPE_ENTITYRELATIONSHIP:
470+
471+
472+ av_bprintf(&mmc->link_buf, "\n %s", sec_data.src_id);
473+
474+ switch (sec_data.link_type) {
475+ case AV_TEXTFORMAT_LINKTYPE_ONETOMANY:
476+ av_bprintf(&mmc->link_buf, "%s", " ||--o{ ");
477+ break;
478+ case AV_TEXTFORMAT_LINKTYPE_MANYTOONE:
479+ av_bprintf(&mmc->link_buf, "%s", " }o--|| ");
480+ break;
481+ case AV_TEXTFORMAT_LINKTYPE_ONETOONE:
482+ av_bprintf(&mmc->link_buf, "%s", " ||--|| ");
483+ break;
484+ case AV_TEXTFORMAT_LINKTYPE_MANYTOMANY:
485+ av_bprintf(&mmc->link_buf, "%s", " }o--o{ ");
486+ break;
487+ default:
488+ av_bprintf(&mmc->link_buf, "%s", " ||--|| ");
489+ break;
490+ }
491+
492+ av_bprintf(&mmc->link_buf, "%s : \"\"", sec_data.dest_id);
493+
494+ break;
495+ }
496+
497+ if (tfc->level == 0) {
498+
499+ writer_put_str(tfc, "\n");
500+ if (mmc->create_html) {
501+ char *token_pos = av_stristr(mmc->diagram_config->html_template, "__###__");
502+ if (!token_pos) {
503+ av_log(tfc, AV_LOG_ERROR, "Unable to locate the required token (__###__) in the html template.");
504+ return;
505+ }
506+ token_pos += strlen("__###__");
507+ writer_put_str(tfc, token_pos);
508+ }
509+ }
510+
511+ if (tfc->level == 1) {
512+
513+ if (mmc->link_buf.len > 0) {
514+ writer_put_str(tfc, mmc->link_buf.str);
515+ av_bprint_clear(&mmc->link_buf);
516+ }
517+
518+ writer_put_str(tfc, "\n");
519+ }
520+}
521+
522+static void mermaid_print_value(AVTextFormatContext *tfc, const char *key,
523+ const char *str, int64_t num, const int is_int)
524+{
525+ MermaidContext *mmc = tfc->priv;
526+ const AVTextFormatSection *section = tf_get_section(tfc, tfc->level);
527+
528+ if (!section)
529+ return;
530+
531+ AVBPrint *buf = &tfc->section_pbuf[tfc->level];
532+ struct section_data sec_data = mmc->section_data[tfc->level];
533+ int exit = 0;
534+
535+ if (section->id_key && !strcmp(section->id_key, key)) {
536+ mmc->section_data[tfc->level].section_id = av_strdup(str);
537+ exit = 1;
538+ }
539+
540+ if (section->dest_id_key && !strcmp(section->dest_id_key, key)) {
541+ mmc->section_data[tfc->level].dest_id = av_strdup(str);
542+ exit = 1;
543+ }
544+
545+ if (section->src_id_key && !strcmp(section->src_id_key, key)) {
546+ mmc->section_data[tfc->level].src_id = av_strdup(str);
547+ exit = 1;
548+ }
549+
550+ if (section->linktype_key && !strcmp(section->linktype_key, key)) {
551+ mmc->section_data[tfc->level].link_type = (AVTextFormatLinkType)num;;
552+ exit = 1;
553+ }
554+
555+ //if (exit)
556+ // return;
557+
558+ if ((section->flags & (AV_TEXTFORMAT_SECTION_FLAG_IS_SHAPE | AV_TEXTFORMAT_SECTION_PRINT_TAGS))
559+ || (section->flags & AV_TEXTFORMAT_SECTION_FLAG_IS_SUBGRAPH && sec_data.subgraph_start_incomplete)) {
560+
561+ if (exit)
562+ return;
563+
564+ switch (mmc->diagram_config->diagram_type) {
565+ case AV_DIAGRAMTYPE_GRAPH:
566+
567+ if (is_int) {
568+ writer_printf(tfc, "<span class=\"%s\">%s: %"PRId64"</span>", key, key, num);
569+ } else {
570+ ////AVBPrint b;
571+ ////av_bprint_init(&b, 0, AV_BPRINT_SIZE_UNLIMITED);
572+ const char *tmp = av_strireplace(str, "\"", "'");
573+ ////av_bprint_escape(&b, str, NULL, AV_ESCAPE_MODE_AUTO, AV_ESCAPE_FLAG_STRICT);
574+ writer_printf(tfc, "<span class=\"%s\">%s</span>", key, tmp);
575+ av_freep(&tmp);
576+ }
577+
578+ break;
579+ case AV_DIAGRAMTYPE_ENTITYRELATIONSHIP:
580+
581+ if (!is_int && str)
582+ {
583+ const char *col_type;
584+
585+ if (key[0] == '_')
586+ return;
587+
588+ if (sec_data.section_id && !strcmp(str, sec_data.section_id))
589+ col_type = "PK";
590+ else if (sec_data.dest_id && !strcmp(str, sec_data.dest_id))
591+ col_type = "FK";
592+ else if (sec_data.src_id && !strcmp(str, sec_data.src_id))
593+ col_type = "FK";
594+ else
595+ col_type = "";
596+
597+ MM_INDENT();
598+
599+ if (is_int)
600+ writer_printf(tfc, " %s %"PRId64" %s\n", key, num, col_type);
601+ else
602+ writer_printf(tfc, " %s %s %s\n", key, str, col_type);
603+ }
604+ break;
605+ }
606+
607+ } else if (section->flags & AV_TEXTFORMAT_SECTION_FLAG_HAS_LINKS) {
608+
609+ if (exit)
610+ return;
611+
612+ if (buf->len > 0)
613+ av_bprintf(buf, "%s", "<br>");
614+
615+ av_bprintf(buf, "");
616+ if (is_int)
617+ av_bprintf(buf, "<span>%s: %"PRId64"</span>", key, num);
618+ else
619+ av_bprintf(buf, "<span>%s</span>", str);
620+
621+ mmc->nb_link_captions[tfc->level]++;
622+ }
623+}
624+
625+static inline void mermaid_print_str(AVTextFormatContext *tfc, const char *key, const char *value)
626+{
627+ mermaid_print_value(tfc, key, value, 0, 0);
628+}
629+
630+static void mermaid_print_int(AVTextFormatContext *tfc, const char *key, int64_t value)
631+{
632+ mermaid_print_value(tfc, key, NULL, value, 1);
633+}
634+
635+const AVTextFormatter avtextformatter_mermaid = {
636+ .name = "mermaid",
637+ .priv_size = sizeof(MermaidContext),
638+ .init = mermaid_init,
639+ .print_section_header = mermaid_print_section_header,
640+ .print_section_footer = mermaid_print_section_footer,
641+ .print_integer = mermaid_print_int,
642+ .print_string = mermaid_print_str,
643+ .flags = AV_TEXTFORMAT_FLAG_IS_DIAGRAM_FORMATTER,
644+ .priv_class = &mermaid_class,
645+};
646+
647+
648+const AVTextFormatter avtextformatter_mermaidhtml = {
649+ .name = "mermaidhtml",
650+ .priv_size = sizeof(MermaidContext),
651+ .init = mermaid_init_html,
652+ .print_section_header = mermaid_print_section_header,
653+ .print_section_footer = mermaid_print_section_footer,
654+ .print_integer = mermaid_print_int,
655+ .print_string = mermaid_print_str,
656+ .flags = AV_TEXTFORMAT_FLAG_IS_DIAGRAM_FORMATTER,
657+ .priv_class = &mermaid_class,
658+};
fftools/textformat/tf_mermaid.h+41−0
@@ -0,0 +1,41 @@
1+/*
2+ * Copyright (c) The FFmpeg developers
3+ *
4+ * This file is part of FFmpeg.
5+ *
6+ * FFmpeg is free software; you can redistribute it and/or
7+ * modify it under the terms of the GNU Lesser General Public
8+ * License as published by the Free Software Foundation; either
9+ * version 2.1 of the License, or (at your option) any later version.
10+ *
11+ * FFmpeg is distributed in the hope that it will be useful,
12+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+ * Lesser General Public License for more details.
15+ *
16+ * You should have received a copy of the GNU Lesser General Public
17+ * License along with FFmpeg; if not, write to the Free Software
18+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19+ */
20+
21+#ifndef FFTOOLS_TEXTFORMAT_TF_MERMAID_H
22+#define FFTOOLS_TEXTFORMAT_TF_MERMAID_H
23+
24+typedef enum {
25+ AV_DIAGRAMTYPE_GRAPH,
26+ AV_DIAGRAMTYPE_ENTITYRELATIONSHIP,
27+} AVDiagramType;
28+
29+typedef struct AVDiagramConfig {
30+ AVDiagramType diagram_type;
31+ const char *diagram_css;
32+ const char *html_template;
33+} AVDiagramConfig;
34+
35+
36+void av_diagram_init(AVTextFormatContext *tfc, AVDiagramConfig *diagram_config);
37+
38+void av_mermaid_set_html_template(AVTextFormatContext *tfc, const char *html_template);
39+
40+
41+#endif /* FFTOOLS_TEXTFORMAT_TF_MERMAID_H */

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t