diff mbox series

[FFmpeg-devel,v1] ffmpeg: add optional JSON output of inputs, outputs, mapping, and progress

Message ID 20220609124700.81727-1-ingo@datarhei.com
State New
Headers show
Series [FFmpeg-devel,v1] ffmpeg: add optional JSON output of inputs, outputs, mapping, and progress | expand

Checks

Context Check Description
andriy/make_fate_x86 success Make fate finished
andriy/make_x86 warning New warnings during build

Commit Message

Ingo Oppermann June 9, 2022, 12:47 p.m. UTC
In order to make a running ffmpeg process easier to monitor and parse by
programs that call the ffmpeg binary and process its output, this patch adds
the command line option -jsonstats. This option is a modifier for the
(no)stats option which provides a more verbose output in JSON format than the
default output. It enables the additional output of the input streams,
their mapping to the outputs (including the filter graphs), and the output
streams as JSON. Each output is on a single line and is prefixed with
"json.inputs:", "json.mapping:", and "json.outputs:" respectively, followed by
the JSON data. The -jsonstats option is disabled by default.

The inputs and outputs are arrays and for each input and output stream, the
information in the JSON is similar to the default dump of the inputs and
outputs.

The stream mapping includes an array of the filter graphs and a mapping
representation similar to the output from to graph2dot.c program.

The current progress report is replaced by a JSON representation which is
prefixed with "json.progress:" followed by JSON data, and each report will be
on a new line. The progress data contains values similar to the default data
for each input and output stream and a summary.

Together with the -progress option, the described JSON data instead of the
default data will be written to the provided target.

Signed-off-by: Ingo Oppermann <ingo@datarhei.com>
---
 doc/ffmpeg.texi      |  10 ++
 fftools/ffmpeg.c     | 198 +++++++++++++++++++++++++++-
 fftools/ffmpeg.h     |   1 +
 fftools/ffmpeg_mux.c | 307 +++++++++++++++++++++++++++++++++++++++++++
 fftools/ffmpeg_opt.c | 115 ++++++++++++++++
 5 files changed, 629 insertions(+), 2 deletions(-)


base-commit: 5d5a01419928d0c00bae54f730eede150cd5b268

Comments

Ingo Oppermann July 25, 2022, 5:47 p.m. UTC | #1
> On 9 Jun 2022, at 14:47, Ingo Oppermann <ingo@datarhei.com> wrote:
> 
> In order to make a running ffmpeg process easier to monitor and parse by
> programs that call the ffmpeg binary and process its output, this patch adds
> the command line option -jsonstats. This option is a modifier for the
> (no)stats option which provides a more verbose output in JSON format than the
> default output. It enables the additional output of the input streams,
> their mapping to the outputs (including the filter graphs), and the output
> streams as JSON. Each output is on a single line and is prefixed with
> "json.inputs:", "json.mapping:", and "json.outputs:" respectively, followed by
> the JSON data. The -jsonstats option is disabled by default.
> 
> The inputs and outputs are arrays and for each input and output stream, the
> information in the JSON is similar to the default dump of the inputs and
> outputs.
> 
> The stream mapping includes an array of the filter graphs and a mapping
> representation similar to the output from to graph2dot.c program.
> 
> The current progress report is replaced by a JSON representation which is
> prefixed with "json.progress:" followed by JSON data, and each report will be
> on a new line. The progress data contains values similar to the default data
> for each input and output stream and a summary.
> 
> Together with the -progress option, the described JSON data instead of the
> default data will be written to the provided target.
> 
> Signed-off-by: Ingo Oppermann <ingo@datarhei.com>
> ---
> doc/ffmpeg.texi      |  10 ++
> fftools/ffmpeg.c     | 198 +++++++++++++++++++++++++++-
> fftools/ffmpeg.h     |   1 +
> fftools/ffmpeg_mux.c | 307 +++++++++++++++++++++++++++++++++++++++++++
> fftools/ffmpeg_opt.c | 115 ++++++++++++++++
> 5 files changed, 629 insertions(+), 2 deletions(-)
> 
> diff --git a/doc/ffmpeg.texi b/doc/ffmpeg.texi
> index 0d7e1a479d..16fcd9970a 100644
> --- a/doc/ffmpeg.texi
> +++ b/doc/ffmpeg.texi
> @@ -784,6 +784,13 @@ disable it you need to specify @code{-nostats}.
> @item -stats_period @var{time} (@emph{global})
> Set period at which encoding progress/statistics are updated. Default is 0.5 seconds.
> 
> +@item -jsonstats (@emph{global})
> +Print inputs, outputs, stream mapping, and encoding progress/statistics. It is off by
> +default. It modifies the output of @code{-stats} to be JSON. The inputs, outputs,
> +stream mapping, and progress information are written on one line and are prefixed
> +with @var{json.inputs:}, @var{json.outputs:}, @var{json.mapping:}, and @var{json.progress:}
> +respectively followed by the JSON data.
> +
> @item -progress @var{url} (@emph{global})
> Send program-friendly progress information to @var{url}.
> 
> @@ -792,6 +799,9 @@ the encoding process. It is made of "@var{key}=@var{value}" lines. @var{key}
> consists of only alphanumeric characters. The last key of a sequence of
> progress information is always "progress".
> 
> +If @code{-jsonstats} is enabled, the progress information is written as JSON with
> +the prefixes and data 
> +
> The update period is set using @code{-stats_period}.
> 
> @anchor{stdin option}
> diff --git a/fftools/ffmpeg.c b/fftools/ffmpeg.c
> index 5ed287c522..eea1491ed1 100644
> --- a/fftools/ffmpeg.c
> +++ b/fftools/ffmpeg.c
> @@ -1505,7 +1505,7 @@ static void print_final_stats(int64_t total_size)
>     }
> }
> 
> -static void print_report(int is_last_report, int64_t timer_start, int64_t cur_time)
> +static void print_default_report(int is_last_report, int64_t timer_start, int64_t cur_time)
> {
>     AVBPrint buf, buf_script;
>     OutputStream *ost;
> @@ -1695,7 +1695,7 @@ static void print_report(int is_last_report, int64_t timer_start, int64_t cur_ti
>     }
>     av_bprint_finalize(&buf, NULL);
> 
> -    if (progress_avio) {
> +    if (progress_avio && !print_jsonstats) {
>         av_bprintf(&buf_script, "progress=%s\n",
>                    is_last_report ? "end" : "continue");
>         avio_write(progress_avio, buf_script.str,
> @@ -1715,6 +1715,200 @@ static void print_report(int is_last_report, int64_t timer_start, int64_t cur_ti
>         print_final_stats(total_size);
> }
> 
> +/**
> + * Print progress report in JSON format
> + *
> + * @param is_last_report Whether this is the last report
> + * @param timer_start Time when the processing started
> + * @param cur_time Current processing time of the stream
> + */
> +static void print_json_report(int is_last_report, int64_t timer_start, int64_t cur_time)
> +{
> +    AVBPrint buf;
> +    InputStream *ist;
> +    OutputStream *ost;
> +    uint64_t stream_size, total_packets = 0, total_size = 0;
> +    AVCodecContext *enc;
> +    int i, j;
> +    double speed;
> +    int64_t pts = INT64_MIN + 1;
> +    static int first_report = 1;
> +    static int64_t last_time = -1;
> +    int hours, mins, secs, us;
> +    const char *hours_sign;
> +    float t, q;
> +
> +    if (!is_last_report) {
> +        if (last_time == -1) {
> +            last_time = cur_time;
> +        }
> +        if (((cur_time - last_time) < stats_period && !first_report) ||
> +            (first_report && nb_output_dumped < nb_output_files))
> +            return;
> +        last_time = cur_time;
> +    }
> +
> +    t = (cur_time - timer_start) / 1000000.0;
> +
> +    av_bprint_init(&buf, 0, AV_BPRINT_SIZE_UNLIMITED);
> +
> +    av_bprintf(&buf, "json.progress:{");
> +    av_bprintf(&buf, "\"inputs\":[");
> +    for (i = 0; i < nb_input_files; i++) {
> +        InputFile *f = input_files[i];
> +
> +        for (j = 0; j < f->nb_streams; j++) {
> +            ist = input_streams[f->ist_index + j];
> +
> +            av_bprintf(&buf, "{");
> +            av_bprintf(&buf, "\"index\":%d,\"stream\":%d,", i, j);
> +
> +            av_bprintf(&buf,
> +                "\"frame\":%" PRIu64 ",\"packet\":%" PRIu64 ",",
> +                !ist->frames_decoded ? ist->nb_packets : ist->frames_decoded,
> +                ist->nb_packets);
> +
> +            av_bprintf(&buf, "\"size_bytes\":%" PRIu64, ist->data_size);
> +
> +            if (i == (nb_input_files - 1) && j == (f->nb_streams - 1)) {
> +                av_bprintf(&buf, "}");
> +            } else {
> +                av_bprintf(&buf, "},");
> +            }
> +        }
> +    }
> +
> +    av_bprintf(&buf, "],");
> +
> +    av_bprintf(&buf, "\"outputs\":[");
> +    for (i = 0; i < nb_output_streams; i++) {
> +        q = -1;
> +        ost = output_streams[i];
> +        enc = ost->enc_ctx;
> +        if (!ost->stream_copy) {
> +            q = ost->quality / (float)FF_QP2LAMBDA;
> +        }
> +
> +        av_bprintf(&buf, "{");
> +        av_bprintf(
> +            &buf, "\"index\":%d,\"stream\":%d,", ost->file_index, ost->index);
> +
> +        av_bprintf(&buf,
> +            "\"frame\":%" PRIu64 ",\"packet\":%" PRIu64 ",",
> +            !ost->frames_encoded ? ost->packets_written : ost->frames_encoded,
> +            ost->packets_written);
> +
> +        if (enc->codec_type == AVMEDIA_TYPE_VIDEO) {
> +            av_bprintf(&buf, "\"q\":%.1f,", q);
> +        }
> +
> +        /* compute min output value */
> +        if (av_stream_get_end_pts(ost->st) != AV_NOPTS_VALUE) {
> +            pts = FFMAX(pts,
> +                av_rescale_q(av_stream_get_end_pts(ost->st),
> +                    ost->st->time_base,
> +                    AV_TIME_BASE_Q));
> +            if (copy_ts) {
> +                if (copy_ts_first_pts == AV_NOPTS_VALUE && pts > 1)
> +                    copy_ts_first_pts = pts;
> +                if (copy_ts_first_pts != AV_NOPTS_VALUE)
> +                    pts -= copy_ts_first_pts;
> +            }
> +        }
> +
> +        total_packets += ost->packets_written;
> +
> +        if (is_last_report) {
> +            nb_frames_drop += ost->last_dropped;
> +        }
> +
> +        stream_size = ost->data_size + ost->enc_ctx->extradata_size;
> +        total_size += stream_size;
> +
> +        av_bprintf(&buf, "\"size_bytes\":%" PRIu64, stream_size);
> +
> +        if (i == (nb_output_streams - 1)) {
> +            av_bprintf(&buf, "}");
> +        } else {
> +            av_bprintf(&buf, "},");
> +        }
> +    }
> +
> +    av_bprintf(&buf, "],");
> +
> +    av_bprintf(&buf,
> +        "\"packet\":%" PRIu64 ",\"size_bytes\":%" PRIu64 ",",
> +        total_packets,
> +        total_size);
> +
> +    secs = FFABS(pts) / AV_TIME_BASE;
> +    us = FFABS(pts) % AV_TIME_BASE;
> +    mins = secs / 60;
> +    secs %= 60;
> +    hours = mins / 60;
> +    mins %= 60;
> +    hours_sign = (pts < 0) ? "-" : "";
> +
> +    if (pts != AV_NOPTS_VALUE) {
> +        av_bprintf(&buf,
> +            "\"time\":\"%s%dh%dm%d.%ds\",",
> +            hours_sign,
> +            hours,
> +            mins,
> +            secs,
> +            (100 * us) / AV_TIME_BASE);
> +    }
> +
> +    speed = t != 0.0 ? (double)pts / AV_TIME_BASE / t : -1;
> +    av_bprintf(&buf, "\"speed\":%.3g,", speed);
> +
> +    av_bprintf(&buf, "\"dup\":%d,\"drop\":%d", nb_frames_dup, nb_frames_drop);
> +    av_bprintf(&buf, "}\n");
> +
> +    if (print_stats || is_last_report) {
> +        if (AV_LOG_INFO > av_log_get_level()) {
> +            fprintf(stderr, "%s", buf.str);
> +        } else {
> +            av_log(NULL, AV_LOG_INFO, "%s", buf.str);
> +        }
> +
> +        fflush(stderr);
> +    }
> +
> +    if (progress_avio) {
> +        avio_write(progress_avio, buf.str, FFMIN(buf.len, buf.size - 1));
> +        avio_flush(progress_avio);
> +        if (is_last_report) {
> +            av_bprint_clear(&buf);
> +            av_bprintf(&buf, "ffmpeg.progress:NULL\n");
> +            avio_write(progress_avio, buf.str, FFMIN(buf.len, buf.size - 1));
> +            int ret;
> +            if ((ret = avio_closep(&progress_avio)) < 0) {
> +                av_log(NULL,
> +                    AV_LOG_ERROR,
> +                    "Error closing progress log, loss of information possible: %s\n",
> +                    av_err2str(ret));
> +            }
> +        }
> +    }
> +
> +    first_report = 0;
> +
> +    av_bprint_finalize(&buf, NULL);
> +}
> +
> +static void print_report(int is_last_report, int64_t timer_start, int64_t cur_time)
> +{
> +    if (!print_stats && !is_last_report && !progress_avio)
> +        return;
> +
> +    if (print_jsonstats == 1) {
> +        print_json_report(is_last_report, timer_start, cur_time);
> +    } else {
> +        print_default_report(is_last_report, timer_start, cur_time);
> +    }
> +}
> +
> static int ifilter_parameters_from_codecpar(InputFilter *ifilter, AVCodecParameters *par)
> {
>     int ret;
> diff --git a/fftools/ffmpeg.h b/fftools/ffmpeg.h
> index 7326193caf..14968869d0 100644
> --- a/fftools/ffmpeg.h
> +++ b/fftools/ffmpeg.h
> @@ -631,6 +631,7 @@ extern int debug_ts;
> extern int exit_on_error;
> extern int abort_on_flags;
> extern int print_stats;
> +extern int print_jsonstats;
> extern int64_t stats_period;
> extern int qp_hist;
> extern int stdin_interaction;
> diff --git a/fftools/ffmpeg_mux.c b/fftools/ffmpeg_mux.c
> index 794d580635..c7771faad7 100644
> --- a/fftools/ffmpeg_mux.c
> +++ b/fftools/ffmpeg_mux.c
> @@ -21,14 +21,20 @@
> 
> #include "ffmpeg.h"
> 
> +#include "libavutil/bprint.h"
> +#include "libavutil/channel_layout.h"
> #include "libavutil/fifo.h"
> #include "libavutil/intreadwrite.h"
> #include "libavutil/log.h"
> #include "libavutil/mem.h"
> +#include "libavutil/pixdesc.h"
> #include "libavutil/timestamp.h"
> 
> +#include "libavcodec/avcodec.h"
> #include "libavcodec/packet.h"
> 
> +#include "libavfilter/avfilter.h"
> +
> #include "libavformat/avformat.h"
> #include "libavformat/avio.h"
> 
> @@ -226,6 +232,305 @@ fail:
>     return ret;
> }
> 
> +/**
> + * Write a graph as JSON to an initialized buffer
> + *
> + * @param buf Pointer to an initialized AVBPrint buffer
> + * @param graph Pointer to a AVFilterGraph
> + */
> +static void print_json_graph(AVBPrint *buf, AVFilterGraph *graph)
> +{
> +    int i, j;
> +
> +    if (!graph) {
> +        av_bprintf(buf, "null\n");
> +        return;
> +    }
> +
> +    av_bprintf(buf, "[");
> +
> +    for (i = 0; i < graph->nb_filters; i++) {
> +        const AVFilterContext *filter_ctx = graph->filters[i];
> +
> +        for (j = 0; j < filter_ctx->nb_outputs; j++) {
> +            AVFilterLink *link = filter_ctx->outputs[j];
> +            if (link) {
> +                const AVFilterContext *dst_filter_ctx = link->dst;
> +
> +                av_bprintf(buf,
> +                    "{\"src_name\":\"%s\",\"src_filter\":\"%s\",\"dst_name\":\"%s\",\"dst_filter\":\"%s\",",
> +                    filter_ctx->name,
> +                    filter_ctx->filter->name,
> +                    dst_filter_ctx->name,
> +                    dst_filter_ctx->filter->name);
> +                av_bprintf(buf,
> +                    "\"inpad\":\"%s\",\"outpad\":\"%s\",",
> +                    avfilter_pad_get_name(link->srcpad, 0),
> +                    avfilter_pad_get_name(link->dstpad, 0));
> +                av_bprintf(buf,
> +                    "\"timebase\":\"%d/%d\",",
> +                    link->time_base.num,
> +                    link->time_base.den);
> +
> +                if (link->type == AVMEDIA_TYPE_VIDEO) {
> +                    const AVPixFmtDescriptor *desc =
> +                        av_pix_fmt_desc_get(link->format);
> +                    av_bprintf(buf,
> +                        "\"type\":\"video\",\"format\":\"%s\",\"width\":%d,\"height\":%d",
> +                        desc->name,
> +                        link->w,
> +                        link->h);
> +                } else if (link->type == AVMEDIA_TYPE_AUDIO) {
> +                    char layout[255];
> +                    av_channel_layout_describe(
> +                        &link->ch_layout, layout, sizeof(layout));
> +                    av_bprintf(buf,
> +                        "\"type\":\"audio\",\"format\":\"%s\",\"sampling_hz\":%d,\"layout\":\"%s\"",
> +                        av_get_sample_fmt_name(link->format),
> +                        link->sample_rate,
> +                        layout);
> +                }
> +
> +                if (i == (graph->nb_filters - 1)) {
> +                    av_bprintf(buf, "}");
> +                } else {
> +                    av_bprintf(buf, "},");
> +                }
> +            }
> +        }
> +    }
> +
> +    av_bprintf(buf, "]");
> +}
> +
> +/**
> + * Print all outputs in JSON format
> + */
> +static void print_json_outputs()
> +{
> +    static int ost_all_initialized = 0;
> +    int i, j, k;
> +    int nb_initialized = 0;
> +    AVBPrint buf;
> +
> +    if (!print_jsonstats) {
> +        return;
> +    }
> +
> +    if (ost_all_initialized == 1) {
> +        return;
> +    }
> +
> +    // count how many outputs are initialized
> +    for (i = 0; i < nb_output_streams; i++) {
> +        OutputStream *ost = output_streams[i];
> +        if (ost->initialized) {
> +            nb_initialized++;
> +        }
> +    }
> +
> +    // only when all outputs are initialized, dump the outputs
> +    if (nb_initialized == nb_output_streams) {
> +        ost_all_initialized = 1;
> +    }
> +
> +    if (ost_all_initialized != 1) {
> +        return;
> +    }
> +
> +    av_bprint_init(&buf, 0, AV_BPRINT_SIZE_UNLIMITED);
> +
> +    av_bprintf(&buf, "json.outputs:[");
> +    for (i = 0; i < nb_output_streams; i++) {
> +        OutputStream *ost = output_streams[i];
> +        OutputFile *f = output_files[ost->file_index];
> +        AVFormatContext *ctx = f->ctx;
> +        AVStream *st = ost->st;
> +        AVDictionaryEntry *lang =
> +            av_dict_get(st->metadata, "language", NULL, 0);
> +        AVCodecContext *enc = ost->enc_ctx;
> +        char *url = NULL;
> +
> +        if (av_escape(&url,
> +                ctx->url,
> +                "\\\"",
> +                AV_ESCAPE_MODE_BACKSLASH,
> +                AV_UTF8_FLAG_ACCEPT_ALL) < 0) {
> +            url = av_strdup("-");
> +        }
> +
> +        av_bprintf(&buf, "{");
> +        av_bprintf(&buf,
> +            "\"url\":\"%s\",\"format\":\"%s\",\"index\":%d,\"stream\":%d,",
> +            url,
> +            ctx->oformat->name,
> +            ost->file_index,
> +            ost->index);
> +        av_bprintf(&buf,
> +            "\"type\":\"%s\",\"codec\":\"%s\",\"coder\":\"%s\",\"bitrate_kbps\":%" PRId64
> +            ",",
> +            av_get_media_type_string(enc->codec_type),
> +            avcodec_get_name(enc->codec_id),
> +            ost->stream_copy ? "copy"
> +                             : (enc->codec ? enc->codec->name : "unknown"),
> +            enc->bit_rate / 1000);
> +        av_bprintf(&buf,
> +            "\"duration_sec\":%f,\"language\":\"%s\"",
> +            0.0,
> +            lang ? lang->value : "und");
> +
> +        av_free(url);
> +
> +        if (enc->codec_type == AVMEDIA_TYPE_VIDEO) {
> +            float fps = 0;
> +            if (st->avg_frame_rate.den && st->avg_frame_rate.num) {
> +                fps = av_q2d(st->avg_frame_rate);
> +            }
> +
> +            av_bprintf(&buf,
> +                ",\"fps\":%f,\"pix_fmt\":\"%s\",\"width\":%d,\"height\":%d",
> +                fps,
> +                st->codecpar->format == AV_PIX_FMT_NONE
> +                    ? "none"
> +                    : av_get_pix_fmt_name(st->codecpar->format),
> +                st->codecpar->width,
> +                st->codecpar->height);
> +        } else if (enc->codec_type == AVMEDIA_TYPE_AUDIO) {
> +            char layout[128];
> +            av_channel_layout_describe(&enc->ch_layout, layout, sizeof(layout));
> +
> +            av_bprintf(&buf,
> +                ",\"sampling_hz\":%d,\"layout\":\"%s\",\"channels\":%d",
> +                enc->sample_rate,
> +                layout,
> +                enc->ch_layout.nb_channels);
> +        }
> +
> +        if (i == (nb_output_streams - 1)) {
> +            av_bprintf(&buf, "}");
> +        } else {
> +            av_bprintf(&buf, "},");
> +        }
> +    }
> +
> +    av_bprintf(&buf, "]\n");
> +
> +    av_log(NULL, AV_LOG_INFO, "%s", buf.str);
> +
> +    if (progress_avio) {
> +        avio_write(progress_avio, buf.str, FFMIN(buf.len, buf.size - 1));
> +        avio_flush(progress_avio);
> +    }
> +
> +    av_bprint_clear(&buf);
> +
> +    av_bprintf(&buf, "json.mapping:{");
> +    av_bprintf(&buf, "\"graphs\":[");
> +
> +    for (i = 0; i < nb_filtergraphs; i++) {
> +        av_bprintf(&buf, "{\"index\":%d,\"graph\":", i);
> +        print_json_graph(&buf, filtergraphs[i]->graph);
> +
> +        if (i == (nb_filtergraphs - 1)) {
> +            av_bprintf(&buf, "}");
> +        } else {
> +            av_bprintf(&buf, "},");
> +        }
> +    }
> +
> +    av_bprintf(&buf, "],");
> +
> +    // The following is inspired by tools/graph2dot.c
> +
> +    av_bprintf(&buf, "\"mapping\":[");
> +
> +    for (i = 0; i < nb_input_streams; i++) {
> +        InputStream *ist = input_streams[i];
> +
> +        for (j = 0; j < ist->nb_filters; j++) {
> +            if (ist->filters[j]->graph) {
> +                char *name = NULL;
> +                for (k = 0; k < ist->filters[j]->graph->nb_inputs; k++) {
> +                    if (ist->filters[j]->graph->inputs[k]->ist == ist) {
> +                        name = ist->filters[j]->graph->inputs[k]->filter->name;
> +                        break;
> +                    }
> +                }
> +
> +                av_bprintf(&buf,
> +                    "{\"input\":{\"index\":%d,\"stream\":%d},\"graph\":{\"index\":%d,\"name\":\"%s\"},\"output\":null},",
> +                    ist->file_index,
> +                    ist->st->index,
> +                    ist->filters[j]->graph->index,
> +                    name);
> +            }
> +        }
> +    }
> +
> +    for (i = 0; i < nb_output_streams; i++) {
> +        OutputStream *ost = output_streams[i];
> +
> +        if (ost->attachment_filename) {
> +            av_bprintf(&buf,
> +                "{\"input\":null,\"file\":\"%s\",\"output\":{\"index\":%d,\"stream\":%d}},",
> +                ost->attachment_filename,
> +                ost->file_index,
> +                ost->index);
> +            goto next_output;
> +        }
> +
> +        if (ost->filter && ost->filter->graph) {
> +            char *name = NULL;
> +            for (j = 0; j < ost->filter->graph->nb_outputs; j++) {
> +                if (ost->filter->graph->outputs[j]->ost == ost) {
> +                    name = ost->filter->graph->outputs[j]->filter->name;
> +                    break;
> +                }
> +            }
> +            av_bprintf(&buf,
> +                "{\"input\":null,\"graph\":{\"index\":%d,\"name\":\"%s\"},\"output\":{\"index\":%d,\"stream\":%d}}",
> +                ost->filter->graph->index,
> +                name,
> +                ost->file_index,
> +                ost->index);
> +            goto next_output;
> +        }
> +
> +        av_bprintf(&buf,
> +            "{\"input\":{\"index\":%d,\"stream\":%d},\"output\":{\"index\":%d,\"stream\":%d}",
> +            input_streams[ost->source_index]->file_index,
> +            input_streams[ost->source_index]->st->index,
> +            ost->file_index,
> +            ost->index);
> +        av_bprintf(&buf, ",\"copy\":%s", ost->stream_copy ? "true" : "false");
> +
> +        if (ost->sync_ist != input_streams[ost->source_index]) {
> +            av_bprintf(&buf,
> +                ",\"sync\":{\"index\":%d,\"stream\":%d}",
> +                ost->sync_ist->file_index,
> +                ost->sync_ist->st->index);
> +        }
> +
> +        av_bprintf(&buf, "}");
> +
> +    next_output:
> +        if (i != (nb_output_streams - 1)) {
> +            av_bprintf(&buf, ",");
> +        }
> +    }
> +
> +    av_bprintf(&buf, "]}\n");
> +
> +    av_log(NULL, AV_LOG_INFO, "%s", buf.str);
> +
> +    if (progress_avio) {
> +        avio_write(progress_avio, buf.str, FFMIN(buf.len, buf.size - 1));
> +        avio_flush(progress_avio);
> +    }
> +
> +    av_bprint_finalize(&buf, NULL);
> +}
> +
> /* open the muxer when all the streams are initialized */
> int of_check_init(OutputFile *of)
> {
> @@ -251,6 +556,8 @@ int of_check_init(OutputFile *of)
>     av_dump_format(of->ctx, of->index, of->ctx->url, 1);
>     nb_output_dumped++;
> 
> +    print_json_outputs();
> +
>     if (sdp_filename || want_sdp) {
>         ret = print_sdp();
>         if (ret < 0) {
> diff --git a/fftools/ffmpeg_opt.c b/fftools/ffmpeg_opt.c
> index 2c1b3bd0dd..20e991eca5 100644
> --- a/fftools/ffmpeg_opt.c
> +++ b/fftools/ffmpeg_opt.c
> @@ -51,6 +51,7 @@
> #include "libavutil/parseutils.h"
> #include "libavutil/pixdesc.h"
> #include "libavutil/pixfmt.h"
> +#include "libavutil/bprint.h"
> 
> #define DEFAULT_PASS_LOGFILENAME_PREFIX "ffmpeg2pass"
> 
> @@ -169,6 +170,7 @@ int debug_ts          = 0;
> int exit_on_error     = 0;
> int abort_on_flags    = 0;
> int print_stats       = -1;
> +int print_jsonstats   = 0;
> int qp_hist           = 0;
> int stdin_interaction = 1;
> float max_error_rate  = 2.0/3;
> @@ -3434,6 +3436,115 @@ static int open_files(OptionGroupList *l, const char *inout,
>     return 0;
> }
> 
> +/**
> + * Print all inputs in JSON format
> + */
> +static void print_json_inputs()
> +{
> +    if (!print_jsonstats) {
> +        return;
> +    }
> +
> +    AVBPrint buf;
> +    int i, j;
> +
> +    av_bprint_init(&buf, 0, AV_BPRINT_SIZE_UNLIMITED);
> +
> +    av_bprintf(&buf, "json.inputs:[");
> +    for (i = 0; i < nb_input_files; i++) {
> +        InputFile *f = input_files[i];
> +        AVFormatContext *ctx = f->ctx;
> +
> +        float duration = 0;
> +        if (ctx->duration != AV_NOPTS_VALUE) {
> +            duration = (float)(ctx->duration +
> +                           (ctx->duration <= INT64_MAX - 5000 ? 5000 : 0)) /
> +                (float)AV_TIME_BASE;
> +        }
> +
> +        for (j = 0; j < f->nb_streams; j++) {
> +            InputStream *ist = input_streams[f->ist_index + j];
> +            AVCodecContext *dec = ist->dec_ctx;
> +            AVStream *st = ist->st;
> +            AVDictionaryEntry *lang =
> +                av_dict_get(st->metadata, "language", NULL, 0);
> +            char *url = NULL;
> +
> +            if (av_escape(&url,
> +                    ctx->url,
> +                    "\\\"",
> +                    AV_ESCAPE_MODE_BACKSLASH,
> +                    AV_UTF8_FLAG_ACCEPT_ALL) < 0) {
> +                url = av_strdup("-");
> +            }
> +
> +            av_bprintf(&buf, "{");
> +            av_bprintf(&buf,
> +                "\"url\":\"%s\",\"format\":\"%s\",\"index\":%d,\"stream\":%d,",
> +                url,
> +                ctx->iformat->name,
> +                i,
> +                j);
> +            av_bprintf(&buf,
> +                "\"type\":\"%s\",\"codec\":\"%s\",\"coder\":\"%s\",\"bitrate_kbps\":%" PRId64
> +                ",",
> +                av_get_media_type_string(dec->codec_type),
> +                avcodec_get_name(dec->codec_id),
> +                dec->codec ? dec->codec->name : "unknown",
> +                dec->bit_rate / 1000);
> +            av_bprintf(&buf,
> +                "\"duration_sec\":%f,\"language\":\"%s\"",
> +                duration,
> +                lang ? lang->value : "und");
> +
> +            av_free(url);
> +
> +            if (dec->codec_type == AVMEDIA_TYPE_VIDEO) {
> +                float fps = 0;
> +                if (st->avg_frame_rate.den && st->avg_frame_rate.num) {
> +                    fps = av_q2d(st->avg_frame_rate);
> +                }
> +
> +                av_bprintf(&buf,
> +                    ",\"fps\":%f,\"pix_fmt\":\"%s\",\"width\":%d,\"height\":%d",
> +                    fps,
> +                    st->codecpar->format == AV_PIX_FMT_NONE
> +                        ? "none"
> +                        : av_get_pix_fmt_name(st->codecpar->format),
> +                    st->codecpar->width,
> +                    st->codecpar->height);
> +            } else if (dec->codec_type == AVMEDIA_TYPE_AUDIO) {
> +                char layout[128];
> +                av_channel_layout_describe(
> +                    &dec->ch_layout, layout, sizeof(layout));
> +
> +                av_bprintf(&buf,
> +                    ",\"sampling_hz\":%d,\"layout\":\"%s\",\"channels\":%d",
> +                    dec->sample_rate,
> +                    layout,
> +                    dec->ch_layout.nb_channels);
> +            }
> +
> +            if (i == (nb_input_files - 1) && j == (f->nb_streams - 1)) {
> +                av_bprintf(&buf, "}");
> +            } else {
> +                av_bprintf(&buf, "},");
> +            }
> +        }
> +    }
> +
> +    av_bprintf(&buf, "]\n");
> +
> +    av_log(NULL, AV_LOG_INFO, "%s", buf.str);
> +
> +    if (progress_avio) {
> +        avio_write(progress_avio, buf.str, FFMIN(buf.len, buf.size - 1));
> +        avio_flush(progress_avio);
> +    }
> +
> +    av_bprint_finalize(&buf, NULL);
> +}
> +
> int ffmpeg_parse_options(int argc, char **argv)
> {
>     OptionParseContext octx;
> @@ -3467,6 +3578,8 @@ int ffmpeg_parse_options(int argc, char **argv)
>         goto fail;
>     }
> 
> +    print_json_inputs();
> +
>     /* create the complex filtergraphs */
>     ret = init_complex_filters();
>     if (ret < 0) {
> @@ -3688,6 +3801,8 @@ const OptionDef options[] = {
>         "enable automatic conversion filters globally" },
>     { "stats",          OPT_BOOL,                                    { &print_stats },
>         "print progress report during encoding", },
> +    { "jsonstats",      OPT_BOOL,                                    { &print_jsonstats },
> +        "print JSON progress report during encoding", },
>     { "stats_period",    HAS_ARG | OPT_EXPERT,                       { .func_arg = opt_stats_period },
>         "set the period at which ffmpeg updates stats and -progress output", "time" },
>     { "attach",         HAS_ARG | OPT_PERFILE | OPT_EXPERT |
> 
> base-commit: 5d5a01419928d0c00bae54f730eede150cd5b268
> -- 
> 2.32.1 (Apple Git-133)
> 

ping

Any comments on this patch? Thanks

--
Ingo
Anton Khirnov July 28, 2022, 2:09 p.m. UTC | #2
Sorry, I do not like your patch. The problem is that this essentialy
codifies ffmpeg's internal structure and makes into a kind of a public
interface, which makes it harder for us to change it.

Given that I'm currently in the middle of a big reshuffle of ffmpeg's
internals, this patch would conflict with my work.
And even after I'm done, we should think very carefully about exactly
which parts of ffmpeg behaviour (if any at all) we want to guarantee.
Bodecs Bela July 31, 2022, 1:15 p.m. UTC | #3
2022.07.28. 16:09 keltezéssel, Anton Khirnov írta:
> Sorry, I do not like your patch. The problem is that this essentialy
> codifies ffmpeg's internal structure and makes into a kind of a public
> interface, which makes it harder for us to change it.
>
> Given that I'm currently in the middle of a big reshuffle of ffmpeg's
> internals, this patch would conflict with my work.
> And even after I'm done, we should think very carefully about exactly
> which parts of ffmpeg behaviour (if any at all) we want to guarantee.

Hi Anton,

I agree with you and we may split this whole thing into two separate 
decision:

- should we make something in ffmpeg to be able monitored by automation 
(e.g. a "monitoring api" - for long running ffmpeg processes)

- if the answer is yes, then we need to discuss about how we should do it.

My answer is yes for the first question but I haven't got the knowledge 
to make a good proposal for the second question,

but maybe anybody else can.

best,

Bela
diff mbox series

Patch

diff --git a/doc/ffmpeg.texi b/doc/ffmpeg.texi
index 0d7e1a479d..16fcd9970a 100644
--- a/doc/ffmpeg.texi
+++ b/doc/ffmpeg.texi
@@ -784,6 +784,13 @@  disable it you need to specify @code{-nostats}.
 @item -stats_period @var{time} (@emph{global})
 Set period at which encoding progress/statistics are updated. Default is 0.5 seconds.
 
+@item -jsonstats (@emph{global})
+Print inputs, outputs, stream mapping, and encoding progress/statistics. It is off by
+default. It modifies the output of @code{-stats} to be JSON. The inputs, outputs,
+stream mapping, and progress information are written on one line and are prefixed
+with @var{json.inputs:}, @var{json.outputs:}, @var{json.mapping:}, and @var{json.progress:}
+respectively followed by the JSON data.
+
 @item -progress @var{url} (@emph{global})
 Send program-friendly progress information to @var{url}.
 
@@ -792,6 +799,9 @@  the encoding process. It is made of "@var{key}=@var{value}" lines. @var{key}
 consists of only alphanumeric characters. The last key of a sequence of
 progress information is always "progress".
 
+If @code{-jsonstats} is enabled, the progress information is written as JSON with
+the prefixes and data 
+
 The update period is set using @code{-stats_period}.
 
 @anchor{stdin option}
diff --git a/fftools/ffmpeg.c b/fftools/ffmpeg.c
index 5ed287c522..eea1491ed1 100644
--- a/fftools/ffmpeg.c
+++ b/fftools/ffmpeg.c
@@ -1505,7 +1505,7 @@  static void print_final_stats(int64_t total_size)
     }
 }
 
-static void print_report(int is_last_report, int64_t timer_start, int64_t cur_time)
+static void print_default_report(int is_last_report, int64_t timer_start, int64_t cur_time)
 {
     AVBPrint buf, buf_script;
     OutputStream *ost;
@@ -1695,7 +1695,7 @@  static void print_report(int is_last_report, int64_t timer_start, int64_t cur_ti
     }
     av_bprint_finalize(&buf, NULL);
 
-    if (progress_avio) {
+    if (progress_avio && !print_jsonstats) {
         av_bprintf(&buf_script, "progress=%s\n",
                    is_last_report ? "end" : "continue");
         avio_write(progress_avio, buf_script.str,
@@ -1715,6 +1715,200 @@  static void print_report(int is_last_report, int64_t timer_start, int64_t cur_ti
         print_final_stats(total_size);
 }
 
+/**
+ * Print progress report in JSON format
+ *
+ * @param is_last_report Whether this is the last report
+ * @param timer_start Time when the processing started
+ * @param cur_time Current processing time of the stream
+ */
+static void print_json_report(int is_last_report, int64_t timer_start, int64_t cur_time)
+{
+    AVBPrint buf;
+    InputStream *ist;
+    OutputStream *ost;
+    uint64_t stream_size, total_packets = 0, total_size = 0;
+    AVCodecContext *enc;
+    int i, j;
+    double speed;
+    int64_t pts = INT64_MIN + 1;
+    static int first_report = 1;
+    static int64_t last_time = -1;
+    int hours, mins, secs, us;
+    const char *hours_sign;
+    float t, q;
+
+    if (!is_last_report) {
+        if (last_time == -1) {
+            last_time = cur_time;
+        }
+        if (((cur_time - last_time) < stats_period && !first_report) ||
+            (first_report && nb_output_dumped < nb_output_files))
+            return;
+        last_time = cur_time;
+    }
+
+    t = (cur_time - timer_start) / 1000000.0;
+
+    av_bprint_init(&buf, 0, AV_BPRINT_SIZE_UNLIMITED);
+
+    av_bprintf(&buf, "json.progress:{");
+    av_bprintf(&buf, "\"inputs\":[");
+    for (i = 0; i < nb_input_files; i++) {
+        InputFile *f = input_files[i];
+
+        for (j = 0; j < f->nb_streams; j++) {
+            ist = input_streams[f->ist_index + j];
+
+            av_bprintf(&buf, "{");
+            av_bprintf(&buf, "\"index\":%d,\"stream\":%d,", i, j);
+
+            av_bprintf(&buf,
+                "\"frame\":%" PRIu64 ",\"packet\":%" PRIu64 ",",
+                !ist->frames_decoded ? ist->nb_packets : ist->frames_decoded,
+                ist->nb_packets);
+
+            av_bprintf(&buf, "\"size_bytes\":%" PRIu64, ist->data_size);
+
+            if (i == (nb_input_files - 1) && j == (f->nb_streams - 1)) {
+                av_bprintf(&buf, "}");
+            } else {
+                av_bprintf(&buf, "},");
+            }
+        }
+    }
+
+    av_bprintf(&buf, "],");
+
+    av_bprintf(&buf, "\"outputs\":[");
+    for (i = 0; i < nb_output_streams; i++) {
+        q = -1;
+        ost = output_streams[i];
+        enc = ost->enc_ctx;
+        if (!ost->stream_copy) {
+            q = ost->quality / (float)FF_QP2LAMBDA;
+        }
+
+        av_bprintf(&buf, "{");
+        av_bprintf(
+            &buf, "\"index\":%d,\"stream\":%d,", ost->file_index, ost->index);
+
+        av_bprintf(&buf,
+            "\"frame\":%" PRIu64 ",\"packet\":%" PRIu64 ",",
+            !ost->frames_encoded ? ost->packets_written : ost->frames_encoded,
+            ost->packets_written);
+
+        if (enc->codec_type == AVMEDIA_TYPE_VIDEO) {
+            av_bprintf(&buf, "\"q\":%.1f,", q);
+        }
+
+        /* compute min output value */
+        if (av_stream_get_end_pts(ost->st) != AV_NOPTS_VALUE) {
+            pts = FFMAX(pts,
+                av_rescale_q(av_stream_get_end_pts(ost->st),
+                    ost->st->time_base,
+                    AV_TIME_BASE_Q));
+            if (copy_ts) {
+                if (copy_ts_first_pts == AV_NOPTS_VALUE && pts > 1)
+                    copy_ts_first_pts = pts;
+                if (copy_ts_first_pts != AV_NOPTS_VALUE)
+                    pts -= copy_ts_first_pts;
+            }
+        }
+
+        total_packets += ost->packets_written;
+
+        if (is_last_report) {
+            nb_frames_drop += ost->last_dropped;
+        }
+
+        stream_size = ost->data_size + ost->enc_ctx->extradata_size;
+        total_size += stream_size;
+
+        av_bprintf(&buf, "\"size_bytes\":%" PRIu64, stream_size);
+
+        if (i == (nb_output_streams - 1)) {
+            av_bprintf(&buf, "}");
+        } else {
+            av_bprintf(&buf, "},");
+        }
+    }
+
+    av_bprintf(&buf, "],");
+
+    av_bprintf(&buf,
+        "\"packet\":%" PRIu64 ",\"size_bytes\":%" PRIu64 ",",
+        total_packets,
+        total_size);
+
+    secs = FFABS(pts) / AV_TIME_BASE;
+    us = FFABS(pts) % AV_TIME_BASE;
+    mins = secs / 60;
+    secs %= 60;
+    hours = mins / 60;
+    mins %= 60;
+    hours_sign = (pts < 0) ? "-" : "";
+
+    if (pts != AV_NOPTS_VALUE) {
+        av_bprintf(&buf,
+            "\"time\":\"%s%dh%dm%d.%ds\",",
+            hours_sign,
+            hours,
+            mins,
+            secs,
+            (100 * us) / AV_TIME_BASE);
+    }
+
+    speed = t != 0.0 ? (double)pts / AV_TIME_BASE / t : -1;
+    av_bprintf(&buf, "\"speed\":%.3g,", speed);
+
+    av_bprintf(&buf, "\"dup\":%d,\"drop\":%d", nb_frames_dup, nb_frames_drop);
+    av_bprintf(&buf, "}\n");
+
+    if (print_stats || is_last_report) {
+        if (AV_LOG_INFO > av_log_get_level()) {
+            fprintf(stderr, "%s", buf.str);
+        } else {
+            av_log(NULL, AV_LOG_INFO, "%s", buf.str);
+        }
+
+        fflush(stderr);
+    }
+
+    if (progress_avio) {
+        avio_write(progress_avio, buf.str, FFMIN(buf.len, buf.size - 1));
+        avio_flush(progress_avio);
+        if (is_last_report) {
+            av_bprint_clear(&buf);
+            av_bprintf(&buf, "ffmpeg.progress:NULL\n");
+            avio_write(progress_avio, buf.str, FFMIN(buf.len, buf.size - 1));
+            int ret;
+            if ((ret = avio_closep(&progress_avio)) < 0) {
+                av_log(NULL,
+                    AV_LOG_ERROR,
+                    "Error closing progress log, loss of information possible: %s\n",
+                    av_err2str(ret));
+            }
+        }
+    }
+
+    first_report = 0;
+
+    av_bprint_finalize(&buf, NULL);
+}
+
+static void print_report(int is_last_report, int64_t timer_start, int64_t cur_time)
+{
+    if (!print_stats && !is_last_report && !progress_avio)
+        return;
+
+    if (print_jsonstats == 1) {
+        print_json_report(is_last_report, timer_start, cur_time);
+    } else {
+        print_default_report(is_last_report, timer_start, cur_time);
+    }
+}
+
 static int ifilter_parameters_from_codecpar(InputFilter *ifilter, AVCodecParameters *par)
 {
     int ret;
diff --git a/fftools/ffmpeg.h b/fftools/ffmpeg.h
index 7326193caf..14968869d0 100644
--- a/fftools/ffmpeg.h
+++ b/fftools/ffmpeg.h
@@ -631,6 +631,7 @@  extern int debug_ts;
 extern int exit_on_error;
 extern int abort_on_flags;
 extern int print_stats;
+extern int print_jsonstats;
 extern int64_t stats_period;
 extern int qp_hist;
 extern int stdin_interaction;
diff --git a/fftools/ffmpeg_mux.c b/fftools/ffmpeg_mux.c
index 794d580635..c7771faad7 100644
--- a/fftools/ffmpeg_mux.c
+++ b/fftools/ffmpeg_mux.c
@@ -21,14 +21,20 @@ 
 
 #include "ffmpeg.h"
 
+#include "libavutil/bprint.h"
+#include "libavutil/channel_layout.h"
 #include "libavutil/fifo.h"
 #include "libavutil/intreadwrite.h"
 #include "libavutil/log.h"
 #include "libavutil/mem.h"
+#include "libavutil/pixdesc.h"
 #include "libavutil/timestamp.h"
 
+#include "libavcodec/avcodec.h"
 #include "libavcodec/packet.h"
 
+#include "libavfilter/avfilter.h"
+
 #include "libavformat/avformat.h"
 #include "libavformat/avio.h"
 
@@ -226,6 +232,305 @@  fail:
     return ret;
 }
 
+/**
+ * Write a graph as JSON to an initialized buffer
+ *
+ * @param buf Pointer to an initialized AVBPrint buffer
+ * @param graph Pointer to a AVFilterGraph
+ */
+static void print_json_graph(AVBPrint *buf, AVFilterGraph *graph)
+{
+    int i, j;
+
+    if (!graph) {
+        av_bprintf(buf, "null\n");
+        return;
+    }
+
+    av_bprintf(buf, "[");
+
+    for (i = 0; i < graph->nb_filters; i++) {
+        const AVFilterContext *filter_ctx = graph->filters[i];
+
+        for (j = 0; j < filter_ctx->nb_outputs; j++) {
+            AVFilterLink *link = filter_ctx->outputs[j];
+            if (link) {
+                const AVFilterContext *dst_filter_ctx = link->dst;
+
+                av_bprintf(buf,
+                    "{\"src_name\":\"%s\",\"src_filter\":\"%s\",\"dst_name\":\"%s\",\"dst_filter\":\"%s\",",
+                    filter_ctx->name,
+                    filter_ctx->filter->name,
+                    dst_filter_ctx->name,
+                    dst_filter_ctx->filter->name);
+                av_bprintf(buf,
+                    "\"inpad\":\"%s\",\"outpad\":\"%s\",",
+                    avfilter_pad_get_name(link->srcpad, 0),
+                    avfilter_pad_get_name(link->dstpad, 0));
+                av_bprintf(buf,
+                    "\"timebase\":\"%d/%d\",",
+                    link->time_base.num,
+                    link->time_base.den);
+
+                if (link->type == AVMEDIA_TYPE_VIDEO) {
+                    const AVPixFmtDescriptor *desc =
+                        av_pix_fmt_desc_get(link->format);
+                    av_bprintf(buf,
+                        "\"type\":\"video\",\"format\":\"%s\",\"width\":%d,\"height\":%d",
+                        desc->name,
+                        link->w,
+                        link->h);
+                } else if (link->type == AVMEDIA_TYPE_AUDIO) {
+                    char layout[255];
+                    av_channel_layout_describe(
+                        &link->ch_layout, layout, sizeof(layout));
+                    av_bprintf(buf,
+                        "\"type\":\"audio\",\"format\":\"%s\",\"sampling_hz\":%d,\"layout\":\"%s\"",
+                        av_get_sample_fmt_name(link->format),
+                        link->sample_rate,
+                        layout);
+                }
+
+                if (i == (graph->nb_filters - 1)) {
+                    av_bprintf(buf, "}");
+                } else {
+                    av_bprintf(buf, "},");
+                }
+            }
+        }
+    }
+
+    av_bprintf(buf, "]");
+}
+
+/**
+ * Print all outputs in JSON format
+ */
+static void print_json_outputs()
+{
+    static int ost_all_initialized = 0;
+    int i, j, k;
+    int nb_initialized = 0;
+    AVBPrint buf;
+
+    if (!print_jsonstats) {
+        return;
+    }
+
+    if (ost_all_initialized == 1) {
+        return;
+    }
+
+    // count how many outputs are initialized
+    for (i = 0; i < nb_output_streams; i++) {
+        OutputStream *ost = output_streams[i];
+        if (ost->initialized) {
+            nb_initialized++;
+        }
+    }
+
+    // only when all outputs are initialized, dump the outputs
+    if (nb_initialized == nb_output_streams) {
+        ost_all_initialized = 1;
+    }
+
+    if (ost_all_initialized != 1) {
+        return;
+    }
+
+    av_bprint_init(&buf, 0, AV_BPRINT_SIZE_UNLIMITED);
+
+    av_bprintf(&buf, "json.outputs:[");
+    for (i = 0; i < nb_output_streams; i++) {
+        OutputStream *ost = output_streams[i];
+        OutputFile *f = output_files[ost->file_index];
+        AVFormatContext *ctx = f->ctx;
+        AVStream *st = ost->st;
+        AVDictionaryEntry *lang =
+            av_dict_get(st->metadata, "language", NULL, 0);
+        AVCodecContext *enc = ost->enc_ctx;
+        char *url = NULL;
+
+        if (av_escape(&url,
+                ctx->url,
+                "\\\"",
+                AV_ESCAPE_MODE_BACKSLASH,
+                AV_UTF8_FLAG_ACCEPT_ALL) < 0) {
+            url = av_strdup("-");
+        }
+
+        av_bprintf(&buf, "{");
+        av_bprintf(&buf,
+            "\"url\":\"%s\",\"format\":\"%s\",\"index\":%d,\"stream\":%d,",
+            url,
+            ctx->oformat->name,
+            ost->file_index,
+            ost->index);
+        av_bprintf(&buf,
+            "\"type\":\"%s\",\"codec\":\"%s\",\"coder\":\"%s\",\"bitrate_kbps\":%" PRId64
+            ",",
+            av_get_media_type_string(enc->codec_type),
+            avcodec_get_name(enc->codec_id),
+            ost->stream_copy ? "copy"
+                             : (enc->codec ? enc->codec->name : "unknown"),
+            enc->bit_rate / 1000);
+        av_bprintf(&buf,
+            "\"duration_sec\":%f,\"language\":\"%s\"",
+            0.0,
+            lang ? lang->value : "und");
+
+        av_free(url);
+
+        if (enc->codec_type == AVMEDIA_TYPE_VIDEO) {
+            float fps = 0;
+            if (st->avg_frame_rate.den && st->avg_frame_rate.num) {
+                fps = av_q2d(st->avg_frame_rate);
+            }
+
+            av_bprintf(&buf,
+                ",\"fps\":%f,\"pix_fmt\":\"%s\",\"width\":%d,\"height\":%d",
+                fps,
+                st->codecpar->format == AV_PIX_FMT_NONE
+                    ? "none"
+                    : av_get_pix_fmt_name(st->codecpar->format),
+                st->codecpar->width,
+                st->codecpar->height);
+        } else if (enc->codec_type == AVMEDIA_TYPE_AUDIO) {
+            char layout[128];
+            av_channel_layout_describe(&enc->ch_layout, layout, sizeof(layout));
+
+            av_bprintf(&buf,
+                ",\"sampling_hz\":%d,\"layout\":\"%s\",\"channels\":%d",
+                enc->sample_rate,
+                layout,
+                enc->ch_layout.nb_channels);
+        }
+
+        if (i == (nb_output_streams - 1)) {
+            av_bprintf(&buf, "}");
+        } else {
+            av_bprintf(&buf, "},");
+        }
+    }
+
+    av_bprintf(&buf, "]\n");
+
+    av_log(NULL, AV_LOG_INFO, "%s", buf.str);
+
+    if (progress_avio) {
+        avio_write(progress_avio, buf.str, FFMIN(buf.len, buf.size - 1));
+        avio_flush(progress_avio);
+    }
+
+    av_bprint_clear(&buf);
+
+    av_bprintf(&buf, "json.mapping:{");
+    av_bprintf(&buf, "\"graphs\":[");
+
+    for (i = 0; i < nb_filtergraphs; i++) {
+        av_bprintf(&buf, "{\"index\":%d,\"graph\":", i);
+        print_json_graph(&buf, filtergraphs[i]->graph);
+
+        if (i == (nb_filtergraphs - 1)) {
+            av_bprintf(&buf, "}");
+        } else {
+            av_bprintf(&buf, "},");
+        }
+    }
+
+    av_bprintf(&buf, "],");
+
+    // The following is inspired by tools/graph2dot.c
+
+    av_bprintf(&buf, "\"mapping\":[");
+
+    for (i = 0; i < nb_input_streams; i++) {
+        InputStream *ist = input_streams[i];
+
+        for (j = 0; j < ist->nb_filters; j++) {
+            if (ist->filters[j]->graph) {
+                char *name = NULL;
+                for (k = 0; k < ist->filters[j]->graph->nb_inputs; k++) {
+                    if (ist->filters[j]->graph->inputs[k]->ist == ist) {
+                        name = ist->filters[j]->graph->inputs[k]->filter->name;
+                        break;
+                    }
+                }
+
+                av_bprintf(&buf,
+                    "{\"input\":{\"index\":%d,\"stream\":%d},\"graph\":{\"index\":%d,\"name\":\"%s\"},\"output\":null},",
+                    ist->file_index,
+                    ist->st->index,
+                    ist->filters[j]->graph->index,
+                    name);
+            }
+        }
+    }
+
+    for (i = 0; i < nb_output_streams; i++) {
+        OutputStream *ost = output_streams[i];
+
+        if (ost->attachment_filename) {
+            av_bprintf(&buf,
+                "{\"input\":null,\"file\":\"%s\",\"output\":{\"index\":%d,\"stream\":%d}},",
+                ost->attachment_filename,
+                ost->file_index,
+                ost->index);
+            goto next_output;
+        }
+
+        if (ost->filter && ost->filter->graph) {
+            char *name = NULL;
+            for (j = 0; j < ost->filter->graph->nb_outputs; j++) {
+                if (ost->filter->graph->outputs[j]->ost == ost) {
+                    name = ost->filter->graph->outputs[j]->filter->name;
+                    break;
+                }
+            }
+            av_bprintf(&buf,
+                "{\"input\":null,\"graph\":{\"index\":%d,\"name\":\"%s\"},\"output\":{\"index\":%d,\"stream\":%d}}",
+                ost->filter->graph->index,
+                name,
+                ost->file_index,
+                ost->index);
+            goto next_output;
+        }
+
+        av_bprintf(&buf,
+            "{\"input\":{\"index\":%d,\"stream\":%d},\"output\":{\"index\":%d,\"stream\":%d}",
+            input_streams[ost->source_index]->file_index,
+            input_streams[ost->source_index]->st->index,
+            ost->file_index,
+            ost->index);
+        av_bprintf(&buf, ",\"copy\":%s", ost->stream_copy ? "true" : "false");
+
+        if (ost->sync_ist != input_streams[ost->source_index]) {
+            av_bprintf(&buf,
+                ",\"sync\":{\"index\":%d,\"stream\":%d}",
+                ost->sync_ist->file_index,
+                ost->sync_ist->st->index);
+        }
+
+        av_bprintf(&buf, "}");
+
+    next_output:
+        if (i != (nb_output_streams - 1)) {
+            av_bprintf(&buf, ",");
+        }
+    }
+
+    av_bprintf(&buf, "]}\n");
+
+    av_log(NULL, AV_LOG_INFO, "%s", buf.str);
+
+    if (progress_avio) {
+        avio_write(progress_avio, buf.str, FFMIN(buf.len, buf.size - 1));
+        avio_flush(progress_avio);
+    }
+
+    av_bprint_finalize(&buf, NULL);
+}
+
 /* open the muxer when all the streams are initialized */
 int of_check_init(OutputFile *of)
 {
@@ -251,6 +556,8 @@  int of_check_init(OutputFile *of)
     av_dump_format(of->ctx, of->index, of->ctx->url, 1);
     nb_output_dumped++;
 
+    print_json_outputs();
+
     if (sdp_filename || want_sdp) {
         ret = print_sdp();
         if (ret < 0) {
diff --git a/fftools/ffmpeg_opt.c b/fftools/ffmpeg_opt.c
index 2c1b3bd0dd..20e991eca5 100644
--- a/fftools/ffmpeg_opt.c
+++ b/fftools/ffmpeg_opt.c
@@ -51,6 +51,7 @@ 
 #include "libavutil/parseutils.h"
 #include "libavutil/pixdesc.h"
 #include "libavutil/pixfmt.h"
+#include "libavutil/bprint.h"
 
 #define DEFAULT_PASS_LOGFILENAME_PREFIX "ffmpeg2pass"
 
@@ -169,6 +170,7 @@  int debug_ts          = 0;
 int exit_on_error     = 0;
 int abort_on_flags    = 0;
 int print_stats       = -1;
+int print_jsonstats   = 0;
 int qp_hist           = 0;
 int stdin_interaction = 1;
 float max_error_rate  = 2.0/3;
@@ -3434,6 +3436,115 @@  static int open_files(OptionGroupList *l, const char *inout,
     return 0;
 }
 
+/**
+ * Print all inputs in JSON format
+ */
+static void print_json_inputs()
+{
+    if (!print_jsonstats) {
+        return;
+    }
+
+    AVBPrint buf;
+    int i, j;
+
+    av_bprint_init(&buf, 0, AV_BPRINT_SIZE_UNLIMITED);
+
+    av_bprintf(&buf, "json.inputs:[");
+    for (i = 0; i < nb_input_files; i++) {
+        InputFile *f = input_files[i];
+        AVFormatContext *ctx = f->ctx;
+
+        float duration = 0;
+        if (ctx->duration != AV_NOPTS_VALUE) {
+            duration = (float)(ctx->duration +
+                           (ctx->duration <= INT64_MAX - 5000 ? 5000 : 0)) /
+                (float)AV_TIME_BASE;
+        }
+
+        for (j = 0; j < f->nb_streams; j++) {
+            InputStream *ist = input_streams[f->ist_index + j];
+            AVCodecContext *dec = ist->dec_ctx;
+            AVStream *st = ist->st;
+            AVDictionaryEntry *lang =
+                av_dict_get(st->metadata, "language", NULL, 0);
+            char *url = NULL;
+
+            if (av_escape(&url,
+                    ctx->url,
+                    "\\\"",
+                    AV_ESCAPE_MODE_BACKSLASH,
+                    AV_UTF8_FLAG_ACCEPT_ALL) < 0) {
+                url = av_strdup("-");
+            }
+
+            av_bprintf(&buf, "{");
+            av_bprintf(&buf,
+                "\"url\":\"%s\",\"format\":\"%s\",\"index\":%d,\"stream\":%d,",
+                url,
+                ctx->iformat->name,
+                i,
+                j);
+            av_bprintf(&buf,
+                "\"type\":\"%s\",\"codec\":\"%s\",\"coder\":\"%s\",\"bitrate_kbps\":%" PRId64
+                ",",
+                av_get_media_type_string(dec->codec_type),
+                avcodec_get_name(dec->codec_id),
+                dec->codec ? dec->codec->name : "unknown",
+                dec->bit_rate / 1000);
+            av_bprintf(&buf,
+                "\"duration_sec\":%f,\"language\":\"%s\"",
+                duration,
+                lang ? lang->value : "und");
+
+            av_free(url);
+
+            if (dec->codec_type == AVMEDIA_TYPE_VIDEO) {
+                float fps = 0;
+                if (st->avg_frame_rate.den && st->avg_frame_rate.num) {
+                    fps = av_q2d(st->avg_frame_rate);
+                }
+
+                av_bprintf(&buf,
+                    ",\"fps\":%f,\"pix_fmt\":\"%s\",\"width\":%d,\"height\":%d",
+                    fps,
+                    st->codecpar->format == AV_PIX_FMT_NONE
+                        ? "none"
+                        : av_get_pix_fmt_name(st->codecpar->format),
+                    st->codecpar->width,
+                    st->codecpar->height);
+            } else if (dec->codec_type == AVMEDIA_TYPE_AUDIO) {
+                char layout[128];
+                av_channel_layout_describe(
+                    &dec->ch_layout, layout, sizeof(layout));
+
+                av_bprintf(&buf,
+                    ",\"sampling_hz\":%d,\"layout\":\"%s\",\"channels\":%d",
+                    dec->sample_rate,
+                    layout,
+                    dec->ch_layout.nb_channels);
+            }
+
+            if (i == (nb_input_files - 1) && j == (f->nb_streams - 1)) {
+                av_bprintf(&buf, "}");
+            } else {
+                av_bprintf(&buf, "},");
+            }
+        }
+    }
+
+    av_bprintf(&buf, "]\n");
+
+    av_log(NULL, AV_LOG_INFO, "%s", buf.str);
+
+    if (progress_avio) {
+        avio_write(progress_avio, buf.str, FFMIN(buf.len, buf.size - 1));
+        avio_flush(progress_avio);
+    }
+
+    av_bprint_finalize(&buf, NULL);
+}
+
 int ffmpeg_parse_options(int argc, char **argv)
 {
     OptionParseContext octx;
@@ -3467,6 +3578,8 @@  int ffmpeg_parse_options(int argc, char **argv)
         goto fail;
     }
 
+    print_json_inputs();
+
     /* create the complex filtergraphs */
     ret = init_complex_filters();
     if (ret < 0) {
@@ -3688,6 +3801,8 @@  const OptionDef options[] = {
         "enable automatic conversion filters globally" },
     { "stats",          OPT_BOOL,                                    { &print_stats },
         "print progress report during encoding", },
+    { "jsonstats",      OPT_BOOL,                                    { &print_jsonstats },
+        "print JSON progress report during encoding", },
     { "stats_period",    HAS_ARG | OPT_EXPERT,                       { .func_arg = opt_stats_period },
         "set the period at which ffmpeg updates stats and -progress output", "time" },
     { "attach",         HAS_ARG | OPT_PERFILE | OPT_EXPERT |