static GdkPixbuf * capture_interesting_frame (ThumbApp *app) { GdkPixbuf* pixbuf; guint current; const double frame_locations[] = { 1.0 / 3.0, 2.0 / 3.0, 0.1, 0.9, 0.5 }; if (app->duration == -1) { PROGRESS_DEBUG("Video has no duration, so capture 1st frame"); return capture_frame_at_time (app, 0); } /* Test at multiple points in the file to see if we can get an * interesting frame */ for (current = 0; current < G_N_ELEMENTS(frame_locations); current++) { PROGRESS_DEBUG("About to seek to %f", frame_locations[current]); thumb_app_seek (app, frame_locations[current] * app->duration); /* Pull the frame, if it's interesting we bail early */ PROGRESS_DEBUG("About to get frame for iter %d", current); pixbuf = xplayer_gst_playbin_get_frame (app->play); if (pixbuf != NULL && is_image_interesting (pixbuf) != FALSE) { PROGRESS_DEBUG("Frame for iter %d is interesting", current); break; } /* If we get to the end of this loop, we'll end up using * the last image we pulled */ if (current + 1 < G_N_ELEMENTS(frame_locations)) { if (pixbuf != NULL) { g_object_unref (pixbuf); pixbuf = NULL; } } PROGRESS_DEBUG("Frame for iter %d was not interesting", current); } return pixbuf; }
int main (int argc, char *argv[]) { GOptionGroup *options; GOptionContext *context; GError *err = NULL; GdkPixbuf *pixbuf; const char *input, *output; ThumbApp app; context = g_option_context_new ("Thumbnail movies"); options = gst_init_get_option_group (); g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE); g_option_context_add_group (context, options); g_option_context_add_group (context, gtk_get_option_group (TRUE)); if (g_option_context_parse (context, &argc, &argv, &err) == FALSE) { g_print ("couldn't parse command-line options: %s\n", err->message); g_error_free (err); return 1; } #ifdef G_OS_UNIX if (time_limit != FALSE) { errno = 0; if (nice (20) != 20 && errno != 0) g_warning ("Couldn't change nice value of process."); } #endif if (print_progress) { fcntl (fileno (stdout), F_SETFL, O_NONBLOCK); setbuf (stdout, NULL); } if (g_fatal_warnings) { GLogLevelFlags fatal_mask; fatal_mask = g_log_set_always_fatal (G_LOG_FATAL_MASK); fatal_mask |= G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL; g_log_set_always_fatal (fatal_mask); } if (raw_output == FALSE && output_size == -1) output_size = DEFAULT_OUTPUT_SIZE; if (filenames == NULL || g_strv_length (filenames) != 2 || (second_index != -1 && gallery != -1) || (print_progress == TRUE && verbose == TRUE)) { char *help; help = g_option_context_get_help (context, FALSE, NULL); g_print ("%s", help); g_free (help); return 1; } input = filenames[0]; output = filenames[1]; PROGRESS_DEBUG("Initialised libraries, about to create video widget"); PRINT_PROGRESS (2.0); app.input = input; app.output = output; thumb_app_setup_play (&app); thumb_app_set_filename (&app); PROGRESS_DEBUG("Video widget created"); PRINT_PROGRESS (6.0); if (time_limit != FALSE) xplayer_resources_monitor_start (input, 0); PROGRESS_DEBUG("About to open video file"); if (thumb_app_start (&app) == FALSE) { g_print ("xplayer-video-thumbnailer couldn't open file '%s'\n", input); exit (1); } thumb_app_set_error_handler (&app); /* We don't need covers when we're in gallery mode */ if (gallery == -1) thumb_app_check_for_cover (&app); if (thumb_app_get_has_video (&app) == FALSE) { PROGRESS_DEBUG ("xplayer-video-thumbnailer couldn't find a video track in '%s'\n", input); exit (1); } thumb_app_set_duration (&app); PROGRESS_DEBUG("Opened video file: '%s'", input); PRINT_PROGRESS (10.0); if (gallery == -1) { /* If the user has told us to use a frame at a specific second * into the video, just use that frame no matter how boring it * is */ if (second_index != -1) { assert_duration (&app); pixbuf = capture_frame_at_time (&app, second_index * 1000); } else { pixbuf = capture_interesting_frame (&app); } PRINT_PROGRESS (90.0); } else { assert_duration (&app); /* We're producing a gallery of screenshots from throughout the file */ pixbuf = create_gallery (&app); } /* Cleanup */ xplayer_resources_monitor_stop (); thumb_app_cleanup (&app); PRINT_PROGRESS (92.0); if (pixbuf == NULL) { g_print ("xplayer-video-thumbnailer couldn't get a picture from '%s'\n", input); exit (1); } PROGRESS_DEBUG("Saving captured screenshot"); save_pixbuf (pixbuf, output, input, output_size, FALSE); g_object_unref (pixbuf); PRINT_PROGRESS (100.0); return 0; }
static GdkPixbuf * create_gallery (ThumbApp *app) { GdkPixbuf *screenshot, *pixbuf = NULL; cairo_t *cr; cairo_surface_t *surface; PangoLayout *layout; PangoFontDescription *font_desc; gint64 stream_length, screenshot_interval, pos; guint columns = 3, rows, current_column, current_row, x, y; gint screenshot_width = 0, screenshot_height = 0, x_padding = 0, y_padding = 0; gfloat scale = 1.0; gchar *header_text, *duration_text, *filename; /* Calculate how many screenshots we're going to take */ stream_length = app->duration; /* As a default, we have one screenshot per minute of stream, * but adjusted so we don't have any gaps in the resulting gallery. */ if (gallery == 0) { gallery = stream_length / 60000; while (gallery % 3 != 0 && gallery % 4 != 0 && gallery % 5 != 0) { gallery++; } } if (gallery < GALLERY_MIN) gallery = GALLERY_MIN; if (gallery > GALLERY_MAX) gallery = GALLERY_MAX; screenshot_interval = stream_length / gallery; /* Put a lower bound on the screenshot interval so we can't enter an infinite loop below */ if (screenshot_interval == 0) screenshot_interval = 1; PROGRESS_DEBUG ("Producing gallery of %u screenshots, taken at %" G_GINT64_FORMAT " millisecond intervals throughout a %" G_GINT64_FORMAT " millisecond-long stream.", gallery, screenshot_interval, stream_length); /* Calculate how to arrange the screenshots so we don't get ones orphaned on the last row. * At this point, only deal with arrangements of 3, 4 or 5 columns. */ y = G_MAXUINT; for (x = 3; x <= 5; x++) { if (gallery % x == 0 || x - gallery % x < y) { y = x - gallery % x; columns = x; /* Have we found an optimal solution already? */ if (y == x) break; } } rows = ceil ((gfloat) gallery / (gfloat) columns); PROGRESS_DEBUG ("Outputting as %u rows and %u columns.", rows, columns); /* Take the screenshots and composite them into a pixbuf */ current_column = current_row = x = y = 0; for (pos = screenshot_interval; pos <= stream_length; pos += screenshot_interval) { if (pos == stream_length) screenshot = capture_frame_at_time (app, pos - 1); else screenshot = capture_frame_at_time (app, pos); if (pixbuf == NULL) { screenshot_width = gdk_pixbuf_get_width (screenshot); screenshot_height = gdk_pixbuf_get_height (screenshot); /* Calculate a scaling factor so that screenshot_width -> output_size */ scale = (float) output_size / (float) screenshot_width; x_padding = x = MAX (output_size * 0.05, 1); y_padding = y = MAX (scale * screenshot_height * 0.05, 1); PROGRESS_DEBUG ("Scaling each screenshot by %f.", scale); /* Create our massive pixbuf */ pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, FALSE, 8, columns * output_size + (columns + 1) * x_padding, (guint) (rows * scale * screenshot_height + (rows + 1) * y_padding)); gdk_pixbuf_fill (pixbuf, 0x000000ff); PROGRESS_DEBUG ("Created output pixbuf (%ux%u).", gdk_pixbuf_get_width (pixbuf), gdk_pixbuf_get_height (pixbuf)); } /* Composite the screenshot into our gallery */ gdk_pixbuf_composite (screenshot, pixbuf, x, y, output_size, scale * screenshot_height, (gdouble) x, (gdouble) y, scale, scale, GDK_INTERP_BILINEAR, 255); g_object_unref (screenshot); PROGRESS_DEBUG ("Composited screenshot from %" G_GINT64_FORMAT " milliseconds (address %u) at (%u,%u).", pos, GPOINTER_TO_UINT (screenshot), x, y); /* We print progress in the range 10% (MIN_PROGRESS) to 50% (MAX_PROGRESS - MIN_PROGRESS) / 2.0 */ PRINT_PROGRESS (MIN_PROGRESS + (current_row * columns + current_column) * (((MAX_PROGRESS - MIN_PROGRESS) / gallery) / 2.0)); current_column = (current_column + 1) % columns; x += output_size + x_padding; if (current_column == 0) { x = x_padding; y += scale * screenshot_height + y_padding; current_row++; } } PROGRESS_DEBUG ("Converting pixbuf to a Cairo surface."); /* Load the pixbuf into a Cairo surface and overlay the text. The height is the height of * the gallery plus the necessary height for 3 lines of header (at ~18px each), plus some * extra padding. */ surface = cairo_image_surface_create (CAIRO_FORMAT_RGB24, gdk_pixbuf_get_width (pixbuf), gdk_pixbuf_get_height (pixbuf) + GALLERY_HEADER_HEIGHT + y_padding); cr = cairo_create (surface); cairo_surface_destroy (surface); /* First, copy across the gallery pixbuf */ gdk_cairo_set_source_pixbuf (cr, pixbuf, 0.0, GALLERY_HEADER_HEIGHT + y_padding); cairo_rectangle (cr, 0.0, GALLERY_HEADER_HEIGHT + y_padding, gdk_pixbuf_get_width (pixbuf), gdk_pixbuf_get_height (pixbuf)); cairo_fill (cr); g_object_unref (pixbuf); /* Build the header information */ duration_text = xplayer_time_to_string (stream_length); filename = NULL; if (strstr (app->input, "://")) { char *local; local = g_filename_from_uri (app->input, NULL, NULL); filename = g_path_get_basename (local); g_free (local); } if (filename == NULL) filename = g_path_get_basename (app->input); /* Translators: The first string is "Filename" (as translated); the second is an actual filename. The third string is "Resolution" (as translated); the fourth and fifth are screenshot height and width, respectively. The sixth string is "Duration" (as translated); the seventh is the movie duration in words. */ header_text = g_markup_printf_escaped (_("<b>%s</b>: %s\n<b>%s</b>: %d\303\227%d\n<b>%s</b>: %s"), _("Filename"), filename, _("Resolution"), screenshot_width, screenshot_height, _("Duration"), duration_text); g_free (duration_text); g_free (filename); PROGRESS_DEBUG ("Writing header text with Pango."); /* Write out some header information */ layout = pango_cairo_create_layout (cr); font_desc = pango_font_description_from_string ("Sans 18px"); pango_layout_set_font_description (layout, font_desc); pango_font_description_free (font_desc); pango_layout_set_markup (layout, header_text, -1); g_free (header_text); cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); /* white */ cairo_move_to (cr, (gdouble) x_padding, (gdouble) y_padding); pango_cairo_show_layout (cr, layout); /* Go through each screenshot and write its timestamp */ current_column = current_row = 0; x = x_padding + output_size; y = y_padding * 2 + GALLERY_HEADER_HEIGHT + scale * screenshot_height; font_desc = pango_font_description_from_string ("Sans 10px"); pango_layout_set_font_description (layout, font_desc); pango_font_description_free (font_desc); PROGRESS_DEBUG ("Writing screenshot timestamps with Pango."); for (pos = screenshot_interval; pos <= stream_length; pos += screenshot_interval) { gchar *timestamp_text; gint layout_width, layout_height; timestamp_text = xplayer_time_to_string (pos); pango_layout_set_text (layout, timestamp_text, -1); pango_layout_get_pixel_size (layout, &layout_width, &layout_height); /* Display the timestamp in the bottom-right corner of the current screenshot */ cairo_move_to (cr, x - layout_width - 0.02 * output_size, y - layout_height - 0.02 * scale * screenshot_height); /* We have to stroke the text so it's visible against screenshots of the same * foreground color. */ pango_cairo_layout_path (cr, layout); cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); /* black */ cairo_stroke_preserve (cr); cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); /* white */ cairo_fill (cr); PROGRESS_DEBUG ("Writing timestamp \"%s\" at (%f,%f).", timestamp_text, x - layout_width - 0.02 * output_size, y - layout_height - 0.02 * scale * screenshot_height); /* We print progress in the range 50% (MAX_PROGRESS - MIN_PROGRESS) / 2.0) to 90% (MAX_PROGRESS) */ PRINT_PROGRESS (MIN_PROGRESS + (MAX_PROGRESS - MIN_PROGRESS) / 2.0 + (current_row * columns + current_column) * (((MAX_PROGRESS - MIN_PROGRESS) / gallery) / 2.0)); g_free (timestamp_text); current_column = (current_column + 1) % columns; x += output_size + x_padding; if (current_column == 0) { x = x_padding + output_size; y += scale * screenshot_height + y_padding; current_row++; } } g_object_unref (layout); PROGRESS_DEBUG ("Converting Cairo surface back to pixbuf."); /* Create a new pixbuf from the Cairo context */ pixbuf = cairo_surface_to_pixbuf (cairo_get_target (cr)); cairo_destroy (cr); return pixbuf; }
int main (int argc, char *argv[]) { GOptionGroup *options; GOptionContext *context; GError *err = NULL; BaconVideoWidget *bvw; GdkPixbuf *pixbuf; const char *input, *output; callback_data data; g_thread_init (NULL); context = g_option_context_new ("Thumbnail movies"); options = bacon_video_widget_get_option_group (); g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE); g_option_context_add_group (context, options); #ifndef THUMB_DEBUG g_type_init (); #else g_option_context_add_group (context, gtk_get_option_group (TRUE)); #endif if (g_option_context_parse (context, &argc, &argv, &err) == FALSE) { g_print ("couldn't parse command-line options: %s\n", err->message); g_error_free (err); return 1; } #ifdef G_OS_UNIX if (time_limit != FALSE) { errno = 0; if (nice (20) != 20 && errno != 0) g_warning ("Couldn't change nice value of process."); } #endif if (print_progress) { fcntl (fileno (stdout), F_SETFL, O_NONBLOCK); setbuf (stdout, NULL); } if (g_fatal_warnings) { GLogLevelFlags fatal_mask; fatal_mask = g_log_set_always_fatal (G_LOG_FATAL_MASK); fatal_mask |= G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL; g_log_set_always_fatal (fatal_mask); } if (raw_output == FALSE && output_size == -1) output_size = DEFAULT_OUTPUT_SIZE; if (filenames == NULL || g_strv_length (filenames) != 2 || (second_index != -1 && gallery != -1) || (print_progress == TRUE && verbose == TRUE)) { char *help; help = g_option_context_get_help (context, FALSE, NULL); g_print ("%s", help); g_free (help); return 1; } input = filenames[0]; output = filenames[1]; PROGRESS_DEBUG("Initialised libraries, about to create video widget"); PRINT_PROGRESS (2.0); bvw = BACON_VIDEO_WIDGET (bacon_video_widget_new (-1, -1, BVW_USE_TYPE_CAPTURE, &err)); if (err != NULL) { g_print ("totem-video-thumbnailer couldn't create the video " "widget.\nReason: %s.\n", err->message); g_error_free (err); exit (1); } data.input = input; data.output = output; g_signal_connect (G_OBJECT (bvw), "got-metadata", G_CALLBACK (on_got_metadata_event), &data); PROGRESS_DEBUG("Video widget created"); PRINT_PROGRESS (6.0); if (time_limit != FALSE) totem_resources_monitor_start (input, 0); PROGRESS_DEBUG("About to open video file"); if (bacon_video_widget_open (bvw, input, NULL, &err) == FALSE) { g_print ("totem-video-thumbnailer couldn't open file '%s'\n" "Reason: %s.\n", input, err->message); g_error_free (err); exit (1); } PROGRESS_DEBUG("Opened video file: '%s'", input); PRINT_PROGRESS (10.0); if (gallery == -1) { /* If the user has told us to use a frame at a specific second * into the video, just use that frame no matter how boring it * is */ if (second_index != -1) pixbuf = capture_frame_at_time (bvw, input, output, second_index); else pixbuf = capture_interesting_frame (bvw, input, output); PRINT_PROGRESS (90.0); } else { /* We're producing a gallery of screenshots from throughout the file */ pixbuf = create_gallery (bvw, input, output); } /* Cleanup */ bacon_video_widget_close (bvw); totem_resources_monitor_stop (); gtk_widget_destroy (GTK_WIDGET (bvw)); PRINT_PROGRESS (92.0); if (pixbuf == NULL) { g_print ("totem-video-thumbnailer couldn't get a picture from " "'%s'\n", input); exit (1); } PROGRESS_DEBUG("Saving captured screenshot"); save_pixbuf (pixbuf, output, input, output_size, FALSE); g_object_unref (pixbuf); PRINT_PROGRESS (100.0); return 0; }