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 */