Merge lp:~jhodapp/media-hub/fix-music-next into lp:media-hub
- fix-music-next
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Manuel de la Peña |
Approved revision: | 50 |
Merged at revision: | 51 |
Proposed branch: | lp:~jhodapp/media-hub/fix-music-next |
Merge into: | lp:media-hub |
Diff against target: |
537 lines (+246/-34) 9 files modified
README (+4/-0) src/core/media/CMakeLists.txt (+0/-1) src/core/media/engine.h (+1/-0) src/core/media/gstreamer/engine.cpp (+18/-1) src/core/media/gstreamer/engine.h (+2/-1) src/core/media/gstreamer/playbin.h (+3/-0) src/core/media/player_implementation.cpp (+143/-29) src/core/media/powerd_service.h (+2/-2) src/core/media/util/timeout.h (+73/-0) |
To merge this branch: | bzr merge lp:~jhodapp/media-hub/fix-music-next |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Manuel de la Peña (community) | Approve | ||
PS Jenkins bot | continuous-integration | Needs Fixing | |
Review via email: mp+230326@code.launchpad.net |
Commit message
Allow music to advance to the next song when the device is not charging
Description of the change
Allow music to advance to the next song when the device is not charging
PS Jenkins bot (ps-jenkins) wrote : | # |
- 50. By Jim Hodapp
-
Make sure the display wakelock is cleared after video playback stops.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:50
http://
Executed test runs:
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Manuel de la Peña (mandel) : | # |
- 51. By Jim Hodapp
-
Rework the system and display wakelock logic. Should take care of the edge case issues.
- 52. By Jim Hodapp
-
Make sure a display or system wakelock are not held if the client disconnects during playback
- 53. By Jim Hodapp
-
Remove some debugging statements and improve the code comments
Preview Diff
1 | === modified file 'README' |
2 | --- README 2014-01-13 21:51:14 +0000 |
3 | +++ README 2014-08-13 01:41:13 +0000 |
4 | @@ -6,6 +6,10 @@ |
5 | cmake .. |
6 | make |
7 | |
8 | +To Run From Console: |
9 | +-------------------- |
10 | +CORE_UBUNTU_MEDIA_SERVICE_VIDEO_SINK_NAME=mirsink CORE_UBUNTU_MEDIA_SERVICE_AUDIO_SINK_NAME=pulsesink src/core/media/media-hub-server |
11 | + |
12 | To Run Unit Tests: |
13 | ------------------ |
14 | |
15 | |
16 | === modified file 'src/core/media/CMakeLists.txt' |
17 | --- src/core/media/CMakeLists.txt 2014-04-04 14:31:43 +0000 |
18 | +++ src/core/media/CMakeLists.txt 2014-08-13 01:41:13 +0000 |
19 | @@ -1,5 +1,4 @@ |
20 | pkg_check_modules(PC_GSTREAMER_1_0 REQUIRED gstreamer-1.0) |
21 | - |
22 | include_directories(${PC_GSTREAMER_1_0_INCLUDE_DIRS} ${HYBRIS_MEDIA_CFLAGS}) |
23 | |
24 | add_library( |
25 | |
26 | === modified file 'src/core/media/engine.h' |
27 | --- src/core/media/engine.h 2014-04-25 17:53:00 +0000 |
28 | +++ src/core/media/engine.h 2014-08-13 01:41:13 +0000 |
29 | @@ -118,6 +118,7 @@ |
30 | |
31 | virtual const core::Signal<void>& about_to_finish_signal() const = 0; |
32 | virtual const core::Signal<uint64_t>& seeked_to_signal() const = 0; |
33 | + virtual const core::Signal<void>& client_disconnected_signal() const = 0; |
34 | virtual const core::Signal<void>& end_of_stream_signal() const = 0; |
35 | virtual const core::Signal<core::ubuntu::media::Player::PlaybackStatus>& playback_status_changed_signal() const = 0; |
36 | }; |
37 | |
38 | === modified file 'src/core/media/gstreamer/engine.cpp' |
39 | --- src/core/media/gstreamer/engine.cpp 2014-04-25 17:53:00 +0000 |
40 | +++ src/core/media/gstreamer/engine.cpp 2014-08-13 01:41:13 +0000 |
41 | @@ -1,5 +1,5 @@ |
42 | /* |
43 | - * Copyright © 2013 Canonical Ltd. |
44 | + * Copyright © 2013-2014 Canonical Ltd. |
45 | * |
46 | * This program is free software: you can redistribute it and/or modify it |
47 | * under the terms of the GNU Lesser General Public License version 3, |
48 | @@ -175,6 +175,11 @@ |
49 | seeked_to(value); |
50 | } |
51 | |
52 | + void on_client_disconnected() |
53 | + { |
54 | + client_disconnected(); |
55 | + } |
56 | + |
57 | void on_end_of_stream() |
58 | { |
59 | end_of_stream(); |
60 | @@ -214,6 +219,11 @@ |
61 | &Private::on_seeked_to, |
62 | this, |
63 | std::placeholders::_1))), |
64 | + client_disconnected_connection( |
65 | + playbin.signals.client_disconnected.connect( |
66 | + std::bind( |
67 | + &Private::on_client_disconnected, |
68 | + this))), |
69 | on_end_of_stream_connection( |
70 | playbin.signals.on_end_of_stream.connect( |
71 | std::bind( |
72 | @@ -236,10 +246,12 @@ |
73 | core::ScopedConnection on_tag_available_connection; |
74 | core::ScopedConnection on_volume_changed_connection; |
75 | core::ScopedConnection on_seeked_to_connection; |
76 | + core::ScopedConnection client_disconnected_connection; |
77 | core::ScopedConnection on_end_of_stream_connection; |
78 | |
79 | core::Signal<void> about_to_finish; |
80 | core::Signal<uint64_t> seeked_to; |
81 | + core::Signal<void> client_disconnected; |
82 | core::Signal<void> end_of_stream; |
83 | core::Signal<media::Player::PlaybackStatus> playback_status_changed; |
84 | }; |
85 | @@ -383,6 +395,11 @@ |
86 | return d->seeked_to; |
87 | } |
88 | |
89 | +const core::Signal<void>& gstreamer::Engine::client_disconnected_signal() const |
90 | +{ |
91 | + return d->client_disconnected; |
92 | +} |
93 | + |
94 | const core::Signal<void>& gstreamer::Engine::end_of_stream_signal() const |
95 | { |
96 | return d->end_of_stream; |
97 | |
98 | === modified file 'src/core/media/gstreamer/engine.h' |
99 | --- src/core/media/gstreamer/engine.h 2014-04-25 17:53:00 +0000 |
100 | +++ src/core/media/gstreamer/engine.h 2014-08-13 01:41:13 +0000 |
101 | @@ -1,5 +1,5 @@ |
102 | /* |
103 | - * Copyright © 2013 Canonical Ltd. |
104 | + * Copyright © 2013-2014 Canonical Ltd. |
105 | * |
106 | * This program is free software: you can redistribute it and/or modify it |
107 | * under the terms of the GNU Lesser General Public License version 3, |
108 | @@ -53,6 +53,7 @@ |
109 | |
110 | const core::Signal<void>& about_to_finish_signal() const; |
111 | const core::Signal<uint64_t>& seeked_to_signal() const; |
112 | + const core::Signal<void>& client_disconnected_signal() const; |
113 | const core::Signal<void>& end_of_stream_signal() const; |
114 | const core::Signal<core::ubuntu::media::Player::PlaybackStatus>& playback_status_changed_signal() const; |
115 | |
116 | |
117 | === modified file 'src/core/media/gstreamer/playbin.h' |
118 | --- src/core/media/gstreamer/playbin.h 2014-04-25 21:23:10 +0000 |
119 | +++ src/core/media/gstreamer/playbin.h 2014-08-13 01:41:13 +0000 |
120 | @@ -116,6 +116,8 @@ |
121 | // in a state that is ready for the next client that connects to the |
122 | // service |
123 | reset_pipeline(); |
124 | + // Signal to the Player class that the client side has disconnected |
125 | + signals.client_disconnected(); |
126 | } |
127 | |
128 | void reset_pipeline() |
129 | @@ -411,6 +413,7 @@ |
130 | core::Signal<uint64_t> on_seeked_to; |
131 | core::Signal<void> on_end_of_stream; |
132 | core::Signal<media::Player::PlaybackStatus> on_playback_status_changed; |
133 | + core::Signal<void> client_disconnected; |
134 | } signals; |
135 | }; |
136 | } |
137 | |
138 | === modified file 'src/core/media/player_implementation.cpp' |
139 | --- src/core/media/player_implementation.cpp 2014-06-24 17:03:27 +0000 |
140 | +++ src/core/media/player_implementation.cpp 2014-08-13 01:41:13 +0000 |
141 | @@ -13,9 +13,11 @@ |
142 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
143 | * |
144 | * Authored by: Thomas Voß <thomas.voss@canonical.com> |
145 | + * Jim Hodapp <jim.hodapp@canonical.com> |
146 | */ |
147 | |
148 | #include "player_implementation.h" |
149 | +#include "util/timeout.h" |
150 | |
151 | #include <unistd.h> |
152 | |
153 | @@ -28,6 +30,7 @@ |
154 | |
155 | #include <exception> |
156 | #include <iostream> |
157 | +#include <mutex> |
158 | |
159 | #define UNUSED __attribute__((unused)) |
160 | |
161 | @@ -38,6 +41,14 @@ |
162 | |
163 | struct media::PlayerImplementation::Private |
164 | { |
165 | + enum class wakelock_clear_t |
166 | + { |
167 | + WAKELOCK_CLEAR_INACTIVE, |
168 | + WAKELOCK_CLEAR_DISPLAY, |
169 | + WAKELOCK_CLEAR_SYSTEM, |
170 | + WAKELOCK_CLEAR_INVALID |
171 | + }; |
172 | + |
173 | Private(PlayerImplementation* parent, |
174 | const dbus::types::ObjectPath& session_path, |
175 | const std::shared_ptr<media::Service>& service, |
176 | @@ -51,7 +62,10 @@ |
177 | session_path.as_string() + "/TrackList", |
178 | engine->meta_data_extractor())), |
179 | sys_lock_name("media-hub-music-playback"), |
180 | - active_display_on_request(false), |
181 | + disp_cookie(-1), |
182 | + system_wakelock_count(0), |
183 | + display_wakelock_count(0), |
184 | + previous_state(Engine::State::stopped), |
185 | key(key) |
186 | { |
187 | auto bus = std::shared_ptr<dbus::Bus>(new dbus::Bus(core::dbus::WellKnownBus::system)); |
188 | @@ -63,6 +77,14 @@ |
189 | auto uscreen_stub_service = dbus::Service::use_service(bus, dbus::traits::Service<core::UScreen>::interface_name()); |
190 | uscreen_session = uscreen_stub_service->object_for_path(dbus::types::ObjectPath("/com/canonical/Unity/Screen")); |
191 | |
192 | + /* |
193 | + * Wakelock state logic: |
194 | + * |
195 | + * PLAYING->READY: delay 4 seconds and try to clear current wakelock type |
196 | + * PLAYING->PAUSED or PLAYING->STOPPED: delay 4 seconds and try to clear current wakelock type |
197 | + * READY->PAUSED: request a new wakelock (system or display) |
198 | + * PLAYING->PAUSED: delay 4 seconds and try to clear current wakelock type |
199 | + */ |
200 | engine->state().changed().connect( |
201 | [parent, this](const Engine::State& state) |
202 | { |
203 | @@ -71,97 +93,178 @@ |
204 | case Engine::State::ready: |
205 | { |
206 | parent->playback_status().set(media::Player::ready); |
207 | - clear_power_state(); |
208 | + if (previous_state == Engine::State::playing) |
209 | + { |
210 | + wakelock_timeout.reset(new timeout(4000, true, std::bind(&Private::clear_wakelock, |
211 | + this, std::placeholders::_1), current_wakelock_type())); |
212 | + } |
213 | break; |
214 | } |
215 | case Engine::State::playing: |
216 | { |
217 | parent->playback_status().set(media::Player::playing); |
218 | - request_power_state(); |
219 | + if (previous_state == Engine::State::stopped || previous_state == Engine::State::paused) |
220 | + { |
221 | + request_power_state(); |
222 | + } |
223 | break; |
224 | } |
225 | case Engine::State::stopped: |
226 | { |
227 | parent->playback_status().set(media::Player::stopped); |
228 | - clear_power_state(); |
229 | break; |
230 | } |
231 | case Engine::State::paused: |
232 | { |
233 | parent->playback_status().set(media::Player::paused); |
234 | - clear_power_state(); |
235 | + if (previous_state == Engine::State::ready) |
236 | + { |
237 | + request_power_state(); |
238 | + } |
239 | + else if (previous_state == Engine::State::playing) |
240 | + { |
241 | + wakelock_timeout.reset(new timeout(4000, true, std::bind(&Private::clear_wakelock, |
242 | + this, std::placeholders::_1), current_wakelock_type())); |
243 | + } |
244 | break; |
245 | } |
246 | default: |
247 | break; |
248 | }; |
249 | + |
250 | + // Keep track of the previous Engine playback state: |
251 | + previous_state = state; |
252 | }); |
253 | |
254 | } |
255 | |
256 | + ~Private() |
257 | + { |
258 | + // Make sure that we don't hold on to the wakelocks if media-hub-server |
259 | + // ever gets restarted manually or automatically |
260 | + clear_wakelocks(); |
261 | + } |
262 | + |
263 | void request_power_state() |
264 | { |
265 | try |
266 | { |
267 | if (parent->is_video_source()) |
268 | { |
269 | - if (!active_display_on_request) |
270 | + if (display_wakelock_count == 0) |
271 | { |
272 | auto result = uscreen_session->invoke_method_synchronously<core::UScreen::keepDisplayOn, int>(); |
273 | if (result.is_error()) |
274 | throw std::runtime_error(result.error().print()); |
275 | - |
276 | disp_cookie = result.value(); |
277 | - active_display_on_request = true; |
278 | + cout << "Requested new display wakelock" << endl; |
279 | + } |
280 | + |
281 | + { |
282 | + // Keep track of how many display wakelocks have been requested |
283 | + std::lock_guard<std::mutex> lock(wakelock_mutex); |
284 | + ++display_wakelock_count; |
285 | } |
286 | } |
287 | else |
288 | { |
289 | - if (sys_cookie.empty()) |
290 | + if (system_wakelock_count == 0) |
291 | { |
292 | auto result = powerd_session->invoke_method_synchronously<core::Powerd::requestSysState, std::string>(sys_lock_name, static_cast<int>(1)); |
293 | if (result.is_error()) |
294 | throw std::runtime_error(result.error().print()); |
295 | - |
296 | sys_cookie = result.value(); |
297 | + cout << "Requested new system wakelock" << endl; |
298 | + } |
299 | + |
300 | + { |
301 | + // Keep track of how many system wakelocks have been requested |
302 | + std::lock_guard<std::mutex> lock(wakelock_mutex); |
303 | + ++system_wakelock_count; |
304 | } |
305 | } |
306 | } |
307 | - catch(std::exception& e) |
308 | + catch(const std::exception& e) |
309 | { |
310 | std::cerr << "Warning: failed to request power state: "; |
311 | std::cerr << e.what() << std::endl; |
312 | } |
313 | } |
314 | |
315 | - void clear_power_state() |
316 | + void clear_wakelock(const wakelock_clear_t &wakelock) |
317 | { |
318 | + cout << __PRETTY_FUNCTION__ << endl; |
319 | try |
320 | { |
321 | - if (parent->is_video_source()) |
322 | - { |
323 | - if (active_display_on_request) |
324 | - { |
325 | - uscreen_session->invoke_method_synchronously<core::UScreen::removeDisplayOnRequest, void>(disp_cookie); |
326 | - active_display_on_request = false; |
327 | - } |
328 | - } |
329 | - else |
330 | - { |
331 | - if (!sys_cookie.empty()) |
332 | - { |
333 | - powerd_session->invoke_method_synchronously<core::Powerd::clearSysState, void>(sys_cookie); |
334 | - sys_cookie.clear(); |
335 | - } |
336 | + switch (wakelock) |
337 | + { |
338 | + case wakelock_clear_t::WAKELOCK_CLEAR_INACTIVE: |
339 | + break; |
340 | + case wakelock_clear_t::WAKELOCK_CLEAR_SYSTEM: |
341 | + { |
342 | + std::lock_guard<std::mutex> lock(wakelock_mutex); |
343 | + --system_wakelock_count; |
344 | + } |
345 | + // Only actually clear the system wakelock once the count reaches zero |
346 | + if (system_wakelock_count == 0) |
347 | + { |
348 | + cout << "Clearing system wakelock" << endl; |
349 | + powerd_session->invoke_method_synchronously<core::Powerd::clearSysState, void>(sys_cookie); |
350 | + sys_cookie.clear(); |
351 | + } |
352 | + break; |
353 | + case wakelock_clear_t::WAKELOCK_CLEAR_DISPLAY: |
354 | + { |
355 | + std::lock_guard<std::mutex> lock(wakelock_mutex); |
356 | + --display_wakelock_count; |
357 | + } |
358 | + // Only actually clear the display wakelock once the count reaches zero |
359 | + if (display_wakelock_count == 0) |
360 | + { |
361 | + cout << "Clearing display wakelock" << endl; |
362 | + uscreen_session->invoke_method_synchronously<core::UScreen::removeDisplayOnRequest, void>(disp_cookie); |
363 | + disp_cookie = -1; |
364 | + } |
365 | + break; |
366 | + case wakelock_clear_t::WAKELOCK_CLEAR_INVALID: |
367 | + default: |
368 | + cerr << "Can't clear invalid wakelock type" << endl; |
369 | } |
370 | } |
371 | - catch(std::exception& e) |
372 | + catch(const std::exception& e) |
373 | { |
374 | std::cerr << "Warning: failed to clear power state: "; |
375 | std::cerr << e.what() << std::endl; |
376 | } |
377 | } |
378 | |
379 | + wakelock_clear_t current_wakelock_type() const |
380 | + { |
381 | + return (parent->is_video_source()) ? |
382 | + wakelock_clear_t::WAKELOCK_CLEAR_DISPLAY : wakelock_clear_t::WAKELOCK_CLEAR_SYSTEM; |
383 | + } |
384 | + |
385 | + void clear_wakelocks() |
386 | + { |
387 | + // Clear both types of wakelocks (display and system) |
388 | + if (system_wakelock_count > 0) |
389 | + { |
390 | + { |
391 | + std::lock_guard<std::mutex> lock(wakelock_mutex); |
392 | + system_wakelock_count = 1; |
393 | + } |
394 | + clear_wakelock(wakelock_clear_t::WAKELOCK_CLEAR_SYSTEM); |
395 | + } |
396 | + if (display_wakelock_count > 0) |
397 | + { |
398 | + { |
399 | + std::lock_guard<std::mutex> lock(wakelock_mutex); |
400 | + display_wakelock_count = 1; |
401 | + } |
402 | + clear_wakelock(wakelock_clear_t::WAKELOCK_CLEAR_DISPLAY); |
403 | + } |
404 | + } |
405 | + |
406 | PlayerImplementation* parent; |
407 | std::shared_ptr<Service> service; |
408 | std::shared_ptr<Engine> engine; |
409 | @@ -171,8 +274,12 @@ |
410 | std::shared_ptr<dbus::Object> uscreen_session; |
411 | std::string sys_lock_name; |
412 | int disp_cookie; |
413 | - bool active_display_on_request; |
414 | std::string sys_cookie; |
415 | + std::mutex wakelock_mutex; |
416 | + uint8_t system_wakelock_count; |
417 | + uint8_t display_wakelock_count; |
418 | + std::unique_ptr<timeout> wakelock_timeout; |
419 | + Engine::State previous_state; |
420 | PlayerImplementation::PlayerKey key; |
421 | }; |
422 | |
423 | @@ -240,6 +347,13 @@ |
424 | } |
425 | }); |
426 | |
427 | + d->engine->client_disconnected_signal().connect([this]() |
428 | + { |
429 | + // If the client disconnects, make sure both wakelock types |
430 | + // are cleared |
431 | + d->clear_wakelocks(); |
432 | + }); |
433 | + |
434 | d->engine->seeked_to_signal().connect([this](uint64_t value) |
435 | { |
436 | seeked_to()(value); |
437 | |
438 | === modified file 'src/core/media/powerd_service.h' |
439 | --- src/core/media/powerd_service.h 2014-06-16 22:28:53 +0000 |
440 | +++ src/core/media/powerd_service.h 2014-08-13 01:41:13 +0000 |
441 | @@ -41,7 +41,7 @@ |
442 | static std::string s = "com.canonical.powerd"; |
443 | return s; |
444 | } |
445 | - |
446 | + |
447 | struct requestSysState |
448 | { |
449 | static std::string name() |
450 | @@ -62,7 +62,7 @@ |
451 | static std::string s = "clearSysState"; |
452 | return s; |
453 | } |
454 | - |
455 | + |
456 | static const std::chrono::milliseconds default_timeout() { return std::chrono::seconds{1}; } |
457 | |
458 | typedef Powerd Interface; |
459 | |
460 | === added directory 'src/core/media/util' |
461 | === added file 'src/core/media/util/timeout.h' |
462 | --- src/core/media/util/timeout.h 1970-01-01 00:00:00 +0000 |
463 | +++ src/core/media/util/timeout.h 2014-08-13 01:41:13 +0000 |
464 | @@ -0,0 +1,73 @@ |
465 | +/* |
466 | + * |
467 | + * This program is free software: you can redistribute it and/or modify it |
468 | + * under the terms of the GNU Lesser General Public License version 3, |
469 | + * as published by the Free Software Foundation. |
470 | + * |
471 | + * This program is distributed in the hope that it will be useful, |
472 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
473 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
474 | + * GNU Lesser General Public License for more details. |
475 | + * |
476 | + * You should have received a copy of the GNU Lesser General Public License |
477 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
478 | + * |
479 | + * Authored by: Jim Hodapp <jim.hodapp@canonical.com> |
480 | + * |
481 | + * Adapted from: http://stackoverflow.com/questions/14650885/how-to-create-timer-events-using-c-11 |
482 | + * |
483 | + */ |
484 | + |
485 | +#ifndef TIMEOUT_H_ |
486 | +#define TIMEOUT_H_ |
487 | + |
488 | +#include <functional> |
489 | +#include <chrono> |
490 | +#include <future> |
491 | +#include <cstdio> |
492 | + |
493 | +namespace core |
494 | +{ |
495 | +namespace ubuntu |
496 | +{ |
497 | +namespace media |
498 | +{ |
499 | + |
500 | +class timeout |
501 | +{ |
502 | +public: |
503 | + /** |
504 | + * @brief Start a timeout with millisecond resolution |
505 | + * @param timeout_ms Timeout value in milliseconds |
506 | + * @param async Whether to block while the timeout proceeds or not |
507 | + * @param f The function to call when the timeout expires |
508 | + * @param args Any arguments to pass to f when the timeout expires |
509 | + */ |
510 | + template <class callable, class... arguments> |
511 | + timeout(int timeout_ms, bool async, callable&& f, arguments&&... args) |
512 | + { |
513 | + std::function<typename std::result_of<callable(arguments...)>::type()> |
514 | + task(std::bind(std::forward<callable>(f), std::forward<arguments>(args)...)); |
515 | + |
516 | + if (async) |
517 | + { |
518 | + // Timeout without blocking |
519 | + std::thread([timeout_ms, task]() { |
520 | + std::this_thread::sleep_for(std::chrono::milliseconds(timeout_ms)); |
521 | + task(); |
522 | + }).detach(); |
523 | + } |
524 | + else |
525 | + { |
526 | + // Timeout with blocking |
527 | + std::this_thread::sleep_for(std::chrono::milliseconds(timeout_ms)); |
528 | + task(); |
529 | + } |
530 | + } |
531 | +}; |
532 | + |
533 | +} |
534 | +} |
535 | +} |
536 | + |
537 | +#endif // TIMEOUT_H_ |
PASSED: Continuous integration, rev:49 jenkins. qa.ubuntu. com/job/ media-hub- ci/82/ jenkins. qa.ubuntu. com/job/ media-hub- utopic- amd64-ci/ 24 jenkins. qa.ubuntu. com/job/ media-hub- utopic- armhf-ci/ 23 jenkins. qa.ubuntu. com/job/ media-hub- utopic- armhf-ci/ 23/artifact/ work/output/ *zip*/output. zip jenkins. qa.ubuntu. com/job/ media-hub- utopic- i386-ci/ 22
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/media- hub-ci/ 82/rebuild
http://