Merge lp:~jamesh/thumbnailer/vs-thumb-async into lp:thumbnailer/devel

Proposed by James Henstridge
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
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.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Michi Henning (michihenning) wrote :

The thumbnailer test gets one failure here (deterministically):

[ RUN ] ThumbnailerTest.empty_file
Changing to state PAUSED
State changed to 2
/home/michi/src/thumbnailer/vs-thumb-async/tests/thumbnailer/thumbnailer_test.cpp:646: Failure
Value of: spy.wait(5000)
  Actual: false
Expected: true
[ FAILED ] ThumbnailerTest.empty_file (6119 ms)

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
259. By James Henstridge

Bail out on errors from any element rather than just the playbin.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
260. By James Henstridge

Fix test for error message on empty files.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Michi Henning (michihenning) wrote :

I'm getting this when building with vivid:

[ 18%] Building CXX object src/vs-thumb/CMakeFiles/vs-thumb.dir/thumbnailextractor.cpp.o
/home/michi/src/thumbnailer/vs-thumb-async/src/vs-thumb/thumbnailextractor.cpp: In destructor ‘unity::thumbnailer::internal::ThumbnailExtractor::~ThumbnailExtractor()’:
/home/michi/src/thumbnailer/vs-thumb-async/src/vs-thumb/thumbnailextractor.cpp:165:40: error: ‘gst_bus_remove_watch’ was not declared in this scope
         gst_bus_remove_watch(bus_.get());
                                        ^
src/vs-thumb/CMakeFiles/vs-thumb.dir/build.make:77: recipe for target 'src/vs-thumb/CMakeFiles/vs-thumb.dir/thumbnailextractor.cpp.o' failed

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.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
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.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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)

Subscribers

People subscribed via source and target branches

to all changes: