/* Copyright 2016 Software Freedom Conservancy Inc. * * This is a Vala-rewrite of GStreamer snapshot example. Adapted from earlier * work from Wim Taymans. * * This software is licensed under the GNU LGPL (version 2.1 or later). * See the COPYING file in this distribution. */ // Shotwell Thumbnailer takes in a video file and returns a thumbnail to stdout. This is // a replacement for totem-video-thumbnailer class ShotwellThumbnailer { const string caps_string = """video/x-raw,format=RGB,pixel-aspect-ratio=1/1"""; public static int main(string[] args) { dynamic Gst.Element pipeline, sink; string descr; Gdk.Pixbuf pixbuf; uint8[]? pngdata; int64 duration, position; Gst.StateChangeReturn ret; OutputStream out; if (args.length != 3) { stdout.printf("usage: %s \n Writes video thumbnail to output\n", args[0]); return 1; } var out_file = File.new_for_commandline_arg (args[2]); try { out = out_file.append_to(FileCreateFlags.NONE); debug("Writing thumbnail to %s", args[2]); } catch (Error err) { warning("Failed to append to image file %s", err.message); return 1; } if (Posix.nice (19) < 0) { debug ("Failed to reduce thumbnailer nice level. Continuing anyway"); } Gst.init(ref args); var registry = Gst.Registry.@get (); var features = registry.feature_filter ((f) => { return f.get_name ().has_prefix ("vaapi"); }, false); foreach (var feature in features) { debug ("Removing registry feature %s", feature.get_name ()); registry.remove_feature (feature); } descr = "playbin uri=\"%s\" audio-sink=fakesink video-sink=\"gdkpixbufsink name=sink\"".printf(File.new_for_commandline_arg(args[1]).get_uri()); try { // Create new pipeline. pipeline = Gst.parse_launch(descr); // Get sink. sink = pipeline.video_sink; // Set to PAUSED to make the first frame arrive in the sink. ret = pipeline.set_state(Gst.State.PAUSED); if (ret == Gst.StateChangeReturn.FAILURE) { warning("Failed to play the file: couldn't set state\n"); return 3; } else if (ret == Gst.StateChangeReturn.NO_PREROLL) { warning("Live sources not supported yet.\n"); return 4; } // This can block for up to 5 seconds. If your machine is really overloaded, // it might time out before the pipeline prerolled and we generate an error. A // better way is to run a mainloop and catch errors there. ret = pipeline.get_state(null, null, 5 * Gst.SECOND); if (ret == Gst.StateChangeReturn.FAILURE) { warning("Failed to play the file: couldn't get state.\n"); return 3; } /* get the duration */ if (!pipeline.query_duration (Gst.Format.TIME, out duration)) { warning("Failed to query file for duration\n"); return 3; } position = 1 * Gst.SECOND; /* seek to the a position in the file. Most files have a black first frame so * by seeking to somewhere else we have a bigger chance of getting something * more interesting. An optimisation would be to detect black images and then * seek a little more */ pipeline.seek_simple (Gst.Format.TIME, Gst.SeekFlags.KEY_UNIT | Gst.SeekFlags.FLUSH, position); ret = pipeline.get_state(null, null, 5 * Gst.SECOND); if (ret == Gst.StateChangeReturn.FAILURE) { warning("Failed to play the file: couldn't get state.\n"); return 3; } sink.get ("last-pixbuf", out pixbuf); Gst.TagList tags; Signal.emit_by_name(pipeline, "get-video-tags", 0, out tags); var direction = Gdk.PixbufRotation.NONE; if (tags != null) { string orientation = null; if (tags.get_string_index (Gst.Tags.IMAGE_ORIENTATION, 0, out orientation)) { if (orientation != null) { switch (orientation) { case "rotate-90": direction = Gdk.PixbufRotation.CLOCKWISE; break; case "rotate-180": direction = Gdk.PixbufRotation.UPSIDEDOWN; break; case "rotate-270": direction = Gdk.PixbufRotation.COUNTERCLOCKWISE; break; default: break; } } } } // Save the pixbuf. if (direction != Gdk.PixbufRotation.NONE) { pixbuf = pixbuf.rotate_simple(direction); } pixbuf.save_to_buffer(out pngdata, "png"); out.write(pngdata); // cleanup and exit. pipeline.set_state(Gst.State.NULL); } catch (Error e) { warning(e.message); return 2; } return 0; } }