summaryrefslogtreecommitdiff
path: root/thumbnailer/shotwell-video-thumbnailer.vala
blob: 93db2aa432d306c1701b7f6bf57841c11837df4d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/* Copyright 2011-2014 Yorba Foundation
 * 
 * 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) {
        Gst.Element pipeline, sink;
        int width, height;
        Gst.Sample sample;
        string descr;
        Gdk.Pixbuf pixbuf;
        int64 duration, position;
        Gst.StateChangeReturn ret;
        bool res;
        
        Gst.init(ref args);
        
        if (args.length != 2) {
            stdout.printf("usage: %s [filename]\n Writes video thumbnail to stdout\n", args[0]);
            return 1;
        }
        
        descr = "filesrc location=\"%s\" ! decodebin ! videoconvert ! videoscale ! ".printf(args[1]) +
            "appsink name=sink caps=\"%s\"".printf(caps_string);
        
        try {
            // Create new pipeline.
            pipeline = Gst.parse_launch(descr);
            
            // Get sink.
            sink = ((Gst.Bin) pipeline).get_by_name("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) {
                stderr.printf("Failed to play the file: couldn't set state\n");
                return 3;
            } else if (ret == Gst.StateChangeReturn.NO_PREROLL) {
                stderr.printf("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) {
                stderr.printf("Failed to play the file: couldn't get state.\n");
                return 3;
            }

            /* get the duration */
            pipeline.query_duration (Gst.Format.TIME, out duration);

            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);

            /* get the preroll buffer from appsink, this block untils appsink really
             * prerolls */
            GLib.Signal.emit_by_name (sink, "pull-preroll", out sample, null);

            // if we have a buffer now, convert it to a pixbuf. It's possible that we
            // don't have a buffer because we went EOS right away or had an error.
            if (sample != null) {
                Gst.Buffer buffer;
                Gst.Caps caps;
                unowned Gst.Structure s;
                Gst.MapInfo mapinfo;
                uint8[]? pngdata;

                // Get the snapshot buffer format now. We set the caps on the appsink so
                // that it can only be an rgb buffer. The only thing we have not specified
                // on the caps is the height, which is dependent on the pixel-aspect-ratio
                // of the source material.
                caps = sample.get_caps();
                if (caps == null) {
                    stderr.printf("could not get snapshot format\n");
                    return 5;
                }
                
                s = caps.get_structure(0);
                
                // We need to get the final caps on the buffer to get the size.
                res = s.get_int("width", out width);
                res |= s.get_int("height", out height);
                if (!res) {
                    stderr.printf("Could not get snapshot dimension\n");
                    return 6;
                }

                buffer = sample.get_buffer();
                buffer.map(out mapinfo, Gst.MapFlags.READ);
                
                if (mapinfo.data == null || mapinfo.data.length == 0) {
                    stderr.printf("Could not get snapshot data buffer\n");
                    return 7;
                }
                
                // Create pixmap from buffer and save, gstreamer video buffers have a stride
                // that is rounded up to the nearest multiple of 4.
                pixbuf = new Gdk.Pixbuf.from_data(mapinfo.data, Gdk.Colorspace.RGB, false, 8,
                    width, height, (((width * 3)+3)&~3), null);
                
                // Save the pixbuf.
                pixbuf.save_to_buffer(out pngdata, "png");
                stdout.write(pngdata);
                buffer.unmap(mapinfo);
            } else {
                stderr.printf("Could not make snapshot\n");
                return 10;
            }
            
            // cleanup and exit.
            pipeline.set_state(Gst.State.NULL);
            
        } catch (Error e) {
            stderr.printf(e.message);
            return 2;
        }
        
        return 0;
    }
}