Merge lp:~jamesh/thumbnailer/vs-thumb-async into lp:thumbnailer/devel
- vs-thumb-async
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Michi Henning |
Approved revision: | 261 |
Merged at revision: | 257 |
Proposed branch: | lp:~jamesh/thumbnailer/vs-thumb-async |
Merge into: | lp:thumbnailer/devel |
Diff against target: |
822 lines (+282/-244) 5 files modified
src/vs-thumb/thumbnailextractor.cpp (+194/-139) src/vs-thumb/thumbnailextractor.h (+29/-7) src/vs-thumb/vs-thumb.cpp (+27/-26) tests/thumbnailer/thumbnailer_test.cpp (+1/-1) tests/vs-thumb/vs-thumb_test.cpp (+31/-71) |
To merge this branch: | bzr merge lp:~jamesh/thumbnailer/vs-thumb-async |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michi Henning (community) | Approve | ||
PS Jenkins bot (community) | continuous-integration | Approve | |
Review via email: mp+268184@code.launchpad.net |
Commit message
Convert the ThumbnailExtractor class to run the extraction within the event loop.
Description of the change
Convert the ThumbnailExtractor class to run the extraction within the event loop. This will allow us to convert vs-thumb to a D-Bus service that can manage multiple extractions simultaneously.
PS Jenkins bot (ps-jenkins) wrote : | # |
Michi Henning (michihenning) wrote : | # |
The thumbnailer test gets one failure here (deterministica
[ RUN ] ThumbnailerTest
Changing to state PAUSED
State changed to 2
/home/michi/
Value of: spy.wait(5000)
Actual: false
Expected: true
[ FAILED ] ThumbnailerTest
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:258
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 259. By James Henstridge
-
Bail out on errors from any element rather than just the playbin.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:259
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 260. By James Henstridge
-
Fix test for error message on empty files.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:260
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Michi Henning (michihenning) wrote : | # |
I'm getting this when building with vivid:
[ 18%] Building CXX object src/vs-
/home/michi/
/home/michi/
src/vs-
Some change in GStreamer version?
- 261. By James Henstridge
-
gst_bus_
remove_ watch() doesn't exist in GStreamer 1.4, so use
g_source_remove instead.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:261
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Michi Henning (michihenning) wrote : | # |
Looks good to me, thanks! Eventually, we'll have to get the test coverage up. But, seeing that we'll probably make a few more changes to vs-thumb, it's probably premature to do this now.
Preview Diff
1 | === modified file 'src/vs-thumb/thumbnailextractor.cpp' |
2 | --- src/vs-thumb/thumbnailextractor.cpp 2015-06-22 23:50:46 +0000 |
3 | +++ src/vs-thumb/thumbnailextractor.cpp 2015-08-19 03:01:01 +0000 |
4 | @@ -28,8 +28,6 @@ |
5 | #pragma GCC diagnostic ignored "-Wcast-qual" |
6 | #pragma GCC diagnostic ignored "-Wcast-align" |
7 | |
8 | -#include <gdk-pixbuf/gdk-pixbuf.h> |
9 | -#include <gst/gst.h> |
10 | #include <gst/tag/tag.h> |
11 | |
12 | #include <internal/gobj_memory.h> |
13 | @@ -60,59 +58,6 @@ |
14 | throw std::runtime_error(message); |
15 | } |
16 | |
17 | -void change_state(GstElement* element, GstState state) |
18 | -{ |
19 | - GstStateChangeReturn ret = gst_element_set_state(element, state); |
20 | - switch (ret) |
21 | - { |
22 | - case GST_STATE_CHANGE_NO_PREROLL: |
23 | - case GST_STATE_CHANGE_SUCCESS: |
24 | - return; |
25 | - case GST_STATE_CHANGE_ASYNC: |
26 | - // The change is happening in a background thread, which we |
27 | - // will wait on below. |
28 | - break; |
29 | - case GST_STATE_CHANGE_FAILURE: |
30 | - default: |
31 | - throw_error("change_state(): Could not change element state"); |
32 | - } |
33 | - |
34 | - // We're in the async case here, so pop messages off the bus until |
35 | - // it is done. |
36 | - gobj_ptr<GstBus> bus(gst_element_get_bus(element)); |
37 | - while (true) |
38 | - { |
39 | - std::unique_ptr<GstMessage, decltype(&gst_message_unref)> message( |
40 | - gst_bus_timed_pop_filtered(bus.get(), GST_CLOCK_TIME_NONE, |
41 | - static_cast<GstMessageType>(GST_MESSAGE_ASYNC_DONE | GST_MESSAGE_ERROR)), |
42 | - gst_message_unref); |
43 | - if (!message) |
44 | - { |
45 | - break; |
46 | - } |
47 | - |
48 | - switch (GST_MESSAGE_TYPE(message.get())) |
49 | - { |
50 | - case GST_MESSAGE_ASYNC_DONE: |
51 | - if (GST_MESSAGE_SRC(message.get()) == GST_OBJECT(element)) |
52 | - { |
53 | - return; |
54 | - } |
55 | - break; |
56 | - case GST_MESSAGE_ERROR: |
57 | - { |
58 | - GError* error = nullptr; |
59 | - gst_message_parse_error(message.get(), &error, nullptr); |
60 | - throw_error("change_state(): reading async messages", error); |
61 | - break; |
62 | - } |
63 | - default: |
64 | - /* ignore other message types */ |
65 | - ; |
66 | - } |
67 | - } |
68 | -} |
69 | - |
70 | class BufferMap final |
71 | { |
72 | public: |
73 | @@ -161,16 +106,6 @@ |
74 | |
75 | } // namespace |
76 | |
77 | -struct ThumbnailExtractor::Private |
78 | -{ |
79 | - gobj_ptr<GstElement> playbin; |
80 | - gint64 duration = -1; |
81 | - |
82 | - std::unique_ptr<GstSample, decltype(&gst_sample_unref)> sample{nullptr, gst_sample_unref}; |
83 | - GdkPixbufRotation sample_rotation = GDK_PIXBUF_ROTATE_NONE; |
84 | - bool sample_raw = true; |
85 | -}; |
86 | - |
87 | /* GstPlayFlags flags from playbin. |
88 | * |
89 | * GStreamer does not install headers for the enums of individual |
90 | @@ -192,14 +127,13 @@ |
91 | } GstPlayFlags; |
92 | |
93 | ThumbnailExtractor::ThumbnailExtractor() |
94 | - : p(new Private) |
95 | { |
96 | GstElement* pb = gst_element_factory_make("playbin", "playbin"); |
97 | if (!pb) |
98 | { |
99 | throw_error("ThumbnailExtractor(): Could not create playbin"); |
100 | } |
101 | - p->playbin.reset(static_cast<GstElement*>(g_object_ref_sink(pb))); |
102 | + playbin_.reset(static_cast<GstElement*>(g_object_ref_sink(pb))); |
103 | |
104 | GstElement* audio_sink = gst_element_factory_make("fakesink", "audio-fake-sink"); |
105 | if (!audio_sink) |
106 | @@ -214,57 +148,170 @@ |
107 | } |
108 | |
109 | g_object_set(video_sink, "sync", TRUE, nullptr); |
110 | - g_object_set(p->playbin.get(), "audio-sink", audio_sink, "video-sink", video_sink, "flags", |
111 | - GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_AUDIO, nullptr); |
112 | + g_object_set(playbin_.get(), |
113 | + "audio-sink", audio_sink, |
114 | + "video-sink", video_sink, |
115 | + "flags", GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_AUDIO, |
116 | + nullptr); |
117 | + |
118 | + bus_.reset(gst_element_get_bus(pb)); |
119 | + bus_watch_id_ = gst_bus_add_watch( |
120 | + bus_.get(), &ThumbnailExtractor::on_new_message, this); |
121 | } |
122 | |
123 | ThumbnailExtractor::~ThumbnailExtractor() |
124 | { |
125 | + if (bus_watch_id_ != 0) |
126 | + { |
127 | + g_source_remove(bus_watch_id_); |
128 | + } |
129 | + if (playbin_) |
130 | + { |
131 | + reset(); |
132 | + } |
133 | +} |
134 | + |
135 | +void ThumbnailExtractor::extract(std::string const& uri, std::function<void(GdkPixbuf* const)> callback) |
136 | +{ |
137 | + callback_ = callback; |
138 | + set_uri(uri); |
139 | +} |
140 | + |
141 | +void ThumbnailExtractor::finished(GdkPixbuf* const thumbnail) |
142 | +{ |
143 | + if (callback_) |
144 | + { |
145 | + callback_(thumbnail); |
146 | + callback_ = nullptr; |
147 | + } |
148 | reset(); |
149 | } |
150 | |
151 | void ThumbnailExtractor::reset() |
152 | { |
153 | - change_state(p->playbin.get(), GST_STATE_NULL); |
154 | - p->sample.reset(); |
155 | - p->sample_rotation = GDK_PIXBUF_ROTATE_NONE; |
156 | - p->sample_raw = true; |
157 | + gst_element_set_state(playbin_.get(), GST_STATE_NULL); |
158 | + is_seeking_ = false; |
159 | +} |
160 | + |
161 | +gboolean ThumbnailExtractor::on_new_message(GstBus */*bus*/, GstMessage *message, void *user_data) |
162 | +{ |
163 | + auto extractor = reinterpret_cast<ThumbnailExtractor*>(user_data); |
164 | + |
165 | + switch (GST_MESSAGE_TYPE(message)) |
166 | + { |
167 | + case GST_MESSAGE_STATE_CHANGED: |
168 | + if (GST_MESSAGE_SRC(message) != GST_OBJECT(extractor->playbin_.get())) |
169 | + { |
170 | + break; |
171 | + } |
172 | + GstState newstate; |
173 | + gst_message_parse_state_changed(message, nullptr, &newstate, nullptr); |
174 | + extractor->state_changed(newstate); |
175 | + break; |
176 | + case GST_MESSAGE_ASYNC_DONE: |
177 | + if (GST_MESSAGE_SRC(message) != GST_OBJECT(extractor->playbin_.get())) |
178 | + { |
179 | + break; |
180 | + } |
181 | + fprintf(stderr, "Got Async done message, is_seeking == %d\n", extractor->is_seeking_); |
182 | + if (extractor->is_seeking_) |
183 | + { |
184 | + // Check to make sure the state change after the seek has |
185 | + // truely completed. |
186 | + if (gst_element_get_state( |
187 | + extractor->playbin_.get(), |
188 | + nullptr, nullptr, 0) != GST_STATE_CHANGE_ASYNC) |
189 | + { |
190 | + extractor->is_seeking_ = false; |
191 | + extractor->seek_done(); |
192 | + } |
193 | + } |
194 | + break; |
195 | + case GST_MESSAGE_ERROR: |
196 | + { |
197 | + GError* error = nullptr; |
198 | + gst_message_parse_error(message, &error, nullptr); |
199 | + extractor->bus_error(error); |
200 | + g_error_free(error); |
201 | + break; |
202 | + } |
203 | + default: |
204 | + /* ignore other message types */ |
205 | + ; |
206 | + } |
207 | + |
208 | + return G_SOURCE_CONTINUE; |
209 | } |
210 | |
211 | void ThumbnailExtractor::set_uri(const std::string& uri) |
212 | { |
213 | reset(); |
214 | - g_object_set(p->playbin.get(), "uri", uri.c_str(), nullptr); |
215 | + g_object_set(playbin_.get(), "uri", uri.c_str(), nullptr); |
216 | + |
217 | fprintf(stderr, "Changing to state PAUSED\n"); |
218 | - change_state(p->playbin.get(), GST_STATE_PAUSED); |
219 | - |
220 | - if (!gst_element_query_duration(p->playbin.get(), GST_FORMAT_TIME, &p->duration)) |
221 | + auto ret = gst_element_set_state(playbin_.get(), GST_STATE_PAUSED); |
222 | + if (ret == GST_STATE_CHANGE_FAILURE) |
223 | { |
224 | - p->duration = -1; |
225 | - } |
226 | + throw_error("set_uri(): Could not change pipeline state to paused."); |
227 | + } |
228 | +} |
229 | + |
230 | +void ThumbnailExtractor::state_changed(GstState state) |
231 | +{ |
232 | + fprintf(stderr, "State changed to %d\n", (int)state); |
233 | + switch (state) { |
234 | + case GST_STATE_PAUSED: |
235 | + if (has_video()) |
236 | + { |
237 | + if (!is_seeking_) |
238 | + { |
239 | + // Seek somewhere that will hopefully give a relevant image. |
240 | + int64_t duration, seek_point; |
241 | + if (!gst_element_query_duration(playbin_.get(), GST_FORMAT_TIME, &duration)) |
242 | + { |
243 | + seek_point = 2 * duration / 7; |
244 | + } |
245 | + else |
246 | + { |
247 | + seek_point = 10 * GST_SECOND; |
248 | + } |
249 | + fprintf(stderr, "Seeking to position %d\n", (int)(seek_point / GST_SECOND)); |
250 | + is_seeking_ = true; |
251 | + gst_element_seek_simple(playbin_.get(), GST_FORMAT_TIME, |
252 | + static_cast<GstSeekFlags>(GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT), seek_point); |
253 | + } |
254 | + } |
255 | + else |
256 | + { |
257 | + extract_audio_cover_art(); |
258 | + } |
259 | + break; |
260 | + default: |
261 | + break; |
262 | + } |
263 | +} |
264 | + |
265 | +void ThumbnailExtractor::seek_done() |
266 | +{ |
267 | + extract_video_frame(); |
268 | +} |
269 | + |
270 | +void ThumbnailExtractor::bus_error(GError *error) |
271 | +{ |
272 | + fprintf(stderr, "Error: %s\n", error->message); |
273 | + finished(nullptr); |
274 | } |
275 | |
276 | bool ThumbnailExtractor::has_video() |
277 | { |
278 | int n_video = 0; |
279 | - g_object_get(p->playbin.get(), "n-video", &n_video, nullptr); |
280 | + g_object_get(playbin_.get(), "n-video", &n_video, nullptr); |
281 | + fprintf(stderr, "Number of video streams == %d\n", n_video); |
282 | return n_video > 0; |
283 | } |
284 | |
285 | -bool ThumbnailExtractor::extract_video_frame() |
286 | +void ThumbnailExtractor::extract_video_frame() |
287 | { |
288 | - gint64 seek_point = 10 * GST_SECOND; |
289 | - if (p->duration >= 0) |
290 | - { |
291 | - seek_point = 2 * p->duration / 7; |
292 | - } |
293 | - fprintf(stderr, "Seeking to position %d\n", (int)(seek_point / GST_SECOND)); |
294 | - gst_element_seek_simple(p->playbin.get(), GST_FORMAT_TIME, |
295 | - static_cast<GstSeekFlags>(GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT), seek_point); |
296 | - fprintf(stderr, "Waiting for seek to complete\n"); |
297 | - gst_element_get_state(p->playbin.get(), nullptr, nullptr, GST_CLOCK_TIME_NONE); |
298 | - fprintf(stderr, "Done.\n"); |
299 | - |
300 | // Retrieve sample from the playbin |
301 | fprintf(stderr, "Requesting sample frame.\n"); |
302 | std::unique_ptr<GstCaps, decltype(&gst_caps_unref)> desired_caps( |
303 | @@ -272,18 +319,20 @@ |
304 | 1, nullptr), |
305 | gst_caps_unref); |
306 | GstSample* s; |
307 | - g_signal_emit_by_name(p->playbin.get(), "convert-sample", desired_caps.get(), &s); |
308 | + g_signal_emit_by_name(playbin_.get(), "convert-sample", desired_caps.get(), &s); |
309 | if (!s) |
310 | { |
311 | - return false; |
312 | + fprintf(stderr, "Could not extract sample\n"); |
313 | + finished(nullptr); |
314 | + return; |
315 | } |
316 | - p->sample.reset(s); |
317 | - p->sample_raw = true; |
318 | + std::unique_ptr<GstSample, decltype(&gst_sample_unref)> sample( |
319 | + s, gst_sample_unref); |
320 | |
321 | // Does the sample need to be rotated? |
322 | - p->sample_rotation = GDK_PIXBUF_ROTATE_NONE; |
323 | + GdkPixbufRotation rotation = GDK_PIXBUF_ROTATE_NONE; |
324 | GstTagList* tags = nullptr; |
325 | - g_signal_emit_by_name(p->playbin.get(), "get-video-tags", 0, &tags); |
326 | + g_signal_emit_by_name(playbin_.get(), "get-video-tags", 0, &tags); |
327 | if (tags) |
328 | { |
329 | char* orientation = nullptr; |
330 | @@ -291,80 +340,90 @@ |
331 | { |
332 | if (!strcmp(orientation, "rotate-90")) |
333 | { |
334 | - p->sample_rotation = GDK_PIXBUF_ROTATE_CLOCKWISE; |
335 | + rotation = GDK_PIXBUF_ROTATE_CLOCKWISE; |
336 | } |
337 | else if (!strcmp(orientation, "rotate-180")) |
338 | { |
339 | - p->sample_rotation = GDK_PIXBUF_ROTATE_UPSIDEDOWN; |
340 | + rotation = GDK_PIXBUF_ROTATE_UPSIDEDOWN; |
341 | } |
342 | else if (!strcmp(orientation, "rotate-270")) |
343 | { |
344 | - p->sample_rotation = GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE; |
345 | + rotation = GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE; |
346 | } |
347 | } |
348 | gst_tag_list_unref(tags); |
349 | } |
350 | - return true; |
351 | + save_screenshot(sample.get(), rotation, true); |
352 | } |
353 | |
354 | -bool ThumbnailExtractor::extract_audio_cover_art() |
355 | +void ThumbnailExtractor::extract_audio_cover_art() |
356 | { |
357 | + fprintf(stderr, "Extracting cover art\n"); |
358 | GstTagList* tags = nullptr; |
359 | - g_signal_emit_by_name(p->playbin.get(), "get-audio-tags", 0, &tags); |
360 | + g_signal_emit_by_name(playbin_.get(), "get-audio-tags", 0, &tags); |
361 | if (!tags) |
362 | { |
363 | - return false; |
364 | + finished(nullptr); |
365 | + return; |
366 | } |
367 | - p->sample.reset(); |
368 | - p->sample_rotation = GDK_PIXBUF_ROTATE_NONE; |
369 | - p->sample_raw = false; |
370 | + std::unique_ptr<GstSample, decltype(&gst_sample_unref)> sample( |
371 | + nullptr, gst_sample_unref); |
372 | bool found_cover = false; |
373 | for (int i = 0; !found_cover; i++) |
374 | { |
375 | - GstSample* sample; |
376 | - if (!gst_tag_list_get_sample_index(tags, GST_TAG_IMAGE, i, &sample)) |
377 | + GstSample* s; |
378 | + if (!gst_tag_list_get_sample_index(tags, GST_TAG_IMAGE, i, &s)) |
379 | { |
380 | break; |
381 | } |
382 | // Check the type of this image |
383 | - auto caps = gst_sample_get_caps(sample); |
384 | + auto caps = gst_sample_get_caps(s); |
385 | auto structure = gst_caps_get_structure(caps, 0); |
386 | int type = GST_TAG_IMAGE_TYPE_UNDEFINED; |
387 | gst_structure_get_enum(structure, "image-type", GST_TYPE_TAG_IMAGE_TYPE, &type); |
388 | if (type == GST_TAG_IMAGE_TYPE_FRONT_COVER) |
389 | { |
390 | - p->sample.reset(gst_sample_ref(sample)); |
391 | + sample.reset(gst_sample_ref(s)); |
392 | found_cover = true; |
393 | } |
394 | - else if (type == GST_TAG_IMAGE_TYPE_UNDEFINED && !p->sample) |
395 | + else if (type == GST_TAG_IMAGE_TYPE_UNDEFINED && !sample) |
396 | { |
397 | // Save the first unknown image tag, but continue scanning |
398 | // in case there is one marked as the cover. |
399 | - p->sample.reset(gst_sample_ref(sample)); |
400 | + sample.reset(gst_sample_ref(s)); |
401 | } |
402 | - gst_sample_unref(sample); |
403 | + gst_sample_unref(s); |
404 | } |
405 | gst_tag_list_unref(tags); |
406 | - return bool(p->sample); |
407 | + if (!sample) |
408 | + { |
409 | + finished(nullptr); |
410 | + return; |
411 | + } |
412 | + save_screenshot(sample.get(), GDK_PIXBUF_ROTATE_NONE, false); |
413 | } |
414 | |
415 | -void ThumbnailExtractor::save_screenshot(const std::string& filename) |
416 | +void ThumbnailExtractor::save_screenshot(GstSample* sample, GdkPixbufRotation rotation, bool raw) |
417 | { |
418 | - if (!p->sample) |
419 | + if (!sample) |
420 | { |
421 | - throw_error("save_screenshot(): Could not retrieve screenshot"); |
422 | + fprintf(stderr, "save_screenshot(): Could not retrieve screenshot\n"); |
423 | + finished(nullptr); |
424 | + return; |
425 | } |
426 | |
427 | // Construct a pixbuf from the sample |
428 | fprintf(stderr, "Creating pixbuf from sample\n"); |
429 | BufferMap buffermap; |
430 | gobj_ptr<GdkPixbuf> image; |
431 | - if (p->sample_raw) |
432 | + if (raw) |
433 | { |
434 | - GstCaps* sample_caps = gst_sample_get_caps(p->sample.get()); |
435 | + GstCaps* sample_caps = gst_sample_get_caps(sample); |
436 | if (!sample_caps) |
437 | { |
438 | - throw_error("save_screenshot(): Could not retrieve caps for sample buffer"); |
439 | + fprintf(stderr, "save_screenshot(): Could not retrieve caps for sample buffer\n"); |
440 | + finished(nullptr); |
441 | + return; |
442 | } |
443 | GstStructure* sample_struct = gst_caps_get_structure(sample_caps, 0); |
444 | int width = 0, height = 0; |
445 | @@ -372,10 +431,12 @@ |
446 | gst_structure_get_int(sample_struct, "height", &height); |
447 | if (width <= 0 || height <= 0) |
448 | { |
449 | - throw_error("save_screenshot(): Could not retrieve image dimensions"); |
450 | + fprintf(stderr, "save_screenshot(): Could not retrieve image dimensions\n"); |
451 | + finished(nullptr); |
452 | + return; |
453 | } |
454 | |
455 | - buffermap.map(gst_sample_get_buffer(p->sample.get())); |
456 | + buffermap.map(gst_sample_get_buffer(sample)); |
457 | image.reset(gdk_pixbuf_new_from_data(buffermap.data(), GDK_COLORSPACE_RGB, FALSE, 8, width, height, |
458 | GST_ROUND_UP_4(width * 3), nullptr, nullptr)); |
459 | } |
460 | @@ -383,7 +444,7 @@ |
461 | { |
462 | gobj_ptr<GdkPixbufLoader> loader(gdk_pixbuf_loader_new()); |
463 | |
464 | - buffermap.map(gst_sample_get_buffer(p->sample.get())); |
465 | + buffermap.map(gst_sample_get_buffer(sample)); |
466 | GError* error = nullptr; |
467 | if (gdk_pixbuf_loader_write(loader.get(), buffermap.data(), buffermap.size(), &error) && |
468 | gdk_pixbuf_loader_close(loader.get(), &error)) |
469 | @@ -396,29 +457,23 @@ |
470 | } |
471 | else |
472 | { |
473 | - throw_error("save_screenshot(): decoding image", error); |
474 | + fprintf(stderr, "save_screenshot(): decoding image: %s\n", error->message); |
475 | + g_error_free(error); |
476 | + finished(nullptr); |
477 | + return; |
478 | } |
479 | } |
480 | |
481 | - if (p->sample_rotation != GDK_PIXBUF_ROTATE_NONE) |
482 | + if (rotation != GDK_PIXBUF_ROTATE_NONE) |
483 | { |
484 | - GdkPixbuf* rotated = gdk_pixbuf_rotate_simple(image.get(), p->sample_rotation); |
485 | + GdkPixbuf* rotated = gdk_pixbuf_rotate_simple(image.get(), rotation); |
486 | if (rotated) |
487 | { |
488 | image.reset(rotated); |
489 | } |
490 | } |
491 | |
492 | - // Saving as TIFF with no compression here to avoid artefacts due to converting to jpg twice. |
493 | - // (The main thumbnailer saves as jpg.) By staying lossless here, we |
494 | - // keep all the policy decisions about image quality in the main thumbnailer. |
495 | - fprintf(stderr, "Saving pixbuf to tiff\n"); |
496 | - GError* error = nullptr; |
497 | - if (!gdk_pixbuf_save(image.get(), filename.c_str(), "tiff", &error, "compression", "1", nullptr)) |
498 | - { |
499 | - throw_error("save_screenshot(): saving image", error); |
500 | - } |
501 | - fprintf(stderr, "Done.\n"); |
502 | + finished(image.get()); |
503 | } |
504 | |
505 | } // namespace internal |
506 | |
507 | === modified file 'src/vs-thumb/thumbnailextractor.h' |
508 | --- src/vs-thumb/thumbnailextractor.h 2015-05-26 23:55:06 +0000 |
509 | +++ src/vs-thumb/thumbnailextractor.h 2015-08-19 03:01:01 +0000 |
510 | @@ -18,6 +18,17 @@ |
511 | |
512 | #pragma once |
513 | |
514 | +#include <internal/gobj_memory.h> |
515 | + |
516 | +#pragma GCC diagnostic push |
517 | +#pragma GCC diagnostic ignored "-Wold-style-cast" |
518 | +#pragma GCC diagnostic ignored "-Wcast-qual" |
519 | +#pragma GCC diagnostic ignored "-Wcast-align" |
520 | +#include <gdk-pixbuf/gdk-pixbuf.h> |
521 | +#include <gst/gst.h> |
522 | +#pragma GCC diagnostic pop |
523 | + |
524 | +#include <functional> |
525 | #include <memory> |
526 | #include <string> |
527 | |
528 | @@ -32,21 +43,32 @@ |
529 | |
530 | class ThumbnailExtractor final |
531 | { |
532 | - struct Private; |
533 | - |
534 | public: |
535 | ThumbnailExtractor(); |
536 | ~ThumbnailExtractor(); |
537 | |
538 | + void extract(std::string const& uri, std::function<void(GdkPixbuf* const)> callback); |
539 | void reset(); |
540 | + |
541 | +private: |
542 | + gobj_ptr<GstElement> playbin_; |
543 | + gobj_ptr<GstBus> bus_; |
544 | + unsigned int bus_watch_id_ = 0; |
545 | + bool is_seeking_ = false; |
546 | + |
547 | + std::function<void(GdkPixbuf* const)> callback_; |
548 | + |
549 | + void finished(GdkPixbuf* const thumbnail); |
550 | void set_uri(const std::string& uri); |
551 | bool has_video(); |
552 | - bool extract_video_frame(); |
553 | - bool extract_audio_cover_art(); |
554 | - void save_screenshot(const std::string& filename); |
555 | + void extract_video_frame(); |
556 | + void extract_audio_cover_art(); |
557 | + void save_screenshot(GstSample* sample, GdkPixbufRotation rotation, bool raw); |
558 | |
559 | -private: |
560 | - std::unique_ptr<Private> p; |
561 | + static gboolean on_new_message(GstBus *bus, GstMessage *message, void *user_data); |
562 | + void state_changed(GstState state); |
563 | + void bus_error(GError *error); |
564 | + void seek_done(); |
565 | }; |
566 | |
567 | } // namespace internal |
568 | |
569 | === modified file 'src/vs-thumb/vs-thumb.cpp' |
570 | --- src/vs-thumb/vs-thumb.cpp 2015-05-28 22:16:02 +0000 |
571 | +++ src/vs-thumb/vs-thumb.cpp 2015-08-19 03:01:01 +0000 |
572 | @@ -54,28 +54,6 @@ |
573 | return uri; |
574 | } |
575 | |
576 | -bool extract_thumbnail(string const& uri, string const& ofname) |
577 | -{ |
578 | - ThumbnailExtractor extractor; |
579 | - |
580 | - extractor.set_uri(uri); |
581 | - if (extractor.has_video()) |
582 | - { |
583 | - if (!extractor.extract_video_frame()) |
584 | - { |
585 | - return false; |
586 | - } |
587 | - } |
588 | - else |
589 | - { |
590 | - if (!extractor.extract_audio_cover_art()) |
591 | - { |
592 | - return false; |
593 | - } |
594 | - } |
595 | - extractor.save_screenshot(ofname); |
596 | - return true; |
597 | -} |
598 | } |
599 | |
600 | int main(int argc, char** argv) |
601 | @@ -88,7 +66,6 @@ |
602 | } |
603 | string uri; |
604 | string outfile(argv[2]); |
605 | - bool success = false; |
606 | |
607 | try |
608 | { |
609 | @@ -100,15 +77,39 @@ |
610 | return 1; |
611 | } |
612 | |
613 | + std::unique_ptr<GMainLoop, decltype(&g_main_loop_unref)> main_loop( |
614 | + g_main_loop_new(nullptr, false), g_main_loop_unref); |
615 | + bool success = false; |
616 | + auto callback = [&](GdkPixbuf* const thumbnail) { |
617 | + if (thumbnail) |
618 | + { |
619 | + // Saving as TIFF with no compression here to avoid |
620 | + // artefacts due to converting to jpg twice. |
621 | + // (The main thumbnailer saves as jpg.) By staying |
622 | + // lossless here, we keep all the policy decisions about |
623 | + // image quality in the main thumbnailer. |
624 | + fprintf(stderr, "Saving pixbuf to tiff\n"); |
625 | + GError* error = nullptr; |
626 | + success = gdk_pixbuf_save(thumbnail, outfile.c_str(), "tiff", &error, "compression", "1", nullptr); |
627 | + if (!success) |
628 | + { |
629 | + fprintf(stderr, "save_screenshot(): saving image: %s\n", error->message); |
630 | + g_error_free(error); |
631 | + } |
632 | + } |
633 | + g_main_loop_quit(main_loop.get()); |
634 | + }; |
635 | + ThumbnailExtractor extractor; |
636 | try |
637 | { |
638 | - success = extract_thumbnail(uri, outfile); |
639 | + extractor.extract(uri, callback); |
640 | } |
641 | catch (exception const& e) |
642 | { |
643 | - fprintf(stderr, "Error creating thumbnail: %s\n", e.what()); |
644 | - return 2; |
645 | + fprintf(stderr, "Error starting extraction \"%s\": %s\n", argv[1], e.what()); |
646 | + return 1; |
647 | } |
648 | + g_main_loop_run(main_loop.get()); |
649 | |
650 | return success ? 0 : 1; |
651 | } |
652 | |
653 | === modified file 'tests/thumbnailer/thumbnailer_test.cpp' |
654 | --- tests/thumbnailer/thumbnailer_test.cpp 2015-07-30 04:28:48 +0000 |
655 | +++ tests/thumbnailer/thumbnailer_test.cpp 2015-08-19 03:01:01 +0000 |
656 | @@ -653,7 +653,7 @@ |
657 | catch (unity::ResourceException const& e) |
658 | { |
659 | string msg = e.what(); |
660 | - EXPECT_NE(string::npos, msg.find("extractor pipeline failed")) << msg; |
661 | + EXPECT_NE(string::npos, msg.find("could not extract screenshot")) << msg; |
662 | } |
663 | } |
664 | |
665 | |
666 | === modified file 'tests/vs-thumb/vs-thumb_test.cpp' |
667 | --- tests/vs-thumb/vs-thumb_test.cpp 2015-06-01 06:19:23 +0000 |
668 | +++ tests/vs-thumb/vs-thumb_test.cpp 2015-08-19 03:01:01 +0000 |
669 | @@ -42,37 +42,13 @@ |
670 | const char MP4_LANDSCAPE_TEST_FILE[] = TESTDATADIR "/gegl-landscape.mp4"; |
671 | const char MP4_PORTRAIT_TEST_FILE[] = TESTDATADIR "/gegl-portrait.mp4"; |
672 | const char VORBIS_TEST_FILE[] = TESTDATADIR "/testsong.ogg"; |
673 | +const char EMPTY_TEST_FILE[] = TESTDATADIR "/empty"; |
674 | |
675 | class ExtractorTest : public ::testing::Test |
676 | { |
677 | protected: |
678 | - ExtractorTest() |
679 | - { |
680 | - } |
681 | - virtual ~ExtractorTest() |
682 | - { |
683 | - } |
684 | - |
685 | - virtual void SetUp() override |
686 | - { |
687 | - tempdir = "./vsthumb-test.XXXXXX"; |
688 | - if (mkdtemp(const_cast<char*>(tempdir.data())) == nullptr) |
689 | - { |
690 | - tempdir = ""; |
691 | - throw std::runtime_error("Could not create temporary directory"); |
692 | - } |
693 | - } |
694 | - |
695 | - virtual void TearDown() override |
696 | - { |
697 | - if (!tempdir.empty()) |
698 | - { |
699 | - std::string cmd = "rm -rf \"" + tempdir + "\""; |
700 | - ASSERT_EQ(system(cmd.c_str()), 0); |
701 | - } |
702 | - } |
703 | - |
704 | - std::string tempdir; |
705 | + ExtractorTest() = default; |
706 | + virtual ~ExtractorTest() = default; |
707 | }; |
708 | |
709 | std::string filename_to_uri(const std::string& filename) |
710 | @@ -86,17 +62,20 @@ |
711 | return uri.get(); |
712 | } |
713 | |
714 | -gobj_ptr<GdkPixbuf> load_image(const std::string& filename) |
715 | +gobj_ptr<GdkPixbuf> extract(std::string const& filename) |
716 | { |
717 | - GError* error = nullptr; |
718 | - gobj_ptr<GdkPixbuf> image(gdk_pixbuf_new_from_file(filename.c_str(), &error)); |
719 | - if (error) |
720 | - { |
721 | - std::string message(error->message); |
722 | - g_error_free(error); |
723 | - throw std::runtime_error(message); |
724 | - } |
725 | - return std::move(image); |
726 | + gobj_ptr<GdkPixbuf> image; |
727 | + std::unique_ptr<GMainLoop, decltype(&g_main_loop_unref)> main_loop( |
728 | + g_main_loop_new(nullptr, false), g_main_loop_unref); |
729 | + auto callback = [&](GdkPixbuf* const thumbnail) { |
730 | + // We copy the image here, because it may be backed by mmaped memory |
731 | + image.reset(thumbnail ? gdk_pixbuf_copy(thumbnail) : nullptr); |
732 | + g_main_loop_quit(main_loop.get()); |
733 | + }; |
734 | + ThumbnailExtractor extractor; |
735 | + extractor.extract(filename_to_uri(filename), callback); |
736 | + g_main_loop_run(main_loop.get()); |
737 | + return image; |
738 | } |
739 | |
740 | bool supports_decoder(const std::string& format) |
741 | @@ -143,14 +122,8 @@ |
742 | return; |
743 | } |
744 | |
745 | - ThumbnailExtractor extractor; |
746 | - std::string outfile = tempdir + "/out.jpg"; |
747 | - extractor.set_uri(filename_to_uri(THEORA_TEST_FILE)); |
748 | - ASSERT_TRUE(extractor.has_video()); |
749 | - ASSERT_TRUE(extractor.extract_video_frame()); |
750 | - extractor.save_screenshot(outfile); |
751 | - |
752 | - auto image = load_image(outfile); |
753 | + auto image = extract(THEORA_TEST_FILE); |
754 | + ASSERT_NE(nullptr, image.get()); |
755 | EXPECT_EQ(gdk_pixbuf_get_width(image.get()), 1920); |
756 | EXPECT_EQ(gdk_pixbuf_get_height(image.get()), 1080); |
757 | } |
758 | @@ -163,14 +136,8 @@ |
759 | return; |
760 | } |
761 | |
762 | - ThumbnailExtractor extractor; |
763 | - std::string outfile = tempdir + "/out.jpg"; |
764 | - extractor.set_uri(filename_to_uri(MP4_LANDSCAPE_TEST_FILE)); |
765 | - ASSERT_TRUE(extractor.has_video()); |
766 | - ASSERT_TRUE(extractor.extract_video_frame()); |
767 | - extractor.save_screenshot(outfile); |
768 | - |
769 | - auto image = load_image(outfile); |
770 | + auto image = extract(MP4_LANDSCAPE_TEST_FILE); |
771 | + ASSERT_NE(nullptr, image.get()); |
772 | EXPECT_EQ(gdk_pixbuf_get_width(image.get()), 1920); |
773 | EXPECT_EQ(gdk_pixbuf_get_height(image.get()), 1080); |
774 | } |
775 | @@ -183,36 +150,29 @@ |
776 | return; |
777 | } |
778 | |
779 | - ThumbnailExtractor extractor; |
780 | - std::string outfile = tempdir + "/out.jpg"; |
781 | - extractor.set_uri(filename_to_uri(MP4_PORTRAIT_TEST_FILE)); |
782 | - ASSERT_TRUE(extractor.extract_video_frame()); |
783 | - extractor.save_screenshot(outfile); |
784 | - |
785 | - auto image = load_image(outfile); |
786 | + auto image = extract(MP4_PORTRAIT_TEST_FILE); |
787 | + ASSERT_NE(nullptr, image.get()); |
788 | EXPECT_EQ(gdk_pixbuf_get_width(image.get()), 720); |
789 | EXPECT_EQ(gdk_pixbuf_get_height(image.get()), 1280); |
790 | } |
791 | |
792 | TEST_F(ExtractorTest, extract_vorbis_cover_art) |
793 | { |
794 | - ThumbnailExtractor extractor; |
795 | - |
796 | - std::string outfile = tempdir + "/out.jpg"; |
797 | - extractor.set_uri(filename_to_uri(VORBIS_TEST_FILE)); |
798 | - ASSERT_FALSE(extractor.has_video()); |
799 | - ASSERT_TRUE(extractor.extract_audio_cover_art()); |
800 | - extractor.save_screenshot(outfile); |
801 | - |
802 | - auto image = load_image(outfile); |
803 | + auto image = extract(VORBIS_TEST_FILE); |
804 | + ASSERT_NE(nullptr, image.get()); |
805 | EXPECT_EQ(gdk_pixbuf_get_width(image.get()), 200); |
806 | EXPECT_EQ(gdk_pixbuf_get_height(image.get()), 200); |
807 | } |
808 | |
809 | +TEST_F(ExtractorTest, extract_empty) |
810 | +{ |
811 | + auto image = extract(EMPTY_TEST_FILE); |
812 | + ASSERT_EQ(nullptr, image.get()); |
813 | +} |
814 | + |
815 | TEST_F(ExtractorTest, file_not_found) |
816 | { |
817 | - ThumbnailExtractor extractor; |
818 | - EXPECT_THROW(extractor.set_uri(filename_to_uri(TESTDATADIR "/no-such-file.ogv")), std::runtime_error); |
819 | + EXPECT_THROW(extract(TESTDATADIR "/no-such-file.ogv"), std::runtime_error); |
820 | } |
821 | |
822 | int main(int argc, char** argv) |
FAILED: Continuous integration, rev:258 jenkins. qa.ubuntu. com/job/ thumbnailer- devel-ci/ 405/ jenkins. qa.ubuntu. com/job/ thumbnailer- devel-wily- amd64-ci/ 215/console jenkins. qa.ubuntu. com/job/ thumbnailer- devel-wily- armhf-ci/ 220/console jenkins. qa.ubuntu. com/job/ thumbnailer- devel-wily- i386-ci/ 214/console
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/thumbnailer -devel- ci/405/ rebuild
http://