From 972ded33c08a820303ecf050942f0e74400eda7a Mon Sep 17 00:00:00 2001 From: Marton Balint Date: Sun, 4 Nov 2018 22:22:24 +0100 Subject: [PATCH] avfilter/vf_freezedetect: add filter to detect frozen input Signed-off-by: Marton Balint --- Changelog | 1 + configure | 1 + doc/filters.texi | 29 +++++ libavfilter/Makefile | 1 + libavfilter/allfilters.c | 1 + libavfilter/version.h | 2 +- libavfilter/vf_freezedetect.c | 236 ++++++++++++++++++++++++++++++++++ 7 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 libavfilter/vf_freezedetect.c diff --git a/Changelog b/Changelog index 012eed945e5..fb2e43de2c8 100644 --- a/Changelog +++ b/Changelog @@ -6,6 +6,7 @@ version : - AV1 decoding support through libdav1d - dedot filter - chromashift and rgbashift filters +- freezedetect filter version 4.1: diff --git a/configure b/configure index dee901d6076..b4f944cfb05 100755 --- a/configure +++ b/configure @@ -3403,6 +3403,7 @@ firequalizer_filter_deps="avcodec" firequalizer_filter_select="rdft" flite_filter_deps="libflite" framerate_filter_select="scene_sad" +freezedetect_filter_select="scene_sad" frei0r_filter_deps="frei0r libdl" frei0r_src_filter_deps="frei0r libdl" fspp_filter_deps="gpl" diff --git a/doc/filters.texi b/doc/filters.texi index 6b1bf8766b1..d2d9788d8fd 100644 --- a/doc/filters.texi +++ b/doc/filters.texi @@ -10069,6 +10069,35 @@ Select frame after every @code{step} frames. Allowed values are positive integers higher than 0. Default value is @code{1}. @end table +@section freezedetect + +Detect frozen video. + +This filter logs a message and sets frame metadata when it detects that the +input video has no significant change in content during a specified duration. +Video freeze detection calculates the mean average absolute difference of all +the components of video frames and compares it to a noise floor. + +The printed times and duration are expressed in seconds. The +@code{lavfi.freezedetect.freeze_start} metadata key is set on the first frame +whose timestamp equals or exceeds the detection duration and it contains the +timestamp of the first frame of the freeze. The +@code{lavfi.freezedetect.freeze_duration} and +@code{lavfi.freezedetect.freeze_end} metadata keys are set on the first frame +after the freeze. + +The filter accepts the following options: + +@table @option +@item noise, n +Set noise tolerance. Can be specified in dB (in case "dB" is appended to the +specified value) or as a difference ratio between 0 and 1. Default is -60dB, or +0.001. + +@item duration, d +Set freeze duration until notification (default is 2 seconds). +@end table + @anchor{frei0r} @section frei0r diff --git a/libavfilter/Makefile b/libavfilter/Makefile index 4df22db2fc7..1895fa2b0d9 100644 --- a/libavfilter/Makefile +++ b/libavfilter/Makefile @@ -238,6 +238,7 @@ OBJS-$(CONFIG_FPS_FILTER) += vf_fps.o OBJS-$(CONFIG_FRAMEPACK_FILTER) += vf_framepack.o OBJS-$(CONFIG_FRAMERATE_FILTER) += vf_framerate.o OBJS-$(CONFIG_FRAMESTEP_FILTER) += vf_framestep.o +OBJS-$(CONFIG_FREEZEDETECT_FILTER) += vf_freezedetect.o OBJS-$(CONFIG_FREI0R_FILTER) += vf_frei0r.o OBJS-$(CONFIG_FSPP_FILTER) += vf_fspp.o OBJS-$(CONFIG_GBLUR_FILTER) += vf_gblur.o diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c index 7294486314e..837c99eb75b 100644 --- a/libavfilter/allfilters.c +++ b/libavfilter/allfilters.c @@ -224,6 +224,7 @@ extern AVFilter ff_vf_fps; extern AVFilter ff_vf_framepack; extern AVFilter ff_vf_framerate; extern AVFilter ff_vf_framestep; +extern AVFilter ff_vf_freezedetect; extern AVFilter ff_vf_frei0r; extern AVFilter ff_vf_fspp; extern AVFilter ff_vf_gblur; diff --git a/libavfilter/version.h b/libavfilter/version.h index 2e48b0c4529..76b212305ab 100644 --- a/libavfilter/version.h +++ b/libavfilter/version.h @@ -30,7 +30,7 @@ #include "libavutil/version.h" #define LIBAVFILTER_VERSION_MAJOR 7 -#define LIBAVFILTER_VERSION_MINOR 45 +#define LIBAVFILTER_VERSION_MINOR 46 #define LIBAVFILTER_VERSION_MICRO 100 #define LIBAVFILTER_VERSION_INT AV_VERSION_INT(LIBAVFILTER_VERSION_MAJOR, \ diff --git a/libavfilter/vf_freezedetect.c b/libavfilter/vf_freezedetect.c new file mode 100644 index 00000000000..299a5dfbf67 --- /dev/null +++ b/libavfilter/vf_freezedetect.c @@ -0,0 +1,236 @@ +/* + * 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 + * video freeze detection filter + */ + +#include "libavutil/avassert.h" +#include "libavutil/imgutils.h" +#include "libavutil/opt.h" +#include "libavutil/pixdesc.h" +#include "libavutil/timestamp.h" + +#include "avfilter.h" +#include "filters.h" +#include "scene_sad.h" + +typedef struct FreezeDetectContext { + const AVClass *class; + + ptrdiff_t width[4]; + ptrdiff_t height[4]; + ff_scene_sad_fn sad; + int bitdepth; + AVFrame *reference_frame; + int64_t n; + int64_t reference_n; + int frozen; + + double noise; + int64_t duration; ///< minimum duration of frozen frame until notification +} FreezeDetectContext; + +#define OFFSET(x) offsetof(FreezeDetectContext, x) +#define V AV_OPT_FLAG_VIDEO_PARAM +#define F AV_OPT_FLAG_FILTERING_PARAM + +static const AVOption freezedetect_options[] = { + { "n", "set noise tolerance", OFFSET(noise), AV_OPT_TYPE_DOUBLE, {.dbl=0.001}, 0, 1.0, V|F }, + { "noise", "set noise tolerance", OFFSET(noise), AV_OPT_TYPE_DOUBLE, {.dbl=0.001}, 0, 1.0, V|F }, + { "d", "set minimum duration in seconds", OFFSET(duration), AV_OPT_TYPE_DURATION, {.i64=2000000}, 0, INT64_MAX, V|F }, + { "duration", "set minimum duration in seconds", OFFSET(duration), AV_OPT_TYPE_DURATION, {.i64=2000000}, 0, INT64_MAX, V|F }, + + {NULL} +}; + +AVFILTER_DEFINE_CLASS(freezedetect); + +static int query_formats(AVFilterContext *ctx) +{ + static const enum AVPixelFormat pix_fmts[] = { + AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUYV422, AV_PIX_FMT_RGB24, + AV_PIX_FMT_BGR24, AV_PIX_FMT_YUV422P, AV_PIX_FMT_YUV444P, + AV_PIX_FMT_YUV410P, AV_PIX_FMT_YUV411P, AV_PIX_FMT_GRAY8, + AV_PIX_FMT_YUVJ420P, AV_PIX_FMT_YUVJ422P, AV_PIX_FMT_YUVJ444P, + AV_PIX_FMT_UYVY422, AV_PIX_FMT_NV12, AV_PIX_FMT_NV21, AV_PIX_FMT_ARGB, + AV_PIX_FMT_RGBA, AV_PIX_FMT_ABGR, AV_PIX_FMT_BGRA, AV_PIX_FMT_GRAY16, + AV_PIX_FMT_YUV440P, AV_PIX_FMT_YUVJ440P, AV_PIX_FMT_YUVA420P, + AV_PIX_FMT_YUV420P16, AV_PIX_FMT_YUV422P16, AV_PIX_FMT_YUV444P16, + AV_PIX_FMT_YA8, AV_PIX_FMT_YUV420P9, AV_PIX_FMT_YUV420P10, + AV_PIX_FMT_YUV422P10, AV_PIX_FMT_YUV444P9, AV_PIX_FMT_YUV444P10, + AV_PIX_FMT_YUV422P9, AV_PIX_FMT_GBRP, AV_PIX_FMT_GBRP9, + AV_PIX_FMT_GBRP10, AV_PIX_FMT_GBRP16, AV_PIX_FMT_YUVA422P, + AV_PIX_FMT_YUVA444P, AV_PIX_FMT_YUVA420P9, AV_PIX_FMT_YUVA422P9, + AV_PIX_FMT_YUVA444P9, AV_PIX_FMT_YUVA420P10, AV_PIX_FMT_YUVA422P10, + AV_PIX_FMT_YUVA444P10, AV_PIX_FMT_YUVA420P16, AV_PIX_FMT_YUVA422P16, + AV_PIX_FMT_YUVA444P16, AV_PIX_FMT_NV16, AV_PIX_FMT_YVYU422, + AV_PIX_FMT_GBRAP, AV_PIX_FMT_GBRAP16, AV_PIX_FMT_YUV420P12, + AV_PIX_FMT_YUV420P14, AV_PIX_FMT_YUV422P12, AV_PIX_FMT_YUV422P14, + AV_PIX_FMT_YUV444P12, AV_PIX_FMT_YUV444P14, AV_PIX_FMT_GBRP12, + AV_PIX_FMT_GBRP14, AV_PIX_FMT_YUVJ411P, AV_PIX_FMT_YUV440P10, + AV_PIX_FMT_YUV440P12, AV_PIX_FMT_GBRAP12, AV_PIX_FMT_GBRAP10, + AV_PIX_FMT_GRAY12, AV_PIX_FMT_GRAY10, AV_PIX_FMT_GRAY9, + AV_PIX_FMT_GRAY14, + AV_PIX_FMT_NONE + }; + + AVFilterFormats *fmts_list = ff_make_format_list(pix_fmts); + if (!fmts_list) + return AVERROR(ENOMEM); + return ff_set_common_formats(ctx, fmts_list); +} + +static int config_input(AVFilterLink *inlink) +{ + AVFilterContext *ctx = inlink->dst; + FreezeDetectContext *s = ctx->priv; + const AVPixFmtDescriptor *pix_desc = av_pix_fmt_desc_get(inlink->format); + + s->bitdepth = pix_desc->comp[0].depth; + + for (int plane = 0; plane < 4; plane++) { + ptrdiff_t line_size = av_image_get_linesize(inlink->format, inlink->w, plane); + s->width[plane] = line_size >> (s->bitdepth > 8); + s->height[plane] = inlink->h >> ((plane == 1 || plane == 2) ? pix_desc->log2_chroma_h : 0); + } + + s->sad = ff_scene_sad_get_fn(s->bitdepth == 8 ? 8 : 16); + if (!s->sad) + return AVERROR(EINVAL); + + return 0; +} + +static av_cold void uninit(AVFilterContext *ctx) +{ + FreezeDetectContext *s = ctx->priv; + av_frame_free(&s->reference_frame); +} + +static int is_frozen(FreezeDetectContext *s, AVFrame *reference, AVFrame *frame) +{ + uint64_t sad = 0; + uint64_t count = 0; + double mafd; + for (int plane = 0; plane < 4; plane++) { + if (s->width[plane]) { + uint64_t plane_sad; + s->sad(frame->data[plane], frame->linesize[plane], + reference->data[plane], reference->linesize[plane], + s->width[plane], s->height[plane], &plane_sad); + sad += plane_sad; + count += s->width[plane] * s->height[plane]; + } + } + emms_c(); + mafd = (double)sad / count / (1ULL << s->bitdepth); + return (mafd <= s->noise); +} + +static int set_meta(FreezeDetectContext *s, AVFrame *frame, const char *key, const char *value) +{ + av_log(s, AV_LOG_INFO, "%s: %s\n", key, value); + return av_dict_set(&frame->metadata, key, value, 0); +} + +static int activate(AVFilterContext *ctx) +{ + int ret; + AVFilterLink *inlink = ctx->inputs[0]; + AVFilterLink *outlink = ctx->outputs[0]; + FreezeDetectContext *s = ctx->priv; + AVFrame *frame; + + FF_FILTER_FORWARD_STATUS_BACK(outlink, inlink); + + ret = ff_inlink_consume_frame(inlink, &frame); + if (ret < 0) + return ret; + + if (frame) { + int frozen = 0; + s->n++; + + if (s->reference_frame) { + int64_t duration; + if (s->reference_frame->pts == AV_NOPTS_VALUE || frame->pts == AV_NOPTS_VALUE || frame->pts < s->reference_frame->pts) // Discontinuity? + duration = inlink->frame_rate.num > 0 ? av_rescale_q(s->n - s->reference_n, av_inv_q(inlink->frame_rate), AV_TIME_BASE_Q) : 0; + else + duration = av_rescale_q(frame->pts - s->reference_frame->pts, inlink->time_base, AV_TIME_BASE_Q); + + frozen = is_frozen(s, s->reference_frame, frame); + if (duration >= s->duration) { + if (frozen) { + if (!s->frozen) + set_meta(s, frame, "lavfi.freezedetect.freeze_start", av_ts2timestr(s->reference_frame->pts, &inlink->time_base)); + } else { + set_meta(s, frame, "lavfi.freezedetect.freeze_duration", av_ts2timestr(duration, &AV_TIME_BASE_Q)); + set_meta(s, frame, "lavfi.freezedetect.freeze_end", av_ts2timestr(frame->pts, &inlink->time_base)); + } + s->frozen = frozen; + } + } + + if (!frozen) { + av_frame_free(&s->reference_frame); + s->reference_frame = av_frame_clone(frame); + s->reference_n = s->n; + if (!s->reference_frame) { + av_frame_free(&frame); + return AVERROR(ENOMEM); + } + } + return ff_filter_frame(outlink, frame); + } + + FF_FILTER_FORWARD_STATUS(inlink, outlink); + FF_FILTER_FORWARD_WANTED(outlink, inlink); + + return FFERROR_NOT_READY; +} + +static const AVFilterPad freezedetect_inputs[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_VIDEO, + .config_props = config_input, + }, + { NULL } +}; + +static const AVFilterPad freezedetect_outputs[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_VIDEO, + }, + { NULL } +}; + +AVFilter ff_vf_freezedetect = { + .name = "freezedetect", + .description = NULL_IF_CONFIG_SMALL("Detects frozen video input."), + .priv_size = sizeof(FreezeDetectContext), + .priv_class = &freezedetect_class, + .uninit = uninit, + .query_formats = query_formats, + .inputs = freezedetect_inputs, + .outputs = freezedetect_outputs, + .activate = activate, +}; -- 2.39.2