From patchwork Thu Feb 25 08:33:55 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Werner Robitza X-Patchwork-Id: 25984 Return-Path: X-Original-To: patchwork@ffaux-bg.ffmpeg.org Delivered-To: patchwork@ffaux-bg.ffmpeg.org Received: from ffbox0-bg.mplayerhq.hu (ffbox0-bg.ffmpeg.org [79.124.17.100]) by ffaux.localdomain (Postfix) with ESMTP id 40DA744B4E3 for ; Thu, 25 Feb 2021 10:34:13 +0200 (EET) Received: from [127.0.1.1] (localhost [127.0.0.1]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTP id 187B46880FF; Thu, 25 Feb 2021 10:34:13 +0200 (EET) 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 60903680466 for ; Thu, 25 Feb 2021 10:34:06 +0200 (EET) Received: by mail-ed1-f44.google.com with SMTP id h10so5731358edl.6 for ; Thu, 25 Feb 2021 00:34:06 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=EC/0osjKzeWxQ141c02valNqNQ6nSm1UXZuIPw/8RPM=; b=MzNKxZi4NMIQppYL3NlDRqqVvAazvZuSK/q3jFG2Shp86Sr4pxGrR4cfsjU6GzbVuH IRS47Klid5Sa4wBKLb/BHTEilYoRp6wJMdKsmmazMv5nOJNjy5M165m9PCVoywCJG2tq moaMfhRQtbqIf4muRj5gm41/4/1RaOrk/WEjAwhODZ7GklG433DWkQNj0+eVOVW/5Bzn ZylJLca9LhyB49BqZn8JDZR9QFdMAYDOqsUR6bxyf4uUYWXhK4P7kCDi1cz9FWLrwQhH 0ljbjvObzj22haOdOmXf2qXsKykTLQKiBVIX54smF5/puGTztA7ANFaXFdI3qLFAf0oW oJeg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=EC/0osjKzeWxQ141c02valNqNQ6nSm1UXZuIPw/8RPM=; b=YPsOwY369SrNCiyNJYzTox//TEcLSIicEpZJ9jUMDRbUHtjurkzlU2mtr4qoOXr3Uu P+ElKHbTLjdPXQq6AnX+L+3A9eYRxZDWFFCCkebMQg18uvGmmRrib7u8O2T7vyOHWioC M0cWQ1aPaKOJHIu65qJTRJ3FyEX850cdwGcoTLQgFFbrcBsYPBmXsoQ+8wS7hRkpQuGv 4lc3dCBJX8+ly2z0YMIjzXtaTCL5L3UVQ09rMJnEJEMj2vmkqgc7OtuIvXhUCEyS/NAE kpnMgFYpAAKeNod6fNN1BMqmhDObFMDxwEecgZ1HeEYk3YCTzFtZ+Unu6OdobX6PJGQ6 zW5w== X-Gm-Message-State: AOAM531SBzJV1dB9upRwMyiNR427EdQYLqRlPuKJ8x2D/h9x0zi9kikY p4lST82uXbSsdFWTipFFZPIqC+lACWM= X-Google-Smtp-Source: ABdhPJzDyoGuoMURgfyH87xBvTC61EdfinuzBbwUAhPeHjHV2fXt4C4D5z8lldt9ttQ4otK84VdMKQ== X-Received: by 2002:aa7:c150:: with SMTP id r16mr1735910edp.96.1614242045362; Thu, 25 Feb 2021 00:34:05 -0800 (PST) Received: from localhost.localdomain (80-110-81-45.cgn.dynamic.surfer.at. [80.110.81.45]) by smtp.gmail.com with ESMTPSA id e22sm2930820edu.61.2021.02.25.00.34.04 (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); Thu, 25 Feb 2021 00:34:04 -0800 (PST) From: Werner Robitza To: ffmpeg-devel@ffmpeg.org Date: Thu, 25 Feb 2021 09:33:55 +0100 Message-Id: <20210225083355.72736-1-werner.robitza@gmail.com> X-Mailer: git-send-email 2.30.0 In-Reply-To: <20210224195354.60821-1-werner.robitza@gmail.com> References: <20210224195354.60821-1-werner.robitza@gmail.com> MIME-Version: 1.0 Subject: [FFmpeg-devel] [PATCH] filters/metadata: add CSV output support X-BeenThere: ffmpeg-devel@ffmpeg.org X-Mailman-Version: 2.1.20 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: Werner Robitza Errors-To: ffmpeg-devel-bounces@ffmpeg.org Sender: "ffmpeg-devel" This adds a new option to the metadata filter that allows outputting CSV data. The separator can be set via another option. Special characters are handled via escaping. Signed-off-by: Werner Robitza --- doc/filters.texi | 14 ++++ libavfilter/f_metadata.c | 155 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 158 insertions(+), 11 deletions(-) diff --git a/doc/filters.texi b/doc/filters.texi index 426cb158da..0b56d73565 100644 --- a/doc/filters.texi +++ b/doc/filters.texi @@ -25134,6 +25134,20 @@ with AV_LOG_INFO loglevel. @item direct Reduces buffering in print mode when output is written to a URL set using @var{file}. +@item format +Set the output format for the print mode. + +@table @samp +@item default +Default output format + +@item csv +Comma-separated output +@end table + +@item csv_sep +Set the CSV separator (only valid for CSV format). Default is @code{,}. Must be +one character and cannot be a period (@code{.}). @end table @subsection Examples diff --git a/libavfilter/f_metadata.c b/libavfilter/f_metadata.c index 598257b15b..45f5eeddcd 100644 --- a/libavfilter/f_metadata.c +++ b/libavfilter/f_metadata.c @@ -58,6 +58,11 @@ enum MetadataFunction { METADATAF_NB }; +enum MetadataFormat { + METADATA_FORMAT_DEFAULT, + METADATA_FORMAT_CSV +}; + static const char *const var_names[] = { "VALUE1", "VALUE2", @@ -85,6 +90,9 @@ typedef struct MetadataContext { AVIOContext* avio_context; char *file_str; + int format; + char *csv_sep; + int (*compare)(struct MetadataContext *s, const char *value1, const char *value2); void (*print)(AVFilterContext *ctx, const char *msg, ...) av_printf_format(2, 3); @@ -114,6 +122,10 @@ static const AVOption filt_name##_options[] = { \ { "expr", "set expression for expr function", OFFSET(expr_str), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, FLAGS }, \ { "file", "set file where to print metadata information", OFFSET(file_str), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 0, FLAGS }, \ { "direct", "reduce buffering when printing to user-set file or pipe", OFFSET(direct), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, FLAGS }, \ + { "format", "set output format", OFFSET(format), AV_OPT_TYPE_INT, {.i64 = 0 }, 0, METADATAF_NB-1, FLAGS, "format" }, \ + { "default", "default format", 0, AV_OPT_TYPE_CONST, {.i64 = METADATA_FORMAT_DEFAULT }, 0, 0, FLAGS, "format" }, \ + { "csv", "comma-separated", 0, AV_OPT_TYPE_CONST, {.i64 = METADATA_FORMAT_CSV }, 0, 0, FLAGS, "format" }, \ + { "csv_sep", "set CSV separator (only valid for CSV format)", OFFSET(csv_sep), AV_OPT_TYPE_STRING, {.str=","}, 0, 0, FLAGS }, \ { NULL } \ } @@ -202,6 +214,58 @@ static void print_file(AVFilterContext *ctx, const char *msg, ...) va_end(argument_list); } +static void print_csv_escaped(AVFilterContext *ctx, const char *src) +{ + MetadataContext *s = ctx->priv; + + char meta_chars[] = {s->csv_sep[0], '"', '\n', '\r', '\0'}; + int needs_quoting = !!src[strcspn(src, meta_chars)]; + + // allocate space for two extra quotes and possibly every char escaped + char buf[strlen(src) * 2 + 2]; + + int pos = 0; + + if (needs_quoting) + buf[pos++] = '"'; + + for (int i = 0; i < strlen(src); i++) { + if (src[i] == '"') + buf[pos++] = '\"'; + buf[pos++] = src[i]; + } + + if (needs_quoting) + buf[pos++] = '"'; + + buf[pos] = '\0'; + + s->print(ctx, "%s", buf); +} + +static void csv_escape(const char *src, char **dst, const char csv_sep) +{ + char meta_chars[] = {csv_sep, '"', '\n', '\r', '\0'}; + int needs_quoting = !!src[strcspn(src, meta_chars)]; + + int pos = 0; + + if (needs_quoting) + *dst[pos++] = '"'; + + for (int i = 0; i < strlen(src); i++) + { + if (src[i] == '"') + *dst[pos++] = '\"'; + *dst[pos++] = src[i]; + } + + if (needs_quoting) + *dst[pos++] = '"'; + + *dst[pos] = '\0'; +} + static av_cold int init(AVFilterContext *ctx) { MetadataContext *s = ctx->priv; @@ -282,6 +346,33 @@ static av_cold int init(AVFilterContext *ctx) s->avio_context->direct = AVIO_FLAG_DIRECT; } + if (s->format == METADATA_FORMAT_CSV) { + if (strlen(s->csv_sep) == 0) { + av_log(ctx, AV_LOG_ERROR, + "No CSV separator supplied\n"); + return AVERROR(EINVAL); + } + if (strlen(s->csv_sep) > 1) { + av_log(ctx, AV_LOG_ERROR, + "CSV separator must be one character only\n"); + return AVERROR(EINVAL); + } + if (s->csv_sep[0] == '.') { + av_log(ctx, AV_LOG_ERROR, + "CSV separator cannot be a single period ('.')\n"); + return AVERROR(EINVAL); + } + s->print( + ctx, + "%s%s%s%s%s%s%s%s%s\n", + "frame", s->csv_sep, + "pts", s->csv_sep, + "pts_time", s->csv_sep, + "key", s->csv_sep, + "value" + ); + } + return 0; } @@ -332,17 +423,59 @@ static int filter_frame(AVFilterLink *inlink, AVFrame *frame) } return ff_filter_frame(outlink, frame); case METADATA_PRINT: - if (!s->key && e) { - s->print(ctx, "frame:%-4"PRId64" pts:%-7s pts_time:%s\n", - inlink->frame_count_out, av_ts2str(frame->pts), av_ts2timestr(frame->pts, &inlink->time_base)); - s->print(ctx, "%s=%s\n", e->key, e->value); - while ((e = av_dict_get(*metadata, "", e, AV_DICT_IGNORE_SUFFIX)) != NULL) { - s->print(ctx, "%s=%s\n", e->key, e->value); - } - } else if (e && e->value && (!s->value || (e->value && s->compare(s, e->value, s->value)))) { - s->print(ctx, "frame:%-4"PRId64" pts:%-7s pts_time:%s\n", - inlink->frame_count_out, av_ts2str(frame->pts), av_ts2timestr(frame->pts, &inlink->time_base)); - s->print(ctx, "%s=%s\n", s->key, e->value); + switch(s->format) { + case METADATA_FORMAT_DEFAULT: + if (!s->key && e) { + s->print(ctx, "frame:%-4"PRId64" pts:%-7s pts_time:%s\n", + inlink->frame_count_out, av_ts2str(frame->pts), av_ts2timestr(frame->pts, &inlink->time_base)); + s->print(ctx, "%s=%s\n", e->key, e->value); + while ((e = av_dict_get(*metadata, "", e, AV_DICT_IGNORE_SUFFIX)) != NULL) { + s->print(ctx, "%s=%s\n", e->key, e->value); + } + } else if (e && e->value && (!s->value || (e->value && s->compare(s, e->value, s->value)))) { + s->print(ctx, "frame:%-4"PRId64" pts:%-7s pts_time:%s\n", + inlink->frame_count_out, av_ts2str(frame->pts), av_ts2timestr(frame->pts, &inlink->time_base)); + s->print(ctx, "%s=%s\n", s->key, e->value); + } + break; + case METADATA_FORMAT_CSV: + if (!s->key && e) { + s->print( + ctx, "%"PRId64"%s%s%s%s%s", + inlink->frame_count_out, s->csv_sep, + av_ts2str(frame->pts), s->csv_sep, + av_ts2timestr(frame->pts, &inlink->time_base), s->csv_sep + ); + print_csv_escaped(ctx, e->key); + s->print(ctx, "%s", s->csv_sep); + print_csv_escaped(ctx, e->value); + while ((e = av_dict_get(*metadata, "", e, AV_DICT_IGNORE_SUFFIX)) != NULL) { + s->print( + ctx, "%"PRId64"%s%s%s%s%s", + inlink->frame_count_out, s->csv_sep, + av_ts2str(frame->pts), s->csv_sep, + av_ts2timestr(frame->pts, &inlink->time_base), s->csv_sep + ); + print_csv_escaped(ctx, e->key); + s->print(ctx, "%s", s->csv_sep); + print_csv_escaped(ctx, e->value); + s->print(ctx, "\n"); + } + } else if (e && e->value && (!s->value || (e->value && s->compare(s, e->value, s->value)))) { + s->print( + ctx, "%"PRId64"%s%s%s%s%s", + inlink->frame_count_out, s->csv_sep, + av_ts2str(frame->pts), s->csv_sep, + av_ts2timestr(frame->pts, &inlink->time_base), s->csv_sep + ); + print_csv_escaped(ctx, e->key); + s->print(ctx, "%s", s->csv_sep); + print_csv_escaped(ctx, e->value); + s->print(ctx, "\n"); + } + break; + default: + av_assert0(0); } return ff_filter_frame(outlink, frame); case METADATA_DELETE: