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