1 /*
2  * Copyright (C) 2003,2004 Bastien Nocera <hadess@hadess.net>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA.
17  *
18  * The Totem project hereby grant permission for non-gpl compatible GStreamer
19  * plugins to be used and distributed together with GStreamer and Totem. This
20  * permission are above and beyond the permissions granted by the GPL license
21  * Totem is covered by.
22  *
23  * Monday 7th February 2005: Christian Schaller: Add exception clause.
24  * See license_change file for details.
25  *
26  */
27 
28 #include "config.h"
29 
30 #define GST_USE_UNSTABLE_API 1
31 
32 #include <glib/gstdio.h>
33 #include <glib/gi18n.h>
34 #include <gst/gst.h>
35 #include <totem-pl-parser.h>
36 
37 #include <locale.h>
38 #include <errno.h>
39 #include <unistd.h>
40 #include <string.h>
41 #include <math.h>
42 #include <stdlib.h>
43 #include <fcntl.h>
44 #include <sys/types.h>
45 #include <sys/stat.h>
46 
47 #include "gst/totem-gst-helpers.h"
48 #include "gst/totem-gst-pixbuf-helpers.h"
49 #include "totem-resources.h"
50 
51 #ifdef G_HAVE_ISO_VARARGS
52 #define PROGRESS_DEBUG(...) { if (verbose != FALSE) g_message (__VA_ARGS__); }
53 #elif defined(G_HAVE_GNUC_VARARGS)
54 #define PROGRESS_DEBUG(format...) { if (verbose != FALSE) g_message (format); }
55 #endif
56 
57 /* The main() function controls progress in the first and last 10% */
58 #define PRINT_PROGRESS(p) { if (print_progress) g_printf ("%f%% complete\n", p); }
59 #define MIN_PROGRESS 10.0
60 #define MAX_PROGRESS 90.0
61 
62 #define BORING_IMAGE_VARIANCE 256.0		/* Tweak this if necessary */
63 #define DEFAULT_OUTPUT_SIZE 256
64 
65 static gboolean raw_output = FALSE;
66 static int output_size = -1;
67 static gboolean time_limit = TRUE;
68 static gboolean verbose = FALSE;
69 static gboolean print_progress = FALSE;
70 static gint64 second_index = -1;
71 static char **filenames = NULL;
72 
73 typedef struct {
74 	const char *output;
75 	const char *input;
76 	GstElement *play;
77 	gint64      duration;
78 } ThumbApp;
79 
80 static void save_pixbuf (GdkPixbuf *pixbuf, const char *path,
81 			 const char *video_path, int size, gboolean is_still);
82 
83 static void
entry_parsed_cb(TotemPlParser * parser,const char * uri,GHashTable * metadata,char ** new_url)84 entry_parsed_cb (TotemPlParser *parser,
85 		 const char    *uri,
86 		 GHashTable    *metadata,
87 		 char         **new_url)
88 {
89 	*new_url = g_strdup (uri);
90 }
91 
92 static char *
get_special_url(GFile * file)93 get_special_url (GFile *file)
94 {
95 	char *path, *orig_uri, *uri, *mime_type;
96 	TotemPlParser *parser;
97 	TotemPlParserResult res;
98 
99 	path = g_file_get_path (file);
100 
101 	mime_type = g_content_type_guess (path, NULL, 0, NULL);
102 	g_free (path);
103 	if (g_strcmp0 (mime_type, "application/x-cd-image") != 0) {
104 		g_free (mime_type);
105 		return NULL;
106 	}
107 	g_free (mime_type);
108 
109 	uri = NULL;
110 	orig_uri = g_file_get_uri (file);
111 
112 	parser = totem_pl_parser_new ();
113 	g_signal_connect (parser, "entry-parsed",
114 			  G_CALLBACK (entry_parsed_cb), &uri);
115 
116 	res = totem_pl_parser_parse (parser, orig_uri, FALSE);
117 
118 	g_free (orig_uri);
119 	g_object_unref (parser);
120 
121 	if (res == TOTEM_PL_PARSER_RESULT_SUCCESS)
122 		return uri;
123 
124 	g_free (uri);
125 
126 	return NULL;
127 }
128 
129 static gboolean
is_special_uri(const char * uri)130 is_special_uri (const char *uri)
131 {
132 	if (g_str_has_prefix (uri, "dvd://") ||
133 	    g_str_has_prefix (uri, "vcd://"))
134 		return TRUE;
135 
136 	return FALSE;
137 }
138 
139 static void
thumb_app_set_filename(ThumbApp * app)140 thumb_app_set_filename (ThumbApp *app)
141 {
142 	GFile *file;
143 	char *uri;
144 
145 	if (is_special_uri (app->input)) {
146 		g_object_set (app->play, "uri", app->input, NULL);
147 		return;
148 	}
149 
150 	file = g_file_new_for_commandline_arg (app->input);
151 	uri = get_special_url (file);
152 	if (uri == NULL)
153 		uri = g_file_get_uri (file);
154 	g_object_unref (file);
155 
156 	PROGRESS_DEBUG("setting URI %s", uri);
157 
158 	g_object_set (app->play, "uri", uri, NULL);
159 	g_free (uri);
160 }
161 
162 static GstBusSyncReply
error_handler(GstBus * bus,GstMessage * message,GstElement * play)163 error_handler (GstBus *bus,
164 	       GstMessage *message,
165 	       GstElement *play)
166 {
167 	GstMessageType msg_type;
168 
169 	msg_type = GST_MESSAGE_TYPE (message);
170 	switch (msg_type) {
171 	case GST_MESSAGE_ERROR:
172 		totem_gst_message_print (message, play, "totem-video-thumbnailer-error");
173 		exit (1);
174 	case GST_MESSAGE_EOS:
175 		exit (0);
176 
177 	case GST_MESSAGE_ASYNC_DONE:
178 	case GST_MESSAGE_UNKNOWN:
179 	case GST_MESSAGE_WARNING:
180 	case GST_MESSAGE_INFO:
181 	case GST_MESSAGE_TAG:
182 	case GST_MESSAGE_BUFFERING:
183 	case GST_MESSAGE_STATE_CHANGED:
184 	case GST_MESSAGE_STATE_DIRTY:
185 	case GST_MESSAGE_STEP_DONE:
186 	case GST_MESSAGE_CLOCK_PROVIDE:
187 	case GST_MESSAGE_CLOCK_LOST:
188 	case GST_MESSAGE_NEW_CLOCK:
189 	case GST_MESSAGE_STRUCTURE_CHANGE:
190 	case GST_MESSAGE_STREAM_STATUS:
191 	case GST_MESSAGE_APPLICATION:
192 	case GST_MESSAGE_ELEMENT:
193 	case GST_MESSAGE_SEGMENT_START:
194 	case GST_MESSAGE_SEGMENT_DONE:
195 	case GST_MESSAGE_DURATION_CHANGED:
196 	case GST_MESSAGE_LATENCY:
197 	case GST_MESSAGE_ASYNC_START:
198 	case GST_MESSAGE_REQUEST_STATE:
199 	case GST_MESSAGE_STEP_START:
200 	case GST_MESSAGE_QOS:
201 	case GST_MESSAGE_PROGRESS:
202 	case GST_MESSAGE_TOC:
203 	case GST_MESSAGE_RESET_TIME:
204 	case GST_MESSAGE_STREAM_START:
205 	case GST_MESSAGE_ANY:
206 	case GST_MESSAGE_NEED_CONTEXT:
207 	case GST_MESSAGE_HAVE_CONTEXT:
208 	default:
209 		/* Ignored */
210 		;;
211 	}
212 
213 	return GST_BUS_PASS;
214 }
215 
216 static void
thumb_app_cleanup(ThumbApp * app)217 thumb_app_cleanup (ThumbApp *app)
218 {
219 	gst_element_set_state (app->play, GST_STATE_NULL);
220 	g_clear_object (&app->play);
221 }
222 
223 static void
thumb_app_set_error_handler(ThumbApp * app)224 thumb_app_set_error_handler (ThumbApp *app)
225 {
226 	GstBus *bus;
227 
228 	bus = gst_element_get_bus (app->play);
229 	gst_bus_set_sync_handler (bus, (GstBusSyncHandler) error_handler, app->play, NULL);
230 	g_object_unref (bus);
231 }
232 
233 static void
check_cover_for_stream(ThumbApp * app,const char * signal_name)234 check_cover_for_stream (ThumbApp   *app,
235 			const char *signal_name)
236 {
237 	GdkPixbuf *pixbuf;
238 	GstTagList *tags = NULL;
239 
240 	g_signal_emit_by_name (G_OBJECT (app->play), signal_name, 0, &tags);
241 
242 	if (!tags)
243 		return;
244 
245 	pixbuf = totem_gst_tag_list_get_cover (tags);
246 	if (!pixbuf) {
247 		gst_tag_list_unref (tags);
248 		return;
249 	}
250 
251 	PROGRESS_DEBUG("Saving cover image to %s", app->output);
252 	thumb_app_cleanup (app);
253 	save_pixbuf (pixbuf, app->output, app->input, output_size, TRUE);
254 	g_object_unref (pixbuf);
255 
256 	exit (0);
257 }
258 
259 static void
thumb_app_check_for_cover(ThumbApp * app)260 thumb_app_check_for_cover (ThumbApp *app)
261 {
262 	PROGRESS_DEBUG ("Checking whether file has cover");
263 	check_cover_for_stream (app, "get-audio-tags");
264 	check_cover_for_stream (app, "get-video-tags");
265 }
266 
267 static gboolean
thumb_app_set_duration(ThumbApp * app)268 thumb_app_set_duration (ThumbApp *app)
269 {
270 	gint64 len = -1;
271 
272 	if (gst_element_query_duration (app->play, GST_FORMAT_TIME, &len) && len != -1) {
273 		app->duration = len / GST_MSECOND;
274 		return TRUE;
275 	}
276 	app->duration = -1;
277 	return FALSE;
278 }
279 
280 static void
assert_duration(ThumbApp * app)281 assert_duration (ThumbApp *app)
282 {
283 	if (app->duration != -1)
284 		return;
285 	g_print ("totem-video-thumbnailer couldn't get the duration of file '%s'\n", app->input);
286 	exit (1);
287 }
288 
289 static gboolean
thumb_app_get_has_video(ThumbApp * app)290 thumb_app_get_has_video (ThumbApp *app)
291 {
292 	guint n_video;
293 	g_object_get (app->play, "n-video", &n_video, NULL);
294 	return n_video > 0;
295 }
296 
297 static gboolean
thumb_app_start(ThumbApp * app)298 thumb_app_start (ThumbApp *app)
299 {
300 	GstBus *bus;
301 	GstMessageType events;
302 	gboolean terminate = FALSE;
303 	gboolean async_received = FALSE;
304 
305 	gst_element_set_state (app->play, GST_STATE_PAUSED);
306 	bus = gst_element_get_bus (app->play);
307 	events = GST_MESSAGE_ASYNC_DONE | GST_MESSAGE_ERROR;
308 
309 	while (terminate == FALSE) {
310 		GstMessage *message;
311 		GstElement *src;
312 
313 		message = gst_bus_timed_pop_filtered (bus,
314 		                                      GST_CLOCK_TIME_NONE,
315 		                                      events);
316 
317 		src = (GstElement*)GST_MESSAGE_SRC (message);
318 
319 		switch (GST_MESSAGE_TYPE (message)) {
320 		case GST_MESSAGE_ASYNC_DONE:
321 			if (src == app->play) {
322 				async_received = TRUE;
323 				terminate = TRUE;
324 			}
325 			break;
326 		case GST_MESSAGE_ERROR:
327 			totem_gst_message_print (message, app->play, "totem-video-thumbnailer-error");
328 			terminate = TRUE;
329 			break;
330 
331 		case GST_MESSAGE_UNKNOWN:
332 		case GST_MESSAGE_EOS:
333 		case GST_MESSAGE_WARNING:
334 		case GST_MESSAGE_INFO:
335 		case GST_MESSAGE_TAG:
336 		case GST_MESSAGE_BUFFERING:
337 		case GST_MESSAGE_STATE_CHANGED:
338 		case GST_MESSAGE_STATE_DIRTY:
339 		case GST_MESSAGE_STEP_DONE:
340 		case GST_MESSAGE_CLOCK_PROVIDE:
341 		case GST_MESSAGE_CLOCK_LOST:
342 		case GST_MESSAGE_NEW_CLOCK:
343 		case GST_MESSAGE_STRUCTURE_CHANGE:
344 		case GST_MESSAGE_STREAM_STATUS:
345 		case GST_MESSAGE_APPLICATION:
346 		case GST_MESSAGE_ELEMENT:
347 		case GST_MESSAGE_SEGMENT_START:
348 		case GST_MESSAGE_SEGMENT_DONE:
349 		case GST_MESSAGE_DURATION_CHANGED:
350 		case GST_MESSAGE_LATENCY:
351 		case GST_MESSAGE_ASYNC_START:
352 		case GST_MESSAGE_REQUEST_STATE:
353 		case GST_MESSAGE_STEP_START:
354 		case GST_MESSAGE_QOS:
355 		case GST_MESSAGE_PROGRESS:
356 		case GST_MESSAGE_TOC:
357 		case GST_MESSAGE_RESET_TIME:
358 		case GST_MESSAGE_STREAM_START:
359 		case GST_MESSAGE_ANY:
360 		case GST_MESSAGE_NEED_CONTEXT:
361 		case GST_MESSAGE_HAVE_CONTEXT:
362 		default:
363 			/* Ignore */
364 			;;
365 		}
366 
367 		gst_message_unref (message);
368 	}
369 
370 	gst_object_unref (bus);
371 
372 	if (async_received) {
373 		/* state change succeeded */
374 		GST_DEBUG ("state change to %s succeeded", gst_element_state_get_name (GST_STATE_PAUSED));
375 	}
376 
377 	return async_received;
378 }
379 
380 static void
thumb_app_setup_play(ThumbApp * app)381 thumb_app_setup_play (ThumbApp *app)
382 {
383 	GstElement *play;
384 	GstElement *audio_sink, *video_sink;
385 
386 	play = gst_element_factory_make ("playbin", "play");
387 	audio_sink = gst_element_factory_make ("fakesink", "audio-fake-sink");
388 	video_sink = gst_element_factory_make ("fakesink", "video-fake-sink");
389 	g_object_set (video_sink, "sync", TRUE, NULL);
390 
391 	g_object_set (play,
392 		      "audio-sink", audio_sink,
393 		      "video-sink", video_sink,
394 		      "flags", GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_AUDIO,
395 		      NULL);
396 
397 	app->play = play;
398 
399 	totem_gst_disable_display_decoders ();
400 }
401 
402 static void
thumb_app_seek(ThumbApp * app,gint64 _time)403 thumb_app_seek (ThumbApp *app,
404 		gint64    _time)
405 {
406 	gst_element_seek (app->play, 1.0,
407 			  GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT,
408 			  GST_SEEK_TYPE_SET, _time * GST_MSECOND,
409 			  GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE);
410 	/* And wait for this seek to complete */
411 	gst_element_get_state (app->play, NULL, NULL, GST_CLOCK_TIME_NONE);
412 }
413 
414 /* This function attempts to detect images that are mostly solid images
415  * It does this by calculating the statistical variance of the
416  * black-and-white image */
417 static gboolean
is_image_interesting(GdkPixbuf * pixbuf)418 is_image_interesting (GdkPixbuf *pixbuf)
419 {
420 	/* We're gonna assume 8-bit samples. If anyone uses anything different,
421 	 * it doesn't really matter cause it's gonna be ugly anyways */
422 	int rowstride = gdk_pixbuf_get_rowstride(pixbuf);
423 	int height = gdk_pixbuf_get_height(pixbuf);
424 	guchar* buffer = gdk_pixbuf_get_pixels(pixbuf);
425 	int num_samples = (rowstride * height);
426 	int i;
427 	float x_bar = 0.0f;
428 	float variance = 0.0f;
429 
430 	/* FIXME: If this proves to be a performance issue, this function
431 	 * can be modified to perhaps only check 3 rows. I doubt this'll
432 	 * be a problem though. */
433 
434 	/* Iterate through the image to calculate x-bar */
435 	for (i = 0; i < num_samples; i++) {
436 		x_bar += (float) buffer[i];
437 	}
438 	x_bar /= ((float) num_samples);
439 
440 	/* Calculate the variance */
441 	for (i = 0; i < num_samples; i++) {
442 		float tmp = ((float) buffer[i] - x_bar);
443 		variance += tmp * tmp;
444 	}
445 	variance /= ((float) (num_samples - 1));
446 
447 	return (variance > BORING_IMAGE_VARIANCE);
448 }
449 
450 static GdkPixbuf *
scale_pixbuf(GdkPixbuf * pixbuf,int size,gboolean is_still)451 scale_pixbuf (GdkPixbuf *pixbuf, int size, gboolean is_still)
452 {
453 	GdkPixbuf *result;
454 	int width, height;
455 	int d_width, d_height;
456 
457 	if (size != -1) {
458 		height = gdk_pixbuf_get_height (pixbuf);
459 		width = gdk_pixbuf_get_width (pixbuf);
460 
461 		if (width > height) {
462 			d_width = size;
463 			d_height = size * height / width;
464 		} else {
465 			d_height = size;
466 			d_width = size * width / height;
467 		}
468 	} else {
469 		d_width = d_height = -1;
470 	}
471 
472 	if (size <= 256) {
473 		GdkPixbuf *small;
474 
475 		small = gdk_pixbuf_scale_simple (pixbuf, d_width, d_height, GDK_INTERP_BILINEAR);
476 		result = small;
477 	} else {
478 		if (size > 0)
479 			result = gdk_pixbuf_scale_simple (pixbuf, d_width, d_height, GDK_INTERP_BILINEAR);
480 		else
481 			result = g_object_ref (pixbuf);
482 	}
483 
484 	return result;
485 }
486 
487 static void
save_pixbuf(GdkPixbuf * pixbuf,const char * path,const char * video_path,int size,gboolean is_still)488 save_pixbuf (GdkPixbuf *pixbuf, const char *path,
489 	     const char *video_path, int size, gboolean is_still)
490 {
491 	int width, height;
492 	char *a_width, *a_height;
493 	GdkPixbuf *with_holes;
494 	GError *err = NULL;
495 	gboolean ret;
496 
497 	height = gdk_pixbuf_get_height (pixbuf);
498 	width = gdk_pixbuf_get_width (pixbuf);
499 
500 	/* If we're outputting a raw image without a size,
501 	 * don't scale the pixbuf or add borders */
502 	if (raw_output != FALSE && size == -1)
503 		with_holes = g_object_ref (pixbuf);
504 	else if (raw_output != FALSE)
505 		with_holes = scale_pixbuf (pixbuf, size, TRUE);
506 	else
507 		with_holes = scale_pixbuf (pixbuf, size, is_still);
508 
509 	a_width = g_strdup_printf ("%d", width);
510 	a_height = g_strdup_printf ("%d", height);
511 
512 	ret = gdk_pixbuf_save (with_holes, path, "png", &err,
513 			       "tEXt::Thumb::Image::Width", a_width,
514 			       "tEXt::Thumb::Image::Height", a_height,
515 			       NULL);
516 
517 	if (ret == FALSE) {
518 		if (err != NULL) {
519 			g_print ("totem-video-thumbnailer couldn't write the thumbnail '%s' for video '%s': %s\n", path, video_path, err->message);
520 			g_error_free (err);
521 		} else {
522 			g_print ("totem-video-thumbnailer couldn't write the thumbnail '%s' for video '%s'\n", path, video_path);
523 		}
524 
525 		g_object_unref (with_holes);
526 		return;
527 	}
528 
529 	g_object_unref (with_holes);
530 }
531 
532 static GdkPixbuf *
capture_frame_at_time(ThumbApp * app,gint64 milliseconds)533 capture_frame_at_time (ThumbApp   *app,
534 		       gint64 milliseconds)
535 {
536 	if (milliseconds != 0)
537 		thumb_app_seek (app, milliseconds);
538 
539 	return totem_gst_playbin_get_frame (app->play);
540 }
541 
542 static GdkPixbuf *
capture_interesting_frame(ThumbApp * app)543 capture_interesting_frame (ThumbApp *app)
544 {
545 	GdkPixbuf* pixbuf;
546 	guint current;
547 	const double frame_locations[] = {
548 		1.0 / 3.0,
549 		2.0 / 3.0,
550 		0.1,
551 		0.9,
552 		0.5
553 	};
554 
555 	if (app->duration == -1) {
556 		PROGRESS_DEBUG("Video has no duration, so capture 1st frame");
557 		return capture_frame_at_time (app, 0);
558 	}
559 
560 	/* Test at multiple points in the file to see if we can get an
561 	 * interesting frame */
562 	for (current = 0; current < G_N_ELEMENTS(frame_locations); current++)
563 	{
564 		PROGRESS_DEBUG("About to seek to %f", frame_locations[current]);
565 		thumb_app_seek (app, frame_locations[current] * app->duration);
566 
567 		/* Pull the frame, if it's interesting we bail early */
568 		PROGRESS_DEBUG("About to get frame for iter %d", current);
569 		pixbuf = totem_gst_playbin_get_frame (app->play);
570 		if (pixbuf != NULL && is_image_interesting (pixbuf) != FALSE) {
571 			PROGRESS_DEBUG("Frame for iter %d is interesting", current);
572 			break;
573 		}
574 
575 		/* If we get to the end of this loop, we'll end up using
576 		 * the last image we pulled */
577 		if (current + 1 < G_N_ELEMENTS(frame_locations))
578 			g_clear_object (&pixbuf);
579 		PROGRESS_DEBUG("Frame for iter %d was not interesting", current);
580 	}
581 	return pixbuf;
582 }
583 
584 static const GOptionEntry entries[] = {
585 	{ "size", 's', 0, G_OPTION_ARG_INT, &output_size, "Size of the thumbnail in pixels", NULL },
586 	{ "raw", 'r', 0, G_OPTION_ARG_NONE, &raw_output, "Output the raw picture of the video without scaling or adding borders", NULL },
587 	{ "no-limit", 'l', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &time_limit, "Don't limit the thumbnailing time to 30 seconds", NULL },
588 	{ "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, "Output debug information", NULL },
589 	{ "time", 't', 0, G_OPTION_ARG_INT64, &second_index, "Choose this time (in seconds) as the thumbnail", NULL },
590 	{ "print-progress", 'p', 0, G_OPTION_ARG_NONE, &print_progress, "Only print progress updates (can't be used with --verbose)", NULL },
591 	{ G_OPTION_REMAINING, '\0', 0, G_OPTION_ARG_FILENAME_ARRAY, &filenames, NULL, "[INPUT FILE] [OUTPUT FILE]" },
592 	{ NULL }
593 };
594 
main(int argc,char * argv[])595 int main (int argc, char *argv[])
596 {
597 	GOptionGroup *options;
598 	GOptionContext *context;
599 	GError *err = NULL;
600 	GdkPixbuf *pixbuf;
601 	const char *input, *output;
602 	ThumbApp app;
603 
604 	setlocale (LC_ALL, "");
605 	bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR);
606 	bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
607 	textdomain (GETTEXT_PACKAGE);
608 
609 	/* Call before the global thread pool is setup */
610 	errno = 0;
611 	if (nice (20) != 20 && errno != 0)
612 		g_warning ("Couldn't change nice value of process.");
613 
614 	context = g_option_context_new ("Thumbnail movies");
615 	options = gst_init_get_option_group ();
616 	g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE);
617 	g_option_context_add_group (context, options);
618 
619 	if (g_option_context_parse (context, &argc, &argv, &err) == FALSE) {
620 		g_print ("couldn't parse command-line options: %s\n", err->message);
621 		g_error_free (err);
622 		return 1;
623 	}
624 
625 	if (print_progress) {
626 		fcntl (fileno (stdout), F_SETFL, O_NONBLOCK);
627 		setbuf (stdout, NULL);
628 	}
629 
630 	if (raw_output == FALSE && output_size == -1)
631 		output_size = DEFAULT_OUTPUT_SIZE;
632 
633 	if (filenames == NULL || g_strv_length (filenames) != 2 ||
634 	    (print_progress == TRUE && verbose == TRUE)) {
635 		char *help;
636 		help = g_option_context_get_help (context, FALSE, NULL);
637 		g_print ("%s", help);
638 		g_free (help);
639 		return 1;
640 	}
641 	input = filenames[0];
642 	output = filenames[1];
643 
644 	PROGRESS_DEBUG("Initialised libraries, about to create video widget");
645 	PRINT_PROGRESS (2.0);
646 
647 	app.input = input;
648 	app.output = output;
649 
650 	thumb_app_setup_play (&app);
651 	thumb_app_set_filename (&app);
652 
653 	PROGRESS_DEBUG("Video widget created");
654 	PRINT_PROGRESS (6.0);
655 
656 	if (time_limit != FALSE)
657 		totem_resources_monitor_start (input, 0);
658 
659 	PROGRESS_DEBUG("About to open video file");
660 
661 	if (thumb_app_start (&app) == FALSE) {
662 		g_print ("totem-video-thumbnailer couldn't open file '%s'\n", input);
663 		exit (1);
664 	}
665 	thumb_app_set_error_handler (&app);
666 
667 	thumb_app_check_for_cover (&app);
668 	if (thumb_app_get_has_video (&app) == FALSE) {
669 		PROGRESS_DEBUG ("totem-video-thumbnailer couldn't find a video track in '%s'\n", input);
670 		exit (1);
671 	}
672 	thumb_app_set_duration (&app);
673 
674 	PROGRESS_DEBUG("Opened video file: '%s'", input);
675 	PRINT_PROGRESS (10.0);
676 
677 	/* If the user has told us to use a frame at a specific second
678 	 * into the video, just use that frame no matter how boring it
679 	 * is */
680 	if (second_index != -1) {
681 		assert_duration (&app);
682 		pixbuf = capture_frame_at_time (&app, second_index * 1000);
683 	} else {
684 		pixbuf = capture_interesting_frame (&app);
685 	}
686 	PRINT_PROGRESS (90.0);
687 
688 	/* Cleanup */
689 	totem_resources_monitor_stop ();
690 	thumb_app_cleanup (&app);
691 	PRINT_PROGRESS (92.0);
692 
693 	if (pixbuf == NULL) {
694 		g_print ("totem-video-thumbnailer couldn't get a picture from '%s'\n", input);
695 		exit (1);
696 	}
697 
698 	PROGRESS_DEBUG("Saving captured screenshot to %s", output);
699 	save_pixbuf (pixbuf, output, input, output_size, FALSE);
700 	g_object_unref (pixbuf);
701 	PRINT_PROGRESS (100.0);
702 
703 	return 0;
704 }
705 
706