Thilo Borgmann via ffmpeg-devel:
> ---
> Changelog | 1 +
> MAINTAINERS | 1 +
> doc/filters.texi | 33 +++++
> libavfilter/Makefile | 1 +
> libavfilter/allfilters.c | 1 +
> libavfilter/version.h | 2 +-
> libavfilter/vf_fsync.c | 304 +++++++++++++++++++++++++++++++++++++++
> 7 files changed, 342 insertions(+), 1 deletion(-)
> create mode 100644 libavfilter/vf_fsync.c
>
> diff --git a/Changelog b/Changelog
> index 67ef92eb02..a25278d227 100644
> --- a/Changelog
> +++ b/Changelog
> @@ -9,6 +9,7 @@ version <next>:
> - aap filter
> - demuxing, decoding, filtering, encoding, and muxing in the
> ffmpeg CLI now all run in parallel
> +- fsync filter
>
> version 6.1:
> - libaribcaption decoder
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 39b37ee0c5..4257fcad98 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -343,6 +343,7 @@ Filters:
> vf_delogo.c Jean Delvare (CC <[email protected]>)
> vf_drawbox.c/drawgrid Andrey Utkin
> vf_extractplanes.c Paul B Mahol
> + vf_fsync.c Thilo Borgmann
> vf_histogram.c Paul B Mahol
> vf_hqx.c Clément Bœsch
> vf_idet.c Pascal Massimino
> diff --git a/doc/filters.texi b/doc/filters.texi
> index 6d00ba2c3f..9f19cba9df 100644
> --- a/doc/filters.texi
> +++ b/doc/filters.texi
> @@ -14681,6 +14681,39 @@ option may cause flicker since the B-Frames have
> often larger QP. Default is
>
> @end table
>
> +@anchor{fsync}
> +@section fsync
> +
> +Synchronize video frames with an external mapping from a file.
> +
> +For each input PTS given in the map file it either drops or creates as many
> frames as necessary to recreate the sequence of output frames given in the
> map file.
> +
> +This filter is useful to recreate the output frames of a framerate
> conversion by the @ref{fps} filter, recorded into a map file using the ffmpeg
> option @code{-stats_mux_pre}, and do further processing to the corresponding
> frames e.g. quality comparison.
> +
> +Each line of the map file must contain three items per input frame, the
> input PTS (decimal), the output PTS (decimal) and the output TIMEBASE
> (decimal/decimal), seperated by a space.
> +This file format corresponds to the output of
> @code{-stats_mux_pre_fmt="@{ptsi@} @{pts@} @{tb@}"}.
> +
> +The filter assumes the map file is sorted by increasing input PTS.
> +
> +The filter accepts the following options:
> +@table @option
> +
> +@item file, f
> +The filename of the map file to be used.
> +@end table
> +
> +Example:
> +@example
> +# Convert a video to 25 fps and record a MAP_FILE file with the default
> format of this filter
> +ffmpeg -i INPUT -vf fps=fps=25 -stats_mux_pre MAP_FILE -stats_mux_pre_fmt
> "@{ptsi@} @{pts@} @{tb@}" OUTPUT
> +
> +# Sort MAP_FILE by increasing input PTS
> +sort -n MAP_FILE
> +
> +# Use INPUT, OUTPUT and the MAP_FILE from above to compare the corresponding
> frames in INPUT and OUTPUT via SSIM
> +ffmpeg -i INPUT -i OUTPUT -filter_complex
> '[0:v]fsync=file=MAP_FILE[ref];[1:v][ref]ssim' -f null -
> +@end example
> +
> @section gblur
>
> Apply Gaussian blur filter.
> diff --git a/libavfilter/Makefile b/libavfilter/Makefile
> index 63725f91b4..612616dfb4 100644
> --- a/libavfilter/Makefile
> +++ b/libavfilter/Makefile
> @@ -323,6 +323,7 @@ OBJS-$(CONFIG_FREEZEDETECT_FILTER) +=
> vf_freezedetect.o
> OBJS-$(CONFIG_FREEZEFRAMES_FILTER) += vf_freezeframes.o
> OBJS-$(CONFIG_FREI0R_FILTER) += vf_frei0r.o
> OBJS-$(CONFIG_FSPP_FILTER) += vf_fspp.o qp_table.o
> +OBJS-$(CONFIG_FSYNC_FILTER) += vf_fsync.o
> OBJS-$(CONFIG_GBLUR_FILTER) += vf_gblur.o
> OBJS-$(CONFIG_GBLUR_VULKAN_FILTER) += vf_gblur_vulkan.o vulkan.o
> vulkan_filter.o
> OBJS-$(CONFIG_GEQ_FILTER) += vf_geq.o
> diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
> index ed7c32be94..b32ffb2d71 100644
> --- a/libavfilter/allfilters.c
> +++ b/libavfilter/allfilters.c
> @@ -299,6 +299,7 @@ extern const AVFilter ff_vf_freezedetect;
> extern const AVFilter ff_vf_freezeframes;
> extern const AVFilter ff_vf_frei0r;
> extern const AVFilter ff_vf_fspp;
> +extern const AVFilter ff_vf_fsync;
> extern const AVFilter ff_vf_gblur;
> extern const AVFilter ff_vf_gblur_vulkan;
> extern const AVFilter ff_vf_geq;
> diff --git a/libavfilter/version.h b/libavfilter/version.h
> index 7642b670d1..59330858bd 100644
> --- a/libavfilter/version.h
> +++ b/libavfilter/version.h
> @@ -31,7 +31,7 @@
>
> #include "version_major.h"
>
> -#define LIBAVFILTER_VERSION_MINOR 14
> +#define LIBAVFILTER_VERSION_MINOR 15
> #define LIBAVFILTER_VERSION_MICRO 100
>
>
> diff --git a/libavfilter/vf_fsync.c b/libavfilter/vf_fsync.c
> new file mode 100644
> index 0000000000..3ce6f22d06
> --- /dev/null
> +++ b/libavfilter/vf_fsync.c
> @@ -0,0 +1,304 @@
> +/*
> + * Copyright (c) 2023 Thilo Borgmann <thilo.borgmann _at_ mail.de>
> + *
> + * This file is part of FFmpeg.
> + *
> + * FFmpeg is free software; you can redistribute it and/or
> + * modify it under the terms of the GNU Lesser General Public
> + * License as published by the Free Software Foundation; either
> + * version 2.1 of the License, or (at your option) any later version.
> + *
> + * FFmpeg is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> + * Lesser General Public License for more details.
> + *
> + * You should have received a copy of the GNU Lesser General Public
> + * License along with FFmpeg; if not, write to the Free Software
> + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
> USA
> + */
> +
> +/**
> + * @file
> + * Filter for syncing video frames from external source
> + *
> + * @author Thilo Borgmann <thilo.borgmann _at_ mail.de>
> + */
> +
> +#include "libavutil/avstring.h"
> +#include "libavutil/error.h"
> +#include "libavutil/opt.h"
> +#include "libavformat/avio.h"
> +#include "video.h"
> +#include "filters.h"
> +
> +#define BUF_SIZE 256
> +
> +typedef struct FsyncContext {
> + const AVClass *class;
> + AVIOContext *avio_ctx; // reading the map file
> + AVFrame *last_frame; // buffering the last frame for duplicating
> eventually
> + char *filename; // user-specified map file
> + char *buf; // line buffer for the map file
> + char *cur; // current position in the line buffer
> + char *end; // end pointer of the line buffer
> + int64_t ptsi; // input pts to map to [0-N] output pts
> + int64_t pts; // output pts
> + int64_t tb_num; // output timebase num
> + int64_t tb_den; // output timebase den
> +} FsyncContext;
> +
> +#define OFFSET(x) offsetof(FsyncContext, x)
> +#define DEFINE_OPTIONS(filt_name, FLAGS)
Why is this a macro?
\
> +static const AVOption filt_name##_options[] = {
> \
> + { "file", "set the file name to use for frame sync", OFFSET(filename),
> AV_OPT_TYPE_STRING, { .str = "" }, .flags=FLAGS }, \
> + { "f", "set the file name to use for frame sync", OFFSET(filename),
> AV_OPT_TYPE_STRING, { .str = "" }, .flags=FLAGS }, \
> + { NULL }
> \
> +}
> +
> +// fills the buffer from cur to end, add \0 at EOF
> +static int buf_fill(FsyncContext *ctx)
> +{
> + int ret;
> + int num = ctx->end - ctx->cur;
> +
> + ret = avio_read(ctx->avio_ctx, ctx->cur, num);
> + if (ret < 0)
> + return ret;
> + if (ret < num) {
> + *(ctx->cur + ret) = '\0';
> + }
> +
> + return ret;
> +}
> +
> +// copies cur to end to the beginning and fills the rest
> +static int buf_reload(FsyncContext *ctx)
> +{
> + int i, ret;
> + int num = ctx->end - ctx->cur;
> +
> + for (i = 0; i < num; i++) {
> + ctx->buf[i] = *ctx->cur++;
> + }
> +
> + ctx->cur = ctx->buf + i;
> + ret = buf_fill(ctx);
> + if (ret < 0)
> + return ret;
> + ctx->cur = ctx->buf;
I wonder whether you should not just use avio_read_to_bprint() for all
of this.
> +
> + return ret;
> +}
> +
> +// skip from cur over eol
> +static void buf_skip_eol(FsyncContext *ctx)
> +{
> + char *i;
> + for (i = ctx->cur; i < ctx->end; i++) {
> + if (*i != '\n')// && *i != '\r')
> + break;
> + }
> + ctx->cur = i;
> +}
> +
> +// get number of bytes from cur until eol
> +static int buf_get_line_count(FsyncContext *ctx)
> +{
> + int ret = 0;
> + char *i;
> + for (i = ctx->cur; i < ctx->end; i++, ret++) {
> + if (*i == '\0' || *i == '\n')
> + return ret;
If you unconditionally added a single \0 to the end of the buffer, you
could use strchr() here.
> + }
> +
> + return -1;
> +}
> +
> +// get number of bytes from cur to '\0'
> +static int buf_get_zero(FsyncContext *ctx)
> +{
> + int ret = 0;
> + char *i;
> + for (i = ctx->cur; i < ctx->end; i++, ret++) {
> + if (*i == '\0')
> + return ret;
strnlen?
> + }
> +
> + return ret;
> +}
> +
> +static int activate(AVFilterContext *ctx)
> +{
> + FsyncContext *s = ctx->priv;
> + AVFilterLink *inlink = ctx->inputs[0];
> + AVFilterLink *outlink = ctx->outputs[0];
> +
> + int ret, line_count;
> + AVFrame *frame;
> +
> + FF_FILTER_FORWARD_STATUS_BACK(outlink, inlink);
> +
> + buf_skip_eol(s);
> + line_count = buf_get_line_count(s);
> + if (line_count < 0) {
> + line_count = buf_reload(s);
> + if (line_count < 0)
> + return line_count;
> + line_count = buf_get_line_count(s);
> + if (line_count < 0)
> + return line_count;
> + }
> +
> + if (avio_feof(s->avio_ctx) && buf_get_zero(s) < 3) {
> + av_log(ctx, AV_LOG_DEBUG, "End of file. To zero = %i\n",
> buf_get_zero(s));
> + goto end;
> + }
> +
> + if (s->last_frame) {
> + ret = av_sscanf(s->cur, "%"PRId64" %"PRId64" %d/%d", &s->ptsi,
> &s->pts, &s->tb_num, &s->tb_den);
> + if (ret != 4) {
> + av_log(ctx, AV_LOG_ERROR, "Unexpected format found (%i / 4).\n",
> ret);
> + ff_outlink_set_status(outlink, AVERROR_INVALIDDATA,
> AV_NOPTS_VALUE);
> + return AVERROR_INVALIDDATA;
> + }
> +
> + av_log(ctx, AV_LOG_DEBUG, "frame %lli ", s->last_frame->pts);
> +
> + if (s->last_frame->pts >= s->ptsi) {
> + av_log(ctx, AV_LOG_DEBUG, ">= %lli: DUP LAST with pts = %lli\n",
> s->ptsi, s->pts);
> +
> + // clone frame
> + frame = av_frame_clone(s->last_frame);
> + if (!frame) {
> + ff_outlink_set_status(outlink, AVERROR(ENOMEM),
> AV_NOPTS_VALUE);
> + return AVERROR(ENOMEM);
> + }
> + av_frame_copy_props(frame, s->last_frame);
Unnecessary, as the properties of frame are already equivalent to the
properties of last_frame.
> +
> + // set output pts and timebase
> + frame->pts = s->pts;
> + frame->time_base = av_make_q((int)s->tb_num, (int)s->tb_den);
> +
> + // advance cur to eol, skip over eol in the next call
> + s->cur += line_count;
> +
> + // call again
> + if (ff_inoutlink_check_flow(inlink, outlink))
> + ff_filter_set_ready(ctx, 100);
> +
> + // filter frame
> + return ff_filter_frame(outlink, frame);
> + } else if (s->last_frame->pts < s->ptsi) {
> + av_log(ctx, AV_LOG_DEBUG, "< %lli: DROP\n", s->ptsi);
> + av_frame_free(&s->last_frame);
> +
> + // call again
> + if (ff_inoutlink_check_flow(inlink, outlink))
> + ff_filter_set_ready(ctx, 100);
> +
> + return 0;
> + }
> + }
> +
> +end:
> + ret = ff_inlink_consume_frame(inlink, &s->last_frame);
> + if (ret < 0)
> + return ret;
> +
> + FF_FILTER_FORWARD_STATUS(inlink, outlink);
> + FF_FILTER_FORWARD_WANTED(outlink, inlink);
> +
> + return FFERROR_NOT_READY;
> +}
> +
> +static int fsync_config_props(AVFilterLink* outlink)
> +{
> + AVFilterContext *ctx = outlink->src;
> + FsyncContext *s = ctx->priv;
> + int ret;
> +
> + // read first line to get output timebase
> + ret = av_sscanf(s->cur, "%"PRId64" %"PRId64" %d/%d", &s->ptsi, &s->pts,
> &s->tb_num, &s->tb_den);
tb_num, tb_den are int64_t, yet you use %d to read into them.
> + if (ret != 4) {
> + av_log(ctx, AV_LOG_ERROR, "Unexpected format found (%i of 4).\n",
> ret);
> + ff_outlink_set_status(outlink, AVERROR_INVALIDDATA, AV_NOPTS_VALUE);
> + return AVERROR_INVALIDDATA;
> + }
> +
> + outlink->frame_rate = av_make_q(1, 0); // unknown or dynamic
> + outlink->time_base = av_make_q((int)s->tb_num, (int)s->tb_den);
> +
> + return 0;
> +}
> +
> +static av_cold int fsync_init(AVFilterContext *ctx)
> +{
> + FsyncContext *s = ctx->priv;
> + int ret;
> +
> + av_log(ctx, AV_LOG_DEBUG, "filename: %s\n", s->filename);
> +
> + s->buf = av_malloc(BUF_SIZE);
> + if (!s->buf)
> + return AVERROR(ENOMEM);
> +
> + ret = avio_open(&s->avio_ctx, s->filename, AVIO_FLAG_READ);
Given that you are using avio, you probably need a avformat dependency
in configure.
> + if (ret < 0)
> + return ret;
> +
> + s->cur = s->buf;
> + s->end = s->buf + BUF_SIZE;
> +
> + ret = buf_fill(s);
> + if (ret < 0)
> + return ret;
> +
> + return 0;
> +}
> +
> +static av_cold void fsync_uninit(AVFilterContext *ctx)
> +{
> + FsyncContext *s = ctx->priv;
> +
> + avio_close(s->avio_ctx);
avio_closep()
> + av_freep(&s->buf);
> + av_frame_unref(s->last_frame);
I expect that this needs to be changed to av_frame_free(). Anyway, you
should run your tests via valgrind/asan.
> +}
> +
> +DEFINE_OPTIONS(fsync, AV_OPT_FLAG_VIDEO_PARAM|AV_OPT_FLAG_FILTERING_PARAM);
> +AVFILTER_DEFINE_CLASS(fsync);
> +
> +static const enum AVPixelFormat pix_fmts[] = {
> + AV_PIX_FMT_GRAY8,
> + AV_PIX_FMT_GBRP, AV_PIX_FMT_GBRAP,
> + AV_PIX_FMT_YUV422P, AV_PIX_FMT_YUV420P,
> + AV_PIX_FMT_YUV444P, AV_PIX_FMT_YUV440P,
> + AV_PIX_FMT_YUV411P, AV_PIX_FMT_YUV410P,
> + AV_PIX_FMT_YUVJ440P, AV_PIX_FMT_YUVJ411P, AV_PIX_FMT_YUVJ420P,
> + AV_PIX_FMT_YUVJ422P, AV_PIX_FMT_YUVJ444P,
> + AV_PIX_FMT_YUVA444P, AV_PIX_FMT_YUVA422P, AV_PIX_FMT_YUVA420P,
> + AV_PIX_FMT_NONE
> +};
Why is this filter limited to specific pixel formats? Shouldn't it
accept all of them?
> +
> +static const AVFilterPad avfilter_vf_fsync_outputs[] = {
Don't use a name that indicates that this is exported. (I know that
other filters have the same issue...)
> + {
> + .name = "default",
> + .type = AVMEDIA_TYPE_VIDEO,
> + .config_props = fsync_config_props,
> + },
> +};
> +
> +const AVFilter ff_vf_fsync = {
> + .name = "fsync",
> + .description = NULL_IF_CONFIG_SMALL("Synchronize video frames from
> external source."),
> + .init = fsync_init,
> + .uninit = fsync_uninit,
> + .priv_size = sizeof(FsyncContext),
> + .priv_class = &fsync_class,
> + .activate = activate,
> + FILTER_PIXFMTS_ARRAY(pix_fmts),
> + FILTER_INPUTS(ff_video_default_filterpad),
> + FILTER_OUTPUTS(avfilter_vf_fsync_outputs),
> + .flags = AVFILTER_FLAG_METADATA_ONLY,
> +};
_______________________________________________
ffmpeg-devel mailing list
[email protected]
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
To unsubscribe, visit link above, or email
[email protected] with subject "unsubscribe".