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 <jdelv...@suse.com>)
>    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
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-requ...@ffmpeg.org with subject "unsubscribe".

Reply via email to