From patchwork Thu Jun 9 12:47:00 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Ingo Oppermann X-Patchwork-Id: 36094 Delivered-To: ffmpegpatchwork2@gmail.com Received: by 2002:a05:6a20:6914:b0:82:6b11:2509 with SMTP id q20csp350655pzj; Thu, 9 Jun 2022 05:47:33 -0700 (PDT) X-Google-Smtp-Source: ABdhPJwkorGdZwCYZWD/VNxBdki6TGH2ictGJ9Ry8f7gNFRn3Jrsda/+1i/P3mvJgV9kTEk3wlxS X-Received: by 2002:a17:906:fc12:b0:711:d2e9:99d9 with SMTP id ov18-20020a170906fc1200b00711d2e999d9mr18161389ejb.734.1654778853006; Thu, 09 Jun 2022 05:47:33 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1654778853; cv=none; d=google.com; s=arc-20160816; b=eT8dqd74oYB83bm18uMr/MVIg9316fQ1soMrgFrnD4EbkXUAVuUFEJJGRcR0GyiKF/ VC+y7Z3mJq/S5MzFvaxCsEaxTnCK/xNe4d6/jZyojtbqEQ62gLR50UOi8hATNaDHlbV5 a5O9qaJ7+m1OC2Clal0hIhXEzkJ6VwRerlnWzKvhhmEFi6yC7c1r10Zw7Mkelf9ZiM8Z DxP+i1jJQt75KjqCTz0tiblOnaGN7n74aDmw6h/rhUfX+VQm7cJwlJV75yLHoM5F9VUl XHGD7b9gxRNh3iyWKdnoDiDbchtA83nvzGnuP1fQjSNDg3/8rFZnngM8DqpNRL1xVeSR ulMQ== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816; h=sender:errors-to:content-transfer-encoding:cc:reply-to :list-subscribe:list-help:list-post:list-archive:list-unsubscribe :list-id:precedence:subject:mime-version:message-id:date:to:from :dkim-signature:delivered-to; bh=lqLpPlul/QYUyE1EkXRQB0AMBKd7jafidZlHwuRK3fg=; b=j6VINUhwQX6QUrjqnbk86QAScIDkV7sRVKkx11WSBowc3Q+45OOmtvOS7uC3FijCtj IE40hlgp3p4MOneVTBByKLs2hdKdkyZzlBJizGrP8SguLSkVfu1IKkaSiH0EiPcXgb3Z irDfCCpvJSX7YCfi/eYT1r/4jqxepcWWAS4fE44njr/A0a6ir6DSWLetR3zXZUbXj1ZX nBWB0RQklISfgKWzuzWSXW8wm7BLQtPzJdAHL32W+X4VKPm4VctVh4YBZl3fZel3zwNp ZvAPX+VXRGQ0iXPiBpMGaAqcsUZdOz62hu+l1pcLR90syfpcGQm/yGVNdooz9GcZJALO NH1w== ARC-Authentication-Results: i=1; mx.google.com; dkim=neutral (body hash did not verify) header.i=@datarhei.com header.s=google header.b=Dy1NtEYG; spf=pass (google.com: domain of ffmpeg-devel-bounces@ffmpeg.org designates 79.124.17.100 as permitted sender) smtp.mailfrom=ffmpeg-devel-bounces@ffmpeg.org Return-Path: Received: from ffbox0-bg.mplayerhq.hu (ffbox0-bg.ffmpeg.org. [79.124.17.100]) by mx.google.com with ESMTP id o15-20020a170906974f00b00705973ded91si3315301ejy.453.2022.06.09.05.47.32; Thu, 09 Jun 2022 05:47:32 -0700 (PDT) Received-SPF: pass (google.com: domain of ffmpeg-devel-bounces@ffmpeg.org designates 79.124.17.100 as permitted sender) client-ip=79.124.17.100; Authentication-Results: mx.google.com; dkim=neutral (body hash did not verify) header.i=@datarhei.com header.s=google header.b=Dy1NtEYG; spf=pass (google.com: domain of ffmpeg-devel-bounces@ffmpeg.org designates 79.124.17.100 as permitted sender) smtp.mailfrom=ffmpeg-devel-bounces@ffmpeg.org Received: from [127.0.1.1] (localhost [127.0.0.1]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTP id 035C568B3AB; Thu, 9 Jun 2022 15:47:28 +0300 (EEST) X-Original-To: ffmpeg-devel@ffmpeg.org Delivered-To: ffmpeg-devel@ffmpeg.org Received: from mail-ed1-f44.google.com (mail-ed1-f44.google.com [209.85.208.44]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTPS id 0974D68B3AB for ; Thu, 9 Jun 2022 15:47:19 +0300 (EEST) Received: by mail-ed1-f44.google.com with SMTP id b8so14376130edj.11 for ; Thu, 09 Jun 2022 05:47:19 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=datarhei.com; s=google; h=from:to:cc:subject:date:message-id:mime-version :content-transfer-encoding; bh=zkg8YLryBeS/0nm61w6wveiQkUhF6goBzPmcYkQsvlc=; b=Dy1NtEYGcIy9FFEU/QKcoTTeH5PesM01FCUqdKKhBLtzJBqwXSxsov3Y6P7nWqw0Qk kRv0eJulj64hIstFy0jYU3od4IG1cjYAHs67VXrQx5YfbRarIthyujWD4AMpzbR+CprS rW4yX5tu6CvrS2N55sRHQeEpl3HqzDFHNubUwsnPOnVrfs14ILl3ThFO1aPw8Xl5NYy3 dn4h0rFOs7/WJhN0xkNbDkHx6ROsuH6dWyOSjy1CPCXpRAEZwt5L7k7SWimN/ZW+3mvb mbGLes/tiv1V7sFQEMcCdO9IJUSDpnUxji1FBb2RPzfLWwsULWNa5CQDgZ9+rimgmw1y hEMQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:from:to:cc:subject:date:message-id:mime-version :content-transfer-encoding; bh=zkg8YLryBeS/0nm61w6wveiQkUhF6goBzPmcYkQsvlc=; b=QJR8D1so8s/ib65X0Lc2Hs3cEOG7RKWgin7Yq0Vy4G3y/dwopO1h5qpjLzC63BZBlF pO/PRElU/3CWP1t0d4C3yhgH8R7CdfrvJ4gbUUa5lmPPI5T8nP1ORcikikNhY6xgE8qO WdD9vWawh/MnVJu79tY438bh3dlLXLn5zGRQ+JHJeL/5hNNfv6yZbvLJA334JMmIqWnZ mOCxOFSCeGC9B3Rg1LEUyyY2+bW6GpGNFBeCMuGcWoSiiXD2pdairKaEJkZEOaIcUsHe 22mwQ2ynUt9nyBrH0k6LMKa60JSkK/Ns5pHsSSHhOoNKK5dpjWKbDvlT9Up6pR5YuJVp q6RQ== X-Gm-Message-State: AOAM532qImI/OMq8ulXyQSn6mI8hnIdSa40svbCSZ4nadMCpmRfxGABb FfEhGPYdUwuULrZTC1ofNaYnFjDacIdK X-Received: by 2002:a05:6402:f0d:b0:42d:d813:c13a with SMTP id i13-20020a0564020f0d00b0042dd813c13amr45977016eda.207.1654778837708; Thu, 09 Jun 2022 05:47:17 -0700 (PDT) Received: from localhost.localdomain (adsl-178-38-92-185.adslplus.ch. [178.38.92.185]) by smtp.gmail.com with ESMTPSA id j1-20020a508a81000000b0042aca5edba7sm14382008edj.57.2022.06.09.05.47.16 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 09 Jun 2022 05:47:17 -0700 (PDT) From: Ingo Oppermann To: ffmpeg-devel@ffmpeg.org Date: Thu, 9 Jun 2022 14:47:00 +0200 Message-Id: <20220609124700.81727-1-ingo@datarhei.com> X-Mailer: git-send-email 2.32.1 (Apple Git-133) MIME-Version: 1.0 Subject: [FFmpeg-devel] [PATCH v1] ffmpeg: add optional JSON output of inputs, outputs, mapping, and progress X-BeenThere: ffmpeg-devel@ffmpeg.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: FFmpeg development discussions and patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: FFmpeg development discussions and patches Cc: Ingo Oppermann Errors-To: ffmpeg-devel-bounces@ffmpeg.org Sender: "ffmpeg-devel" X-TUID: 6eKYmJaFEimn 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 --- 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 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 |