libextractor

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

test_thumbnailgst.c (16033B)


      1 /*
      2      This file is part of libextractor.
      3      Copyright (C) 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/test_thumbnailgst.c
     22  * @brief testcase for the thumbnailgst plugin
     23  * @author Christian Grothoff
     24  *
     25  * The encoder's output depends on the GStreamer version, so the JPEG can
     26  * only be checked for its magic number.  The PNG round, on the other
     27  * hand, is decoded: testdata/thumbnail_pattern.ogv is black for the
     28  * first 20 seconds and red afterwards, so the colour of the thumbnail
     29  * says whether the plugin really sampled where it claims to.
     30  */
     31 #include "platform.h"
     32 #include "test_lib.h"
     33 #include "test_media_lib.h"
     34 #include <gst/gst.h>
     35 #ifndef WINDOWS
     36 #include <sys/wait.h>
     37 #endif
     38 
     39 
     40 /**
     41  * The English subtitle track of testdata/subtitles.mkv, as SRT (see
     42  * contrib/gen_testmedia.sh).
     43  */
     44 #define SRT_EN \
     45         "1\n00:00:01,000 --> 00:00:04,000\nHello, world!\n\n" \
     46         "2\n00:00:12,500 --> 00:00:15,250\nThis is the second line\n" \
     47         "spanning two rows.\n\n" \
     48         "3\n00:00:33,000 --> 00:00:38,000\nGoodbye.\n\n"
     49 
     50 /**
     51  * The German subtitle track of testdata/subtitles.mkv.
     52  */
     53 #define SRT_DE \
     54         "1\n00:00:01,000 --> 00:00:04,000\nGr\xC3\xBC\xC3\x9F Gott, Welt!\n\n" \
     55         "2\n00:00:12,500 --> 00:00:15,250\nDas ist die zweite Zeile\n" \
     56         "\xC3\xBC" "ber zwei Reihen.\n\n" \
     57         "3\n00:00:33,000 --> 00:00:38,000\nAuf Wiedersehen.\n\n"
     58 
     59 /**
     60  * The French (ASS) subtitle track of testdata/subtitles.mkv; the
     61  * override tags ("{\i1}") are gone and "\N" became a line break.
     62  */
     63 #define SRT_FR \
     64         "1\n00:00:01,000 --> 00:00:04,000\nBonjour, le monde !\n\n" \
     65         "2\n00:00:12,500 --> 00:00:15,250\nDeuxi\xC3\xA8me ligne\n" \
     66         "sur deux rangs.\n\n"
     67 
     68 /**
     69  * Tile size for the default "size=128" and the 160x90 test video.
     70  */
     71 #define TILE_W 128
     72 #define TILE_H 72
     73 
     74 
     75 /**
     76  * Check that the thumbnail was taken from the red part of the video.
     77  *
     78  * @param data the PNG the plugin produced
     79  * @param data_len number of bytes in @a data
     80  * @return non-zero if the thumbnail looks right
     81  */
     82 static int
     83 validate_red (const void *data,
     84               size_t data_len)
     85 {
     86   return TEST_media_png_center_is (data,
     87                                    data_len,
     88                                    0xFE,
     89                                    0x00,
     90                                    0x00);
     91 }
     92 
     93 
     94 /**
     95  * Run ET_main() in a child process.  Once GStreamer has been
     96  * initialized in a process (which ET_main's in-process leg does), the
     97  * fork() of a later out-of-process extraction inherits a GStreamer that
     98  * no longer works in the child, so every round gets a fresh process.
     99  *
    100  * @param plugin plugin name and options
    101  * @param ps problems to solve
    102  * @return 0 on success
    103  */
    104 static int
    105 run_round (const char *plugin,
    106            struct ProblemSet *ps)
    107 {
    108 #ifndef WINDOWS
    109   pid_t pid;
    110   int status;
    111 
    112   fprintf (stderr,
    113            "Round `%s'\n",
    114            plugin);
    115   pid = fork ();
    116   if (0 == pid)
    117     _exit (ET_main (plugin,
    118                     ps));
    119   if (-1 == pid)
    120     return 1;
    121   if (pid != waitpid (pid,
    122                       &status,
    123                       0))
    124     return 1;
    125   if ( (! WIFEXITED (status)) ||
    126        (0 != WEXITSTATUS (status)) )
    127   {
    128     fprintf (stderr,
    129              "Round `%s' FAILED\n",
    130              plugin);
    131     return 1;
    132   }
    133   return 0;
    134 #else
    135   return ET_main (plugin,
    136                   ps);
    137 #endif
    138 }
    139 
    140 
    141 /**
    142  * Check (in a child process, see run_round()) whether GStreamer has
    143  * all of the given elements.
    144  *
    145  * @param names NULL-terminated list of element factory names
    146  * @return 1 if all are available
    147  */
    148 static int
    149 have_elements (const char *const *names)
    150 {
    151 #ifndef WINDOWS
    152   pid_t pid;
    153   int status;
    154 
    155   pid = fork ();
    156   if (0 == pid)
    157   {
    158     unsigned int i;
    159 
    160     gst_init (NULL, NULL);
    161     for (i = 0; NULL != names[i]; i++)
    162     {
    163       GstElementFactory *f = gst_element_factory_find (names[i]);
    164 
    165       if (NULL == f)
    166       {
    167         fprintf (stderr,
    168                  "GStreamer element `%s' missing, skipping\n",
    169                  names[i]);
    170         _exit (1);
    171       }
    172       gst_object_unref (f);
    173     }
    174     _exit (0);
    175   }
    176   if (-1 == pid)
    177     return 0;
    178   if (pid != waitpid (pid,
    179                       &status,
    180                       0))
    181     return 0;
    182   return (WIFEXITED (status) &&
    183           (0 == WEXITSTATUS (status)));
    184 #else
    185   (void) names;
    186   return 1;
    187 #endif
    188 }
    189 
    190 
    191 /**
    192  * What a plugin run produced.
    193  */
    194 struct Result
    195 {
    196   /**
    197    * Number of thumbnails.
    198    */
    199   unsigned int thumbnails;
    200 
    201   /**
    202    * Number of SRT dumps.
    203    */
    204   unsigned int srts;
    205 
    206   /**
    207    * Last thumbnail.
    208    */
    209   char *thumbnail;
    210 
    211   /**
    212    * Size of @e thumbnail.
    213    */
    214   size_t thumbnail_len;
    215 
    216   /**
    217    * MIME type of @e thumbnail.
    218    */
    219   char mime[32];
    220 };
    221 
    222 
    223 /**
    224  * Record the meta data of interest.
    225  *
    226  * @param cls the `struct Result`
    227  * @param plugin_name name of the plugin
    228  * @param type type of the meta data
    229  * @param format format of the meta data
    230  * @param data_mime_type MIME type of the meta data
    231  * @param data the meta data
    232  * @param data_len number of bytes in @a data
    233  * @return 0 (continue)
    234  */
    235 static int
    236 collect_cb (void *cls,
    237             const char *plugin_name,
    238             enum EXTRACTOR_MetaType type,
    239             enum EXTRACTOR_MetaFormat format,
    240             const char *data_mime_type,
    241             const char *data,
    242             size_t data_len)
    243 {
    244   struct Result *r = cls;
    245 
    246   (void) plugin_name;
    247   (void) format;
    248   if (EXTRACTOR_METATYPE_SUBTITLE_TEXT == type)
    249     r->srts++;
    250   if (EXTRACTOR_METATYPE_THUMBNAIL == type)
    251   {
    252     r->thumbnails++;
    253     free (r->thumbnail);
    254     r->thumbnail = malloc (data_len);
    255     if (NULL != r->thumbnail)
    256       memcpy (r->thumbnail, data, data_len);
    257     r->thumbnail_len = data_len;
    258     snprintf (r->mime,
    259               sizeof (r->mime),
    260               "%s",
    261               (NULL != data_mime_type) ? data_mime_type : "");
    262   }
    263   return 0;
    264 }
    265 
    266 
    267 /**
    268  * Run the plugin (out of process only, so that GStreamer is never
    269  * initialized in this process) and record what it produced.
    270  *
    271  * @param config plugin name and options
    272  * @param[out] r where to store the result
    273  */
    274 static void
    275 collect (const char *config,
    276          struct Result *r)
    277 {
    278   struct EXTRACTOR_PluginList *pl;
    279 
    280   memset (r, 0, sizeof (*r));
    281   pl = EXTRACTOR_plugin_add_config (NULL,
    282                                     config,
    283                                     EXTRACTOR_OPTION_DEFAULT_POLICY);
    284   if (NULL == pl)
    285     return;
    286   EXTRACTOR_extract (pl,
    287                      "testdata/subtitles.mkv",
    288                      NULL, 0,
    289                      &collect_cb,
    290                      r);
    291   EXTRACTOR_plugin_remove_all (pl);
    292 }
    293 
    294 
    295 /**
    296  * Check a gallery run: exactly one PNG of @a w x @a h pixels (0 to
    297  * accept any size), no SRT, within the size limit, and none of the
    298  * first @a check_tiles tiles predominantly black or white.
    299  *
    300  * @param config plugin name and options
    301  * @param w expected width, 0 for any
    302  * @param h expected height, 0 for any
    303  * @param check_tiles number of tiles to inspect
    304  * @param colors expected dominant channel of each inspected tile
    305  *        (0 red, 2 blue), NULL for no check
    306  * @return 0 on success
    307  */
    308 static int
    309 check_gallery (const char *config,
    310                uint32_t w,
    311                uint32_t h,
    312                unsigned int check_tiles,
    313                const unsigned int *colors)
    314 {
    315   struct Result r;
    316   uint32_t pw;
    317   uint32_t ph;
    318   uint8_t *img;
    319   unsigned int t;
    320   int ret = 0;
    321 
    322   collect (config,
    323            &r);
    324   if ( (1 != r.thumbnails) ||
    325        (0 != r.srts) ||
    326        (0 != strcmp (r.mime, "image/png")) ||
    327        (r.thumbnail_len > 4 * 1024 * 1024) )
    328   {
    329     fprintf (stderr,
    330              "%s: got %u thumbnails (%s, %u bytes), %u SRTs\n",
    331              config,
    332              r.thumbnails,
    333              r.mime,
    334              (unsigned int) r.thumbnail_len,
    335              r.srts);
    336     free (r.thumbnail);
    337     return 1;
    338   }
    339   img = TEST_media_png_decode (r.thumbnail,
    340                                r.thumbnail_len,
    341                                &pw,
    342                                &ph);
    343   if (NULL == img)
    344   {
    345     fprintf (stderr,
    346              "%s: cannot decode PNG\n",
    347              config);
    348     free (r.thumbnail);
    349 #if TEST_MEDIA_HAVE_PNG
    350     return 1;
    351 #else
    352     return 0;
    353 #endif
    354   }
    355   free (r.thumbnail);
    356   if ( ( (0 != w) && (pw != w) ) ||
    357        ( (0 != h) && (ph != h) ) )
    358   {
    359     fprintf (stderr,
    360              "%s: gallery is %ux%u, expected %ux%u\n",
    361              config,
    362              (unsigned int) pw,
    363              (unsigned int) ph,
    364              (unsigned int) w,
    365              (unsigned int) h);
    366     free (img);
    367     return 1;
    368   }
    369   for (t = 0; t < check_tiles; t++)
    370   {
    371     uint32_t cols = pw / TILE_W;
    372     uint32_t x0 = (t % cols) * TILE_W;
    373     uint32_t y0 = (t / cols) * TILE_H;
    374     uint64_t sum[3] = { 0, 0, 0 };
    375     uint64_t black = 0;
    376     uint64_t white = 0;
    377     uint64_t total = (uint64_t) TILE_W * TILE_H;
    378     uint32_t x;
    379     uint32_t y;
    380 
    381     if (y0 + TILE_H > ph)
    382       break;
    383     for (y = y0; y < y0 + TILE_H; y++)
    384       for (x = x0; x < x0 + TILE_W; x++)
    385       {
    386         const uint8_t *px = &img[((size_t) y * pw + x) * 3];
    387 
    388         if ( (px[0] < 48) && (px[1] < 48) && (px[2] < 48) )
    389           black++;
    390         if ( (px[0] > 208) && (px[1] > 208) && (px[2] > 208) )
    391           white++;
    392         sum[0] += px[0];
    393         sum[1] += px[1];
    394         sum[2] += px[2];
    395       }
    396     if ( (2 * black > total) ||
    397          (2 * white > total) )
    398     {
    399       fprintf (stderr,
    400                "%s: tile %u is predominantly %s\n",
    401                config,
    402                t,
    403                (2 * black > total) ? "black" : "white");
    404       ret = 1;
    405     }
    406     if ( (NULL != colors) &&
    407          ( (sum[colors[t]] / total < 160) ||
    408            (sum[(colors[t] + 1) % 3] / total > 96) ) )
    409     {
    410       fprintf (stderr,
    411                "%s: tile %u has mean rgb(%u,%u,%u), wrong colour\n",
    412                config,
    413                t,
    414                (unsigned int) (sum[0] / total),
    415                (unsigned int) (sum[1] / total),
    416                (unsigned int) (sum[2] / total));
    417       ret = 1;
    418     }
    419   }
    420   free (img);
    421   return ret;
    422 }
    423 
    424 
    425 /**
    426  * Check that a run produces a thumbnail but no SRT.
    427  *
    428  * @param config plugin name and options
    429  * @return 0 on success
    430  */
    431 static int
    432 check_no_srt (const char *config)
    433 {
    434   struct Result r;
    435 
    436   collect (config,
    437            &r);
    438   free (r.thumbnail);
    439   if ( (0 != r.srts) ||
    440        (1 != r.thumbnails) )
    441   {
    442     fprintf (stderr,
    443              "%s: got %u SRTs and %u thumbnails, expected 0 and 1\n",
    444              config,
    445              r.srts,
    446              r.thumbnails);
    447     return 1;
    448   }
    449   return 0;
    450 }
    451 
    452 
    453 /**
    454  * Main function for the thumbnailgst testcase.
    455  *
    456  * @param argc number of arguments (ignored)
    457  * @param argv arguments (ignored)
    458  * @return 0 on success
    459  */
    460 int
    461 main (int argc, char *argv[])
    462 {
    463   static const char *const srt_elements[] = {
    464     "appsrc", "parsebin", "matroskademux", "fakesink",
    465     "playbin", "theoradec", "jpegenc", NULL
    466   };
    467   static const char *const gallery_elements[] = {
    468     "appsrc", "appsink", "playbin", "matroskademux", "theoradec",
    469     "videoconvert", "videoscale", "pngenc", "fakesink", NULL
    470   };
    471   static const unsigned int colors4[] = { 0, 0, 2, 2 };
    472   uint8_t jpeg_magic[] = { 0xFF, 0xD8, 0xFF };
    473   uint8_t png_magic[] = { 137, 'P', 'N', 'G', '\r', '\n', 26, '\n' };
    474   struct SolutionData thumbnail_jpeg_sol[] = {
    475     {
    476       EXTRACTOR_METATYPE_THUMBNAIL,
    477       EXTRACTOR_METAFORMAT_BINARY,
    478       "image/jpeg",
    479       (const char *) jpeg_magic,
    480       sizeof (jpeg_magic),
    481       0
    482     },
    483     { 0, 0, NULL, NULL, 0, -1 }
    484   };
    485   struct SolutionData thumbnail_png_sol[] = {
    486     {
    487       EXTRACTOR_METATYPE_THUMBNAIL,
    488       EXTRACTOR_METAFORMAT_BINARY,
    489       "image/png",
    490       (const char *) png_magic,
    491       sizeof (png_magic),
    492       0,
    493       0,
    494       &validate_red
    495     },
    496     { 0, 0, NULL, NULL, 0, -1 }
    497   };
    498 #define SRT_SOL(name, text) \
    499         struct SolutionData name[] = { \
    500           { \
    501             EXTRACTOR_METATYPE_THUMBNAIL, \
    502             EXTRACTOR_METAFORMAT_BINARY, \
    503             "image/jpeg", \
    504             (const char *) jpeg_magic, \
    505             sizeof (jpeg_magic), \
    506             0 \
    507           }, \
    508           { \
    509             EXTRACTOR_METATYPE_SUBTITLE_TEXT, \
    510             EXTRACTOR_METAFORMAT_UTF8, \
    511             "application/x-subrip", \
    512             text, \
    513             sizeof (text), \
    514             0 \
    515           }, \
    516           { 0, 0, NULL, NULL, 0, -1 } \
    517         }
    518 
    519 
    520   SRT_SOL (srt_en_sol, SRT_EN);
    521   SRT_SOL (srt_eng_sol, SRT_EN);
    522   SRT_SOL (srt_de_sol, SRT_DE);
    523   SRT_SOL (srt_fr_sol, SRT_FR);
    524 #undef SRT_SOL
    525   struct ProblemSet ps_jpeg[] = {
    526     { "testdata/thumbnail_pattern.ogv",
    527       thumbnail_jpeg_sol },
    528     { NULL, NULL }
    529   };
    530   struct ProblemSet ps_png[] = {
    531     { "testdata/thumbnail_pattern.ogv",
    532       thumbnail_png_sol },
    533     { NULL, NULL }
    534   };
    535   struct ProblemSet ps_srt_en[] = {
    536     { "testdata/subtitles.mkv", srt_en_sol },
    537     { NULL, NULL }
    538   };
    539   struct ProblemSet ps_srt_eng[] = {
    540     { "testdata/subtitles.mkv", srt_eng_sol },
    541     { NULL, NULL }
    542   };
    543   struct ProblemSet ps_srt_de[] = {
    544     { "testdata/subtitles.mkv", srt_de_sol },
    545     { NULL, NULL }
    546   };
    547   struct ProblemSet ps_srt_fr[] = {
    548     { "testdata/subtitles.mkv", srt_fr_sol },
    549     { NULL, NULL }
    550   };
    551   int ret = 0;
    552 
    553   (void) argc;
    554   (void) argv;
    555   /* collect() does not go through ET_main(), which would set this */
    556   if (0 != putenv ("LIBEXTRACTOR_PREFIX=./.libs/"))
    557     return 1;
    558   /* None of the rounds may initialize GStreamer in this process; see
    559      run_round(). */
    560   ret |= run_round ("thumbnailgst",
    561                     ps_jpeg);
    562   ret |= run_round ("thumbnailgst(format=png,size=32)",
    563                     ps_png);
    564   if (have_elements (srt_elements))
    565   {
    566     ret |= check_no_srt ("thumbnailgst");
    567     ret |= run_round ("thumbnailgst(srt:en)",
    568                       ps_srt_en);
    569     ret |= run_round ("thumbnailgst(srt:eng)",
    570                       ps_srt_eng);
    571     ret |= run_round ("thumbnailgst(srt:de)",
    572                       ps_srt_de);
    573     ret |= run_round ("thumbnailgst(srt:fr)",
    574                       ps_srt_fr);
    575     ret |= check_no_srt ("thumbnailgst(srt:xx)");
    576   }
    577   if (have_elements (gallery_elements))
    578   {
    579     /* 4 tiles of 128x72 are closest to 16:9 as 2x2; tiles 0 and 2 are
    580        sampled at 5s (black) and 25s (white) and must have been moved
    581        into the red and blue stretches */
    582     ret |= check_gallery ("thumbnailgst(gallery:4,format=png)",
    583                           2 * TILE_W,
    584                           2 * TILE_H,
    585                           4,
    586                           colors4);
    587     ret |= check_gallery ("thumbnailgst(gallery:1,format=png)",
    588                           TILE_W,
    589                           TILE_H,
    590                           1,
    591                           NULL);
    592     /* 0.4s slices cannot escape the black and white stretches, so
    593        only the limits are checked */
    594     ret |= check_gallery ("thumbnailgst(gallery:100,format=png)",
    595                           10 * TILE_W,
    596                           10 * TILE_H,
    597                           0,
    598                           NULL);
    599     ret |= check_gallery ("thumbnailgst(size=32,gallery:100,format=png)",
    600                           0,
    601                           0,
    602                           0,
    603                           NULL);
    604   }
    605   return ret;
    606 }
    607 
    608 
    609 /* end of test_thumbnailgst.c */