From: softworkz <softwo...@hotmail.com> --- doc/ffmpeg.texi | 4 + fftools/Makefile | 1 + fftools/ffmpeg.c | 2 +- fftools/ffmpeg.h | 1 + fftools/ffmpeg_filter.c | 2 +- fftools/ffmpeg_opt.c | 4 + fftools/graph/filelauncher.c | 200 +++++++++++++++++++++++++++++++++++ fftools/graph/graphprint.c | 48 ++++++++- fftools/graph/graphprint.h | 32 ++++++ 9 files changed, 291 insertions(+), 3 deletions(-) create mode 100644 fftools/graph/filelauncher.c
diff --git a/doc/ffmpeg.texi b/doc/ffmpeg.texi index 35675b5309..4bcb6d6a01 100644 --- a/doc/ffmpeg.texi +++ b/doc/ffmpeg.texi @@ -1404,6 +1404,10 @@ Writes execution graph details to the specified file in the format set via -prin Sets the output format (available formats are: default, compact, csv, flat, ini, json, xml, mermaid, mermaidhtml) The default format is json. +@item -sg (@emph{global}) +Writes the execution graph to a temporary html file (mermaidhtml format) and +tries to launch it in the default browser. + @item -progress @var{url} (@emph{global}) Send program-friendly progress information to @var{url}. diff --git a/fftools/Makefile b/fftools/Makefile index c1eba733da..f6492cb1d7 100644 --- a/fftools/Makefile +++ b/fftools/Makefile @@ -22,6 +22,7 @@ OBJS-ffmpeg += \ fftools/ffmpeg_opt.o \ fftools/ffmpeg_sched.o \ fftools/graph/graphprint.o \ + fftools/graph/filelauncher.o \ fftools/sync_queue.o \ fftools/thread_queue.o \ fftools/textformat/avtextformat.o \ diff --git a/fftools/ffmpeg.c b/fftools/ffmpeg.c index de607cac93..0a7675fee0 100644 --- a/fftools/ffmpeg.c +++ b/fftools/ffmpeg.c @@ -309,7 +309,7 @@ const AVIOInterruptCB int_cb = { decode_interrupt_cb, NULL }; static void ffmpeg_cleanup(int ret) { - if ((print_graphs || print_graphs_file) && nb_output_files > 0) + if ((print_graphs || print_graphs_file || show_graph) && nb_output_files > 0) print_filtergraphs(filtergraphs, nb_filtergraphs, input_files, nb_input_files, output_files, nb_output_files); if (do_benchmark) { diff --git a/fftools/ffmpeg.h b/fftools/ffmpeg.h index 7fbf0ad532..49fea0307d 100644 --- a/fftools/ffmpeg.h +++ b/fftools/ffmpeg.h @@ -721,6 +721,7 @@ extern int print_graphs; extern char *print_graphs_file; extern char *print_graphs_format; extern int auto_conversion_filters; +extern int show_graph; extern const AVIOInterruptCB int_cb; diff --git a/fftools/ffmpeg_filter.c b/fftools/ffmpeg_filter.c index b774606562..e82e333b7f 100644 --- a/fftools/ffmpeg_filter.c +++ b/fftools/ffmpeg_filter.c @@ -2985,7 +2985,7 @@ read_frames: finish: - if (print_graphs || print_graphs_file) + if (print_graphs || print_graphs_file || show_graph) print_filtergraph(fg, fgt.graph); // EOF is normal termination diff --git a/fftools/ffmpeg_opt.c b/fftools/ffmpeg_opt.c index 3d1efe32f9..24713d640f 100644 --- a/fftools/ffmpeg_opt.c +++ b/fftools/ffmpeg_opt.c @@ -79,6 +79,7 @@ int vstats_version = 2; int print_graphs = 0; char *print_graphs_file = NULL; char *print_graphs_format = NULL; +int show_graph = 0; int auto_conversion_filters = 1; int64_t stats_period = 500000; @@ -1748,6 +1749,9 @@ const OptionDef options[] = { { "print_graphs_format", OPT_TYPE_STRING, 0, { &print_graphs_format }, "set the output printing format (available formats are: default, compact, csv, flat, ini, json, xml, mermaid, mermaidhtml)", "format" }, + { "sg", OPT_TYPE_BOOL, 0, + { &show_graph }, + "create execution graph as temporary html file and try to launch it in the default browser" }, { "auto_conversion_filters", OPT_TYPE_BOOL, OPT_EXPERT, { &auto_conversion_filters }, "enable automatic conversion filters globally" }, diff --git a/fftools/graph/filelauncher.c b/fftools/graph/filelauncher.c new file mode 100644 index 0000000000..b46967e334 --- /dev/null +++ b/fftools/graph/filelauncher.c @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2025 - 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 + */ + +#include <stdio.h> +#include <string.h> + +#if defined(_WIN32) +# include <windows.h> +# include <shellapi.h> +#else +# include <sys/time.h> +# include <time.h> +# include <errno.h> +# include <sys/stat.h> +# include <sys/types.h> +# include <unistd.h> +#endif +#include "graphprint.h" +#include "libavutil/bprint.h" + +int ff_open_html_in_browser(const char *html_path) +{ + if (!html_path || !*html_path) + return -1; + +#if defined(_WIN32) + + { + HINSTANCE rc = ShellExecuteA(NULL, "open", html_path, NULL, NULL, SW_SHOWNORMAL); + if ((UINT_PTR)rc <= 32) { + // Fallback: system("start ...") + char cmd[1024]; + _snprintf_s(cmd, sizeof(cmd), _TRUNCATE, "start \"\" \"%s\"", html_path); + if (system(cmd) != 0) + return -1; + } + return 0; + } + +#elif defined(__APPLE__) + + { + av_log(NULL, AV_LOG_WARNING, "Browser launch not implemented...\n"); + return 0; + } + +#else + + // --- Linux / Unix-like ----------------------- + { + static const char safe_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/-._"; + AVBPrint buf; + + // Due to the way how the temp path and file name are constructed, this check is not + // actually required and just for illustration of which chars can even occur in the path. + for (const char *p = html_path; *p; ++p) { + if (strchr(safe_chars, (unsigned char)*p) == NULL) { + av_log(NULL, AV_LOG_ERROR, "Invalid file path: '%s'.\n", html_path); + return -1; + } + } + + av_bprint_init(&buf, 0, AV_BPRINT_SIZE_AUTOMATIC); + av_bprintf(&buf, "xdg-open '%s' </dev/null 1>/dev/null 2>&1 &", html_path); + + int ret = system(buf.str); + + if (ret != -1 && WIFEXITED(ret) && WEXITSTATUS(ret) == 0) + return 0; + + av_log(NULL, AV_LOG_WARNING, "Could not open '%s' in a browser.\n", html_path); + return -1; + } + +#endif +} + + +int ff_get_temp_dir(char *buf, size_t size) +{ +#if defined(_WIN32) + + // --- Windows ------------------------------------ + { + // GetTempPathA returns length of the string (including trailing backslash). + // If the return value is greater than buffer size, it's an error. + DWORD len = GetTempPathA((DWORD)size, buf); + if (len == 0 || len > size) { + // Could not retrieve or buffer is too small + return -1; + } + return 0; + } + +#else + + static const char *bases[] = { "/tmp", "/var/tmp", NULL }; + AVBPrint bp; + + av_bprint_init(&bp, 0, AV_BPRINT_SIZE_AUTOMATIC); + + uid_t uid = getuid(); + + for (int i = 0; bases[i]; i++) { + av_bprint_clear(&bp); + av_bprintf(&bp, "%s/ffmpeg-%u", bases[i], uid); + + if (mkdir(bp.str, 0700) == -1 && errno != EEXIST) + continue; + + av_bprint_chars(&bp, '/', 1); + + if (bp.len > size - 1) + return -1; + + snprintf(buf, size, "%s", bp.str); + return 0; + } + + av_log(NULL, AV_LOG_ERROR, "Unable to determine temp directory.\n"); + av_bprint_clear(&bp); + return -1; + +#endif +} + +int ff_make_timestamped_html_name(char *buf, size_t size) +{ +#if defined(_WIN32) + + /*----------- Windows version -----------*/ + SYSTEMTIME st; + GetLocalTime(&st); + /* + st.wYear, st.wMonth, st.wDay, + st.wHour, st.wMinute, st.wSecond, st.wMilliseconds + */ + int written = _snprintf_s(buf, size, _TRUNCATE, + "ffmpeg_graph_%04d-%02d-%02d_%02d-%02d-%02d_%03d.html", + st.wYear, + st.wMonth, + st.wDay, + st.wHour, + st.wMinute, + st.wSecond, + st.wMilliseconds); + if (written < 0) + return -1; /* Could not write into buffer */ + return 0; + +#else + + /*----------- macOS / Linux / Unix version -----------*/ + struct timeval tv; + if (gettimeofday(&tv, NULL) != 0) { + return -1; /* gettimeofday failed */ + } + + struct tm local_tm; + localtime_r(&tv.tv_sec, &local_tm); + + int ms = (int)(tv.tv_usec / 1000); /* convert microseconds to milliseconds */ + + /* + local_tm.tm_year is years since 1900, + local_tm.tm_mon is 0-based (0=Jan, 11=Dec) + */ + int written = snprintf(buf, size, + "ffmpeg_graph_%04d-%02d-%02d_%02d-%02d-%02d_%03d.html", + local_tm.tm_year + 1900, + local_tm.tm_mon + 1, + local_tm.tm_mday, + local_tm.tm_hour, + local_tm.tm_min, + local_tm.tm_sec, + ms); + if (written < 0 || (size_t)written >= size) { + return -1; /* Buffer too small or formatting error */ + } + return 0; + +#endif +} diff --git a/fftools/graph/graphprint.c b/fftools/graph/graphprint.c index 852a8f6c0c..346e139494 100644 --- a/fftools/graph/graphprint.c +++ b/fftools/graph/graphprint.c @@ -884,6 +884,11 @@ static int init_graphprint(GraphPrintContext **pgpc, AVBPrint *target_buf) av_bprint_init(target_buf, 0, AV_BPRINT_SIZE_UNLIMITED); + if (show_graph) { + if (!print_graphs_format || strcmp(print_graphs_format, "mermaidhtml") != 0) + print_graphs_format = av_strdup("mermaidhtml"); + } + if (!print_graphs_format) print_graphs_format = av_strdup("json"); if (!print_graphs_format) { @@ -1108,7 +1113,48 @@ cleanup: int print_filtergraphs(FilterGraph **graphs, int nb_graphs, InputFile **ifiles, int nb_ifiles, OutputFile **ofiles, int nb_ofiles) { - int ret = print_filtergraphs_priv(graphs, nb_graphs, ifiles, nb_ifiles, ofiles, nb_ofiles); + int ret; + + if (show_graph) { + char buf[2048]; + AVBPrint bp; + + av_bprint_init(&bp, 0, AV_BPRINT_SIZE_UNLIMITED); + + print_graphs = 0; + + ret = ff_get_temp_dir(buf, sizeof(buf)); + if (ret) { + av_log(NULL, AV_LOG_ERROR, "Error getting temp directory path for graph output file\n"); + goto fail; + } + + av_bprint_append_data(&bp, buf, strlen(buf)); + + ret = ff_make_timestamped_html_name(buf, sizeof(buf)); + if (ret) { + av_log(NULL, AV_LOG_ERROR, "Error creating temp file name for graph output file\n"); + goto fail; + } + + av_bprint_append_data(&bp, buf, strlen(buf)); + + av_bprint_finalize(&bp, &print_graphs_file); + } + + ret = print_filtergraphs_priv(graphs, nb_graphs, ifiles, nb_ifiles, ofiles, nb_ofiles); + + if (!ret && show_graph) { + av_log(NULL, AV_LOG_INFO, "Execution graph saved as: %s\n", print_graphs_file); + av_log(NULL, AV_LOG_INFO, "Trying to launch graph in browser...\n"); + + ret = ff_open_html_in_browser(print_graphs_file); + if (ret) { + av_log(NULL, AV_LOG_ERROR, "Browser could not be launched for execution graph display\nPlease open manually: %s\n", print_graphs_file); + } + } + +fail: ff_resman_uninit(); return ret; } diff --git a/fftools/graph/graphprint.h b/fftools/graph/graphprint.h index 9f043cc273..43f769870b 100644 --- a/fftools/graph/graphprint.h +++ b/fftools/graph/graphprint.h @@ -27,4 +27,36 @@ int print_filtergraphs(FilterGraph **graphs, int nb_graphs, InputFile **ifiles, int print_filtergraph(FilterGraph *fg, AVFilterGraph *graph); +/** + * Open an HTML file in the default browser (Windows, macOS, Linux/Unix). + * + * @param html_path Absolute or relative path to the HTML file. + * @return 0 on success, -1 on failure. + * + * NOTE: This uses system() calls for non-Windows, and ShellExecute on Windows. + * Exercise caution if 'html_path' is untrusted (possible command injection). + */ +int ff_open_html_in_browser(const char *html_path); + +/** + * Retrieve the system's temporary directory. + * + * @param buf Output buffer to store the temp directory path (including trailing slash) + * @param size Size of the output buffer in bytes + * @return 0 on success, -1 on failure (buffer too small or other errors) + * + * Note: On most platforms, the path will include a trailing slash (e.g. "C:\\Users\\...\\Temp\\" on Windows, "/tmp/" on Unix). + */ +int ff_get_temp_dir(char *buf, size_t size); + +/** + * Create a timestamped HTML filename, e.g.: + * ffmpeg_graph_2024-01-01_22-12-59_123.html + * + * @param buf Pointer to buffer where the result is stored + * @param size Size of the buffer in bytes + * @return 0 on success, -1 on error (e.g. buffer too small) + */ +int ff_make_timestamped_html_name(char *buf, size_t size); + #endif /* FFTOOLS_GRAPH_GRAPHPRINT_H */ -- ffmpeg-codebot _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-requ...@ffmpeg.org with subject "unsubscribe".