libextractor

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

thumbnailffmpeg_extractor.c (13049B)


      1 /*
      2      This file is part of libextractor.
      3      Copyright (C) 2008, 2012, 2026 Vidyut Samanta and Christian Grothoff
      4 
      5      libextractor is free software; you can redistribute it and/or modify
      6      it under the terms of the GNU General Public License as published
      7      by the Free Software Foundation; either version 3, or (at your
      8      option) any later version.
      9 
     10      libextractor is distributed in the hope that it will be useful, but
     11      WITHOUT ANY WARRANTY; without even the implied warranty of
     12      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     13      General Public License for more details.
     14 
     15      You should have received a copy of the GNU General Public License
     16      along with libextractor; see the file COPYING.  If not, write to the
     17      Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
     18      Boston, MA 02110-1301, USA.
     19  */
     20 /**
     21  * @file plugins/thumbnailffmpeg_extractor.c
     22  * @author Christian Grothoff
     23  * @brief this extractor produces a binary encoded thumbnail of a frame
     24  *        taken from a video, using FFmpeg for demuxing, decoding and
     25  *        scaling
     26  *
     27  * Unlike the gtk thumbnailer, which can only re-scale a still image, this
     28  * plugin seeks about 30s into a video and encodes the frame it finds
     29  * there as a small JPEG (or PNG).  A previous incarnation of this plugin
     30  * was removed in 2021 because it had accumulated compatibility code for
     31  * every libav and FFmpeg API since 2008; this one deliberately requires
     32  * FFmpeg 5.1 or newer and uses only the send/receive APIs, so that there
     33  * is nothing to keep in sync with upstream churn.
     34  */
     35 #include "platform.h"
     36 #include "extractor.h"
     37 #include "mediautil.h"
     38 #include "mediaffmpeg.h"
     39 
     40 #include <libavutil/imgutils.h>
     41 #include <libswscale/swscale.h>
     42 
     43 /**
     44  * Give up after this many decoded frames, no matter what the timestamps
     45  * say.  A malformed file can otherwise keep us decoding forever.
     46  */
     47 #define MAX_FRAMES 2048
     48 
     49 /**
     50  * Mean absolute deviation below which we consider a frame to be a flat
     51  * surface (a fade-in, a black leader) and not worth showing.
     52  */
     53 #define FLAT_FRAME_MAD 2
     54 
     55 /**
     56  * How far to jump ahead when we hit a flat frame, in seconds.
     57  */
     58 #define FLAT_FRAME_SKIP 5
     59 
     60 /**
     61  * How often we are willing to do that.
     62  */
     63 #define MAX_FLAT_RETRIES 2
     64 
     65 /**
     66  * JPEG quantizer scale; smaller is better and bigger.
     67  */
     68 #define JPEG_QSCALE 5
     69 
     70 
     71 /**
     72  * Is @a frame a flat surface?  Cheap heuristic over the first plane; we
     73  * only want to catch the completely uniform black frame of a fade-in.
     74  *
     75  * @param frame frame to inspect
     76  * @return 1 if the frame carries no discernible detail
     77  */
     78 static int
     79 is_flat (const AVFrame *frame)
     80 {
     81   const uint8_t *p = frame->data[0];
     82   int stride = frame->linesize[0];
     83   uint64_t sum = 0;
     84   uint64_t dev = 0;
     85   unsigned int n = 0;
     86   int x;
     87   int y;
     88 
     89   if ( (NULL == p) ||
     90        (stride <= 0) ||
     91        (frame->height <= 0) )
     92     return 0;
     93   for (y = 0; y < frame->height; y += 4)
     94     for (x = 0; x < stride; x += 4)
     95     {
     96       sum += p[y * stride + x];
     97       n++;
     98     }
     99   if (0 == n)
    100     return 0;
    101   sum /= n;
    102   for (y = 0; y < frame->height; y += 4)
    103     for (x = 0; x < stride; x += 4)
    104     {
    105       uint8_t v = p[y * stride + x];
    106 
    107       dev += (v > sum) ? (v - sum) : (sum - v);
    108     }
    109   return (dev / n) < FLAT_FRAME_MAD;
    110 }
    111 
    112 
    113 /**
    114  * Scale @a frame down and encode it as JPEG or PNG.
    115  *
    116  * @param frame the decoded frame to convert
    117  * @param opt plugin options (target size and format)
    118  * @param[out] output set to the encoded image, to be freed with av_free()
    119  * @param[out] output_size set to the number of bytes in @a output
    120  * @return 0 on success
    121  */
    122 static int
    123 encode_thumbnail (const AVFrame *frame,
    124                   const struct EXTRACTOR_MediaOptions *opt,
    125                   uint8_t **output,
    126                   size_t *output_size)
    127 {
    128   const AVCodec *codec;
    129   AVCodecContext *ctx = NULL;
    130   struct SwsContext *sws = NULL;
    131   AVFrame *dst = NULL;
    132   AVPacket *pkt = NULL;
    133   enum AVPixelFormat dst_fmt;
    134   unsigned int width;
    135   unsigned int height;
    136   int ret = -1;
    137   int r;
    138 
    139   if (! EXTRACTOR_media_thumbnail_size_ (frame->width,
    140                                          frame->height,
    141                                          opt->size,
    142                                          &width,
    143                                          &height))
    144     return -1;
    145   if (! sws_isSupportedInput (frame->format))
    146     return -1;
    147   if (opt->png)
    148   {
    149     dst_fmt = AV_PIX_FMT_RGB24;
    150     codec = avcodec_find_encoder (AV_CODEC_ID_PNG);
    151   }
    152   else
    153   {
    154     dst_fmt = AV_PIX_FMT_YUVJ420P;
    155     codec = avcodec_find_encoder (AV_CODEC_ID_MJPEG);
    156   }
    157   if (NULL == codec)
    158     return -1;
    159   if (NULL == (sws = sws_getContext (frame->width,
    160                                      frame->height,
    161                                      frame->format,
    162                                      (int) width,
    163                                      (int) height,
    164                                      dst_fmt,
    165                                      SWS_BILINEAR,
    166                                      NULL,
    167                                      NULL,
    168                                      NULL)))
    169     return -1;
    170   if (NULL == (dst = av_frame_alloc ()))
    171     goto cleanup;
    172   dst->format = dst_fmt;
    173   dst->width = (int) width;
    174   dst->height = (int) height;
    175   dst->pts = 0;
    176   if (0 != av_frame_get_buffer (dst, 0))
    177     goto cleanup;
    178   if (0 > sws_scale (sws,
    179                      (const uint8_t *const *) frame->data,
    180                      frame->linesize,
    181                      0,
    182                      frame->height,
    183                      dst->data,
    184                      dst->linesize))
    185     goto cleanup;
    186   if (NULL == (ctx = avcodec_alloc_context3 (codec)))
    187     goto cleanup;
    188   ctx->width = (int) width;
    189   ctx->height = (int) height;
    190   ctx->pix_fmt = dst_fmt;
    191   ctx->time_base = (AVRational) { 1, 25 };
    192   ctx->thread_count = 1;
    193   if (opt->png)
    194   {
    195     ctx->compression_level = 9;
    196   }
    197   else
    198   {
    199     ctx->color_range = AVCOL_RANGE_JPEG;
    200     ctx->flags |= AV_CODEC_FLAG_QSCALE;
    201     ctx->global_quality = FF_QP2LAMBDA * JPEG_QSCALE;
    202   }
    203   if (0 != avcodec_open2 (ctx, codec, NULL))
    204     goto cleanup;
    205   if (NULL == (pkt = av_packet_alloc ()))
    206     goto cleanup;
    207   dst->quality = ctx->global_quality;
    208   if (0 != avcodec_send_frame (ctx, dst))
    209     goto cleanup;
    210   r = avcodec_receive_packet (ctx, pkt);
    211   if (AVERROR (EAGAIN) == r)
    212   {
    213     /* encoder wants to be flushed before it hands anything back */
    214     if (0 != avcodec_send_frame (ctx, NULL))
    215       goto cleanup;
    216     r = avcodec_receive_packet (ctx, pkt);
    217   }
    218   if (0 != r)
    219     goto cleanup;
    220   if ( (pkt->size <= 0) ||
    221        (pkt->size > EXTRACTOR_MEDIA_MAX_THUMBNAIL) )
    222     goto cleanup;
    223   if (NULL == (*output = av_malloc (pkt->size)))
    224     goto cleanup;
    225   memcpy (*output,
    226           pkt->data,
    227           pkt->size);
    228   *output_size = (size_t) pkt->size;
    229   ret = 0;
    230 cleanup:
    231   if (NULL != pkt)
    232     av_packet_free (&pkt);
    233   if (NULL != ctx)
    234     avcodec_free_context (&ctx);
    235   if (NULL != dst)
    236     av_frame_free (&dst);
    237   sws_freeContext (sws);
    238   return ret;
    239 }
    240 
    241 
    242 /**
    243  * Decode the video stream until we reach @a target seconds, and hand back
    244  * the frame we find there.  If we never get there (short file, broken
    245  * timestamps), the last frame we did decode is returned instead.
    246  *
    247  * @param io open I/O state
    248  * @param ctx open decoder for @a stream
    249  * @param stream index of the video stream
    250  * @param target position we are aiming for, in seconds
    251  * @param[out] frame frame to fill in
    252  * @return 0 if @a frame was filled in
    253  */
    254 static int
    255 decode_frame_at (struct EXTRACTOR_FFmpegIO *io,
    256                  AVCodecContext *ctx,
    257                  int stream,
    258                  double target,
    259                  AVFrame *frame)
    260 {
    261   AVPacket *pkt;
    262   AVFrame *last;
    263   AVRational tb = io->fmt->streams[stream]->time_base;
    264   unsigned int frames = 0;
    265   unsigned int retries = 0;
    266   int have_last = 0;
    267   int ret = -1;
    268 
    269   if (NULL == (pkt = av_packet_alloc ()))
    270     return -1;
    271   if (NULL == (last = av_frame_alloc ()))
    272   {
    273     av_packet_free (&pkt);
    274     return -1;
    275   }
    276   while (frames < MAX_FRAMES)
    277   {
    278     int r;
    279 
    280     if (EXTRACTOR_media_deadline_expired_ (&io->deadline))
    281       break;
    282     r = av_read_frame (io->fmt, pkt);
    283     if (0 != r)
    284     {
    285       /* end of file: flush the decoder and take what is left */
    286       avcodec_send_packet (ctx, NULL);
    287     }
    288     else if (pkt->stream_index != stream)
    289     {
    290       av_packet_unref (pkt);
    291       continue;
    292     }
    293     else if (0 != avcodec_send_packet (ctx, pkt))
    294     {
    295       av_packet_unref (pkt);
    296       continue;
    297     }
    298     av_packet_unref (pkt);
    299     while (0 == avcodec_receive_frame (ctx, frame))
    300     {
    301       double ts;
    302 
    303       frames++;
    304       ts = (AV_NOPTS_VALUE == frame->best_effort_timestamp)
    305            ? -1.0
    306            : frame->best_effort_timestamp * av_q2d (tb);
    307       if ( (ts < target) &&
    308            (frames < MAX_FRAMES) )
    309       {
    310         /* not there yet; hold on to it in case we never arrive */
    311         av_frame_unref (last);
    312         if (0 == av_frame_ref (last, frame))
    313           have_last = 1;
    314         av_frame_unref (frame);
    315         continue;
    316       }
    317       if ( (retries < MAX_FLAT_RETRIES) &&
    318            is_flat (frame) )
    319       {
    320         /* a black leader or a fade-in; try a bit further in */
    321         retries++;
    322         target = ((ts < 0.0) ? target : ts) + FLAT_FRAME_SKIP;
    323         av_frame_unref (last);
    324         if (0 == av_frame_ref (last, frame))
    325           have_last = 1;
    326         av_frame_unref (frame);
    327         continue;
    328       }
    329       av_frame_unref (last);
    330       av_frame_free (&last);
    331       av_packet_free (&pkt);
    332       return 0;
    333     }
    334     if (0 != r)
    335       break;   /* end of file, and the decoder had nothing left either */
    336   }
    337   if (have_last)
    338   {
    339     av_frame_unref (frame);
    340     if (0 == av_frame_ref (frame, last))
    341       ret = 0;
    342   }
    343   av_frame_unref (last);
    344   av_frame_free (&last);
    345   av_packet_free (&pkt);
    346   return ret;
    347 }
    348 
    349 
    350 /**
    351  * Main method for the ffmpeg-thumbnailer plugin.
    352  *
    353  * @param ec extraction context
    354  */
    355 void
    356 EXTRACTOR_thumbnailffmpeg_extract_method (struct EXTRACTOR_ExtractContext *ec);
    357 
    358 void
    359 EXTRACTOR_thumbnailffmpeg_extract_method (struct EXTRACTOR_ExtractContext *ec)
    360 {
    361   struct EXTRACTOR_MediaOptions opt;
    362   struct EXTRACTOR_FFmpegIO io;
    363   AVCodecContext *ctx = NULL;
    364   AVFrame *frame = NULL;
    365   uint8_t *thumbnail = NULL;
    366   size_t thumbnail_size = 0;
    367   double target;
    368   int stream;
    369 
    370   EXTRACTOR_media_options_parse_ (ec->config,
    371                                   &opt);
    372   EXTRACTOR_media_deadline_start_ (&io.deadline,
    373                                    opt.deadline);
    374   io.ec = ec;
    375   if (! EXTRACTOR_media_is_media_ (ec, 0))
    376     return;
    377   if (0 != EXTRACTOR_ffmpeg_open_ (&io))
    378     return;
    379   stream = EXTRACTOR_ffmpeg_open_stream_ (&io,
    380                                           AVMEDIA_TYPE_VIDEO,
    381                                           &ctx);
    382   if (0 > stream)
    383     goto cleanup;
    384   if ( (ctx->width <= 0) ||
    385        (ctx->height <= 0) ||
    386        (ctx->width > EXTRACTOR_MEDIA_MAX_DIMENSION) ||
    387        (ctx->height > EXTRACTOR_MEDIA_MAX_DIMENSION) ||
    388        (((int64_t) ctx->width) * ctx->height > EXTRACTOR_MEDIA_MAX_PIXELS) )
    389     goto cleanup;
    390   target = EXTRACTOR_ffmpeg_seek_ (&io,
    391                                    stream,
    392                                    ctx,
    393                                    opt.offset);
    394   if (NULL == (frame = av_frame_alloc ()))
    395     goto cleanup;
    396   if (0 != decode_frame_at (&io,
    397                             ctx,
    398                             stream,
    399                             target,
    400                             frame))
    401     goto cleanup;
    402   if (0 != encode_thumbnail (frame,
    403                              &opt,
    404                              &thumbnail,
    405                              &thumbnail_size))
    406     goto cleanup;
    407   ec->proc (ec->cls,
    408             "thumbnailffmpeg",
    409             EXTRACTOR_METATYPE_THUMBNAIL,
    410             EXTRACTOR_METAFORMAT_BINARY,
    411             opt.png ? "image/png" : "image/jpeg",
    412             (const char *) thumbnail,
    413             thumbnail_size);
    414 cleanup:
    415   if (NULL != thumbnail)
    416     av_free (thumbnail);
    417   if (NULL != frame)
    418     av_frame_free (&frame);
    419   if (NULL != ctx)
    420     avcodec_free_context (&ctx);
    421   EXTRACTOR_ffmpeg_close_ (&io);
    422 }
    423 
    424 
    425 /**
    426  * Keep the diagnostics of the media framework out of the output of the
    427  * calling application; a decoder complaining about a broken file is not
    428  * something the application asked to see.
    429  *
    430  * ("force-kill" would be tempting here, to get a fresh process for every
    431  * file, but the host cannot know about that special without loading the
    432  * plugin in-process: it only finds out that the child is gone once it has
    433  * already handed it the next file, which is then silently lost.)
    434  *
    435  * @return special options for this plugin
    436  */
    437 const char *
    438 EXTRACTOR_thumbnailffmpeg_options (void);
    439 
    440 const char *
    441 EXTRACTOR_thumbnailffmpeg_options ()
    442 {
    443   return "close-stderr";
    444 }
    445 
    446 
    447 /**
    448  * Silence FFmpeg's logging.
    449  */
    450 void __attribute__ ((constructor))
    451 thumbnailffmpeg_init (void);
    452 
    453 void __attribute__ ((constructor))
    454 thumbnailffmpeg_init ()
    455 {
    456   EXTRACTOR_ffmpeg_silence_ ();
    457 }
    458 
    459 
    460 /* end of thumbnailffmpeg_extractor.c */