libextractor

GNU libextractor
Log | Files | Refs | Submodules | README | LICENSE

commit c60eb77862edaaa23e6ef95863e90460d70ec873
parent d1fa87910de843d6aa62ff9992b51275d5f0c4ea
Author: Christian Grothoff <christian@grothoff.org>
Date:   Fri,  7 Aug 2026 03:41:48 +0200

resurrect video-extractors

Diffstat:
M.gitignore | 4++++
MChangeLog | 12++++++++++++
MMakefile.am | 3++-
MREADME | 8+++++++-
Mconfigure.ac | 56+++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcontrib/apparmor/libextractor | 8++++++++
Acontrib/gen_testmedia.sh | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdoc/libextractor.texi | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/fuzz/Makefile.am | 38+++++++++++++++++++++++++++++++++++++-
Msrc/fuzz/fuzz_plugin_name.h | 12++++++++++++
Msrc/main/extractor.c | 20++++++++++++++++++--
Msrc/main/extractor_ipc_gnu.c | 10++++++++++
Msrc/main/extractor_ipc_w32.c | 9+++++++++
Msrc/plugins/Makefile.am | 130++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Asrc/plugins/mediaffmpeg.c | 276+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/mediaffmpeg.h | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/mediagst.c | 278+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/mediagst.h | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/mediautil.c | 284+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/mediautil.h | 207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/previewgst_extractor.c | 408+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/previewopus_extractor.c | 570+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/plugins/test_lib.c | 4++++
Msrc/plugins/test_lib.h | 14++++++++++++++
Asrc/plugins/test_media_lib.c | 321+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/test_media_lib.h | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/test_previewgst.c | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/test_previewopus.c | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/test_thumbnailffmpeg.c | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/test_thumbnailgst.c | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/testdata/README.media | 42++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/testdata/preview_tone.ogg | 0
Asrc/plugins/testdata/preview_tone.ogv | 0
Asrc/plugins/testdata/thumbnail_pattern.ogv | 0
Asrc/plugins/thumbnailffmpeg_extractor.c | 460+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/thumbnailgst_extractor.c | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
36 files changed, 4358 insertions(+), 9 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -105,6 +105,10 @@ src/plugins/*.log src/plugins/*.trs src/plugins/test-suite.log src/plugins/test_archive +src/plugins/test_previewgst +src/plugins/test_previewopus +src/plugins/test_thumbnailffmpeg +src/plugins/test_thumbnailgst src/plugins/test_deb src/plugins/test_dvi src/plugins/test_elf diff --git a/ChangeLog b/ChangeLog @@ -1,3 +1,15 @@ +Fri Aug 7 03:40:00 AM CEST 2026 + Add video thumbnail and audio preview plugins, in a GStreamer and an + FFmpeg (>= 5.1) variant each: thumbnailgst, thumbnailffmpeg, + previewgst and previewopus. The audio previews are Ogg Opus and are + now reported with the MIME type "audio/ogg" rather than the + "audio/opus" the plugin removed in 1.11 used, as the latter is not + the type for Opus in an Ogg container (RFC 7845). + Forget a channel on the plugin as well when the plugin process dies, + instead of handing the next file to a freed struct EXTRACTOR_Channel; + restart a plugin whose process is already gone when the next file is + submitted, rather than skipping that file. -CG + Thu Jul 30 11:14:02 PM CEST 2026 Add new RTF and msoffice plugins. Fix interesting bugs and performance issues in ancient unzip logic. diff --git a/Makefile.am b/Makefile.am @@ -1,7 +1,8 @@ # This Makefile.am is in the public domain SUBDIRS = m4 po src doc . EXTRA_DIST = config.rpath \ - ABOUT-NLS + ABOUT-NLS \ + contrib/gen_testmedia.sh pkgconfigdatadir = $(libdir)/pkgconfig pkgconfigdata_DATA = libextractor.pc diff --git a/README b/README @@ -55,6 +55,8 @@ The following dependencies are all optional, but should be available in order for maximum coverage: * libarchive +* libavformat / libavcodec / libavutil / libswscale / libswresample + (FFmpeg 5.1 or later; for the video thumbnail and audio preview plugins) * libbz2 (bzip2) * libexiv2 * libflac @@ -62,7 +64,11 @@ available in order for maximum coverage: * libglib (glib) * libgtk+ * libgsf -* libgstreamer +* libgstreamer (1.8 or later, with gst-plugins-base and gst-plugins-good; + the video thumbnail and audio preview plugins additionally need the + gstreamer-video, gstreamer-app and gstreamer-controller libraries, and + at run time the "playback", "videoconvertscale", "jpeg", "opus" and + "ogg" GStreamer plugins) * libjpeg (v8 or later) * libmagic (file) * libmpeg2 diff --git a/configure.ac b/configure.ac @@ -428,9 +428,11 @@ AC_CHECK_LIB(gif, DGifOpen, AC_MSG_CHECKING(for magic_open -lmagic) SAVED_AM_LDFLAGS=$AM_LDFLAGS +have_magic=no AC_CHECK_LIB(magic, magic_open, [AC_CHECK_HEADERS([magic.h], - AM_CONDITIONAL(HAVE_MAGIC, true), + [AM_CONDITIONAL(HAVE_MAGIC, true) + have_magic=yes], AM_CONDITIONAL(HAVE_MAGIC, false))], AM_CONDITIONAL(HAVE_MAGIC, false)) @@ -629,6 +631,46 @@ AS_IF([test "x$with_gstreamer" = "xyes"], [ AM_CONDITIONAL(HAVE_GSTREAMER, test x$have_gstreamer = xyes -a x$have_gstreamer_pbutils = xyes -a x$have_gstreamer_tag = xyes -a x$have_gstreamer_app = xyes -a ! x$without_glib = xtrue) +# The video thumbnailer and the audio preview plugin need more of +# GStreamer than the metadata plugin does: gstreamer-video-1.0 for +# gst_video_convert_sample() and gstreamer-controller-1.0 for the fade. +# 1.8 (2016) is the first release that ships opusenc in -base. +have_gstreamer_video=no +have_gstreamer_controller=no +AS_IF([test "x$with_gstreamer" = "xyes"], [ + PKG_CHECK_MODULES([GSTREAMER_VIDEO], [gstreamer-video-1.0 >= 1.8], [have_gstreamer_video=yes], [have_gstreamer_video=no]) + PKG_CHECK_MODULES([GSTREAMER_CONTROLLER], [gstreamer-controller-1.0 >= 1.8], [have_gstreamer_controller=yes], [have_gstreamer_controller=no]) +]) +gstreamer_media=no +AS_IF([test "x$have_gstreamer" = "xyes" -a "x$have_gstreamer_app" = "xyes" -a "x$have_gstreamer_video" = "xyes" -a "x$have_gstreamer_controller" = "xyes" -a "x$have_magic" = "xyes" -a ! "x$without_glib" = "xtrue"], + [gstreamer_media=yes]) +AM_CONDITIONAL(HAVE_GSTREAMER_MEDIA, test "x$gstreamer_media" = "xyes") + +# FFmpeg-based video thumbnailer and audio preview plugin. Note the +# version floor: FFmpeg 5.1 is the first release with the AVChannelLayout +# API, and supporting anything older would mean re-introducing the +# per-version compatibility code that got the predecessors of these two +# plugins deleted in 2021. +AC_ARG_WITH([ffmpeg], + AS_HELP_STRING([--without-ffmpeg], [do not build the FFmpeg-based plugins]), + [], [with_ffmpeg=yes]) +have_ffmpeg=no +have_swresample=no +AS_IF([test "x$with_ffmpeg" = "xyes"], [ + PKG_CHECK_MODULES([FFMPEG], + [libavformat >= 59.27 libavcodec >= 59.37 libavutil >= 57.28 libswscale >= 6.7], + [have_ffmpeg=yes], [have_ffmpeg=no]) + PKG_CHECK_MODULES([FFMPEG_SWR], [libswresample >= 4.7], [have_swresample=yes], [have_swresample=no]) +]) +ffmpeg_video=no +AS_IF([test "x$have_ffmpeg" = "xyes" -a "x$have_magic" = "xyes"], + [ffmpeg_video=yes]) +AM_CONDITIONAL(HAVE_FFMPEG, test "x$ffmpeg_video" = "xyes") +ffmpeg_audio=no +AS_IF([test "x$ffmpeg_video" = "xyes" -a "x$have_swresample" = "xyes"], + [ffmpeg_audio=yes]) +AM_CONDITIONAL(HAVE_FFMPEG_AUDIO, test "x$ffmpeg_audio" = "xyes") + ABI_GSF AM_CONDITIONAL(HAVE_GSF, test "x$have_gsf" = "xtrue") @@ -790,5 +832,17 @@ AS_IF([test x$have_gstreamer = xyes -a x$have_gstreamer_pbutils = xyes -a x$have [AC_MSG_NOTICE([NOTICE: gstreamer enabled])])], [AC_MSG_NOTICE([NOTICE: gstreamer not found, gstreamer support disabled])]) +AS_IF([test "x$gstreamer_media" = "xyes"], + [AC_MSG_NOTICE([NOTICE: gstreamer video thumbnail and audio preview plugins enabled])], + [AC_MSG_NOTICE([NOTICE: gstreamer-video/-controller/-app or libmagic missing, gstreamer thumbnail and preview plugins disabled])]) + +AS_IF([test "x$ffmpeg_video" = "xyes"], + [AC_MSG_NOTICE([NOTICE: FFmpeg video thumbnail plugin enabled])], + [AC_MSG_NOTICE([NOTICE: FFmpeg >= 5.1 or libmagic not found, FFmpeg thumbnail plugin disabled])]) + +AS_IF([test "x$ffmpeg_audio" = "xyes"], + [AC_MSG_NOTICE([NOTICE: FFmpeg opus audio preview plugin enabled])], + [AC_MSG_NOTICE([NOTICE: libswresample not found, FFmpeg opus audio preview plugin disabled])]) + AS_IF([test "x$HAVE_APPARMOR_TRUE" = "x#"], [AC_MSG_NOTICE([NOTICE: libapparmor not found, apparmor support disabled])]) diff --git a/contrib/apparmor/libextractor b/contrib/apparmor/libextractor @@ -14,6 +14,14 @@ profile libextractor { @{PROC}/fd/ r, /sys/devices/system/cpu/ r, /usr/lib/gstreamer-1.0/gst-plugin-scanner rix, + # the GStreamer-based plugins (thumbnailgst, previewgst) load their + # decoders and encoders from here at run time + /usr/lib/gstreamer-1.0/*.so mr, + /usr/lib/@{multiarch}/gstreamer-1.0/*.so mr, + /usr/lib/@{multiarch}/gstreamer-1.0/gst-plugin-scanner rix, + # note that no network rule appears anywhere in this profile: the + # media plugins feed FFmpeg and GStreamer from a pipe and must never + # open a stream of their own /usr/local/lib/libextractor/libextractor_*.so* mr, /usr/local/lib/libextractor_common.so.* mr, /usr/share/file/misc/magic.mgc r, diff --git a/contrib/gen_testmedia.sh b/contrib/gen_testmedia.sh @@ -0,0 +1,58 @@ +#!/bin/sh +# This file is in the public domain. +# +# Regenerate the media files that the video thumbnail and audio preview +# plugins are tested against. They are checked into the repository (and +# shipped in the tarball), so you only need this if you want to change +# them -- or if you want to convince yourself that they really are +# synthetic and carry nobody's copyright. +# +# Everything here comes out of FFmpeg's lavfi test sources (solid colours, +# a box, a sine wave), so the result is not derived from any third party +# work. The files are dedicated to the public domain (CC0); see +# src/plugins/testdata/README.media. +# +# Usage: contrib/gen_testmedia.sh [output-directory] +set -e + +OUT="${1:-src/plugins/testdata}" +FFMPEG="${FFMPEG:-ffmpeg}" + +if ! command -v "$FFMPEG" > /dev/null 2>&1; then + echo "$0: $FFMPEG not found" >&2 + exit 1 +fi + +# The video changes colour 20s in, and the thumbnailers are expected to +# sample around 30s, so a thumbnail of the wrong moment is a thumbnail of +# the wrong colour and the test notices. The white box keeps the frames +# from being perfectly flat, which the thumbnailers deliberately skip. +$FFMPEG -hide_banner -loglevel error -y \ + -f lavfi -i "color=c=black:s=160x120:r=5:d=20,drawbox=x=10:y=10:w=24:h=24:color=white:t=fill" \ + -f lavfi -i "color=c=red:s=160x120:r=5:d=70,drawbox=x=116:y=76:w=24:h=24:color=white:t=fill" \ + -filter_complex "[0:v][1:v]concat=n=2:v=1:a=0[v]" \ + -map "[v]" -c:v libtheora -q:v 3 \ + "$OUT/thumbnail_pattern.ogv" + +# A 30s tone that changes pitch halfway through, so that a preview taken +# from the wrong place is at least in principle detectable. 8kHz mono is +# plenty for something that gets re-encoded at 24 kbps. +$FFMPEG -hide_banner -loglevel error -y \ + -f lavfi -i "sine=f=440:d=15:r=8000" \ + -f lavfi -i "sine=f=880:d=15:r=8000" \ + -filter_complex "[0:a][1:a]concat=n=2:v=0:a=1[a]" \ + -map "[a]" -c:a libvorbis -q:a -2 -ac 1 \ + "$OUT/preview_tone.ogg" + +# The same audio in a file that also has a video stream, so that the +# preview plugins are exercised on a video container too. +$FFMPEG -hide_banner -loglevel error -y \ + -f lavfi -i "color=c=black:s=160x120:r=5:d=10,drawbox=x=10:y=10:w=24:h=24:color=white:t=fill" \ + -f lavfi -i "color=c=red:s=160x120:r=5:d=20,drawbox=x=116:y=76:w=24:h=24:color=white:t=fill" \ + -f lavfi -i "sine=f=440:d=15:r=8000" \ + -f lavfi -i "sine=f=880:d=15:r=8000" \ + -filter_complex "[0:v][1:v]concat=n=2:v=1:a=0[v];[2:a][3:a]concat=n=2:v=0:a=1[a]" \ + -map "[v]" -map "[a]" -c:v libtheora -q:v 3 -c:a libvorbis -q:a -2 -ac 1 \ + "$OUT/preview_tone.ogv" + +ls -l "$OUT/thumbnail_pattern.ogv" "$OUT/preview_tone.ogg" "$OUT/preview_tone.ogv" diff --git a/doc/libextractor.texi b/doc/libextractor.texi @@ -200,7 +200,11 @@ libavformat-dev @item libswscale-dev @item +libswresample-dev +@item libgstreamer1.0-dev +@item +libgstreamer-plugins-base1.0-dev @end itemize For Subversion access and compilation one also needs: @@ -838,9 +842,16 @@ S3M @item SID @item -ThumbnailFFMPEG (using libavformat and related libav-libraries, including libswscale) +PreviewGst (audio preview, using GStreamer) +@item +PreviewOpus (audio preview, using FFmpeg 5.1 or later) +@item +ThumbnailFFMPEG (video thumbnail, using libavformat, libavcodec and +libswscale of FFmpeg 5.1 or later) @item -ThumbnailGtk (using libgtk) +ThumbnailGst (video thumbnail, using GStreamer 1.8 or later) +@item +ThumbnailGtk (still image thumbnail, using gdk-pixbuf) @item TIFF (with libtiff, tested with v4) @item @@ -855,6 +866,79 @@ ZIP also supported (as well as meta data embedded by @file{gzip} itself) if zlib or libbz2 are available. +@section Video thumbnails and audio previews + +Four of the plugins in that list do not report meta data that was found +in the file, but meta data computed @emph{from} the file: a small image +taken from a video (@code{EXTRACTOR_METATYPE_THUMBNAIL}) and a short +audio excerpt re-encoded as Opus in an Ogg container +(@code{EXTRACTOR_METATYPE_AUDIO_PREVIEW}). Both are reported as +@code{EXTRACTOR_METAFORMAT_BINARY}. + +Each feature comes in two implementations, one on top of GStreamer and +one on top of FFmpeg, and which of them are built depends on what +@command{configure} found: + +@table @code +@item thumbnailgst +@itemx thumbnailffmpeg +Decode the video and emit a frame from about 30 seconds in, scaled to +fit a 128x128 box, as JPEG. Frames that are a perfectly flat surface +(a black leader, a fade-in) are skipped. +@item previewgst +@itemx previewopus +Decode about 15 seconds of audio from about 30 seconds in, fade it in +and out, and emit it as a mono 24 kbit/s Ogg Opus stream with the MIME +type @code{audio/ogg}. +@end table + +In both cases the position is limited to a third of the duration, so a +short file is sampled proportionally earlier. + +If both backends are installed, both plugins run and you get two +thumbnails (or two previews) for the same file. Which one you want is +an application decision; disable the other one in the configuration +string, as in + +@example +pl = EXTRACTOR_plugin_add_defaults (EXTRACTOR_OPTION_DEFAULT_POLICY); +pl = EXTRACTOR_plugin_add_config (pl, + "-thumbnailffmpeg:-previewopus", + EXTRACTOR_OPTION_DEFAULT_POLICY); +@end example + +All four accept a comma separated list of options in the plugin +configuration string: + +@table @code +@item size=@var{pixels} +Edge length of the box the thumbnail must fit into (default: 128). +@item format=jpeg|png +Image format of the thumbnail (default: @code{jpeg}). +@item offset=@var{seconds} +How far into the media to sample (default: 30). +@item length=@var{seconds} +Length of the audio preview (default: 15). +@item bitrate=@var{bits-per-second} +Opus bitrate of the audio preview (default: 24000). +@item deadline=@var{milliseconds} +Wall clock budget for the whole extraction; when it runs out, the +plugin reports nothing rather than keeping the application waiting +(default: 10000). +@end table + +For example, @code{"thumbnailgst(size=256,format=png)"} asks for a +256x256 PNG. + +Two caveats apply to the GStreamer variants. Their decoders and +encoders are themselves GStreamer plugins, resolved at run time, so a +system that lacks the @code{jpeg} or @code{opus} GStreamer plugin +builds them happily and then gets no results; and because GStreamer +does not survive a @code{fork()} of a process that has already +initialized it, an application that runs these plugins +@code{EXTRACTOR_OPTION_IN_PROCESS} should not also use out-of-process +plugins in the same process afterwards. + @node Writing new Plugins @chapter Writing new Plugins diff --git a/src/fuzz/Makefile.am b/src/fuzz/Makefile.am @@ -346,6 +346,40 @@ fuzz_mime_LDADD = $(XLIB) -lmagic nodist_fuzz_mime_SOURCES = $(PLUGIN_DIR)/mime_extractor.c endif +# The FFmpeg-based media plugins. What gets instrumented here is our own +# glue -- the AVIO callbacks against the fault-injecting extraction +# context of fuzz_ec.h, the option parser and the size arithmetic -- and +# not FFmpeg's demuxers, which upstream fuzzes far more thoroughly than +# we could. Their GStreamer counterparts are deliberately absent: they +# dlopen() their decoders and start threads, which does not fit an +# in-process deterministic harness (the gstreamer plugin has no target +# here either). +if HAVE_FFMPEG +THUMBNAILFFMPEG_FUZZER = fuzz_thumbnailffmpeg +fuzz_thumbnailffmpeg_SOURCES = fuzz_plugin.c +fuzz_thumbnailffmpeg_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + $(FFMPEG_CFLAGS) \ + -DLE_FUZZ_PLUGIN=thumbnailffmpeg -DLE_FUZZ_ID=THUMBNAILFFMPEG +fuzz_thumbnailffmpeg_LDADD = $(XLIB) $(FFMPEG_LIBS) -lmagic +nodist_fuzz_thumbnailffmpeg_SOURCES = \ + $(PLUGIN_DIR)/thumbnailffmpeg_extractor.c \ + $(PLUGIN_DIR)/mediaffmpeg.c \ + $(PLUGIN_DIR)/mediautil.c +endif + +if HAVE_FFMPEG_AUDIO +PREVIEWOPUS_FUZZER = fuzz_previewopus +fuzz_previewopus_SOURCES = fuzz_plugin.c +fuzz_previewopus_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + $(FFMPEG_CFLAGS) $(FFMPEG_SWR_CFLAGS) \ + -DLE_FUZZ_PLUGIN=previewopus -DLE_FUZZ_ID=PREVIEWOPUS +fuzz_previewopus_LDADD = $(XLIB) $(FFMPEG_LIBS) $(FFMPEG_SWR_LIBS) -lmagic +nodist_fuzz_previewopus_SOURCES = \ + $(PLUGIN_DIR)/previewopus_extractor.c \ + $(PLUGIN_DIR)/mediaffmpeg.c \ + $(PLUGIN_DIR)/mediautil.c +endif + if HAVE_GSF OLE2_FUZZER = fuzz_ole2 fuzz_ole2_SOURCES = fuzz_plugin.c @@ -369,7 +403,9 @@ check_PROGRAMS = \ $(OGG_FUZZER) \ $(ARCHIVE_FUZZER) \ $(MIME_FUZZER) \ - $(OLE2_FUZZER) + $(OLE2_FUZZER) \ + $(THUMBNAILFFMPEG_FUZZER) \ + $(PREVIEWOPUS_FUZZER) TESTS = $(check_PROGRAMS) diff --git a/src/fuzz/fuzz_plugin_name.h b/src/fuzz/fuzz_plugin_name.h @@ -111,6 +111,8 @@ #define LE_ID_XM 29 #define LE_ID_ZIP 30 #define LE_ID_OLE2 31 +#define LE_ID_THUMBNAILFFMPEG 32 +#define LE_ID_PREVIEWOPUS 33 #define LE_FUZZ_IDVAL LE_FUZZ_CAT (LE_ID_, LE_FUZZ_ID) @@ -166,6 +168,16 @@ #define LE_FUZZ_MAGIC "" #define LE_FUZZ_SHAPE LE_SHAPE_RAW +#elif LE_FUZZ_IDVAL == LE_ID_THUMBNAILFFMPEG +/* Ogg, which is what the media plugins are tested against; libmagic has + to recognize it as video/* or the plugin declines to look at all */ +#define LE_FUZZ_MAGIC "OggS\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00" +#define LE_FUZZ_SHAPE LE_SHAPE_RAW + +#elif LE_FUZZ_IDVAL == LE_ID_PREVIEWOPUS +#define LE_FUZZ_MAGIC "OggS\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00" +#define LE_FUZZ_SHAPE LE_SHAPE_RAW + #elif LE_FUZZ_IDVAL == LE_ID_MPEG #define LE_FUZZ_MAGIC "\x00\x00\x01\xba" #define LE_FUZZ_SHAPE LE_SHAPE_RAW diff --git a/src/main/extractor.c b/src/main/extractor.c @@ -413,9 +413,24 @@ do_extract (struct EXTRACTOR_PluginList *plugins, &start, sizeof (start)) ) ) { - LOG ("Failed to send EXTRACT_START message to plugin\n"); + /* Plugins that asked for "force-kill" exit after every file, so a + channel that is gone here is not an error at all -- but unless we + restart the plugin now, this file would silently be skipped by + it, and only every other file would ever be processed. */ EXTRACTOR_IPC_channel_destroy_ (pos->channel); pos->channel = NULL; + if (NULL != shm) + pos->channel = EXTRACTOR_IPC_channel_create_ (pos, + shm); + if ( (NULL != pos->channel) && + (-1 == EXTRACTOR_IPC_channel_send_ (pos->channel, + &start, + sizeof (start)) ) ) + { + LOG ("Failed to send EXTRACT_START message to plugin\n"); + EXTRACTOR_IPC_channel_destroy_ (pos->channel); + pos->channel = NULL; + } } } done = 0; @@ -506,7 +521,8 @@ do_extract (struct EXTRACTOR_PluginList *plugins, (data_available = EXTRACTOR_IPC_shared_memory_set_ (shm, ds, min_seek, - DEFAULT_SHM_SIZE))) + DEFAULT_SHM_SIZE)) + ) { LOG ("Failed to seek; full reset\n"); abort_all_channels (plugins); diff --git a/src/main/extractor_ipc_gnu.c b/src/main/extractor_ipc_gnu.c @@ -549,6 +549,8 @@ EXTRACTOR_IPC_channel_recv_ (struct EXTRACTOR_Channel **channels, if (MAX_META_DATA == channel->mdata_size) { LOG ("Inbound message from channel too large, aborting\n"); + channel->plugin->channel = NULL; + channel->plugin->round_finished = 1; EXTRACTOR_IPC_channel_destroy_ (channel); channels[i] = NULL; continue; @@ -560,6 +562,8 @@ EXTRACTOR_IPC_channel_recv_ (struct EXTRACTOR_Channel **channels, channel->mdata_size))) { LOG_STRERROR ("realloc"); + channel->plugin->channel = NULL; + channel->plugin->round_finished = 1; EXTRACTOR_IPC_channel_destroy_ (channel); channels[i] = NULL; continue; @@ -584,6 +588,12 @@ EXTRACTOR_IPC_channel_recv_ (struct EXTRACTOR_Channel **channels, LOG_STRERROR ("read"); LOG ("Read error from channel, closing channel %s\n", channel->plugin->libname); + /* The plugin is gone -- a crash in a plugin is exactly what this + library is built to survive. Forget the channel on the plugin + as well, or the next file would be handed to a + `struct EXTRACTOR_Channel' that has already been freed. */ + channel->plugin->channel = NULL; + channel->plugin->round_finished = 1; EXTRACTOR_IPC_channel_destroy_ (channel); channels[i] = NULL; continue; diff --git a/src/main/extractor_ipc_w32.c b/src/main/extractor_ipc_w32.c @@ -789,6 +789,8 @@ EXTRACTOR_IPC_channel_recv_ (struct EXTRACTOR_Channel **channels, if (MAX_META_DATA == channels[i]->mdata_size) { LOG ("Inbound message from channel too large, aborting\n"); + channels[i]->plugin->channel = NULL; + channels[i]->plugin->round_finished = 1; EXTRACTOR_IPC_channel_destroy_ (channels[i]); channels[i] = NULL; continue; @@ -800,6 +802,8 @@ EXTRACTOR_IPC_channel_recv_ (struct EXTRACTOR_Channel **channels, channels[i]->mdata_size))) { LOG_STRERROR ("realloc"); + channels[i]->plugin->channel = NULL; + channels[i]->plugin->round_finished = 1; EXTRACTOR_IPC_channel_destroy_ (channels[i]); channels[i] = NULL; continue; @@ -822,6 +826,11 @@ EXTRACTOR_IPC_channel_recv_ (struct EXTRACTOR_Channel **channels, SetErrnoFromWinError (error); if (! bresult) LOG_STRERROR ("ReadFile"); + /* the plugin is gone; forget the channel here as well, or the + next file would be handed to a `struct EXTRACTOR_Channel' + that has already been freed */ + channels[i]->plugin->channel = NULL; + channels[i]->plugin->round_finished = 1; EXTRACTOR_IPC_channel_destroy_ (channels[i]); channels[i] = NULL; continue; diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am @@ -18,6 +18,14 @@ endif PLUGINFLAGS = $(makesymbolic) $(LE_PLUGIN_LDFLAGS) +# The media tests decode the PNG thumbnail they get back to check that the +# plugin sampled the right moment of the video. Without zlib they fall +# back to checking the magic number only. +if HAVE_ZLIB +TEST_MEDIA_CFLAGS = -DTEST_MEDIA_HAVE_PNG=1 +TEST_MEDIA_LIBS = -lz +endif + SUBDIRS = . EXTRA_DIST = \ @@ -59,6 +67,8 @@ EXTRA_DIST = \ testdata/ogg_courseclear.ogg \ testdata/pdf_extract.pdf \ testdata/png_image.png \ + testdata/preview_tone.ogg \ + testdata/preview_tone.ogv \ testdata/ps_bloomfilter.ps \ testdata/ps_wallace.ps \ testdata/ra3.ra \ @@ -73,13 +83,15 @@ EXTRA_DIST = \ testdata/s3m_2nd_pm.s3m \ testdata/applefile_test.applesingle \ testdata/sid_wizball.sid \ + testdata/thumbnail_pattern.ogv \ testdata/thumbnail_torsten.jpg \ testdata/tiff_haute.tiff \ testdata/wav_noise.wav \ testdata/wav_alert.wav \ testdata/xm_diesel.xm \ testdata/zip_test.zip \ - testdata/README + testdata/README \ + testdata/README.media if HAVE_MAGIC @@ -130,6 +142,28 @@ PLUGIN_GSTREAMER=libextractor_gstreamer.la TEST_GSTREAMER=test_gstreamer endif +# The video thumbnailer and the audio preview plugin need libmagic and +# more of GStreamer than the metadata plugin does; configure folds all of +# that into HAVE_GSTREAMER_MEDIA. +if HAVE_GSTREAMER_MEDIA +PLUGIN_THUMBGST=libextractor_thumbnailgst.la +TEST_THUMBGST=test_thumbnailgst +PLUGIN_PREVGST=libextractor_previewgst.la +TEST_PREVGST=test_previewgst +endif + +# ... and the same two features on top of FFmpeg (>= 5.1), which is a +# separate build option because the two backends are interchangeable. +if HAVE_FFMPEG +PLUGIN_THUMBFFMPEG=libextractor_thumbnailffmpeg.la +TEST_THUMBFFMPEG=test_thumbnailffmpeg +endif + +if HAVE_FFMPEG_AUDIO +PLUGIN_PREVIEWOPUS=libextractor_previewopus.la +TEST_PREVIEWOPUS=test_previewopus +endif + if HAVE_JPEG PLUGIN_JPEG=libextractor_jpeg.la TEST_JPEG=test_jpeg @@ -213,7 +247,11 @@ plugin_LTLIBRARIES = \ $(PLUGIN_MPEG) \ $(PLUGIN_OGG) \ $(PLUGIN_PDF) \ + $(PLUGIN_PREVGST) \ + $(PLUGIN_PREVIEWOPUS) \ $(PLUGIN_RPM) \ + $(PLUGIN_THUMBFFMPEG) \ + $(PLUGIN_THUMBGST) \ $(PLUGIN_VLC) \ $(PLUGIN_TIFF) \ $(PLUGIN_ZLIB) @@ -252,8 +290,12 @@ check_PROGRAMS = \ $(TEST_MSOFFICE) \ $(TEST_OGG) \ $(TEST_PDF) \ + $(TEST_PREVGST) \ + $(TEST_PREVIEWOPUS) \ $(TEST_QT) \ $(TEST_RPM) \ + $(TEST_THUMBFFMPEG) \ + $(TEST_THUMBGST) \ $(TEST_TIFF) \ $(TEST_ZLIB) @@ -389,6 +431,92 @@ test_gstreamer_CFLAGS = \ $(GSTREAMER_CFLAGS) $(GSTREAMER_PBUTILS_CFLAGS) +libextractor_thumbnailgst_la_SOURCES = \ + thumbnailgst_extractor.c \ + mediagst.c mediagst.h \ + mediautil.c mediautil.h +libextractor_thumbnailgst_la_LDFLAGS = \ + $(PLUGINFLAGS) +libextractor_thumbnailgst_la_LIBADD = \ + $(GSTREAMER_LIBS) $(GSTREAMER_APP_LIBS) $(GSTREAMER_VIDEO_LIBS) \ + -lmagic $(XLIB) -lpthread +libextractor_thumbnailgst_la_CFLAGS = \ + $(GSTREAMER_CFLAGS) $(GSTREAMER_APP_CFLAGS) $(GSTREAMER_VIDEO_CFLAGS) + +test_thumbnailgst_SOURCES = \ + test_thumbnailgst.c \ + test_media_lib.c test_media_lib.h +test_thumbnailgst_LDADD = \ + $(top_builddir)/src/plugins/libtest.la \ + $(TEST_MEDIA_LIBS) +test_thumbnailgst_CFLAGS = \ + $(AM_CFLAGS) $(TEST_MEDIA_CFLAGS) + + +libextractor_previewgst_la_SOURCES = \ + previewgst_extractor.c \ + mediagst.c mediagst.h \ + mediautil.c mediautil.h +libextractor_previewgst_la_LDFLAGS = \ + $(PLUGINFLAGS) +libextractor_previewgst_la_LIBADD = \ + $(GSTREAMER_LIBS) $(GSTREAMER_APP_LIBS) $(GSTREAMER_CONTROLLER_LIBS) \ + -lmagic $(XLIB) -lpthread +libextractor_previewgst_la_CFLAGS = \ + $(GSTREAMER_CFLAGS) $(GSTREAMER_APP_CFLAGS) $(GSTREAMER_CONTROLLER_CFLAGS) + +test_previewgst_SOURCES = \ + test_previewgst.c \ + test_media_lib.c test_media_lib.h +test_previewgst_LDADD = \ + $(top_builddir)/src/plugins/libtest.la \ + $(TEST_MEDIA_LIBS) +test_previewgst_CFLAGS = \ + $(AM_CFLAGS) $(TEST_MEDIA_CFLAGS) + + +libextractor_thumbnailffmpeg_la_SOURCES = \ + thumbnailffmpeg_extractor.c \ + mediaffmpeg.c mediaffmpeg.h \ + mediautil.c mediautil.h +libextractor_thumbnailffmpeg_la_LDFLAGS = \ + $(PLUGINFLAGS) +libextractor_thumbnailffmpeg_la_LIBADD = \ + $(FFMPEG_LIBS) -lmagic $(XLIB) +libextractor_thumbnailffmpeg_la_CFLAGS = \ + $(FFMPEG_CFLAGS) + +test_thumbnailffmpeg_SOURCES = \ + test_thumbnailffmpeg.c \ + test_media_lib.c test_media_lib.h +test_thumbnailffmpeg_LDADD = \ + $(top_builddir)/src/plugins/libtest.la \ + $(TEST_MEDIA_LIBS) +test_thumbnailffmpeg_CFLAGS = \ + $(AM_CFLAGS) $(TEST_MEDIA_CFLAGS) + + +libextractor_previewopus_la_SOURCES = \ + previewopus_extractor.c \ + mediaffmpeg.c mediaffmpeg.h \ + mediautil.c mediautil.h +libextractor_previewopus_la_LDFLAGS = \ + $(PLUGINFLAGS) +libextractor_previewopus_la_LIBADD = \ + $(FFMPEG_LIBS) $(FFMPEG_SWR_LIBS) -lmagic $(XLIB) +libextractor_previewopus_la_CFLAGS = \ + $(FFMPEG_CFLAGS) $(FFMPEG_SWR_CFLAGS) + +test_previewopus_SOURCES = \ + test_previewopus.c \ + test_media_lib.c test_media_lib.h +test_previewopus_LDADD = \ + $(top_builddir)/src/plugins/libtest.la \ + $(TEST_MEDIA_LIBS) +test_previewopus_CFLAGS = \ + $(AM_CFLAGS) $(TEST_MEDIA_CFLAGS) + + libextractor_html_la_SOURCES = \ html_extractor.c libextractor_html_la_CFLAGS = \ diff --git a/src/plugins/mediaffmpeg.c b/src/plugins/mediaffmpeg.c @@ -0,0 +1,276 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file plugins/mediaffmpeg.c + * @brief plumbing between an extraction context and FFmpeg's demuxer + * @author Christian Grothoff + */ +#include "platform.h" +#include "mediaffmpeg.h" + + +/** + * Implementation of AVIOContext's read callback. + * + * @param opaque our `struct EXTRACTOR_FFmpegIO` + * @param buf where to write the data + * @param buf_size number of bytes to read + * @return number of bytes read, AVERROR_EOF at the end of the file + */ +static int +read_packet (void *opaque, + uint8_t *buf, + int buf_size) +{ + struct EXTRACTOR_FFmpegIO *io = opaque; + void *data; + ssize_t ret; + + ret = io->ec->read (io->ec->cls, + &data, + buf_size); + if (ret <= 0) + return AVERROR_EOF; + /* the buffer handed back by read() is only valid until the next call */ + memcpy (buf, + data, + ret); + return (int) ret; +} + + +/** + * Implementation of AVIOContext's seek callback. + * + * @param opaque our `struct EXTRACTOR_FFmpegIO` + * @param offset position to seek to + * @param whence SEEK_SET, SEEK_CUR, SEEK_END or AVSEEK_SIZE + * @return new position, or a negative error code + */ +static int64_t +seek_packet (void *opaque, + int64_t offset, + int whence) +{ + struct EXTRACTOR_FFmpegIO *io = opaque; + uint64_t size; + + if (AVSEEK_SIZE == (whence & ~AVSEEK_FORCE)) + { + size = io->ec->get_size (io->ec->cls); + if (UINT64_MAX == size) + return AVERROR (ESPIPE); + return (int64_t) size; + } + return io->ec->seek (io->ec->cls, + offset, + whence & ~AVSEEK_FORCE); +} + + +/** + * Implementation of AVIOInterruptCB; lets us get out of a decoder that is + * taking too long instead of being killed by the parent process. + * + * @param opaque our `struct EXTRACTOR_FFmpegIO` + * @return 1 if FFmpeg should abort + */ +static int +interrupt_callback (void *opaque) +{ + struct EXTRACTOR_FFmpegIO *io = opaque; + + return EXTRACTOR_media_deadline_expired_ (&io->deadline); +} + + +/** + * Swallow the diagnostics FFmpeg would otherwise write to stderr. + * + * @param ptr unused + * @param level unused + * @param fmt unused + * @param vl unused + */ +static void +log_callback (void *ptr, + int level, + const char *fmt, + va_list vl) +{ + (void) ptr; + (void) level; + (void) fmt; + (void) vl; +} + + +void +EXTRACTOR_ffmpeg_silence_ () +{ + /* When something goes wrong in here, FFmpeg's log is the only thing + that will tell you what; keep it available for debugging, since in + the normal (out-of-process) case stderr is closed anyway. */ + if (NULL != getenv ("LIBEXTRACTOR_DEBUG")) + return; + av_log_set_callback (&log_callback); +} + + +int +EXTRACTOR_ffmpeg_open_ (struct EXTRACTOR_FFmpegIO *io) +{ + uint8_t *buffer; + + io->fmt = NULL; + io->avio = NULL; + if (NULL == (buffer = av_malloc (EXTRACTOR_FFMPEG_AVIO_BUFFER))) + return -1; + if (NULL == (io->avio = avio_alloc_context (buffer, + EXTRACTOR_FFMPEG_AVIO_BUFFER, + 0, + io, + &read_packet, + NULL, + &seek_packet))) + { + av_free (buffer); + return -1; + } + if (NULL == (io->fmt = avformat_alloc_context ())) + { + EXTRACTOR_ffmpeg_close_ (io); + return -1; + } + io->fmt->pb = io->avio; + io->fmt->flags |= AVFMT_FLAG_CUSTOM_IO; + io->fmt->probesize = EXTRACTOR_FFMPEG_PROBE_SIZE; + io->fmt->max_analyze_duration = EXTRACTOR_FFMPEG_MAX_ANALYZE; + io->fmt->interrupt_callback.callback = &interrupt_callback; + io->fmt->interrupt_callback.opaque = io; + if (0 != avformat_open_input (&io->fmt, + NULL, + NULL, + NULL)) + { + /* avformat_open_input() freed the context for us */ + io->fmt = NULL; + EXTRACTOR_ffmpeg_close_ (io); + return -1; + } + if (0 > avformat_find_stream_info (io->fmt, + NULL)) + { + EXTRACTOR_ffmpeg_close_ (io); + return -1; + } + return 0; +} + + +void +EXTRACTOR_ffmpeg_close_ (struct EXTRACTOR_FFmpegIO *io) +{ + if (NULL != io->fmt) + avformat_close_input (&io->fmt); + if (NULL != io->avio) + { + /* we passed AVFMT_FLAG_CUSTOM_IO, so this is still ours to free; note + that avio may have replaced the buffer we originally handed it */ + av_free (io->avio->buffer); + avio_context_free (&io->avio); + } +} + + +int +EXTRACTOR_ffmpeg_open_stream_ (struct EXTRACTOR_FFmpegIO *io, + enum AVMediaType type, + AVCodecContext **ctx) +{ + const AVCodec *codec; + AVCodecContext *cc; + int stream; + + *ctx = NULL; + stream = av_find_best_stream (io->fmt, + type, + -1, + -1, + &codec, + 0); + if ( (0 > stream) || + (NULL == codec) ) + return -1; + if (NULL == (cc = avcodec_alloc_context3 (codec))) + return -1; + if (0 > avcodec_parameters_to_context (cc, + io->fmt->streams[stream]->codecpar)) + { + avcodec_free_context (&cc); + return -1; + } + /* one thread: we are in a forked child that is expected to talk to its + parent regularly, and there is nothing here worth parallelizing */ + cc->thread_count = 1; + if (0 != avcodec_open2 (cc, codec, NULL)) + { + avcodec_free_context (&cc); + return -1; + } + *ctx = cc; + return stream; +} + + +double +EXTRACTOR_ffmpeg_seek_ (struct EXTRACTOR_FFmpegIO *io, + int stream, + AVCodecContext *ctx, + unsigned int offset) +{ + double duration; + double target; + int64_t ts; + + duration = (AV_NOPTS_VALUE == io->fmt->duration) + ? -1.0 + : ((double) io->fmt->duration) / AV_TIME_BASE; + target = offset; + if ( (duration > 0.0) && + (target > duration / 3.0) ) + target = duration / 3.0; + if ( (target <= 1.0) || + (duration <= 0.0) ) + return (target <= 1.0) ? 0.0 : target; + ts = av_rescale_q ((int64_t) (target * AV_TIME_BASE), + AV_TIME_BASE_Q, + io->fmt->streams[stream]->time_base); + if (0 > av_seek_frame (io->fmt, + stream, + ts, + AVSEEK_FLAG_BACKWARD)) + return 0.0; /* seeking failed; caller decodes from wherever we are */ + avcodec_flush_buffers (ctx); + return target; +} + + +/* end of mediaffmpeg.c */ diff --git a/src/plugins/mediaffmpeg.h b/src/plugins/mediaffmpeg.h @@ -0,0 +1,144 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file plugins/mediaffmpeg.h + * @brief plumbing between an extraction context and FFmpeg's demuxer + * @author Christian Grothoff + * + * Shared by thumbnailffmpeg and previewopus. Requires FFmpeg 5.1 or + * newer; there is deliberately no compatibility code for older versions. + */ +#ifndef MEDIAFFMPEG_H +#define MEDIAFFMPEG_H + +#include "extractor.h" +#include "mediautil.h" + +#include <libavcodec/avcodec.h> +#include <libavformat/avformat.h> +#include <libavutil/avutil.h> + +/** + * Size of the buffer we give to the AVIOContext. Small on purpose: each + * refill turns into a read on the extraction context, which is what keeps + * the IPC channel from looking hung to the parent process (which kills + * plugins that stay silent for 500ms). + */ +#define EXTRACTOR_FFMPEG_AVIO_BUFFER (16 * 1024) + +/** + * Do not look at more than this much data to identify the streams. + */ +#define EXTRACTOR_FFMPEG_PROBE_SIZE (1024 * 1024) + +/** + * Do not spend more than this much media time on stream analysis. + */ +#define EXTRACTOR_FFMPEG_MAX_ANALYZE (2 * AV_TIME_BASE) + + +/** + * State of an FFmpeg demuxer reading from an extraction context. + */ +struct EXTRACTOR_FFmpegIO +{ + /** + * Extraction context we are reading from. + */ + struct EXTRACTOR_ExtractContext *ec; + + /** + * When we have to give up. + */ + struct EXTRACTOR_MediaDeadline deadline; + + /** + * Format context, NULL if not open. + */ + AVFormatContext *fmt; + + /** + * I/O context wrapping @e ec. + */ + AVIOContext *avio; +}; + + +/** + * Open the input of @a io->ec with FFmpeg and identify its streams. + * @a io->ec and @a io->deadline must already be set. + * + * @param[in,out] io I/O state to complete + * @return 0 on success + */ +int +EXTRACTOR_ffmpeg_open_ (struct EXTRACTOR_FFmpegIO *io); + + +/** + * Close what EXTRACTOR_ffmpeg_open_() opened. + * + * @param[in,out] io I/O state to clean up + */ +void +EXTRACTOR_ffmpeg_close_ (struct EXTRACTOR_FFmpegIO *io); + + +/** + * Find the best stream of @a type and open a decoder for it. + * + * @param io open I/O state + * @param type media type to look for + * @param[out] ctx set to the open decoder + * @return index of the stream, or a negative value on error + */ +int +EXTRACTOR_ffmpeg_open_stream_ (struct EXTRACTOR_FFmpegIO *io, + enum AVMediaType type, + AVCodecContext **ctx); + + +/** + * Determine where in the media we should sample, honouring both the + * requested offset and the (possibly unknown) duration, and seek there. + * + * @param io open I/O state + * @param stream stream to seek in + * @param ctx decoder to flush after seeking + * @param offset desired position in seconds + * @return the position we actually aim for, in seconds; 0 if we could + * not seek and the caller should just decode from the start + */ +double +EXTRACTOR_ffmpeg_seek_ (struct EXTRACTOR_FFmpegIO *io, + int stream, + AVCodecContext *ctx, + unsigned int offset); + + +/** + * Suppress FFmpeg's logging. + */ +void +EXTRACTOR_ffmpeg_silence_ (void); + +#endif + +/* end of mediaffmpeg.h */ diff --git a/src/plugins/mediagst.c b/src/plugins/mediagst.c @@ -0,0 +1,278 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file plugins/mediagst.c + * @brief plumbing between an extraction context and a GStreamer pipeline + * @author Christian Grothoff + */ +#include "platform.h" +#include "mediagst.h" + +#include <gst/app/gstappsrc.h> + + +/** + * Implementation of GstAppSrc's "need-data" callback. Reads data from + * the extraction context and passes it to GStreamer. + * + * @param appsrc the source that ran out of data + * @param size number of bytes requested + * @param io our execution context + */ +static void +feed_data (GstElement *appsrc, + guint size, + struct EXTRACTOR_GstIO *io) +{ + GstBuffer *buffer; + GstMemory *mem; + GstMapInfo mi; + guint accumulated; + ssize_t data_len; + uint8_t *le_data; + + if ( (0 != io->length) && + (io->offset >= io->length) ) + { + gst_app_src_end_of_stream (GST_APP_SRC (appsrc)); + return; + } + if ( (0 != io->length) && + (io->offset + size > io->length) ) + size = io->length - io->offset; + if (0 == size) + { + gst_app_src_end_of_stream (GST_APP_SRC (appsrc)); + return; + } + mem = gst_allocator_alloc (NULL, size, NULL); + if (! gst_memory_map (mem, &mi, GST_MAP_WRITE)) + { + gst_memory_unref (mem); + gst_app_src_end_of_stream (GST_APP_SRC (appsrc)); + return; + } + accumulated = 0; + data_len = 1; + while ( (accumulated < size) && + (data_len > 0) ) + { + data_len = io->ec->read (io->ec->cls, + (void **) &le_data, + size - accumulated); + if (data_len > 0) + { + memcpy (&mi.data[accumulated], + le_data, + data_len); + accumulated += data_len; + } + } + gst_memory_unmap (mem, &mi); + if (0 == accumulated) + { + gst_memory_unref (mem); + gst_app_src_end_of_stream (GST_APP_SRC (appsrc)); + return; + } + gst_memory_resize (mem, 0, accumulated); + buffer = gst_buffer_new (); + gst_buffer_append_memory (buffer, mem); + /* the offsets matter: they are what makes random access work */ + GST_BUFFER_OFFSET (buffer) = io->offset; + GST_BUFFER_OFFSET_END (buffer) = io->offset + accumulated; + io->offset += accumulated; + gst_app_src_push_buffer (GST_APP_SRC (appsrc), buffer); +} + + +/** + * Implementation of GstAppSrc's "seek-data" callback. + * + * @param appsrc the source that wants to seek + * @param position new desired absolute position in the file + * @param io our execution context + * @return TRUE if seeking succeeded + */ +static gboolean +seek_data (GstElement *appsrc, + guint64 position, + struct EXTRACTOR_GstIO *io) +{ + int64_t ret; + + (void) appsrc; + ret = io->ec->seek (io->ec->cls, + position, + SEEK_SET); + if (0 > ret) + return FALSE; + io->offset = (uint64_t) ret; + return io->offset == position; +} + + +/** + * Implementation of playbin's "source-setup" signal: configure the + * appsrc it created for our "appsrc://" URI. + * + * @param playbin the pipeline + * @param source the appsrc to configure + * @param io our execution context + */ +static void +source_setup (GstElement *playbin, + GstElement *source, + struct EXTRACTOR_GstIO *io) +{ + (void) playbin; + if (0 != io->length) + g_object_set (source, + "size", (gint64) io->length, + "stream-type", GST_APP_STREAM_TYPE_RANDOM_ACCESS, + NULL); + else + g_object_set (source, + "size", (gint64) - 1, + "stream-type", GST_APP_STREAM_TYPE_STREAM, + NULL); + g_object_set (source, + "format", GST_FORMAT_BYTES, + NULL); + g_signal_connect (source, + "need-data", + G_CALLBACK (&feed_data), + io); + g_signal_connect (source, + "seek-data", + G_CALLBACK (&seek_data), + io); +} + + +GstElement * +EXTRACTOR_gst_playbin_ (struct EXTRACTOR_GstIO *io, + struct EXTRACTOR_ExtractContext *ec, + unsigned int flags, + GstElement *video_sink, + GstElement *audio_sink) +{ + GstElement *pipeline; + uint64_t size; + + if (! gst_is_initialized ()) + gst_init (NULL, NULL); + io->ec = ec; + io->offset = 0; + size = ec->get_size (ec->cls); + io->length = (UINT64_MAX == size) ? 0 : size; + pipeline = gst_element_factory_make ("playbin", NULL); + if ( (NULL == pipeline) || + (NULL == video_sink) || + (NULL == audio_sink) ) + { + /* gst-plugins-base is not installed, or not completely */ + if (NULL != video_sink) + gst_object_unref (video_sink); + if (NULL != audio_sink) + gst_object_unref (audio_sink); + if (NULL != pipeline) + gst_object_unref (pipeline); + return NULL; + } + g_object_set (pipeline, + "uri", "appsrc://", + "video-sink", video_sink, + "audio-sink", audio_sink, + "flags", flags, + NULL); + g_signal_connect (pipeline, + "source-setup", + G_CALLBACK (&source_setup), + io); + return pipeline; +} + + +int +EXTRACTOR_gst_wait_async_ (GstElement *pipeline, + GstClockTime timeout) +{ + GstBus *bus; + GstMessage *msg; + int ret = 0; + + if (0 == timeout) + return 0; + if (NULL == (bus = gst_element_get_bus (pipeline))) + return 0; + while (1) + { + msg = gst_bus_timed_pop_filtered (bus, + timeout, + GST_MESSAGE_ASYNC_DONE + | GST_MESSAGE_ERROR + | GST_MESSAGE_EOS); + if (NULL == msg) + break; /* timed out */ + if (GST_MESSAGE_ASYNC_DONE != GST_MESSAGE_TYPE (msg)) + { + /* an error, or the file ended before we ever saw a frame */ + gst_message_unref (msg); + break; + } + if (GST_OBJECT (pipeline) == GST_MESSAGE_SRC (msg)) + { + gst_message_unref (msg); + ret = 1; + break; + } + /* ASYNC_DONE from some element inside the pipeline; keep waiting */ + gst_message_unref (msg); + } + gst_object_unref (bus); + return ret; +} + + +gint64 +EXTRACTOR_gst_sample_position_ (GstElement *pipeline, + unsigned int offset, + gint64 *duration) +{ + gint64 target; + gint64 dur = 0; + + target = ((gint64) offset) * GST_SECOND; + if (! gst_element_query_duration (pipeline, + GST_FORMAT_TIME, + &dur)) + dur = 0; + if (dur < 0) + dur = 0; + if ( (0 != dur) && + (target > dur / 3) ) + target = dur / 3; + *duration = dur; + return target; +} + + +/* end of mediagst.c */ diff --git a/src/plugins/mediagst.h b/src/plugins/mediagst.h @@ -0,0 +1,120 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file plugins/mediagst.h + * @brief plumbing between an extraction context and a GStreamer pipeline + * @author Christian Grothoff + * + * Shared by thumbnailgst and previewgst. Both drive a "playbin" reading + * from an "appsrc://" URI that we feed from the extraction context; + * playbin is used rather than a hand-built decodebin pipeline because it + * already knows how to pick a stream and how not to stall on the streams + * we are not interested in. + */ +#ifndef MEDIAGST_H +#define MEDIAGST_H + +#include "extractor.h" +#include "mediautil.h" + +#include <gst/gst.h> + +/** + * Values of playbin's "flags" property. The enum is not part of any + * installed header, so we spell out the bits we use. + */ +#define EXTRACTOR_GST_PLAY_FLAG_VIDEO 0x00000001 + +/** + * Play the audio stream. + */ +#define EXTRACTOR_GST_PLAY_FLAG_AUDIO 0x00000002 + + +/** + * State of a GStreamer pipeline reading from an extraction context. + */ +struct EXTRACTOR_GstIO +{ + /** + * Extraction context we are reading from. + */ + struct EXTRACTOR_ExtractContext *ec; + + /** + * Current read position in the file. + */ + uint64_t offset; + + /** + * Size of the file, 0 if unknown. + */ + uint64_t length; +}; + + +/** + * Create a playbin that reads from @a ec, with the given sinks. The + * pipeline is returned in state NULL; the caller sets it to PAUSED. + * + * @param[out] io I/O state to initialize + * @param ec extraction context to read from + * @param flags value for playbin's "flags" property + * @param video_sink sink to use for video, consumed + * @param audio_sink sink to use for audio, consumed + * @return the pipeline, or NULL if GStreamer could not provide one + */ +GstElement * +EXTRACTOR_gst_playbin_ (struct EXTRACTOR_GstIO *io, + struct EXTRACTOR_ExtractContext *ec, + unsigned int flags, + GstElement *video_sink, + GstElement *audio_sink); + + +/** + * Wait for @a pipeline to finish its current asynchronous state change. + * + * @param pipeline pipeline to watch + * @param timeout how long to wait, in nanoseconds + * @return 1 if the pipeline is ready, 0 on error, EOS or timeout + */ +int +EXTRACTOR_gst_wait_async_ (GstElement *pipeline, + GstClockTime timeout); + + +/** + * Determine where in the media we should start sampling: @a offset + * seconds in, but never past a third of the duration. + * + * @param pipeline prerolled pipeline + * @param offset desired position in seconds + * @param[out] duration set to the duration in nanoseconds, 0 if unknown + * @return position to sample at, in nanoseconds + */ +gint64 +EXTRACTOR_gst_sample_position_ (GstElement *pipeline, + unsigned int offset, + gint64 *duration); + +#endif + +/* end of mediagst.h */ diff --git a/src/plugins/mediautil.c b/src/plugins/mediautil.c @@ -0,0 +1,284 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file plugins/mediautil.c + * @brief helpers shared by the video thumbnail and audio preview plugins + * @author Christian Grothoff + */ +#include "platform.h" +#include "mediautil.h" +#include <magic.h> + +/** + * How many bytes we give libmagic to make up its mind. + */ +#define SNIFF_SIZE (16 * 1024) + + +void +EXTRACTOR_media_options_parse_ (const char *config, + struct EXTRACTOR_MediaOptions *opt) +{ + const char *pos; + + opt->size = 128; + opt->png = 0; + opt->offset = 30; + opt->length = 15; + opt->bitrate = 24000; + opt->deadline = 10000; + if (NULL == config) + return; + pos = config; + while ('\0' != *pos) + { + unsigned long long v; + char *endp; + + if (0 == strncmp (pos, + "format=", + strlen ("format="))) + { + pos += strlen ("format="); + opt->png = (0 == strncmp (pos, + "png", + strlen ("png"))); + } + else if (0 == strncmp (pos, + "size=", + strlen ("size="))) + { + pos += strlen ("size="); + v = strtoull (pos, &endp, 10); + if ( (endp != pos) && + (v >= 8) && + (v <= 1024) ) + opt->size = (unsigned int) v; + } + else if (0 == strncmp (pos, + "offset=", + strlen ("offset="))) + { + pos += strlen ("offset="); + v = strtoull (pos, &endp, 10); + if ( (endp != pos) && + (v <= 24 * 60 * 60) ) + opt->offset = (unsigned int) v; + } + else if (0 == strncmp (pos, + "length=", + strlen ("length="))) + { + pos += strlen ("length="); + v = strtoull (pos, &endp, 10); + if ( (endp != pos) && + (v >= 1) && + (v <= 60) ) + opt->length = (unsigned int) v; + } + else if (0 == strncmp (pos, + "bitrate=", + strlen ("bitrate="))) + { + pos += strlen ("bitrate="); + v = strtoull (pos, &endp, 10); + if ( (endp != pos) && + (v >= 6000) && + (v <= 128000) ) + opt->bitrate = (unsigned int) v; + } + else if (0 == strncmp (pos, + "deadline=", + strlen ("deadline="))) + { + pos += strlen ("deadline="); + v = strtoull (pos, &endp, 10); + if ( (endp != pos) && + (v >= 100) && + (v <= 120000) ) + opt->deadline = (unsigned int) v; + } + /* advance to the next assignment */ + pos = strchr (pos, ','); + if (NULL == pos) + break; + pos++; + } +} + + +int +EXTRACTOR_media_is_media_ (struct EXTRACTOR_ExtractContext *ec, + int want_audio) +{ + magic_t magic; + void *data; + ssize_t iret; + const char *mime; + int ret; + + if (-1 == (iret = ec->read (ec->cls, + &data, + SNIFF_SIZE))) + return 0; + ret = 0; + if (NULL == (magic = magic_open (MAGIC_MIME_TYPE))) + return 0; + if (0 == magic_load (magic, NULL)) + { + mime = magic_buffer (magic, + data, + iret); + if (NULL != mime) + { + if (0 == strncmp (mime, + "video/", + strlen ("video/"))) + ret = 1; + if ( (0 != want_audio) && + (0 == strncmp (mime, + "audio/", + strlen ("audio/"))) ) + ret = 1; + /* Ogg and Matroska files carrying video are frequently reported as + "application/ogg" or "video/x-matroska"; the former would be lost + here, so let it through and let the demuxer decide. */ + if (0 == strcmp (mime, + "application/ogg")) + ret = 1; + } + } + magic_close (magic); + if (0 != ec->seek (ec->cls, + 0, + SEEK_SET)) + return 0; + return ret; +} + + +/** + * Current time in milliseconds since an arbitrary epoch. + * + * @return the time + */ +static uint64_t +now_ms (void) +{ + struct timespec ts; + + if (0 != clock_gettime (CLOCK_MONOTONIC, + &ts)) + return 0; + return ((uint64_t) ts.tv_sec) * 1000ULL + + ((uint64_t) ts.tv_nsec) / 1000000ULL; +} + + +void +EXTRACTOR_media_deadline_start_ (struct EXTRACTOR_MediaDeadline *dl, + unsigned int ms) +{ + dl->expiration = now_ms () + ms; +} + + +int +EXTRACTOR_media_deadline_expired_ (const struct EXTRACTOR_MediaDeadline *dl) +{ + return now_ms () >= dl->expiration; +} + + +uint64_t +EXTRACTOR_media_deadline_remaining_ (const struct EXTRACTOR_MediaDeadline *dl) +{ + uint64_t now = now_ms (); + + if (now >= dl->expiration) + return 0; + return dl->expiration - now; +} + + +int +EXTRACTOR_media_thumbnail_size_ (unsigned int width, + unsigned int height, + unsigned int box, + unsigned int *rwidth, + unsigned int *rheight) +{ + uint64_t w = width; + uint64_t h = height; + + if ( (0 == width) || + (0 == height) || + (width > EXTRACTOR_MEDIA_MAX_DIMENSION) || + (height > EXTRACTOR_MEDIA_MAX_DIMENSION) || + (((uint64_t) width) * height > EXTRACTOR_MEDIA_MAX_PIXELS) ) + return 0; + if (h > box) + { + w = w * box / h; + h = box; + } + if (w > box) + { + h = h * box / w; + w = box; + } + if ( (0 == w) || + (0 == h) ) + return 0; + /* many encoders dislike odd dimensions, and we lose nothing by rounding */ + w &= ~1ULL; + h &= ~1ULL; + if ( (0 == w) || + (0 == h) ) + return 0; + *rwidth = (unsigned int) w; + *rheight = (unsigned int) h; + return 1; +} + + +void +EXTRACTOR_media_fade_s16_ (int16_t *samples, + size_t num_samples, + size_t fade_samples) +{ + size_t i; + + if (0 == fade_samples) + return; + if (fade_samples * 2 > num_samples) + fade_samples = num_samples / 2; + for (i = 0; i < fade_samples; i++) + { + samples[i] = (int16_t) (((int32_t) samples[i]) * (int32_t) i + / (int32_t) fade_samples); + samples[num_samples - 1 - i] = + (int16_t) (((int32_t) samples[num_samples - 1 - i]) * (int32_t) i + / (int32_t) fade_samples); + } +} + + +/* end of mediautil.c */ diff --git a/src/plugins/mediautil.h b/src/plugins/mediautil.h @@ -0,0 +1,207 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file plugins/mediautil.h + * @brief helpers shared by the video thumbnail and audio preview plugins + * @author Christian Grothoff + * + * The four media plugins (thumbnailffmpeg, thumbnailgst, previewopus and + * previewgst) all have to do the same three boring things before they can + * get to work: parse the plugin option string, make sure the input even + * looks like media, and keep an eye on the clock. This file is compiled + * into each of them (plugins are modules and must not depend on each + * other, so this is deliberately a source-level, not a library-level, + * dependency). + */ +#ifndef MEDIAUTIL_H +#define MEDIAUTIL_H + +#include "extractor.h" + +/** + * Maximum number of bytes we are willing to emit for a thumbnail. + */ +#define EXTRACTOR_MEDIA_MAX_THUMBNAIL (100 * 1024) + +/** + * Maximum number of bytes we are willing to emit for an audio preview. + */ +#define EXTRACTOR_MEDIA_MAX_PREVIEW (64 * 1024) + +/** + * Largest image we are willing to scale down, in pixels. Guards against + * absurd dimensions in a hostile file header. + */ +#define EXTRACTOR_MEDIA_MAX_PIXELS (64 * 1024 * 1024) + +/** + * Largest width or height we accept for the source video. + */ +#define EXTRACTOR_MEDIA_MAX_DIMENSION 16384 + + +/** + * Options a media plugin understands, as given in the plugin + * configuration string, i.e. "thumbnailffmpeg(size=256,format=png)". + */ +struct EXTRACTOR_MediaOptions +{ + /** + * Edge length of the box the thumbnail must fit into, in pixels. + */ + unsigned int size; + + /** + * Emit PNG instead of JPEG (option "format=png"). + */ + int png; + + /** + * How many seconds into the media we would like to sample. + */ + unsigned int offset; + + /** + * Length of the audio preview in seconds. + */ + unsigned int length; + + /** + * Target bitrate of the Opus encoder, in bits per second. + */ + unsigned int bitrate; + + /** + * Wall clock budget for the entire extraction, in milliseconds. + */ + unsigned int deadline; +}; + + +/** + * A wall clock deadline. + */ +struct EXTRACTOR_MediaDeadline +{ + /** + * When we must be done, in milliseconds since some arbitrary epoch. + */ + uint64_t expiration; +}; + + +/** + * Initialize @a opt with the defaults, then apply the comma-separated + * "key=value" assignments from @a config. Unknown keys and malformed + * values are ignored; the resulting values are always in range. + * + * @param config configuration string of the plugin, may be NULL + * @param[out] opt set to the resulting options + */ +void +EXTRACTOR_media_options_parse_ (const char *config, + struct EXTRACTOR_MediaOptions *opt); + + +/** + * Check whether the input of @a ec looks like media we should touch, + * using libmagic on the first few kilobytes. Always leaves the + * extraction context rewound to the beginning of the file. + * + * Handing arbitrary bytes to a media framework is expensive and grows + * the attack surface for no gain, so both plugin families gate on this. + * + * @param ec extraction context to inspect + * @param want_audio also accept "audio/*" (in addition to "video/*") + * @return 1 if the input should be processed, 0 if not + */ +int +EXTRACTOR_media_is_media_ (struct EXTRACTOR_ExtractContext *ec, + int want_audio); + + +/** + * Start the clock. + * + * @param[out] dl deadline to initialize + * @param ms budget in milliseconds from now + */ +void +EXTRACTOR_media_deadline_start_ (struct EXTRACTOR_MediaDeadline *dl, + unsigned int ms); + + +/** + * Check whether the budget is used up. + * + * @param dl deadline to check + * @return 1 if we are out of time + */ +int +EXTRACTOR_media_deadline_expired_ (const struct EXTRACTOR_MediaDeadline *dl); + + +/** + * Time left before @a dl expires. + * + * @param dl deadline to check + * @return remaining milliseconds, 0 if expired + */ +uint64_t +EXTRACTOR_media_deadline_remaining_ (const struct EXTRACTOR_MediaDeadline *dl); + + +/** + * Compute the size of a thumbnail with the aspect ratio of a + * @a width x @a height image that fits into a @a box x @a box square. + * Never scales up. + * + * @param width width of the source image + * @param height height of the source image + * @param box edge length of the target square + * @param[out] rwidth width of the thumbnail + * @param[out] rheight height of the thumbnail + * @return 1 on success, 0 if the source dimensions are unusable + */ +int +EXTRACTOR_media_thumbnail_size_ (unsigned int width, + unsigned int height, + unsigned int box, + unsigned int *rwidth, + unsigned int *rheight); + + +/** + * Apply a linear fade-in and fade-out to 16-bit signed mono PCM. + * Without this, a preview cut out of the middle of a track starts and + * ends with an audible click. + * + * @param[in,out] samples PCM data to modify + * @param num_samples number of samples in @a samples + * @param fade_samples length of each ramp in samples + */ +void +EXTRACTOR_media_fade_s16_ (int16_t *samples, + size_t num_samples, + size_t fade_samples); + +#endif + +/* end of mediautil.h */ diff --git a/src/plugins/previewgst_extractor.c b/src/plugins/previewgst_extractor.c @@ -0,0 +1,408 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file plugins/previewgst_extractor.c + * @author Christian Grothoff + * @brief this extractor produces a short audio preview: about 15 seconds + * taken from the middle of the track, re-encoded as Opus in an Ogg + * container, which is the audio equivalent of a thumbnail + * + * This is the GStreamer counterpart of previewopus. The entire job is + * done by the pipeline + * + * playbin(appsrc://) ! audioconvert ! volume ! audioresample + * ! opusenc ! oggmux ! appsink + * + * where the segment boundaries come from a seek with an explicit stop + * position, so that the pipeline reaches EOS -- and thus finishes the Ogg + * stream -- exactly where we want the preview to end. The volume element + * carries a control source that fades the preview in and out; without it + * a segment cut from the middle of a track begins with a click. + */ +#include "platform.h" +#include "extractor.h" +#include "mediautil.h" +#include "mediagst.h" + +#include <gst/app/gstappsink.h> +#include <gst/controller/gstinterpolationcontrolsource.h> +#include <gst/controller/gstdirectcontrolbinding.h> + +/** + * Sample rate we encode at. Opus works internally at 48kHz, so anything + * else would just mean resampling twice. + */ +#define OUTPUT_SAMPLE_RATE 48000 + +/** + * Length of the fade-in and fade-out, in nanoseconds. + */ +#define FADE_NS (250 * GST_MSECOND) + +/** + * Refuse to emit a preview shorter than this. + */ +#define MIN_PREVIEW_NS (500 * GST_MSECOND) + + +/** + * Build the audio sink bin that encodes what playbin decodes. + * + * @param opt plugin options (bitrate) + * @param[out] appsink set to the sink at the end of the bin + * @param[out] volume set to the element carrying the fade + * @return the bin, or NULL if an element is missing + */ +static GstElement * +build_encoder_bin (const struct EXTRACTOR_MediaOptions *opt, + GstElement **appsink, + GstElement **volume) +{ + GstElement *bin; + GstElement *convert; + GstElement *vol; + GstElement *resample; + GstElement *filter; + GstElement *encoder; + GstElement *muxer; + GstElement *sink; + GstCaps *caps; + GstPad *pad; + + convert = gst_element_factory_make ("audioconvert", NULL); + vol = gst_element_factory_make ("volume", NULL); + resample = gst_element_factory_make ("audioresample", NULL); + filter = gst_element_factory_make ("capsfilter", NULL); + encoder = gst_element_factory_make ("opusenc", NULL); + muxer = gst_element_factory_make ("oggmux", NULL); + sink = gst_element_factory_make ("appsink", NULL); + if ( (NULL == convert) || + (NULL == vol) || + (NULL == resample) || + (NULL == filter) || + (NULL == encoder) || + (NULL == muxer) || + (NULL == sink) ) + { + /* gst-plugins-base is not installed, or not completely */ + if (NULL != convert) + gst_object_unref (convert); + if (NULL != vol) + gst_object_unref (vol); + if (NULL != resample) + gst_object_unref (resample); + if (NULL != filter) + gst_object_unref (filter); + if (NULL != encoder) + gst_object_unref (encoder); + if (NULL != muxer) + gst_object_unref (muxer); + if (NULL != sink) + gst_object_unref (sink); + return NULL; + } + caps = gst_caps_new_simple ("audio/x-raw", + "rate", G_TYPE_INT, OUTPUT_SAMPLE_RATE, + "channels", G_TYPE_INT, 1, + NULL); + g_object_set (filter, + "caps", caps, + NULL); + gst_caps_unref (caps); + g_object_set (encoder, + "bitrate", (gint) opt->bitrate, + NULL); + g_object_set (sink, + "sync", FALSE, + "max-buffers", 0, + NULL); + bin = gst_bin_new (NULL); + gst_bin_add_many (GST_BIN (bin), + convert, + vol, + resample, + filter, + encoder, + muxer, + sink, + NULL); + if (! gst_element_link_many (convert, + vol, + resample, + filter, + encoder, + muxer, + sink, + NULL)) + { + gst_object_unref (bin); + return NULL; + } + if (NULL == (pad = gst_element_get_static_pad (convert, "sink"))) + { + gst_object_unref (bin); + return NULL; + } + if (! gst_element_add_pad (bin, + gst_ghost_pad_new ("sink", pad))) + { + gst_object_unref (pad); + gst_object_unref (bin); + return NULL; + } + gst_object_unref (pad); + *appsink = sink; + *volume = vol; + return bin; +} + + +/** + * Make @a volume ramp up at the start of the segment and down at its end. + * + * @param volume the volume element + * @param length length of the segment, in nanoseconds + */ +static void +setup_fade (GstElement *volume, + gint64 length) +{ + GstControlSource *cs; + GstTimedValueControlSource *tv; + gint64 fade = FADE_NS; + + if (length <= 0) + return; + if (fade * 2 > length) + fade = length / 2; + if (0 == fade) + return; + if (NULL == (cs = gst_interpolation_control_source_new ())) + return; + g_object_set (cs, + "mode", GST_INTERPOLATION_MODE_LINEAR, + NULL); + if (! gst_object_add_control_binding ( + GST_OBJECT (volume), + gst_direct_control_binding_new_absolute (GST_OBJECT (volume), + "volume", + cs))) + { + /* not fatal: the preview is merely less pleasant to listen to */ + gst_object_unref (cs); + return; + } + tv = GST_TIMED_VALUE_CONTROL_SOURCE (cs); + gst_timed_value_control_source_set (tv, 0, 0.0); + gst_timed_value_control_source_set (tv, fade, 1.0); + gst_timed_value_control_source_set (tv, length - fade, 1.0); + gst_timed_value_control_source_set (tv, length, 0.0); + gst_object_unref (cs); +} + + +/** + * Collect everything the appsink hands us until it reaches EOS. + * + * @param sink the appsink at the end of our pipeline + * @param deadline when to give up + * @param limit maximum number of bytes to collect + * @return the Ogg stream, or NULL; free with g_byte_array_unref() + */ +static GByteArray * +collect_output (GstElement *sink, + const struct EXTRACTOR_MediaDeadline *deadline, + size_t limit) +{ + GByteArray *out; + + out = g_byte_array_new (); + while (1) + { + GstSample *sample; + GstBuffer *buffer; + GstMapInfo mi; + uint64_t remaining; + + remaining = EXTRACTOR_media_deadline_remaining_ (deadline); + if (0 == remaining) + break; + sample = gst_app_sink_try_pull_sample (GST_APP_SINK (sink), + remaining * GST_MSECOND); + if (NULL == sample) + break; /* end of stream, or we ran out of time */ + buffer = gst_sample_get_buffer (sample); + if ( (NULL != buffer) && + (gst_buffer_map (buffer, &mi, GST_MAP_READ)) ) + { + if (out->len + mi.size <= limit) + g_byte_array_append (out, + mi.data, + mi.size); + else + { + /* the stream would be truncated, and a truncated Ogg file is + worse than none at all */ + gst_buffer_unmap (buffer, &mi); + gst_sample_unref (sample); + g_byte_array_unref (out); + return NULL; + } + gst_buffer_unmap (buffer, &mi); + } + gst_sample_unref (sample); + } + return out; +} + + +/** + * Main method for the gstreamer audio preview plugin. + * + * @param ec extraction context + */ +void +EXTRACTOR_previewgst_extract_method (struct EXTRACTOR_ExtractContext *ec); + +void +EXTRACTOR_previewgst_extract_method (struct EXTRACTOR_ExtractContext *ec) +{ + struct EXTRACTOR_MediaOptions opt; + struct EXTRACTOR_MediaDeadline deadline; + struct EXTRACTOR_GstIO io; + GstElement *pipeline; + GstElement *bin; + GstElement *sink; + GstElement *volume; + GByteArray *output = NULL; + gint64 duration; + gint64 start; + gint64 stop; + size_t limit; + + EXTRACTOR_media_options_parse_ (ec->config, + &opt); + EXTRACTOR_media_deadline_start_ (&deadline, + opt.deadline); + if (! EXTRACTOR_media_is_media_ (ec, 1)) + return; + if (! gst_is_initialized ()) + gst_init (NULL, NULL); + if (NULL == (bin = build_encoder_bin (&opt, + &sink, + &volume))) + return; + if (NULL == (pipeline = + EXTRACTOR_gst_playbin_ (&io, + ec, + EXTRACTOR_GST_PLAY_FLAG_AUDIO, + gst_element_factory_make ("fakesink", + NULL), + bin))) + return; + if (GST_STATE_CHANGE_FAILURE == + gst_element_set_state (pipeline, + GST_STATE_PAUSED)) + goto cleanup; + if (! EXTRACTOR_gst_wait_async_ (pipeline, + EXTRACTOR_media_deadline_remaining_ ( + &deadline) * GST_MSECOND)) + goto cleanup; + start = EXTRACTOR_gst_sample_position_ (pipeline, + opt.offset, + &duration); + stop = start + ((gint64) opt.length) * GST_SECOND; + if ( (0 != duration) && + (stop > duration) ) + stop = duration; + if ( (0 != duration) && + (stop - start < MIN_PREVIEW_NS) ) + goto cleanup; /* too short to be worth anything */ + setup_fade (volume, + stop - start); + if (! gst_element_seek (pipeline, + 1.0, + GST_FORMAT_TIME, + GST_SEEK_FLAG_FLUSH + | GST_SEEK_FLAG_ACCURATE, + GST_SEEK_TYPE_SET, + start, + GST_SEEK_TYPE_SET, + stop)) + goto cleanup; + if (! EXTRACTOR_gst_wait_async_ (pipeline, + EXTRACTOR_media_deadline_remaining_ ( + &deadline) * GST_MSECOND)) + goto cleanup; + if (GST_STATE_CHANGE_FAILURE == + gst_element_set_state (pipeline, + GST_STATE_PLAYING)) + goto cleanup; + /* the caller asked for a preview of a given length and bitrate, so let + that decide the limit, but never emit something absurd */ + limit = (size_t) opt.length * opt.bitrate / 8 + 8192; + if (limit < EXTRACTOR_MEDIA_MAX_PREVIEW) + limit = EXTRACTOR_MEDIA_MAX_PREVIEW; + output = collect_output (sink, + &deadline, + limit); + if (NULL == output) + goto cleanup; + if (0 == output->len) + goto cleanup; + ec->proc (ec->cls, + "previewgst", + EXTRACTOR_METATYPE_AUDIO_PREVIEW, + EXTRACTOR_METAFORMAT_BINARY, + "audio/ogg", + (const char *) output->data, + output->len); +cleanup: + if (NULL != output) + g_byte_array_unref (output); + gst_element_set_state (pipeline, + GST_STATE_NULL); + gst_object_unref (pipeline); +} + + +/** + * Keep the diagnostics of the media framework out of the output of the + * calling application; a decoder complaining about a broken file is not + * something the application asked to see. + * + * ("force-kill" would be tempting here, to get a fresh process for every + * file, but the host cannot know about that special without loading the + * plugin in-process: it only finds out that the child is gone once it has + * already handed it the next file, which is then silently lost.) + * + * @return special options for this plugin + */ +const char * +EXTRACTOR_previewgst_options (void); + +const char * +EXTRACTOR_previewgst_options () +{ + return "close-stderr"; +} + + +/* end of previewgst_extractor.c */ diff --git a/src/plugins/previewopus_extractor.c b/src/plugins/previewopus_extractor.c @@ -0,0 +1,570 @@ +/* + This file is part of libextractor. + Copyright (C) 2013, 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file plugins/previewopus_extractor.c + * @author Bruno Cabral + * @author Christian Grothoff + * @brief this extractor produces a short audio preview: about 15 seconds + * taken from the middle of the track, re-encoded as Opus in an Ogg + * container, which is the audio equivalent of a thumbnail + * + * Uses FFmpeg for demuxing, decoding and resampling and libopus (through + * FFmpeg) for encoding. Requires FFmpeg 5.1 or newer; there is + * deliberately no compatibility code for older versions, since that is + * what got the previous incarnation of this plugin deleted in 2021. + */ +#include "platform.h" +#include "extractor.h" +#include "mediautil.h" +#include "mediaffmpeg.h" + +#include <libavutil/opt.h> +#include <libswresample/swresample.h> + +/** + * Sample rate we encode at. Opus works internally at 48kHz, so anything + * else would just mean resampling twice. + */ +#define OUTPUT_SAMPLE_RATE 48000 + +/** + * Length of the fade-in and fade-out, in milliseconds. Without this a + * preview cut from the middle of a track starts with a click. + */ +#define FADE_MS 250 + +/** + * Refuse to emit a preview shorter than this many milliseconds. + */ +#define MIN_PREVIEW_MS 500 + +/** + * Give up after this many decoded packets. + */ +#define MAX_PACKETS 100000 + + +/** + * State of the Opus encoder and its Ogg muxer. + */ +struct Encoder +{ + /** + * Muxer producing the Ogg stream. + */ + AVFormatContext *oc; + + /** + * The Opus encoder. + */ + AVCodecContext *ctx; + + /** + * Stream in @e oc. + */ + AVStream *st; +}; + + +/** + * Decode the audio stream into 48kHz mono 16 bit PCM, starting at + * @a target seconds and stopping after @a max_samples samples. + * + * @param io open I/O state + * @param ctx open decoder for @a stream + * @param stream index of the audio stream + * @param target position we are aiming for, in seconds + * @param max_samples size of @a pcm in samples + * @param[out] pcm where to write the samples + * @return number of samples written + */ +static size_t +decode_pcm (struct EXTRACTOR_FFmpegIO *io, + AVCodecContext *ctx, + int stream, + double target, + size_t max_samples, + int16_t *pcm) +{ + struct SwrContext *swr = NULL; + AVChannelLayout mono = AV_CHANNEL_LAYOUT_MONO; + AVPacket *pkt; + AVFrame *frame; + AVRational tb = io->fmt->streams[stream]->time_base; + size_t have = 0; + unsigned int packets = 0; + + if (NULL == (pkt = av_packet_alloc ())) + return 0; + if (NULL == (frame = av_frame_alloc ())) + { + av_packet_free (&pkt); + return 0; + } + while ( (have < max_samples) && + (packets < MAX_PACKETS) ) + { + int r; + + if (EXTRACTOR_media_deadline_expired_ (&io->deadline)) + break; + packets++; + r = av_read_frame (io->fmt, pkt); + if (0 != r) + { + avcodec_send_packet (ctx, NULL); + } + else if (pkt->stream_index != stream) + { + av_packet_unref (pkt); + continue; + } + else if (0 != avcodec_send_packet (ctx, pkt)) + { + av_packet_unref (pkt); + continue; + } + av_packet_unref (pkt); + while (0 == avcodec_receive_frame (ctx, frame)) + { + uint8_t *out[1]; + int room; + int got; + double ts; + + ts = (AV_NOPTS_VALUE == frame->best_effort_timestamp) + ? -1.0 + : frame->best_effort_timestamp * av_q2d (tb); + if ( (ts >= 0.0) && + (ts + frame->nb_samples / (double) ((frame->sample_rate > 0) + ? frame->sample_rate + : OUTPUT_SAMPLE_RATE) < target) ) + { + /* still before the segment we want */ + av_frame_unref (frame); + continue; + } + if (NULL == swr) + { + if (0 != swr_alloc_set_opts2 (&swr, + &mono, + AV_SAMPLE_FMT_S16, + OUTPUT_SAMPLE_RATE, + &frame->ch_layout, + frame->format, + frame->sample_rate, + 0, + NULL)) + { + av_frame_unref (frame); + goto done; + } + if (0 > swr_init (swr)) + { + av_frame_unref (frame); + goto done; + } + } + room = (int) (max_samples - have); + out[0] = (uint8_t *) &pcm[have]; + got = swr_convert (swr, + out, + room, + (const uint8_t **) frame->extended_data, + frame->nb_samples); + av_frame_unref (frame); + if (got <= 0) + continue; + have += (size_t) got; + if (have >= max_samples) + goto done; + } + if (0 != r) + break; + } +done: + if (NULL != swr) + swr_free (&swr); + av_frame_free (&frame); + av_packet_free (&pkt); + return have; +} + + +/** + * Set up the Opus encoder and the Ogg muxer, writing into a dynamic + * memory buffer. + * + * @param opt plugin options (bitrate) + * @param[out] enc encoder state to initialize + * @return 0 on success + */ +static int +encoder_start (const struct EXTRACTOR_MediaOptions *opt, + struct Encoder *enc) +{ + const AVCodec *codec; + AVChannelLayout mono = AV_CHANNEL_LAYOUT_MONO; + int experimental = 0; + + memset (enc, 0, sizeof (*enc)); + codec = avcodec_find_encoder_by_name ("libopus"); + if (NULL == codec) + { + /* FFmpeg's own Opus encoder is still marked experimental, but it is + better than emitting nothing at all */ + codec = avcodec_find_encoder (AV_CODEC_ID_OPUS); + experimental = 1; + } + if (NULL == codec) + return -1; + if (0 > avformat_alloc_output_context2 (&enc->oc, + NULL, + "ogg", + NULL)) + return -1; + if (NULL == enc->oc) + return -1; + if (NULL == (enc->ctx = avcodec_alloc_context3 (codec))) + return -1; + enc->ctx->sample_rate = OUTPUT_SAMPLE_RATE; + enc->ctx->bit_rate = opt->bitrate; + enc->ctx->time_base = (AVRational) { 1, OUTPUT_SAMPLE_RATE }; + enc->ctx->thread_count = 1; + if (0 != av_channel_layout_copy (&enc->ctx->ch_layout, + &mono)) + return -1; + if (experimental) + { + enc->ctx->sample_fmt = AV_SAMPLE_FMT_FLTP; + enc->ctx->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL; + } + else + { + enc->ctx->sample_fmt = AV_SAMPLE_FMT_S16; + } + /* the Ogg muxer needs the OpusHead/OpusTags headers as extradata */ + enc->ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + /* libopus defaults to unconstrained VBR, where the bitrate we asked for + is little more than a suggestion -- a pure tone happily comes out at + twice the target. We care about the size of the result, so ask for + the constrained variant. Not an option on the native encoder, which + simply ignores it. */ + if (NULL != enc->ctx->priv_data) + (void) av_opt_set (enc->ctx->priv_data, + "vbr", + "constrained", + 0); + if (0 != avcodec_open2 (enc->ctx, codec, NULL)) + return -1; + if (NULL == (enc->st = avformat_new_stream (enc->oc, NULL))) + return -1; + enc->st->time_base = enc->ctx->time_base; + if (0 > avcodec_parameters_from_context (enc->st->codecpar, + enc->ctx)) + return -1; + if (0 != avio_open_dyn_buf (&enc->oc->pb)) + return -1; + if (0 > avformat_write_header (enc->oc, NULL)) + return -1; + return 0; +} + + +/** + * Hand one packet from the encoder to the muxer. + * + * @param enc encoder state + * @param pkt packet to write, unreferenced on return + * @return 0 on success + */ +static int +encoder_write (struct Encoder *enc, + AVPacket *pkt) +{ + av_packet_rescale_ts (pkt, + enc->ctx->time_base, + enc->st->time_base); + pkt->stream_index = enc->st->index; + return (0 > av_interleaved_write_frame (enc->oc, pkt)) ? -1 : 0; +} + + +/** + * Encode @a num_samples of 48kHz mono PCM and finish the Ogg stream. + * + * @param enc encoder state + * @param pcm samples to encode + * @param num_samples number of samples in @a pcm + * @param[out] output set to the Ogg stream, to be freed with av_free() + * @param[out] output_size set to the number of bytes in @a output + * @return 0 on success + */ +static int +encoder_finish (struct Encoder *enc, + const int16_t *pcm, + size_t num_samples, + uint8_t **output, + size_t *output_size) +{ + AVFrame *frame = NULL; + AVPacket *pkt = NULL; + size_t pos = 0; + int frame_size; + int ret = -1; + int size; + + frame_size = (enc->ctx->frame_size > 0) + ? enc->ctx->frame_size + : OUTPUT_SAMPLE_RATE / 50; + if (NULL == (pkt = av_packet_alloc ())) + goto cleanup; + if (NULL == (frame = av_frame_alloc ())) + goto cleanup; + frame->format = enc->ctx->sample_fmt; + frame->sample_rate = OUTPUT_SAMPLE_RATE; + frame->nb_samples = frame_size; + if (0 != av_channel_layout_copy (&frame->ch_layout, + &enc->ctx->ch_layout)) + goto cleanup; + if (0 != av_frame_get_buffer (frame, 0)) + goto cleanup; + while (pos < num_samples) + { + size_t n = num_samples - pos; + int r; + + if (n > (size_t) frame_size) + n = frame_size; + if (0 != av_frame_make_writable (frame)) + goto cleanup; + if (AV_SAMPLE_FMT_S16 == enc->ctx->sample_fmt) + { + memcpy (frame->data[0], + &pcm[pos], + n * sizeof (int16_t)); + if (n < (size_t) frame_size) + memset (frame->data[0] + n * sizeof (int16_t), + 0, + (frame_size - n) * sizeof (int16_t)); + } + else + { + float *dst = (float *) frame->data[0]; + size_t i; + + for (i = 0; i < n; i++) + dst[i] = pcm[pos + i] / 32768.0f; + for (i = n; i < (size_t) frame_size; i++) + dst[i] = 0.0f; + } + frame->pts = (int64_t) pos; + if (0 != avcodec_send_frame (enc->ctx, frame)) + goto cleanup; + while (0 == (r = avcodec_receive_packet (enc->ctx, pkt))) + if (0 != encoder_write (enc, pkt)) + goto cleanup; + if (AVERROR (EAGAIN) != r) + goto cleanup; + pos += n; + } + if (0 != avcodec_send_frame (enc->ctx, NULL)) + goto cleanup; + while (0 == avcodec_receive_packet (enc->ctx, pkt)) + if (0 != encoder_write (enc, pkt)) + goto cleanup; + if (0 > av_write_trailer (enc->oc)) + goto cleanup; + size = avio_close_dyn_buf (enc->oc->pb, + output); + enc->oc->pb = NULL; + if (size <= 0) + { + if (NULL != *output) + av_free (*output); + *output = NULL; + goto cleanup; + } + *output_size = (size_t) size; + ret = 0; +cleanup: + if (NULL != frame) + av_frame_free (&frame); + if (NULL != pkt) + av_packet_free (&pkt); + return ret; +} + + +/** + * Clean up the encoder state. + * + * @param enc encoder state to free + */ +static void +encoder_cleanup (struct Encoder *enc) +{ + if (NULL != enc->ctx) + avcodec_free_context (&enc->ctx); + if (NULL != enc->oc) + { + if (NULL != enc->oc->pb) + { + uint8_t *leftover = NULL; + + (void) avio_close_dyn_buf (enc->oc->pb, + &leftover); + enc->oc->pb = NULL; + if (NULL != leftover) + av_free (leftover); + } + avformat_free_context (enc->oc); + enc->oc = NULL; + } +} + + +/** + * Main method for the opus-preview plugin. + * + * @param ec extraction context + */ +void +EXTRACTOR_previewopus_extract_method (struct EXTRACTOR_ExtractContext *ec); + +void +EXTRACTOR_previewopus_extract_method (struct EXTRACTOR_ExtractContext *ec) +{ + struct EXTRACTOR_MediaOptions opt; + struct EXTRACTOR_FFmpegIO io; + struct Encoder enc; + AVCodecContext *ctx = NULL; + int16_t *pcm = NULL; + uint8_t *preview = NULL; + size_t preview_size = 0; + size_t max_samples; + size_t have; + size_t limit; + double target; + int stream; + + EXTRACTOR_media_options_parse_ (ec->config, + &opt); + EXTRACTOR_media_deadline_start_ (&io.deadline, + opt.deadline); + io.ec = ec; + memset (&enc, 0, sizeof (enc)); + if (! EXTRACTOR_media_is_media_ (ec, 1)) + return; + if (0 != EXTRACTOR_ffmpeg_open_ (&io)) + return; + stream = EXTRACTOR_ffmpeg_open_stream_ (&io, + AVMEDIA_TYPE_AUDIO, + &ctx); + if (0 > stream) + goto cleanup; + target = EXTRACTOR_ffmpeg_seek_ (&io, + stream, + ctx, + opt.offset); + max_samples = (size_t) opt.length * OUTPUT_SAMPLE_RATE; + if (NULL == (pcm = malloc (max_samples * sizeof (int16_t)))) + goto cleanup; + have = decode_pcm (&io, + ctx, + stream, + target, + max_samples, + pcm); + if (have < (size_t) MIN_PREVIEW_MS * OUTPUT_SAMPLE_RATE / 1000) + goto cleanup; /* too short to be worth anything */ + EXTRACTOR_media_fade_s16_ (pcm, + have, + FADE_MS * OUTPUT_SAMPLE_RATE / 1000); + if (0 != encoder_start (&opt, + &enc)) + goto cleanup; + if (0 != encoder_finish (&enc, + pcm, + have, + &preview, + &preview_size)) + goto cleanup; + /* the caller asked for a preview of a given length and bitrate, so let + that decide the limit, but never emit something absurd */ + limit = (size_t) opt.length * opt.bitrate / 8 + 8192; + if (limit < EXTRACTOR_MEDIA_MAX_PREVIEW) + limit = EXTRACTOR_MEDIA_MAX_PREVIEW; + if (preview_size > limit) + goto cleanup; + ec->proc (ec->cls, + "previewopus", + EXTRACTOR_METATYPE_AUDIO_PREVIEW, + EXTRACTOR_METAFORMAT_BINARY, + "audio/ogg", + (const char *) preview, + preview_size); +cleanup: + if (NULL != preview) + av_free (preview); + encoder_cleanup (&enc); + free (pcm); + if (NULL != ctx) + avcodec_free_context (&ctx); + EXTRACTOR_ffmpeg_close_ (&io); +} + + +/** + * Keep the diagnostics of the media framework out of the output of the + * calling application; a decoder complaining about a broken file is not + * something the application asked to see. + * + * ("force-kill" would be tempting here, to get a fresh process for every + * file, but the host cannot know about that special without loading the + * plugin in-process: it only finds out that the child is gone once it has + * already handed it the next file, which is then silently lost.) + * + * @return special options for this plugin + */ +const char * +EXTRACTOR_previewopus_options (void); + +const char * +EXTRACTOR_previewopus_options () +{ + return "close-stderr"; +} + + +/** + * Silence FFmpeg's logging. + */ +void __attribute__ ((constructor)) +previewopus_init (void); + +void __attribute__ ((constructor)) +previewopus_init () +{ + EXTRACTOR_ffmpeg_silence_ (); +} + + +/* end of previewopus_extractor.c */ diff --git a/src/plugins/test_lib.c b/src/plugins/test_lib.c @@ -113,6 +113,10 @@ process_replies (void *cls, if (NULL != data_mime_type) continue; } + if ( (NULL != sd[i].validate) && + (0 == sd[i].validate (data, + data_len)) ) + continue; sd[i].solved = 1; return 0; } diff --git a/src/plugins/test_lib.h b/src/plugins/test_lib.h @@ -68,6 +68,20 @@ struct SolutionData * Treat solution as a regex that must match. */ int regex; + + /** + * Optional additional check on the meta data, for values that cannot + * be spelled out in a test (an encoder's output, say, of which only + * the header is predictable). Called after type, format, mime type + * and the @e data prefix matched; the item only counts as solved if + * this returns non-zero. NULL to skip. + * + * @param data the meta data the plugin produced + * @param data_len number of bytes in @a data + * @return non-zero if @a data is acceptable + */ + int (*validate)(const void *data, + size_t data_len); }; diff --git a/src/plugins/test_media_lib.c b/src/plugins/test_media_lib.c @@ -0,0 +1,321 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file plugins/test_media_lib.c + * @brief helpers for testing the video thumbnail and audio preview plugins + * @author Christian Grothoff + */ +#include "platform.h" +#include "test_media_lib.h" + +#if TEST_MEDIA_HAVE_PNG +#include <zlib.h> +#endif + +/** + * How far a decoded colour may be off before we call it a mismatch. + */ +#define COLOR_TOLERANCE 48 + + +/** + * Read a big endian 32 bit number. + * + * @param p bytes to read + * @return the number + */ +static uint32_t +get_u32 (const uint8_t *p) +{ + return (((uint32_t) p[0]) << 24) + | (((uint32_t) p[1]) << 16) + | (((uint32_t) p[2]) << 8) + | ((uint32_t) p[3]); +} + + +#if TEST_MEDIA_HAVE_PNG +/** + * The Paeth predictor of the PNG specification. + * + * @param a pixel to the left + * @param b pixel above + * @param c pixel above left + * @return the predicted value + */ +static uint8_t +paeth (uint8_t a, + uint8_t b, + uint8_t c) +{ + int p = (int) a + (int) b - (int) c; + int pa = abs (p - (int) a); + int pb = abs (p - (int) b); + int pc = abs (p - (int) c); + + if ( (pa <= pb) && + (pa <= pc) ) + return a; + if (pb <= pc) + return b; + return c; +} + + +/** + * Undo the per-scanline filters of a decompressed PNG image. + * + * @param[in,out] raw the filtered scanlines, overwritten with the image + * @param width width of the image in pixels + * @param height height of the image in pixels + * @param bpp number of bytes per pixel + * @return 1 on success + */ +static int +unfilter (uint8_t *raw, + uint32_t width, + uint32_t height, + uint32_t bpp) +{ + uint32_t stride = width * bpp; + uint8_t *prev = NULL; + uint32_t y; + + for (y = 0; y < height; y++) + { + uint8_t *row = &raw[y * (stride + 1)]; + uint8_t ft = row[0]; + uint32_t x; + + row++; + for (x = 0; x < stride; x++) + { + uint8_t a = (x >= bpp) ? row[x - bpp] : 0; + uint8_t b = (NULL != prev) ? prev[x] : 0; + uint8_t c = ( (x >= bpp) && (NULL != prev) ) ? prev[x - bpp] : 0; + + switch (ft) + { + case 0: + break; + case 1: + row[x] += a; + break; + case 2: + row[x] += b; + break; + case 3: + row[x] += (uint8_t) (((int) a + (int) b) / 2); + break; + case 4: + row[x] += paeth (a, b, c); + break; + default: + return 0; + } + } + prev = row; + } + return 1; +} + + +#endif + + +int +TEST_media_png_center (const void *data, + size_t data_len, + uint8_t rgb[3]) +{ +#if ! TEST_MEDIA_HAVE_PNG + (void) data; + (void) data_len; + (void) rgb; + return 0; +#else + static const uint8_t magic[] = { 137, 'P', 'N', 'G', '\r', '\n', 26, '\n' }; + const uint8_t *d = data; + uint8_t *idat = NULL; + uint8_t *raw = NULL; + size_t idat_len = 0; + size_t pos = sizeof (magic); + uLongf raw_len; + uint32_t width = 0; + uint32_t height = 0; + uint32_t bpp = 0; + uint32_t stride; + const uint8_t *px; + int ret = 0; + + if ( (data_len < sizeof (magic) + 12) || + (0 != memcmp (d, magic, sizeof (magic))) ) + return 0; + while (pos + 12 <= data_len) + { + uint32_t clen = get_u32 (&d[pos]); + const char *ctype = (const char *) &d[pos + 4]; + const uint8_t *cdata = &d[pos + 8]; + + if ( (clen > data_len) || + (pos + 12 + clen > data_len) ) + break; + if (0 == memcmp (ctype, "IHDR", 4)) + { + if (clen < 13) + goto cleanup; + width = get_u32 (&cdata[0]); + height = get_u32 (&cdata[4]); + if ( (8 != cdata[8]) || + (0 != cdata[12]) ) + goto cleanup; /* not 8 bits per channel, or interlaced */ + if (2 == cdata[9]) + bpp = 3; + else if (6 == cdata[9]) + bpp = 4; + else + goto cleanup; /* palette or greyscale; our encoders emit neither */ + if ( (0 == width) || + (0 == height) || + (width > 4096) || + (height > 4096) ) + goto cleanup; + } + else if (0 == memcmp (ctype, "IDAT", 4)) + { + uint8_t *n = realloc (idat, idat_len + clen); + + if (NULL == n) + goto cleanup; + idat = n; + memcpy (&idat[idat_len], + cdata, + clen); + idat_len += clen; + } + else if (0 == memcmp (ctype, "IEND", 4)) + { + break; + } + pos += 12 + clen; + } + if ( (0 == bpp) || + (NULL == idat) ) + goto cleanup; + stride = width * bpp; + raw_len = (uLongf) height * (stride + 1); + if (NULL == (raw = malloc (raw_len))) + goto cleanup; + if (Z_OK != uncompress (raw, + &raw_len, + idat, + (uLong) idat_len)) + goto cleanup; + if (raw_len != (uLongf) height * (stride + 1)) + goto cleanup; + if (! unfilter (raw, width, height, bpp)) + goto cleanup; + px = &raw[(height / 2) * (stride + 1) + 1 + (width / 2) * bpp]; + rgb[0] = px[0]; + rgb[1] = px[1]; + rgb[2] = px[2]; + ret = 1; +cleanup: + free (raw); + free (idat); + return ret; +#endif +} + + +int +TEST_media_png_center_is (const void *data, + size_t data_len, + uint8_t r, + uint8_t g, + uint8_t b) +{ + uint8_t rgb[3]; + + if (! TEST_media_png_center (data, + data_len, + rgb)) + { +#if TEST_MEDIA_HAVE_PNG + fprintf (stderr, + "Failed to decode the PNG the plugin produced\n"); + return 0; +#else + /* without zlib we cannot look inside; the magic number check that + the caller already did will have to do */ + return 1; +#endif + } + if ( (abs ((int) rgb[0] - (int) r) > COLOR_TOLERANCE) || + (abs ((int) rgb[1] - (int) g) > COLOR_TOLERANCE) || + (abs ((int) rgb[2] - (int) b) > COLOR_TOLERANCE) ) + { + fprintf (stderr, + "Thumbnail centre is rgb(%u,%u,%u), expected rgb(%u,%u,%u); " + "the plugin most likely sampled the wrong position\n", + rgb[0], + rgb[1], + rgb[2], + r, + g, + b); + return 0; + } + return 1; +} + + +int +TEST_media_is_ogg_opus (const void *data, + size_t data_len) +{ + const uint8_t *d = data; + uint8_t segments; + size_t payload; + + /* an Ogg page header is 27 bytes plus one byte per segment */ + if (data_len < 28 + 8) + return 0; + if (0 != memcmp (d, "OggS", 4)) + return 0; + if (0 != d[4]) + return 0; /* stream structure version */ + segments = d[26]; + payload = 27 + (size_t) segments; + if (payload + 8 > data_len) + return 0; + if (0 != memcmp (&d[payload], + "OpusHead", + 8)) + { + fprintf (stderr, + "Ogg stream does not start with an OpusHead header\n"); + return 0; + } + return 1; +} + + +/* end of test_media_lib.c */ diff --git a/src/plugins/test_media_lib.h b/src/plugins/test_media_lib.h @@ -0,0 +1,89 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file plugins/test_media_lib.h + * @brief helpers for testing the video thumbnail and audio preview plugins + * @author Christian Grothoff + * + * Checking that a thumbnailer emitted *a* JPEG only proves that the + * plugin ran, not that it looked where it was told to. The test videos + * therefore change colour at a known point in time and these helpers + * inspect the pixel that comes back. + */ +#ifndef TEST_MEDIA_LIB_H +#define TEST_MEDIA_LIB_H + +#include <stdint.h> +#include <stdlib.h> + +/** + * Decode the PNG in @a data and return its centre pixel. + * + * Only the subset of PNG our own encoders produce is understood: 8 bits + * per channel, RGB or RGBA, not interlaced. Requires zlib; without it + * this always fails and the caller should treat the check as skipped + * (see #TEST_MEDIA_HAVE_PNG). + * + * @param data the PNG image + * @param data_len number of bytes in @a data + * @param[out] rgb set to the red, green and blue value of the centre pixel + * @return 1 on success + */ +int +TEST_media_png_center (const void *data, + size_t data_len, + uint8_t rgb[3]); + + +/** + * Check that @a data is an Ogg stream whose first page carries the + * OpusHead identification header of RFC 7845. + * + * @param data the stream + * @param data_len number of bytes in @a data + * @return 1 if this is Ogg Opus + */ +int +TEST_media_is_ogg_opus (const void *data, + size_t data_len); + + +/** + * Is the centre pixel of the PNG in @a data approximately @a r, @a g, + * @a b? Scaling and re-encoding move colours around a little, hence the + * generous tolerance. + * + * @param data the PNG image + * @param data_len number of bytes in @a data + * @param r expected red value + * @param g expected green value + * @param b expected blue value + * @return 1 if the pixel matches (or if PNG support was compiled out) + */ +int +TEST_media_png_center_is (const void *data, + size_t data_len, + uint8_t r, + uint8_t g, + uint8_t b); + +#endif + +/* end of test_media_lib.h */ diff --git a/src/plugins/test_previewgst.c b/src/plugins/test_previewgst.c @@ -0,0 +1,108 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +/** + * @file plugins/test_previewgst.c + * @brief testcase for the previewgst plugin + * @author Christian Grothoff + * + * What comes out is an Opus stream, and comparing that byte for byte + * would mean pinning down a particular version of libopus. So we check + * that it is a well formed Ogg stream carrying the OpusHead + * identification header of RFC 7845, and that it is neither empty nor + * absurdly large. + */ +#include "platform.h" +#include "test_lib.h" +#include "test_media_lib.h" + + +/** + * Check that the preview is a plausible Ogg Opus stream. + * + * @param data the preview the plugin produced + * @param data_len number of bytes in @a data + * @return non-zero if the preview looks right + */ +static int +validate_opus (const void *data, + size_t data_len) +{ + if (data_len < 1024) + { + fprintf (stderr, + "Audio preview of %u bytes is too short to be 15s of Opus\n", + (unsigned int) data_len); + return 0; + } + return TEST_media_is_ogg_opus (data, + data_len); +} + + +/** + * Main function for the previewgst testcase. + * + * @param argc number of arguments (ignored) + * @param argv arguments (ignored) + * @return 0 on success + */ +int +main (int argc, char *argv[]) +{ + struct SolutionData previewgst_sol[] = { + { + EXTRACTOR_METATYPE_AUDIO_PREVIEW, + EXTRACTOR_METAFORMAT_BINARY, + "audio/ogg", + "OggS", + strlen ("OggS"), + 0, + 0, + &validate_opus + }, + { 0, 0, NULL, NULL, 0, -1 } + }; + struct SolutionData previewgst_video_sol[] = { + { + EXTRACTOR_METATYPE_AUDIO_PREVIEW, + EXTRACTOR_METAFORMAT_BINARY, + "audio/ogg", + "OggS", + strlen ("OggS"), + 0, + 0, + &validate_opus + }, + { 0, 0, NULL, NULL, 0, -1 } + }; + struct ProblemSet ps[] = { + { "testdata/preview_tone.ogg", + previewgst_sol }, + { "testdata/preview_tone.ogv", + previewgst_video_sol }, + { NULL, NULL } + }; + + return ET_main ("previewgst", + ps); +} + + +/* end of test_previewgst.c */ diff --git a/src/plugins/test_previewopus.c b/src/plugins/test_previewopus.c @@ -0,0 +1,109 @@ +/* + This file is part of libextractor. + Copyright (C) 2013, 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +/** + * @file plugins/test_previewopus.c + * @brief testcase for the previewopus plugin + * @author Bruno Cabral + * @author Christian Grothoff + * + * What comes out is an Opus stream, and comparing that byte for byte + * would mean pinning down a particular version of libopus. So we check + * that it is a well formed Ogg stream carrying the OpusHead + * identification header of RFC 7845, and that it is neither empty nor + * absurdly large. + */ +#include "platform.h" +#include "test_lib.h" +#include "test_media_lib.h" + + +/** + * Check that the preview is a plausible Ogg Opus stream. + * + * @param data the preview the plugin produced + * @param data_len number of bytes in @a data + * @return non-zero if the preview looks right + */ +static int +validate_opus (const void *data, + size_t data_len) +{ + if (data_len < 1024) + { + fprintf (stderr, + "Audio preview of %u bytes is too short to be 15s of Opus\n", + (unsigned int) data_len); + return 0; + } + return TEST_media_is_ogg_opus (data, + data_len); +} + + +/** + * Main function for the previewopus testcase. + * + * @param argc number of arguments (ignored) + * @param argv arguments (ignored) + * @return 0 on success + */ +int +main (int argc, char *argv[]) +{ + struct SolutionData previewopus_sol[] = { + { + EXTRACTOR_METATYPE_AUDIO_PREVIEW, + EXTRACTOR_METAFORMAT_BINARY, + "audio/ogg", + "OggS", + strlen ("OggS"), + 0, + 0, + &validate_opus + }, + { 0, 0, NULL, NULL, 0, -1 } + }; + struct SolutionData previewopus_video_sol[] = { + { + EXTRACTOR_METATYPE_AUDIO_PREVIEW, + EXTRACTOR_METAFORMAT_BINARY, + "audio/ogg", + "OggS", + strlen ("OggS"), + 0, + 0, + &validate_opus + }, + { 0, 0, NULL, NULL, 0, -1 } + }; + struct ProblemSet ps[] = { + { "testdata/preview_tone.ogg", + previewopus_sol }, + { "testdata/preview_tone.ogv", + previewopus_video_sol }, + { NULL, NULL } + }; + + return ET_main ("previewopus", + ps); +} + + +/* end of test_previewopus.c */ diff --git a/src/plugins/test_thumbnailffmpeg.c b/src/plugins/test_thumbnailffmpeg.c @@ -0,0 +1,112 @@ +/* + This file is part of libextractor. + Copyright (C) 2013, 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +/** + * @file plugins/test_thumbnailffmpeg.c + * @brief testcase for the thumbnailffmpeg plugin + * @author Christian Grothoff + * + * The encoder's output depends on the FFmpeg version, so the JPEG can + * only be checked for its magic number. The PNG round, on the other + * hand, is decoded: testdata/thumbnail_pattern.ogv is black for the + * first 20 seconds and red afterwards, so the colour of the thumbnail + * says whether the plugin really sampled where it claims to. + */ +#include "platform.h" +#include "test_lib.h" +#include "test_media_lib.h" + + +/** + * Check that the thumbnail was taken from the red part of the video. + * + * @param data the PNG the plugin produced + * @param data_len number of bytes in @a data + * @return non-zero if the thumbnail looks right + */ +static int +validate_red (const void *data, + size_t data_len) +{ + return TEST_media_png_center_is (data, + data_len, + 0xFE, + 0x00, + 0x00); +} + + +/** + * Main function for the thumbnailffmpeg testcase. + * + * @param argc number of arguments (ignored) + * @param argv arguments (ignored) + * @return 0 on success + */ +int +main (int argc, char *argv[]) +{ + uint8_t jpeg_magic[] = { 0xFF, 0xD8, 0xFF }; + uint8_t png_magic[] = { 137, 'P', 'N', 'G', '\r', '\n', 26, '\n' }; + struct SolutionData thumbnail_jpeg_sol[] = { + { + EXTRACTOR_METATYPE_THUMBNAIL, + EXTRACTOR_METAFORMAT_BINARY, + "image/jpeg", + (const char *) jpeg_magic, + sizeof (jpeg_magic), + 0 + }, + { 0, 0, NULL, NULL, 0, -1 } + }; + struct SolutionData thumbnail_png_sol[] = { + { + EXTRACTOR_METATYPE_THUMBNAIL, + EXTRACTOR_METAFORMAT_BINARY, + "image/png", + (const char *) png_magic, + sizeof (png_magic), + 0, + 0, + &validate_red + }, + { 0, 0, NULL, NULL, 0, -1 } + }; + struct ProblemSet ps_jpeg[] = { + { "testdata/thumbnail_pattern.ogv", + thumbnail_jpeg_sol }, + { NULL, NULL } + }; + struct ProblemSet ps_png[] = { + { "testdata/thumbnail_pattern.ogv", + thumbnail_png_sol }, + { NULL, NULL } + }; + int ret; + + ret = ET_main ("thumbnailffmpeg", + ps_jpeg); + if (0 != ret) + return ret; + return ET_main ("thumbnailffmpeg(format=png,size=32)", + ps_png); +} + + +/* end of test_thumbnailffmpeg.c */ diff --git a/src/plugins/test_thumbnailgst.c b/src/plugins/test_thumbnailgst.c @@ -0,0 +1,141 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +/** + * @file plugins/test_thumbnailgst.c + * @brief testcase for the thumbnailgst plugin + * @author Christian Grothoff + * + * The encoder's output depends on the GStreamer version, so the JPEG can + * only be checked for its magic number. The PNG round, on the other + * hand, is decoded: testdata/thumbnail_pattern.ogv is black for the + * first 20 seconds and red afterwards, so the colour of the thumbnail + * says whether the plugin really sampled where it claims to. + */ +#include "platform.h" +#include "test_lib.h" +#include "test_media_lib.h" +#ifndef WINDOWS +#include <sys/wait.h> +#endif + + +/** + * Check that the thumbnail was taken from the red part of the video. + * + * @param data the PNG the plugin produced + * @param data_len number of bytes in @a data + * @return non-zero if the thumbnail looks right + */ +static int +validate_red (const void *data, + size_t data_len) +{ + return TEST_media_png_center_is (data, + data_len, + 0xFE, + 0x00, + 0x00); +} + + +/** + * Main function for the thumbnailgst testcase. + * + * @param argc number of arguments (ignored) + * @param argv arguments (ignored) + * @return 0 on success + */ +int +main (int argc, char *argv[]) +{ + uint8_t jpeg_magic[] = { 0xFF, 0xD8, 0xFF }; + uint8_t png_magic[] = { 137, 'P', 'N', 'G', '\r', '\n', 26, '\n' }; + struct SolutionData thumbnail_jpeg_sol[] = { + { + EXTRACTOR_METATYPE_THUMBNAIL, + EXTRACTOR_METAFORMAT_BINARY, + "image/jpeg", + (const char *) jpeg_magic, + sizeof (jpeg_magic), + 0 + }, + { 0, 0, NULL, NULL, 0, -1 } + }; + struct SolutionData thumbnail_png_sol[] = { + { + EXTRACTOR_METATYPE_THUMBNAIL, + EXTRACTOR_METAFORMAT_BINARY, + "image/png", + (const char *) png_magic, + sizeof (png_magic), + 0, + 0, + &validate_red + }, + { 0, 0, NULL, NULL, 0, -1 } + }; + struct ProblemSet ps_jpeg[] = { + { "testdata/thumbnail_pattern.ogv", + thumbnail_jpeg_sol }, + { NULL, NULL } + }; + struct ProblemSet ps_png[] = { + { "testdata/thumbnail_pattern.ogv", + thumbnail_png_sol }, + { NULL, NULL } + }; +#ifndef WINDOWS + pid_t pid; + int status; + + /* The two rounds must not share a process: ET_main() runs each round + out-of-process and then in-process, and once GStreamer has been + initialized in this process (by the in-process leg of the first + round), the fork() that the out-of-process leg of the second round + performs inherits a GStreamer that no longer works in the child. + So run the first round in a child of our own, before any of that + has happened. */ + pid = fork (); + if (0 == pid) + _exit (ET_main ("thumbnailgst", + ps_jpeg)); + if (-1 == pid) + return 77; /* cannot fork, skip */ + if (pid != waitpid (pid, + &status, + 0)) + return 1; + if ( (! WIFEXITED (status)) || + (0 != WEXITSTATUS (status)) ) + return 1; +#else + int ret; + + ret = ET_main ("thumbnailgst", + ps_jpeg); + if (0 != ret) + return ret; +#endif + return ET_main ("thumbnailgst(format=png,size=32)", + ps_png); +} + + +/* end of test_thumbnailgst.c */ diff --git a/src/plugins/testdata/README.media b/src/plugins/testdata/README.media @@ -0,0 +1,42 @@ +Provenance of the media samples +=============================== + +Unlike the rest of the files in this directory, the three files below +were not found anywhere: they are generated, and the recipe that +generates them is `contrib/gen_testmedia.sh' in this repository. Every +frame and every sample comes out of FFmpeg's synthetic `lavfi' sources +(a solid colour, a box, a sine wave), so there is no third party work +involved and nothing to attribute. They are dedicated to the public +domain (CC0 1.0). + +They exist because the video thumbnail plugins (thumbnailffmpeg, +thumbnailgst) and the audio preview plugins (previewopus, previewgst) +need input that is long enough to seek around in, and none of the other +test files here is even 30 seconds long. Keeping them synthetic also +keeps them small: all three together are smaller than a single one of +the photographs in this directory. + +Theora and Vorbis in an Ogg container were chosen because both plugin +backends can decode them without any additional codec package: FFmpeg has +both built in, and for GStreamer they live in gst-plugins-base. + + thumbnail_pattern.ogv 90s, Theora, 160x120, 5fps, no audio (14K) + + Solid black for the first 20 seconds, then solid red. The + thumbnailers sample about 30 seconds in (bounded by a third of the + duration, which is why the file is 90 and not 35 seconds long), so + a thumbnail taken from the wrong moment comes out black instead of + red, and test_thumbnailffmpeg/test_thumbnailgst notice. Each frame + also carries a small white box: the thumbnailers deliberately skip + perfectly flat frames, which a fade-in leader would produce, and + the box keeps the test from tripping over that. + + preview_tone.ogg 30s, Vorbis, mono, 8kHz (21K) + + A 440Hz tone for 15 seconds followed by an 880Hz tone. 8kHz mono + is more than enough for something that gets re-encoded at 24 kbps. + + preview_tone.ogv 30s, Theora + Vorbis (28K) + + The same audio in a file that also carries video, so that the + preview plugins are exercised on a video container as well. diff --git a/src/plugins/testdata/preview_tone.ogg b/src/plugins/testdata/preview_tone.ogg Binary files differ. diff --git a/src/plugins/testdata/preview_tone.ogv b/src/plugins/testdata/preview_tone.ogv Binary files differ. diff --git a/src/plugins/testdata/thumbnail_pattern.ogv b/src/plugins/testdata/thumbnail_pattern.ogv Binary files differ. diff --git a/src/plugins/thumbnailffmpeg_extractor.c b/src/plugins/thumbnailffmpeg_extractor.c @@ -0,0 +1,460 @@ +/* + This file is part of libextractor. + Copyright (C) 2008, 2012, 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file plugins/thumbnailffmpeg_extractor.c + * @author Christian Grothoff + * @brief this extractor produces a binary encoded thumbnail of a frame + * taken from a video, using FFmpeg for demuxing, decoding and + * scaling + * + * Unlike the gtk thumbnailer, which can only re-scale a still image, this + * plugin seeks about 30s into a video and encodes the frame it finds + * there as a small JPEG (or PNG). A previous incarnation of this plugin + * was removed in 2021 because it had accumulated compatibility code for + * every libav and FFmpeg API since 2008; this one deliberately requires + * FFmpeg 5.1 or newer and uses only the send/receive APIs, so that there + * is nothing to keep in sync with upstream churn. + */ +#include "platform.h" +#include "extractor.h" +#include "mediautil.h" +#include "mediaffmpeg.h" + +#include <libavutil/imgutils.h> +#include <libswscale/swscale.h> + +/** + * Give up after this many decoded frames, no matter what the timestamps + * say. A malformed file can otherwise keep us decoding forever. + */ +#define MAX_FRAMES 2048 + +/** + * Mean absolute deviation below which we consider a frame to be a flat + * surface (a fade-in, a black leader) and not worth showing. + */ +#define FLAT_FRAME_MAD 2 + +/** + * How far to jump ahead when we hit a flat frame, in seconds. + */ +#define FLAT_FRAME_SKIP 5 + +/** + * How often we are willing to do that. + */ +#define MAX_FLAT_RETRIES 2 + +/** + * JPEG quantizer scale; smaller is better and bigger. + */ +#define JPEG_QSCALE 5 + + +/** + * Is @a frame a flat surface? Cheap heuristic over the first plane; we + * only want to catch the completely uniform black frame of a fade-in. + * + * @param frame frame to inspect + * @return 1 if the frame carries no discernible detail + */ +static int +is_flat (const AVFrame *frame) +{ + const uint8_t *p = frame->data[0]; + int stride = frame->linesize[0]; + uint64_t sum = 0; + uint64_t dev = 0; + unsigned int n = 0; + int x; + int y; + + if ( (NULL == p) || + (stride <= 0) || + (frame->height <= 0) ) + return 0; + for (y = 0; y < frame->height; y += 4) + for (x = 0; x < stride; x += 4) + { + sum += p[y * stride + x]; + n++; + } + if (0 == n) + return 0; + sum /= n; + for (y = 0; y < frame->height; y += 4) + for (x = 0; x < stride; x += 4) + { + uint8_t v = p[y * stride + x]; + + dev += (v > sum) ? (v - sum) : (sum - v); + } + return (dev / n) < FLAT_FRAME_MAD; +} + + +/** + * Scale @a frame down and encode it as JPEG or PNG. + * + * @param frame the decoded frame to convert + * @param opt plugin options (target size and format) + * @param[out] output set to the encoded image, to be freed with av_free() + * @param[out] output_size set to the number of bytes in @a output + * @return 0 on success + */ +static int +encode_thumbnail (const AVFrame *frame, + const struct EXTRACTOR_MediaOptions *opt, + uint8_t **output, + size_t *output_size) +{ + const AVCodec *codec; + AVCodecContext *ctx = NULL; + struct SwsContext *sws = NULL; + AVFrame *dst = NULL; + AVPacket *pkt = NULL; + enum AVPixelFormat dst_fmt; + unsigned int width; + unsigned int height; + int ret = -1; + int r; + + if (! EXTRACTOR_media_thumbnail_size_ (frame->width, + frame->height, + opt->size, + &width, + &height)) + return -1; + if (! sws_isSupportedInput (frame->format)) + return -1; + if (opt->png) + { + dst_fmt = AV_PIX_FMT_RGB24; + codec = avcodec_find_encoder (AV_CODEC_ID_PNG); + } + else + { + dst_fmt = AV_PIX_FMT_YUVJ420P; + codec = avcodec_find_encoder (AV_CODEC_ID_MJPEG); + } + if (NULL == codec) + return -1; + if (NULL == (sws = sws_getContext (frame->width, + frame->height, + frame->format, + (int) width, + (int) height, + dst_fmt, + SWS_BILINEAR, + NULL, + NULL, + NULL))) + return -1; + if (NULL == (dst = av_frame_alloc ())) + goto cleanup; + dst->format = dst_fmt; + dst->width = (int) width; + dst->height = (int) height; + dst->pts = 0; + if (0 != av_frame_get_buffer (dst, 0)) + goto cleanup; + if (0 > sws_scale (sws, + (const uint8_t *const *) frame->data, + frame->linesize, + 0, + frame->height, + dst->data, + dst->linesize)) + goto cleanup; + if (NULL == (ctx = avcodec_alloc_context3 (codec))) + goto cleanup; + ctx->width = (int) width; + ctx->height = (int) height; + ctx->pix_fmt = dst_fmt; + ctx->time_base = (AVRational) { 1, 25 }; + ctx->thread_count = 1; + if (opt->png) + { + ctx->compression_level = 9; + } + else + { + ctx->color_range = AVCOL_RANGE_JPEG; + ctx->flags |= AV_CODEC_FLAG_QSCALE; + ctx->global_quality = FF_QP2LAMBDA * JPEG_QSCALE; + } + if (0 != avcodec_open2 (ctx, codec, NULL)) + goto cleanup; + if (NULL == (pkt = av_packet_alloc ())) + goto cleanup; + dst->quality = ctx->global_quality; + if (0 != avcodec_send_frame (ctx, dst)) + goto cleanup; + r = avcodec_receive_packet (ctx, pkt); + if (AVERROR (EAGAIN) == r) + { + /* encoder wants to be flushed before it hands anything back */ + if (0 != avcodec_send_frame (ctx, NULL)) + goto cleanup; + r = avcodec_receive_packet (ctx, pkt); + } + if (0 != r) + goto cleanup; + if ( (pkt->size <= 0) || + (pkt->size > EXTRACTOR_MEDIA_MAX_THUMBNAIL) ) + goto cleanup; + if (NULL == (*output = av_malloc (pkt->size))) + goto cleanup; + memcpy (*output, + pkt->data, + pkt->size); + *output_size = (size_t) pkt->size; + ret = 0; +cleanup: + if (NULL != pkt) + av_packet_free (&pkt); + if (NULL != ctx) + avcodec_free_context (&ctx); + if (NULL != dst) + av_frame_free (&dst); + sws_freeContext (sws); + return ret; +} + + +/** + * Decode the video stream until we reach @a target seconds, and hand back + * the frame we find there. If we never get there (short file, broken + * timestamps), the last frame we did decode is returned instead. + * + * @param io open I/O state + * @param ctx open decoder for @a stream + * @param stream index of the video stream + * @param target position we are aiming for, in seconds + * @param[out] frame frame to fill in + * @return 0 if @a frame was filled in + */ +static int +decode_frame_at (struct EXTRACTOR_FFmpegIO *io, + AVCodecContext *ctx, + int stream, + double target, + AVFrame *frame) +{ + AVPacket *pkt; + AVFrame *last; + AVRational tb = io->fmt->streams[stream]->time_base; + unsigned int frames = 0; + unsigned int retries = 0; + int have_last = 0; + int ret = -1; + + if (NULL == (pkt = av_packet_alloc ())) + return -1; + if (NULL == (last = av_frame_alloc ())) + { + av_packet_free (&pkt); + return -1; + } + while (frames < MAX_FRAMES) + { + int r; + + if (EXTRACTOR_media_deadline_expired_ (&io->deadline)) + break; + r = av_read_frame (io->fmt, pkt); + if (0 != r) + { + /* end of file: flush the decoder and take what is left */ + avcodec_send_packet (ctx, NULL); + } + else if (pkt->stream_index != stream) + { + av_packet_unref (pkt); + continue; + } + else if (0 != avcodec_send_packet (ctx, pkt)) + { + av_packet_unref (pkt); + continue; + } + av_packet_unref (pkt); + while (0 == avcodec_receive_frame (ctx, frame)) + { + double ts; + + frames++; + ts = (AV_NOPTS_VALUE == frame->best_effort_timestamp) + ? -1.0 + : frame->best_effort_timestamp * av_q2d (tb); + if ( (ts < target) && + (frames < MAX_FRAMES) ) + { + /* not there yet; hold on to it in case we never arrive */ + av_frame_unref (last); + if (0 == av_frame_ref (last, frame)) + have_last = 1; + av_frame_unref (frame); + continue; + } + if ( (retries < MAX_FLAT_RETRIES) && + is_flat (frame) ) + { + /* a black leader or a fade-in; try a bit further in */ + retries++; + target = ((ts < 0.0) ? target : ts) + FLAT_FRAME_SKIP; + av_frame_unref (last); + if (0 == av_frame_ref (last, frame)) + have_last = 1; + av_frame_unref (frame); + continue; + } + av_frame_unref (last); + av_frame_free (&last); + av_packet_free (&pkt); + return 0; + } + if (0 != r) + break; /* end of file, and the decoder had nothing left either */ + } + if (have_last) + { + av_frame_unref (frame); + if (0 == av_frame_ref (frame, last)) + ret = 0; + } + av_frame_unref (last); + av_frame_free (&last); + av_packet_free (&pkt); + return ret; +} + + +/** + * Main method for the ffmpeg-thumbnailer plugin. + * + * @param ec extraction context + */ +void +EXTRACTOR_thumbnailffmpeg_extract_method (struct EXTRACTOR_ExtractContext *ec); + +void +EXTRACTOR_thumbnailffmpeg_extract_method (struct EXTRACTOR_ExtractContext *ec) +{ + struct EXTRACTOR_MediaOptions opt; + struct EXTRACTOR_FFmpegIO io; + AVCodecContext *ctx = NULL; + AVFrame *frame = NULL; + uint8_t *thumbnail = NULL; + size_t thumbnail_size = 0; + double target; + int stream; + + EXTRACTOR_media_options_parse_ (ec->config, + &opt); + EXTRACTOR_media_deadline_start_ (&io.deadline, + opt.deadline); + io.ec = ec; + if (! EXTRACTOR_media_is_media_ (ec, 0)) + return; + if (0 != EXTRACTOR_ffmpeg_open_ (&io)) + return; + stream = EXTRACTOR_ffmpeg_open_stream_ (&io, + AVMEDIA_TYPE_VIDEO, + &ctx); + if (0 > stream) + goto cleanup; + if ( (ctx->width <= 0) || + (ctx->height <= 0) || + (ctx->width > EXTRACTOR_MEDIA_MAX_DIMENSION) || + (ctx->height > EXTRACTOR_MEDIA_MAX_DIMENSION) || + (((int64_t) ctx->width) * ctx->height > EXTRACTOR_MEDIA_MAX_PIXELS) ) + goto cleanup; + target = EXTRACTOR_ffmpeg_seek_ (&io, + stream, + ctx, + opt.offset); + if (NULL == (frame = av_frame_alloc ())) + goto cleanup; + if (0 != decode_frame_at (&io, + ctx, + stream, + target, + frame)) + goto cleanup; + if (0 != encode_thumbnail (frame, + &opt, + &thumbnail, + &thumbnail_size)) + goto cleanup; + ec->proc (ec->cls, + "thumbnailffmpeg", + EXTRACTOR_METATYPE_THUMBNAIL, + EXTRACTOR_METAFORMAT_BINARY, + opt.png ? "image/png" : "image/jpeg", + (const char *) thumbnail, + thumbnail_size); +cleanup: + if (NULL != thumbnail) + av_free (thumbnail); + if (NULL != frame) + av_frame_free (&frame); + if (NULL != ctx) + avcodec_free_context (&ctx); + EXTRACTOR_ffmpeg_close_ (&io); +} + + +/** + * Keep the diagnostics of the media framework out of the output of the + * calling application; a decoder complaining about a broken file is not + * something the application asked to see. + * + * ("force-kill" would be tempting here, to get a fresh process for every + * file, but the host cannot know about that special without loading the + * plugin in-process: it only finds out that the child is gone once it has + * already handed it the next file, which is then silently lost.) + * + * @return special options for this plugin + */ +const char * +EXTRACTOR_thumbnailffmpeg_options (void); + +const char * +EXTRACTOR_thumbnailffmpeg_options () +{ + return "close-stderr"; +} + + +/** + * Silence FFmpeg's logging. + */ +void __attribute__ ((constructor)) +thumbnailffmpeg_init (void); + +void __attribute__ ((constructor)) +thumbnailffmpeg_init () +{ + EXTRACTOR_ffmpeg_silence_ (); +} + + +/* end of thumbnailffmpeg_extractor.c */ diff --git a/src/plugins/thumbnailgst_extractor.c b/src/plugins/thumbnailgst_extractor.c @@ -0,0 +1,224 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file plugins/thumbnailgst_extractor.c + * @author Christian Grothoff + * @brief this extractor produces a binary encoded thumbnail of a frame + * taken from a video, using GStreamer + * + * This is the GStreamer counterpart of thumbnailffmpeg: it seeks about + * 30s into a video and encodes the frame it finds there as a small JPEG + * (or PNG). Which of the two is built depends on what was found at + * configure time; if both are installed, both will report a thumbnail and + * it is up to the application to disable one of them in its plugin + * configuration string. + * + * The decoders and the image encoder are themselves GStreamer plugins, + * resolved at run time, so a missing "jpeg" or "playback" plugin cannot + * be detected when libextractor is built. We simply report nothing. + */ +#include "platform.h" +#include "extractor.h" +#include "mediautil.h" +#include "mediagst.h" + +#include <gst/video/video.h> + + +/** + * Scale and encode the raw video @a sample. + * + * @param sample raw frame to convert + * @param opt plugin options (target size and format) + * @param timeout how long the conversion may take, in nanoseconds + * @return the encoded image, NULL on error + */ +static GstSample * +encode_thumbnail (GstSample *sample, + const struct EXTRACTOR_MediaOptions *opt, + GstClockTime timeout) +{ + GstCaps *caps; + GstCaps *to_caps; + GstStructure *s; + GstSample *out; + gint width; + gint height; + unsigned int tw; + unsigned int th; + + if (NULL == (caps = gst_sample_get_caps (sample))) + return NULL; + if (NULL == (s = gst_caps_get_structure (caps, 0))) + return NULL; + if ( (! gst_structure_get_int (s, "width", &width)) || + (! gst_structure_get_int (s, "height", &height)) || + (0 >= width) || + (0 >= height) ) + return NULL; + if (! EXTRACTOR_media_thumbnail_size_ ((unsigned int) width, + (unsigned int) height, + opt->size, + &tw, + &th)) + return NULL; + to_caps = gst_caps_new_simple (opt->png ? "image/png" : "image/jpeg", + "width", G_TYPE_INT, (gint) tw, + "height", G_TYPE_INT, (gint) th, + NULL); + out = gst_video_convert_sample (sample, + to_caps, + timeout, + NULL); + gst_caps_unref (to_caps); + return out; +} + + +/** + * Main method for the gstreamer-thumbnailer plugin. + * + * @param ec extraction context + */ +void +EXTRACTOR_thumbnailgst_extract_method (struct EXTRACTOR_ExtractContext *ec); + +void +EXTRACTOR_thumbnailgst_extract_method (struct EXTRACTOR_ExtractContext *ec) +{ + struct EXTRACTOR_MediaOptions opt; + struct EXTRACTOR_MediaDeadline deadline; + struct EXTRACTOR_GstIO io; + GstElement *pipeline; + GstElement *sink; + GstSample *sample = NULL; + GstSample *thumbnail = NULL; + GstBuffer *buffer; + GstMapInfo mi; + GstClockTime timeout; + gint64 duration; + gint64 target; + + EXTRACTOR_media_options_parse_ (ec->config, + &opt); + EXTRACTOR_media_deadline_start_ (&deadline, + opt.deadline); + if (! EXTRACTOR_media_is_media_ (ec, 0)) + return; + if (! gst_is_initialized ()) + gst_init (NULL, NULL); + sink = gst_element_factory_make ("fakesink", NULL); + if (NULL != sink) + g_object_set (sink, + "sync", FALSE, + NULL); + if (NULL == (pipeline = + EXTRACTOR_gst_playbin_ (&io, + ec, + EXTRACTOR_GST_PLAY_FLAG_VIDEO, + sink, + gst_element_factory_make ("fakesink", + NULL)))) + return; + if (GST_STATE_CHANGE_FAILURE == + gst_element_set_state (pipeline, + GST_STATE_PAUSED)) + goto cleanup; + if (! EXTRACTOR_gst_wait_async_ (pipeline, + EXTRACTOR_media_deadline_remaining_ ( + &deadline) * GST_MSECOND)) + goto cleanup; + target = EXTRACTOR_gst_sample_position_ (pipeline, + opt.offset, + &duration); + if (target >= GST_SECOND) + { + /* a key frame is good enough for a thumbnail and much cheaper than + decoding forward to an exact position */ + if (gst_element_seek_simple (pipeline, + GST_FORMAT_TIME, + GST_SEEK_FLAG_FLUSH + | GST_SEEK_FLAG_KEY_UNIT, + target)) + (void) EXTRACTOR_gst_wait_async_ (pipeline, + EXTRACTOR_media_deadline_remaining_ ( + &deadline) * GST_MSECOND); + } + g_object_get (sink, + "last-sample", + &sample, + NULL); + if (NULL == sample) + goto cleanup; + timeout = EXTRACTOR_media_deadline_remaining_ (&deadline) * GST_MSECOND; + if (0 == timeout) + goto cleanup; + if (NULL == (thumbnail = encode_thumbnail (sample, + &opt, + timeout))) + goto cleanup; + if (NULL == (buffer = gst_sample_get_buffer (thumbnail))) + goto cleanup; + if (! gst_buffer_map (buffer, &mi, GST_MAP_READ)) + goto cleanup; + if ( (0 < mi.size) && + (EXTRACTOR_MEDIA_MAX_THUMBNAIL >= mi.size) ) + ec->proc (ec->cls, + "thumbnailgst", + EXTRACTOR_METATYPE_THUMBNAIL, + EXTRACTOR_METAFORMAT_BINARY, + opt.png ? "image/png" : "image/jpeg", + (const char *) mi.data, + mi.size); + gst_buffer_unmap (buffer, &mi); +cleanup: + if (NULL != thumbnail) + gst_sample_unref (thumbnail); + if (NULL != sample) + gst_sample_unref (sample); + gst_element_set_state (pipeline, + GST_STATE_NULL); + gst_object_unref (pipeline); +} + + +/** + * Keep the diagnostics of the media framework out of the output of the + * calling application; a decoder complaining about a broken file is not + * something the application asked to see. + * + * ("force-kill" would be tempting here, to get a fresh process for every + * file, but the host cannot know about that special without loading the + * plugin in-process: it only finds out that the child is gone once it has + * already handed it the next file, which is then silently lost.) + * + * @return special options for this plugin + */ +const char * +EXTRACTOR_thumbnailgst_options (void); + +const char * +EXTRACTOR_thumbnailgst_options () +{ + return "close-stderr"; +} + + +/* end of thumbnailgst_extractor.c */