Message ID | 20210224195354.60821-1-werner.robitza@gmail.com |
---|---|
State | Superseded |
Headers | show |
Series | [FFmpeg-devel] filters/metadata: add CSV output support | expand |
Context | Check | Description |
---|---|---|
andriy/x86_make | fail | Make failed |
andriy/PPC64_make | warning | Make failed |
Why duplicating code that would give same output as ffprobe? On Wed, Feb 24, 2021 at 9:01 PM Werner Robitza <werner.robitza@gmail.com> wrote: > 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 <werner.robitza@gmail.com> > --- > 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: > -- > 2.30.0 > > _______________________________________________ > ffmpeg-devel mailing list > ffmpeg-devel@ffmpeg.org > https://ffmpeg.org/mailman/listinfo/ffmpeg-devel > > To unsubscribe, visit link above, or email > ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".
On Wed, Feb 24, 2021 at 9:11 PM Paul B Mahol <onemda@gmail.com> wrote: > > Why duplicating code that would give same output as ffprobe? I was told to here: http://ffmpeg.org/pipermail/ffmpeg-devel/2021-February/275510.html Could please show an example on how to achieve this with ffprobe?
Werner Robitza (12021-02-24): > I was told to here: > http://ffmpeg.org/pipermail/ffmpeg-devel/2021-February/275510.html I never told you to duplicate code. Code duplication is almost always a motive for rejection. If you are tempted to duplicate code, what you need to do is to share it between the various places that use it. Making it a proper API if relevant. Ideally, all the writers of ffprobe should be turned into a single libavutil API for use in all components that need to output data. Regards,
On Wed, Feb 24, 2021 at 9:51 PM Nicolas George <george@nsup.org> wrote: > I never told you to duplicate code. Code duplication is almost always a > motive for rejection. > > If you are tempted to duplicate code, what you need to do is to share it > between the various places that use it. Making it a proper API if > relevant. I didn't really duplicate anything; this was mostly from scratch. The case could be made that a function for escaping CSV could be shared. There are at least two such functions already in the code base. > Ideally, all the writers of ffprobe should be turned into a single > libavutil API for use in all components that need to output data. That'd make sense.
On Wed, Feb 24, 2021 at 9:48 PM Werner Robitza <werner.robitza@gmail.com> wrote: > On Wed, Feb 24, 2021 at 9:11 PM Paul B Mahol <onemda@gmail.com> wrote: > > > > Why duplicating code that would give same output as ffprobe? > > I was told to here: > http://ffmpeg.org/pipermail/ffmpeg-devel/2021-February/275510.html > > Could please show an example on how to achieve this with ffprobe? > See same thread where I replied or use search.
On Thu, Feb 25, 2021 at 1:32 AM Paul B Mahol <onemda@gmail.com> wrote: > On Wed, Feb 24, 2021 at 9:48 PM Werner Robitza <werner.robitza@gmail.com> wrote: >> Could please show an example on how to achieve this with ffprobe? > See same thread where I replied or use search. If you are referring to: -vf your_filter,metadata=mode=print That does not work in ffprobe. For instance, using the patch I submitted, I can get easy to parse output like: $ ffmpeg -f lavfi -i testsrc -frames:v 1 -filter:v signalstats,metadata=mode=print:format=csv -f null /dev/null frame,pts,pts_time,key,value 0,0,0,lavfi.signalstats.YMIN,4 … How would I get the same with ffprobe?
Werner Robitza (12021-02-25): > If you are referring to: > > -vf your_filter,metadata=mode=print > > That does not work in ffprobe. Of course, since ffprobe does the printing for you. > For instance, using the patch I submitted, I can get easy to parse output like: Parsing the standard output of any program is bad design and bound to break at some point. Also, I wonder why you chose CSV, which is one of the worse possible formats. Regards,
Werner Robitza (12021-02-24): > I didn't really duplicate anything; this was mostly from scratch. The > case could be made that a function for escaping CSV could be shared. The variable names seemed quite similar. Anyway, duplicating a feature is just as bad as duplicating code. > There are at least two such functions already in the code base. We should not have accepted the second one like that. It does not constitute a precedent. Regards,
On Thu, Feb 25, 2021 at 11:25 AM Nicolas George <george@nsup.org> wrote: > > Werner Robitza (12021-02-25): > > If you are referring to: > > > > -vf your_filter,metadata=mode=print > > > > That does not work in ffprobe. > > Of course, since ffprobe does the printing for you. At the risk of repeating the same question, could you please show a command with ffprobe that achieves the same kind of parsable output that this patch generates? After fiddling around a bit, when I run: ffprobe -loglevel error -f lavfi -i "testsrc,signalstats" -show_entries tags -of csv=nk=0 | head -n 1 I get a line like: packet,tag:lavfi.signalstats.YMIN=16,tag:lavfi.signalstats.YLOW=16,… which is not semantically meaningful, since both keys and values are contained in the individual cells. This makes parsing incredibly more complex than it has to be. > > For instance, using the patch I submitted, I can get easy to parse output like: > > Parsing the standard output of any program is bad design and bound to > break at some point. That was just an example on the CLI. The filter has a file option with which you can output to a well-formatted CSV file, so parsing is not an issue there. > Also, I wonder why you chose CSV, which is one of the worse possible > formats. I feel this is only tangential to the discussion, but: It's well-defined, easy to parse by numerous libraries, can be modified easily, and is a de-facto standard for sharing data in certain communities (. It wouldn't even occur to me why you'd want another format for certain kinds of applications. In particular with data that isn't nested, it's much easier to use in data analysis procedures than JSON or others.
On Thu, Feb 25, 2021 at 11:26 AM Nicolas George <george@nsup.org> wrote: > Werner Robitza (12021-02-24): > > I didn't really duplicate anything; this was mostly from scratch. The > > case could be made that a function for escaping CSV could be shared. > > The variable names seemed quite similar. Anyway, duplicating a feature > is just as bad as duplicating code. Would you accept this patch if this particular function was extracted to an API and re-used by ffprobe? (And potentially segment.c …)
Werner Robitza (12021-02-25): > After fiddling around a bit, when I run: > > ffprobe -loglevel error -f lavfi -i "testsrc,signalstats" > -show_entries tags -of csv=nk=0 | head -n 1 > > I get a line like: > > packet,tag:lavfi.signalstats.YMIN=16,tag:lavfi.signalstats.YLOW=16,… I doubt ffprobe did output an ellipsis character on its own. For simpler parsing, use a different writer than csv. I would recommend flat. Regards,
Werner Robitza (12021-02-25): > Would you accept this patch if this particular function was extracted > to an API and re-used by ffprobe? (And potentially segment.c …) Yes, but I must warn you that the constraints on a libavutil API are stronger than the constraints on helper functions in ffprobe. This is why I re-started the following discussion: https://ffmpeg.org/pipermail/ffmpeg-devel/2021-February/276855.html You can weigh in. Regards,
On Thu, Feb 25, 2021 at 12:47 PM Nicolas George <george@nsup.org> wrote: > Werner Robitza (12021-02-25): > > After fiddling around a bit, when I run: > > > > ffprobe -loglevel error -f lavfi -i "testsrc,signalstats" > > -show_entries tags -of csv=nk=0 | head -n 1 > > > > I get a line like: > > > > packet,tag:lavfi.signalstats.YMIN=16,tag:lavfi.signalstats.YLOW=16,… > > I doubt ffprobe did output an ellipsis character on its own. That's why I wrote "like" – I am sure you were able to interpret the meaning of the ellipsis in this example. > For simpler parsing, use a different writer than csv. I would recommend > flat. That seems to be a custom format, so parsing is inherently more complex, and there's no support for simply loading this in whatever tool you have for data analysis (Excel, MATLAB, R, Python, …). Hence the usefulness of semantically meaningfully structured CSV. I guess I could live with replacing the "flat" output dots with commas, and the equal sign with another comma, but that's bound to break with some metadata value contents.
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:
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 <werner.robitza@gmail.com> --- doc/filters.texi | 14 ++++ libavfilter/f_metadata.c | 155 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 158 insertions(+), 11 deletions(-)