Merge lp:~phablet-team/media-hub/introduce-audio-output-observer-interface into lp:media-hub
- introduce-audio-output-observer-interface
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Ricardo Mendoza |
Approved revision: | 133 |
Merged at revision: | 121 |
Proposed branch: | lp:~phablet-team/media-hub/introduce-audio-output-observer-interface |
Merge into: | lp:media-hub |
Prerequisite: | lp:~thomas-voss/media-hub/introduce-battery-observer-interface |
Diff against target: |
1333 lines (+871/-360) 9 files modified
src/core/media/CMakeLists.txt (+4/-0) src/core/media/audio/ostream_reporter.cpp (+54/-0) src/core/media/audio/ostream_reporter.h (+55/-0) src/core/media/audio/output_observer.cpp (+50/-0) src/core/media/audio/output_observer.h (+76/-0) src/core/media/audio/pulse_audio_output_observer.cpp (+467/-0) src/core/media/audio/pulse_audio_output_observer.h (+128/-0) src/core/media/gstreamer/playbin.cpp (+4/-1) src/core/media/service_implementation.cpp (+33/-359) |
To merge this branch: | bzr merge lp:~phablet-team/media-hub/introduce-audio-output-observer-interface |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
PS Jenkins bot | continuous-integration | Approve | |
Thomas Voß (community) | Needs Fixing | ||
Jim Hodapp (community) | code | Needs Fixing | |
Review via email: mp+242914@code.launchpad.net |
Commit message
Introduce an interface media::
Provide an implementation of media::
Adjust media::
Description of the change
Introduce an interface media::
Provide an implementation of media::
Adjust media::
- 108. By Thomas Voß
-
Remerge prereq branch.
PS Jenkins bot (ps-jenkins) wrote : | # |
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:108
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
Jim Hodapp (jhodapp) wrote : | # |
A few comments below.
- 109. By Thomas Voß
-
[ Jim Hodapp ]
* Resubmitting with prerequisite branch (LP: #1331041)
[ Justin McPherson ]
* Resubmitting with prerequisite branch (LP: #1331041)
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:109
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 110. By Thomas Voß
-
Merge prerequisite branch.
Thomas Voß (thomas-voss) : | # |
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:110
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
deb: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 111. By Thomas Voß
-
[ Jim Hodapp ]
* Error reporting all the way up to the app level from the playbin
pipeline.
[ Ubuntu daily release ]
* New rebuild forced
[ Jim Hodapp ]
* Don't auto-resume playback of videos after a phone call ends. (LP:
#1411273)
[ Ubuntu daily release ]
* New rebuild forced
[ Ricardo Salveti de Araujo ]
* service_implementation: adding debug for call started/ended signals.
Make sure account and connection are available when setting up
account manager (patch from Gustavo Boiko). call_monitor: don't
check caps when hooking up on/off signals, until bug 1409125 is
fixed. Enable parallel building . (LP: #1409125)
[ Jim Hodapp ]
* Pause playback when recording begins. (LP: #1398047)
[ Ricardo Salveti de Araujo ]
* call_monitor.cpp: waiting for bridge to be up, and also protecting
the on_change call (LP: #1408137)
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:111
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 112. By Thomas Voß
-
Merge prereq branch.
- 113. By Thomas Voß
-
Fix FTBFS.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:112
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:113
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 114. By Thomas Voß
-
Merge prereq branch.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:114
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 115. By Thomas Voß
-
Add reporting facilities to the PulseAudioOutpu
tObsever implementation.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:115
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
deb: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 116. By Thomas Voß
-
Consider the adjusted configuration when creating the audio::
OutputObserver instance.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:116
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 117. By Thomas Voß
-
Only subscribe to relevant ports on sink.
- 118. By Thomas Voß
-
Make the Pulse-based AudioOutputObserver implementation more verbose.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:117
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:118
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 119. By Thomas Voß
-
Be explicit about handling state PA_PORT_
AVAILABLE_ UNKNOWN.
Add log line to the gstreamer::Engine to capture if a requested state change has been successfully enqueued.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:119
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 120. By Thomas Voß
-
Monitor a2dp headphone output.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:120
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 121. By Thomas Voß
-
Correctly calculated "monitored" for log output.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:121
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 122. By Thomas Voß
-
Bundle together port properties in a struct Port for reporting purposes.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:122
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 123. By Thomas Voß
-
More logging.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:123
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 124. By Thomas Voß
-
Log result of determining whether a port is available on a sink.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:124
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 125. By Thomas Voß
-
Adjust regex for handling ports.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:125
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 126. By Thomas Voß
-
Merge prereq branch.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:126
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 127. By Thomas Voß
-
off really should be on.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:127
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 128. By Ricardo Mendoza
-
Rewrite observer to deal in Public/Private absolutes, always pausing when Private media goes Public.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:128
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Thomas Voß (thomas-voss) wrote : | # |
A minor niggle inline.
- 129. By Ricardo Mendoza
-
Rework public/private into earpiece/
speaker/ external for better state tracking - 130. By Ricardo Mendoza
-
Wrong type
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:130
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
deb: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 131. By Ricardo Mendoza
-
Dont pause when disconecting headphones, to conform to mh2 behaviour
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:131
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 132. By Thomas Voß
-
* debian/control:
- Removing pre-depends that are not required
- Bumping standards-version to 3.9.6
[ Ricardo Salveti de Araujo ]
* Migrating tests to use ogg instead of mp3/avi removed:
tests/h264.avi tests/test.mp3 added: tests/test-audio-1. ogg
tests/test-video. ogg tests/test.mp3 renamed: tests/test.ogg =>
tests/test-audio. ogg
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:132
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 133. By Ricardo Mendoza
-
Initialize primary and active sink to the non-initialized flag
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:133
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Preview Diff
1 | === modified file 'src/core/media/CMakeLists.txt' |
2 | --- src/core/media/CMakeLists.txt 2015-03-19 00:04:24 +0000 |
3 | +++ src/core/media/CMakeLists.txt 2015-03-19 00:04:25 +0000 |
4 | @@ -91,6 +91,10 @@ |
5 | cover_art_resolver.cpp |
6 | engine.cpp |
7 | |
8 | + audio/pulse_audio_output_observer.cpp |
9 | + audio/ostream_reporter.cpp |
10 | + audio/output_observer.cpp |
11 | + |
12 | power/battery_observer.cpp |
13 | power/state_controller.cpp |
14 | |
15 | |
16 | === added directory 'src/core/media/audio' |
17 | === added file 'src/core/media/audio/ostream_reporter.cpp' |
18 | --- src/core/media/audio/ostream_reporter.cpp 1970-01-01 00:00:00 +0000 |
19 | +++ src/core/media/audio/ostream_reporter.cpp 2015-03-19 00:04:25 +0000 |
20 | @@ -0,0 +1,54 @@ |
21 | +/* |
22 | + * Copyright © 2014 Canonical Ltd. |
23 | + * |
24 | + * This program is free software: you can redistribute it and/or modify it |
25 | + * under the terms of the GNU Lesser General Public License version 3, |
26 | + * as published by the Free Software Foundation. |
27 | + * |
28 | + * This program is distributed in the hope that it will be useful, |
29 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
30 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
31 | + * GNU Lesser General Public License for more details. |
32 | + * |
33 | + * You should have received a copy of the GNU Lesser General Public License |
34 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
35 | + * |
36 | + * Authored by: Thomas Voß <thomas.voss@canonical.com> |
37 | + */ |
38 | + |
39 | +#include <core/media/audio/ostream_reporter.h> |
40 | + |
41 | +namespace audio = core::ubuntu::media::audio; |
42 | + |
43 | +audio::OStreamReporter::OStreamReporter(std::ostream &out) : out{out} |
44 | +{ |
45 | +} |
46 | + |
47 | +void audio::OStreamReporter::connected_to_pulse_audio() |
48 | +{ |
49 | + out << "Connection to PulseAudio has been successfully established." << std::endl; |
50 | +} |
51 | + |
52 | +void audio::OStreamReporter::query_for_default_sink_failed() |
53 | +{ |
54 | + out << "Query for default sink failed." << std::endl; |
55 | +} |
56 | + |
57 | +void audio::OStreamReporter::query_for_default_sink_finished(const std::string& sink_name) |
58 | +{ |
59 | + out << "Default PulseAudio sync has been identified: " << sink_name << std::endl; |
60 | +} |
61 | + |
62 | +void audio::OStreamReporter::query_for_sink_info_finished(const std::string& name, std::uint32_t index, const std::set<Port>& known_ports) |
63 | +{ |
64 | + out << "PulseAudio sink details for " << name << " with index " << index << " is available:" << std::endl; |
65 | + for (const auto& port : known_ports) |
66 | + { |
67 | + if (port.is_monitored) |
68 | + out << " " << port.description << ": " << std::boolalpha << port.is_available << "\n"; |
69 | + } |
70 | +} |
71 | +void audio::OStreamReporter::sink_event_with_index(std::uint32_t index) |
72 | +{ |
73 | + out << "PulseAudio event for sink with index " << index << " received." << std::endl; |
74 | +} |
75 | |
76 | === added file 'src/core/media/audio/ostream_reporter.h' |
77 | --- src/core/media/audio/ostream_reporter.h 1970-01-01 00:00:00 +0000 |
78 | +++ src/core/media/audio/ostream_reporter.h 2015-03-19 00:04:25 +0000 |
79 | @@ -0,0 +1,55 @@ |
80 | +/* |
81 | + * Copyright © 2014 Canonical Ltd. |
82 | + * |
83 | + * This program is free software: you can redistribute it and/or modify it |
84 | + * under the terms of the GNU Lesser General Public License version 3, |
85 | + * as published by the Free Software Foundation. |
86 | + * |
87 | + * This program is distributed in the hope that it will be useful, |
88 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
89 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
90 | + * GNU Lesser General Public License for more details. |
91 | + * |
92 | + * You should have received a copy of the GNU Lesser General Public License |
93 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
94 | + * |
95 | + * Authored by: Thomas Voß <thomas.voss@canonical.com> |
96 | + */ |
97 | +#ifndef CORE_UBUNTU_MEDIA_AUDIO_OSTREAM_REPORTER_H_ |
98 | +#define CORE_UBUNTU_MEDIA_AUDIO_OSTREAM_REPORTER_H_ |
99 | + |
100 | +#include <core/media/audio/pulse_audio_output_observer.h> |
101 | + |
102 | +#include <iosfwd> |
103 | + |
104 | +namespace core |
105 | +{ |
106 | +namespace ubuntu |
107 | +{ |
108 | +namespace media |
109 | +{ |
110 | +namespace audio |
111 | +{ |
112 | +// A PulseAudioOutputObserver::Reporter implementation printing events to |
113 | +// the configured output stream. |
114 | +class OStreamReporter : public PulseAudioOutputObserver::Reporter |
115 | +{ |
116 | +public: |
117 | + // Constructs a new reporter instance, outputting events to the given stream. |
118 | + OStreamReporter(std::ostream& out = std::cout); |
119 | + |
120 | + void connected_to_pulse_audio() override; |
121 | + void query_for_default_sink_failed() override; |
122 | + void query_for_default_sink_finished(const std::string& sink_name) override; |
123 | + void query_for_sink_info_finished(const std::string& name, std::uint32_t index, const std::set<Port>& known_ports) override; |
124 | + void sink_event_with_index(std::uint32_t index) override; |
125 | + |
126 | +private: |
127 | + std::ostream& out; |
128 | +}; |
129 | +} |
130 | +} |
131 | +} |
132 | +} |
133 | + |
134 | +#endif // CORE_UBUNTU_MEDIA_AUDIO_OUTPUT_OSTREAM_REPORTER_H_ |
135 | |
136 | === added file 'src/core/media/audio/output_observer.cpp' |
137 | --- src/core/media/audio/output_observer.cpp 1970-01-01 00:00:00 +0000 |
138 | +++ src/core/media/audio/output_observer.cpp 2015-03-19 00:04:25 +0000 |
139 | @@ -0,0 +1,50 @@ |
140 | +/* |
141 | + * Copyright © 2014 Canonical Ltd. |
142 | + * |
143 | + * This program is free software: you can redistribute it and/or modify it |
144 | + * under the terms of the GNU Lesser General Public License version 3, |
145 | + * as published by the Free Software Foundation. |
146 | + * |
147 | + * This program is distributed in the hope that it will be useful, |
148 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
149 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
150 | + * GNU Lesser General Public License for more details. |
151 | + * |
152 | + * You should have received a copy of the GNU Lesser General Public License |
153 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
154 | + * |
155 | + * Authored by: Thomas Voß <thomas.voss@canonical.com> |
156 | + */ |
157 | + |
158 | +#include <core/media/audio/output_observer.h> |
159 | + |
160 | +#include <core/media/audio/pulse_audio_output_observer.h> |
161 | +#include <core/media/audio/ostream_reporter.h> |
162 | + |
163 | +#include <iostream> |
164 | + |
165 | +namespace audio = core::ubuntu::media::audio; |
166 | + |
167 | +std::ostream& audio::operator<<(std::ostream& out, audio::OutputState state) |
168 | +{ |
169 | + switch (state) |
170 | + { |
171 | + case audio::OutputState::Earpiece: |
172 | + return out << "OutputState::Earpiece"; |
173 | + case audio::OutputState::Speaker: |
174 | + return out << "OutputState::Speaker"; |
175 | + case audio::OutputState::External: |
176 | + return out << "OutputState::External"; |
177 | + } |
178 | + |
179 | + return out; |
180 | +} |
181 | + |
182 | +audio::OutputObserver::Ptr audio::make_platform_default_output_observer() |
183 | +{ |
184 | + audio::PulseAudioOutputObserver::Configuration config; |
185 | + config.sink = "sink.primary"; |
186 | + config.output_port_patterns = {std::regex{"output-wired_head.*|output-a2dp_headphones"}}; |
187 | + config.reporter = std::make_shared<audio::OStreamReporter>(); |
188 | + return std::make_shared<audio::PulseAudioOutputObserver>(config); |
189 | +} |
190 | |
191 | === added file 'src/core/media/audio/output_observer.h' |
192 | --- src/core/media/audio/output_observer.h 1970-01-01 00:00:00 +0000 |
193 | +++ src/core/media/audio/output_observer.h 2015-03-19 00:04:25 +0000 |
194 | @@ -0,0 +1,76 @@ |
195 | +/* |
196 | + * Copyright © 2014 Canonical Ltd. |
197 | + * |
198 | + * This program is free software: you can redistribute it and/or modify it |
199 | + * under the terms of the GNU Lesser General Public License version 3, |
200 | + * as published by the Free Software Foundation. |
201 | + * |
202 | + * This program is distributed in the hope that it will be useful, |
203 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
204 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
205 | + * GNU Lesser General Public License for more details. |
206 | + * |
207 | + * You should have received a copy of the GNU Lesser General Public License |
208 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
209 | + * |
210 | + * Authored by: Thomas Voß <thomas.voss@canonical.com> |
211 | + */ |
212 | +#ifndef CORE_UBUNTU_MEDIA_AUDIO_OUTPUT_OBSERVER_H_ |
213 | +#define CORE_UBUNTU_MEDIA_AUDIO_OUTPUT_OBSERVER_H_ |
214 | + |
215 | +#include <core/property.h> |
216 | + |
217 | +#include <iosfwd> |
218 | +#include <memory> |
219 | + |
220 | +namespace core |
221 | +{ |
222 | +namespace ubuntu |
223 | +{ |
224 | +namespace media |
225 | +{ |
226 | +namespace audio |
227 | +{ |
228 | +// All known states of an audio output. |
229 | +enum class OutputState |
230 | +{ |
231 | + // The output is via a private earpiece (i.e. headphones, headset) |
232 | + Earpiece, |
233 | + // The output is via the internal speaker. |
234 | + Speaker, |
235 | + // The output is via an external device (a2dp, etc) |
236 | + External, |
237 | +}; |
238 | + |
239 | +// Models observation of audio outputs of a device. |
240 | +// Right now, we are only interested in monitoring the |
241 | +// state of external outputs to react accordingly if |
242 | +// wired or bluetooth outputs are connected/disconnected. |
243 | +class OutputObserver |
244 | +{ |
245 | +public: |
246 | + // Save us some typing. |
247 | + typedef std::shared_ptr<OutputObserver> Ptr; |
248 | + |
249 | + virtual ~OutputObserver() = default; |
250 | + |
251 | + // Getable/observable property holding the state of external outputs. |
252 | + virtual const core::Property<OutputState>& external_output_state() const = 0; |
253 | + |
254 | +protected: |
255 | + OutputObserver() = default; |
256 | + OutputObserver(const OutputObserver&) = delete; |
257 | + OutputObserver& operator=(const OutputObserver&) = delete; |
258 | +}; |
259 | + |
260 | +// Pretty prints the given state to the given output stream. |
261 | +std::ostream& operator<<(std::ostream&, OutputState); |
262 | + |
263 | +// Creats a platform default instance for observing audio outputs. |
264 | +OutputObserver::Ptr make_platform_default_output_observer(); |
265 | +} |
266 | +} |
267 | +} |
268 | +} |
269 | + |
270 | +#endif // CORE_UBUNTU_MEDIA_AUDIO_OUTPUT_OBSERVER_H_ |
271 | |
272 | === added file 'src/core/media/audio/pulse_audio_output_observer.cpp' |
273 | --- src/core/media/audio/pulse_audio_output_observer.cpp 1970-01-01 00:00:00 +0000 |
274 | +++ src/core/media/audio/pulse_audio_output_observer.cpp 2015-03-19 00:04:25 +0000 |
275 | @@ -0,0 +1,467 @@ |
276 | +/* |
277 | + * Copyright © 2014 Canonical Ltd. |
278 | + * |
279 | + * This program is free software: you can redistribute it and/or modify it |
280 | + * under the terms of the GNU Lesser General Public License version 3, |
281 | + * as published by the Free Software Foundation. |
282 | + * |
283 | + * This program is distributed in the hope that it will be useful, |
284 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
285 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
286 | + * GNU Lesser General Public License for more details. |
287 | + * |
288 | + * You should have received a copy of the GNU Lesser General Public License |
289 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
290 | + * |
291 | + * Authored by: Thomas Voß <thomas.voss@canonical.com> |
292 | + * Ricardo Mendoza <ricardo.mendoza@canonical.com> |
293 | + */ |
294 | + |
295 | +#include <core/media/audio/pulse_audio_output_observer.h> |
296 | + |
297 | +#include <pulse/pulseaudio.h> |
298 | + |
299 | +#include <cstdint> |
300 | + |
301 | +#include <map> |
302 | +#include <regex> |
303 | +#include <string> |
304 | + |
305 | +namespace audio = core::ubuntu::media::audio; |
306 | + |
307 | +namespace |
308 | +{ |
309 | +// We wrap calls to the pulseaudio client api into its |
310 | +// own namespace and make sure that only managed types |
311 | +// can be passed to calls to pulseaudio. In addition, |
312 | +// we add guards to the function calls to ensure that |
313 | +// they are conly called on the correct thread. |
314 | +namespace pa |
315 | +{ |
316 | +typedef std::shared_ptr<pa_threaded_mainloop> ThreadedMainLoopPtr; |
317 | +ThreadedMainLoopPtr make_threaded_main_loop() |
318 | +{ |
319 | + return ThreadedMainLoopPtr |
320 | + { |
321 | + pa_threaded_mainloop_new(), |
322 | + [](pa_threaded_mainloop* ml) |
323 | + { |
324 | + pa_threaded_mainloop_stop(ml); |
325 | + pa_threaded_mainloop_free(ml); |
326 | + } |
327 | + }; |
328 | +} |
329 | + |
330 | +void start_main_loop(ThreadedMainLoopPtr ml) |
331 | +{ |
332 | + pa_threaded_mainloop_start(ml.get()); |
333 | +} |
334 | + |
335 | +typedef std::shared_ptr<pa_context> ContextPtr; |
336 | +ContextPtr make_context(ThreadedMainLoopPtr main_loop) |
337 | +{ |
338 | + return ContextPtr |
339 | + { |
340 | + pa_context_new(pa_threaded_mainloop_get_api(main_loop.get()), "MediaHubPulseContext"), |
341 | + pa_context_unref |
342 | + }; |
343 | +} |
344 | + |
345 | +void set_state_callback(ContextPtr ctxt, pa_context_notify_cb_t cb, void* cookie) |
346 | +{ |
347 | + pa_context_set_state_callback(ctxt.get(), cb, cookie); |
348 | +} |
349 | + |
350 | +void set_subscribe_callback(ContextPtr ctxt, pa_context_subscribe_cb_t cb, void* cookie) |
351 | +{ |
352 | + pa_context_set_subscribe_callback(ctxt.get(), cb, cookie); |
353 | +} |
354 | + |
355 | +void throw_if_not_on_main_loop(ThreadedMainLoopPtr ml) |
356 | +{ |
357 | + if (not pa_threaded_mainloop_in_thread(ml.get())) throw std::logic_error |
358 | + { |
359 | + "Attempted to call into a pulseaudio object from another" |
360 | + "thread than the pulseaudio mainloop thread." |
361 | + }; |
362 | +} |
363 | + |
364 | +void throw_if_not_connected(ContextPtr ctxt) |
365 | +{ |
366 | + if (pa_context_get_state(ctxt.get()) != PA_CONTEXT_READY ) throw std::logic_error |
367 | + { |
368 | + "Attempted to issue a call against pulseaudio via a non-connected context." |
369 | + }; |
370 | +} |
371 | + |
372 | +void get_server_info_async(ContextPtr ctxt, ThreadedMainLoopPtr ml, pa_server_info_cb_t cb, void* cookie) |
373 | +{ |
374 | + throw_if_not_on_main_loop(ml); throw_if_not_connected(ctxt); |
375 | + pa_operation_unref(pa_context_get_server_info(ctxt.get(), cb, cookie)); |
376 | +} |
377 | + |
378 | +void subscribe_to_events(ContextPtr ctxt, ThreadedMainLoopPtr ml, pa_subscription_mask mask) |
379 | +{ |
380 | + throw_if_not_on_main_loop(ml); throw_if_not_connected(ctxt); |
381 | + pa_operation_unref(pa_context_subscribe(ctxt.get(), mask, nullptr, nullptr)); |
382 | +} |
383 | + |
384 | +void get_index_of_sink_by_name_async(ContextPtr ctxt, ThreadedMainLoopPtr ml, const std::string& name, pa_sink_info_cb_t cb, void* cookie) |
385 | +{ |
386 | + throw_if_not_on_main_loop(ml); throw_if_not_connected(ctxt); |
387 | + pa_operation_unref(pa_context_get_sink_info_by_name(ctxt.get(), name.c_str(), cb, cookie)); |
388 | +} |
389 | + |
390 | +void get_sink_info_by_index_async(ContextPtr ctxt, ThreadedMainLoopPtr ml, std::int32_t index, pa_sink_info_cb_t cb, void* cookie) |
391 | +{ |
392 | + throw_if_not_on_main_loop(ml); throw_if_not_connected(ctxt); |
393 | + pa_operation_unref(pa_context_get_sink_info_by_index(ctxt.get(), index, cb, cookie)); |
394 | +} |
395 | + |
396 | +void connect_async(ContextPtr ctxt) |
397 | +{ |
398 | + pa_context_connect(ctxt.get(), nullptr, static_cast<pa_context_flags_t>(PA_CONTEXT_NOAUTOSPAWN | PA_CONTEXT_NOFAIL), nullptr); |
399 | +} |
400 | + |
401 | +bool is_port_available_on_sink(const pa_sink_info* info, const std::regex& port_pattern) |
402 | +{ |
403 | + if (not info) |
404 | + return false; |
405 | + |
406 | + for (std::uint32_t i = 0; i < info->n_ports; i++) |
407 | + { |
408 | + if (info->ports[i]->available == PA_PORT_AVAILABLE_NO || |
409 | + info->ports[i]->available == PA_PORT_AVAILABLE_UNKNOWN) |
410 | + continue; |
411 | + |
412 | + if (std::regex_match(std::string{info->ports[i]->name}, port_pattern)) |
413 | + return true; |
414 | + } |
415 | + |
416 | + return false; |
417 | +} |
418 | +} |
419 | +} |
420 | + |
421 | +struct audio::PulseAudioOutputObserver::Private |
422 | +{ |
423 | + static void context_notification_cb(pa_context* ctxt, void* cookie) |
424 | + { |
425 | + if (auto thiz = static_cast<Private*>(cookie)) |
426 | + { |
427 | + // Better safe than sorry: Check if we got signaled for the |
428 | + // context we are actually interested in. |
429 | + if (thiz->context.get() != ctxt) |
430 | + return; |
431 | + |
432 | + switch (pa_context_get_state(ctxt)) |
433 | + { |
434 | + case PA_CONTEXT_READY: |
435 | + thiz->on_context_ready(); |
436 | + break; |
437 | + case PA_CONTEXT_FAILED: |
438 | + thiz->on_context_failed(); |
439 | + break; |
440 | + default: |
441 | + break; |
442 | + } |
443 | + } |
444 | + } |
445 | + |
446 | + static void context_subscription_cb(pa_context* ctxt, pa_subscription_event_type_t ev, uint32_t idx, void* cookie) |
447 | + { |
448 | + (void) idx; |
449 | + |
450 | + if (auto thiz = static_cast<Private*>(cookie)) |
451 | + { |
452 | + // Better safe than sorry: Check if we got signaled for the |
453 | + // context we are actually interested in. |
454 | + if (thiz->context.get() != ctxt) |
455 | + return; |
456 | + |
457 | + if ((ev & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK) |
458 | + thiz->on_sink_event_with_index(idx); |
459 | + } |
460 | + } |
461 | + |
462 | + static void query_for_active_sink_finished(pa_context* ctxt, const pa_sink_info* si, int eol, void* cookie) |
463 | + { |
464 | + if (eol) |
465 | + return; |
466 | + |
467 | + if (auto thiz = static_cast<Private*>(cookie)) |
468 | + { |
469 | + // Better safe than sorry: Check if we got signaled for the |
470 | + // context we are actually interested in. |
471 | + if (thiz->context.get() != ctxt) |
472 | + return; |
473 | + |
474 | + thiz->on_query_for_active_sink_finished(si); |
475 | + } |
476 | + } |
477 | + |
478 | + static void query_for_primary_sink_finished(pa_context* ctxt, const pa_sink_info* si, int eol, void* cookie) |
479 | + { |
480 | + if (eol) |
481 | + return; |
482 | + |
483 | + if (auto thiz = static_cast<Private*>(cookie)) |
484 | + { |
485 | + // Better safe than sorry: Check if we got signaled for the |
486 | + // context we are actually interested in. |
487 | + if (thiz->context.get() != ctxt) |
488 | + return; |
489 | + |
490 | + thiz->on_query_for_primary_sink_finished(si); |
491 | + } |
492 | + } |
493 | + |
494 | + static void query_for_server_info_finished(pa_context* ctxt, const pa_server_info* si, void* cookie) |
495 | + { |
496 | + if (not si) |
497 | + return; |
498 | + |
499 | + if (auto thiz = static_cast<Private*>(cookie)) |
500 | + { |
501 | + // Better safe than sorry: Check if we got signaled for the |
502 | + // context we are actually interested in. |
503 | + if (thiz->context.get() != ctxt) |
504 | + return; |
505 | + |
506 | + thiz->on_query_for_server_info_finished(si); |
507 | + } |
508 | + } |
509 | + |
510 | + Private(const audio::PulseAudioOutputObserver::Configuration& config) |
511 | + : config(config), |
512 | + main_loop{pa::make_threaded_main_loop()}, |
513 | + context{pa::make_context(main_loop)}, |
514 | + primary_sink_index(-1), |
515 | + active_sink(std::make_tuple(-1, "")) |
516 | + { |
517 | + for (const auto& pattern : config.output_port_patterns) |
518 | + { |
519 | + outputs.emplace_back(pattern, core::Property<media::audio::OutputState>{media::audio::OutputState::Speaker}); |
520 | + std::get<1>(outputs.back()) | properties.external_output_state; |
521 | + std::get<1>(outputs.back()).changed().connect([](media::audio::OutputState state) |
522 | + { |
523 | + std::cout << "Connection state for port changed to: " << state << std::endl; |
524 | + }); |
525 | + } |
526 | + |
527 | + pa::set_state_callback(context, Private::context_notification_cb, this); |
528 | + pa::set_subscribe_callback(context, Private::context_subscription_cb, this); |
529 | + |
530 | + pa::connect_async(context); |
531 | + pa::start_main_loop(main_loop); |
532 | + } |
533 | + |
534 | + // The connection attempt has been successful and we are connected |
535 | + // to pulseaudio now. |
536 | + void on_context_ready() |
537 | + { |
538 | + config.reporter->connected_to_pulse_audio(); |
539 | + |
540 | + pa::subscribe_to_events(context, main_loop, PA_SUBSCRIPTION_MASK_SINK); |
541 | + |
542 | + if (config.sink == "query.from.server") |
543 | + { |
544 | + pa::get_server_info_async(context, main_loop, Private::query_for_server_info_finished, this); |
545 | + } |
546 | + else |
547 | + { |
548 | + properties.sink = config.sink; |
549 | + // Get primary sink index (default) |
550 | + pa::get_index_of_sink_by_name_async(context, main_loop, config.sink, Private::query_for_primary_sink_finished, this); |
551 | + // Update active sink (could be == default) |
552 | + pa::get_server_info_async(context, main_loop, Private::query_for_server_info_finished, this); |
553 | + } |
554 | + } |
555 | + |
556 | + // Either a connection attempt failed, or an existing connection |
557 | + // was unexpectedly terminated. |
558 | + void on_context_failed() |
559 | + { |
560 | + pa::connect_async(context); |
561 | + } |
562 | + |
563 | + // Something changed on the sink with index idx. |
564 | + void on_sink_event_with_index(std::int32_t index) |
565 | + { |
566 | + config.reporter->sink_event_with_index(index); |
567 | + |
568 | + // Update server info (active sink) |
569 | + pa::get_server_info_async(context, main_loop, Private::query_for_server_info_finished, this); |
570 | + |
571 | + } |
572 | + |
573 | + void on_query_for_active_sink_finished(const pa_sink_info* info) |
574 | + { |
575 | + // Update active sink if a change is registered. |
576 | + if (std::get<0>(active_sink) != info->index) |
577 | + { |
578 | + std::get<0>(active_sink) = info->index; |
579 | + std::get<1>(active_sink) = info->name; |
580 | + if (info->index != static_cast<std::uint32_t>(primary_sink_index)) |
581 | + for (auto& element : outputs) |
582 | + std::get<1>(element) = audio::OutputState::External; |
583 | + } |
584 | + } |
585 | + |
586 | + // Query for primary sink finished. |
587 | + void on_query_for_primary_sink_finished(const pa_sink_info* info) |
588 | + { |
589 | + for (auto& element : outputs) |
590 | + { |
591 | + // Only issue state change if the change happened on the active index. |
592 | + if (std::get<0>(active_sink) != info->index) |
593 | + continue; |
594 | + |
595 | + std::cout << "Checking if port is available " << " -> " << std::boolalpha << pa::is_port_available_on_sink(info, std::get<0>(element)) << std::endl; |
596 | + bool available = pa::is_port_available_on_sink(info, std::get<0>(element)); |
597 | + |
598 | + if (available) |
599 | + { |
600 | + std::get<1>(element) = audio::OutputState::Earpiece; |
601 | + continue; |
602 | + } |
603 | + |
604 | + audio::OutputState state; |
605 | + if (info->index == primary_sink_index) |
606 | + state = audio::OutputState::Speaker; |
607 | + else |
608 | + state = audio::OutputState::External; |
609 | + |
610 | + std::get<1>(element) = state; |
611 | + } |
612 | + |
613 | + std::set<Reporter::Port> known_ports; |
614 | + for (std::uint32_t i = 0; i < info->n_ports; i++) |
615 | + { |
616 | + bool is_monitored = false; |
617 | + |
618 | + for (auto& element : outputs) |
619 | + is_monitored |= std::regex_match(info->ports[i]->name, std::get<0>(element)); |
620 | + |
621 | + known_ports.insert(Reporter::Port |
622 | + { |
623 | + info->ports[i]->name, |
624 | + info->ports[i]->description, |
625 | + info->ports[i]->available == PA_PORT_AVAILABLE_YES, |
626 | + is_monitored |
627 | + }); |
628 | + } |
629 | + |
630 | + properties.known_ports = known_ports; |
631 | + |
632 | + // Initialize sink of primary index (onboard) |
633 | + if (primary_sink_index == -1) |
634 | + primary_sink_index = info->index; |
635 | + |
636 | + config.reporter->query_for_sink_info_finished(info->name, info->index, known_ports); |
637 | + } |
638 | + |
639 | + void on_query_for_server_info_finished(const pa_server_info* info) |
640 | + { |
641 | + // We bail out if we could not determine the default sink name. |
642 | + // In this case, we are not able to carry out audio output observation. |
643 | + if (not info->default_sink_name) |
644 | + { |
645 | + config.reporter->query_for_default_sink_failed(); |
646 | + return; |
647 | + } |
648 | + |
649 | + // Update active sink |
650 | + if (info->default_sink_name != std::get<1>(active_sink)) |
651 | + pa::get_index_of_sink_by_name_async(context, main_loop, info->default_sink_name, Private::query_for_active_sink_finished, this); |
652 | + |
653 | + // Update wired output for primary sink (onboard) |
654 | + pa::get_sink_info_by_index_async(context, main_loop, primary_sink_index, Private::query_for_primary_sink_finished, this); |
655 | + |
656 | + if (properties.sink.get() != config.sink) |
657 | + { |
658 | + config.reporter->query_for_default_sink_finished(info->default_sink_name); |
659 | + properties.sink = config.sink = info->default_sink_name; |
660 | + pa::get_index_of_sink_by_name_async(context, main_loop, config.sink, Private::query_for_primary_sink_finished, this); |
661 | + } |
662 | + } |
663 | + |
664 | + PulseAudioOutputObserver::Configuration config; |
665 | + pa::ThreadedMainLoopPtr main_loop; |
666 | + pa::ContextPtr context; |
667 | + std::int32_t primary_sink_index; |
668 | + std::tuple<uint32_t, std::string> active_sink; |
669 | + std::vector<std::tuple<std::regex, core::Property<media::audio::OutputState>>> outputs; |
670 | + |
671 | + struct |
672 | + { |
673 | + core::Property<std::string> sink; |
674 | + core::Property<std::set<audio::PulseAudioOutputObserver::Reporter::Port>> known_ports; |
675 | + core::Property<audio::OutputState> external_output_state{audio::OutputState::Speaker}; |
676 | + } properties; |
677 | +}; |
678 | + |
679 | +bool audio::PulseAudioOutputObserver::Reporter::Port::operator==(const audio::PulseAudioOutputObserver::Reporter::Port& rhs) const |
680 | +{ |
681 | + return name == rhs.name; |
682 | +} |
683 | + |
684 | +bool audio::PulseAudioOutputObserver::Reporter::Port::operator<(const audio::PulseAudioOutputObserver::Reporter::Port& rhs) const |
685 | +{ |
686 | + return name < rhs.name; |
687 | +} |
688 | + |
689 | +audio::PulseAudioOutputObserver::Reporter::~Reporter() |
690 | +{ |
691 | +} |
692 | + |
693 | +void audio::PulseAudioOutputObserver::Reporter::connected_to_pulse_audio() |
694 | +{ |
695 | +} |
696 | + |
697 | +void audio::PulseAudioOutputObserver::Reporter::query_for_default_sink_failed() |
698 | +{ |
699 | +} |
700 | + |
701 | +void audio::PulseAudioOutputObserver::Reporter::query_for_default_sink_finished(const std::string&) |
702 | +{ |
703 | +} |
704 | + |
705 | +void audio::PulseAudioOutputObserver::Reporter::query_for_sink_info_finished(const std::string&, std::uint32_t, const std::set<Port>&) |
706 | +{ |
707 | +} |
708 | + |
709 | +void audio::PulseAudioOutputObserver::Reporter::sink_event_with_index(std::uint32_t) |
710 | +{ |
711 | +} |
712 | + |
713 | +// Constructs a new instance, or throws std::runtime_error |
714 | +// if connection to pulseaudio fails. |
715 | +audio::PulseAudioOutputObserver::PulseAudioOutputObserver(const Configuration& config) : d{new Private{config}} |
716 | +{ |
717 | + if (not d->config.reporter) throw std::runtime_error |
718 | + { |
719 | + "PulseAudioOutputObserver: Cannot construct for invalid reporter instance." |
720 | + }; |
721 | +} |
722 | + |
723 | +// We provide the name of the sink we are connecting to as a |
724 | +// getable/observable property. This is specifically meant for |
725 | +// consumption by test code. |
726 | +const core::Property<std::string>& audio::PulseAudioOutputObserver::sink() const |
727 | +{ |
728 | + return d->properties.sink; |
729 | +} |
730 | + |
731 | +// The set of ports that have been identified on the configured sink. |
732 | +// Specifically meant for consumption by test code. |
733 | +const core::Property<std::set<audio::PulseAudioOutputObserver::Reporter::Port>>& audio::PulseAudioOutputObserver::known_ports() const |
734 | +{ |
735 | + return d->properties.known_ports; |
736 | +} |
737 | + |
738 | +// Getable/observable property holding the state of external outputs. |
739 | +const core::Property<audio::OutputState>& audio::PulseAudioOutputObserver::external_output_state() const |
740 | +{ |
741 | + return d->properties.external_output_state; |
742 | +} |
743 | |
744 | === added file 'src/core/media/audio/pulse_audio_output_observer.h' |
745 | --- src/core/media/audio/pulse_audio_output_observer.h 1970-01-01 00:00:00 +0000 |
746 | +++ src/core/media/audio/pulse_audio_output_observer.h 2015-03-19 00:04:25 +0000 |
747 | @@ -0,0 +1,128 @@ |
748 | +/* |
749 | + * Copyright © 2014 Canonical Ltd. |
750 | + * |
751 | + * This program is free software: you can redistribute it and/or modify it |
752 | + * under the terms of the GNU Lesser General Public License version 3, |
753 | + * as published by the Free Software Foundation. |
754 | + * |
755 | + * This program is distributed in the hope that it will be useful, |
756 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
757 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
758 | + * GNU Lesser General Public License for more details. |
759 | + * |
760 | + * You should have received a copy of the GNU Lesser General Public License |
761 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
762 | + * |
763 | + * Authored by: Thomas Voß <thomas.voss@canonical.com> |
764 | + * Ricardo Mendoza <ricardo.mendoza@canonical.com> |
765 | + */ |
766 | +#ifndef CORE_UBUNTU_MEDIA_AUDIO_PULSE_AUDIO_OUTPUT_OBSERVER_H_ |
767 | +#define CORE_UBUNTU_MEDIA_AUDIO_PULSE_AUDIO_OUTPUT_OBSERVER_H_ |
768 | + |
769 | +#include <core/media/audio/output_observer.h> |
770 | + |
771 | +#include <iosfwd> |
772 | +#include <memory> |
773 | +#include <regex> |
774 | + |
775 | +namespace core |
776 | +{ |
777 | +namespace ubuntu |
778 | +{ |
779 | +namespace media |
780 | +{ |
781 | +namespace audio |
782 | +{ |
783 | +// Implements the audio::OutputObserver interface |
784 | +// relying on pulse to query the connected ports |
785 | +// of the primary card of the system. |
786 | +class PulseAudioOutputObserver : public OutputObserver |
787 | +{ |
788 | +public: |
789 | + // Save us some typing. |
790 | + typedef std::shared_ptr<PulseAudioOutputObserver> Ptr; |
791 | + |
792 | + // Reporter is responsible for surfacing events from the implementation |
793 | + // that help in resolving/tracking down issues. Default implementation is empty. |
794 | + struct Reporter |
795 | + { |
796 | + // To save us some typing. |
797 | + typedef std::shared_ptr<Reporter> Ptr; |
798 | + |
799 | + // Simple type to help in reporting. |
800 | + struct Port |
801 | + { |
802 | + // Returns true iff the name of both ports are equal. |
803 | + bool operator==(const Port& rhs) const; |
804 | + // Returns true iff the name of the ports differ. |
805 | + bool operator<(const Port& rhs) const; |
806 | + |
807 | + std::string name; // The name of the port. |
808 | + std::string description; // Human-readable description of the port. |
809 | + bool is_available; // True if the port is available. |
810 | + bool is_monitored; // True if the port is monitored by the observer. |
811 | + }; |
812 | + |
813 | + virtual ~Reporter(); |
814 | + // connected_to_pulse_audio is called when a connection with pulse has been established. |
815 | + virtual void connected_to_pulse_audio(); |
816 | + // query_for_default_sink_failed is called when no default sink was returned. |
817 | + virtual void query_for_default_sink_failed(); |
818 | + // query_for_default_sink_finished is called when the default sink query against pulse |
819 | + // has finished, reporting the name of the sink to observers. |
820 | + virtual void query_for_default_sink_finished(const std::string& sink_name); |
821 | + // query_for_sink_info_finished is called when a query for information about a specific sink |
822 | + // has finished, reporting the name, index of the sink as well as the set of ports known to the sink. |
823 | + virtual void query_for_sink_info_finished(const std::string& name, std::uint32_t index, const std::set<Port>& known_ports); |
824 | + // sink_event_with_index is called when something happened on a sink, reporing the index of the |
825 | + // sink. |
826 | + virtual void sink_event_with_index(std::uint32_t index); |
827 | + }; |
828 | + |
829 | + // Construction time arguments go here |
830 | + struct Configuration |
831 | + { |
832 | + // Name of the sink that we should consider. |
833 | + std::string sink |
834 | + { |
835 | + // A special value that requests the implementation to |
836 | + // query pulseaudio for the default configured sink. |
837 | + "query.from.server" |
838 | + }; |
839 | + // Output port name patterns that should be observed on the configured sink. |
840 | + // All patterns have to be valid regular expressions. |
841 | + std::vector<std::regex> output_port_patterns |
842 | + { |
843 | + // Any port is considered with this special value. |
844 | + std::regex{".+"} |
845 | + }; |
846 | + // The Reporter instance that the implementation reports |
847 | + // events to. Must not be null. |
848 | + Reporter::Ptr reporter{std::make_shared<Reporter>()}; |
849 | + }; |
850 | + |
851 | + // Constructs a new instance, throws: |
852 | + // * std::runtime_error if connection to pulseaudio fails. |
853 | + // * std::runtime_error if reporter instance is null. |
854 | + PulseAudioOutputObserver(const Configuration&); |
855 | + |
856 | + // We provide the name of the sink we are connecting to as a |
857 | + // getable/observable property. This is specifically meant for |
858 | + // consumption by test code. |
859 | + const core::Property<std::string>& sink() const; |
860 | + // The set of ports that have been identified on the configured sink. |
861 | + // Specifically meant for consumption by test code. |
862 | + const core::Property<std::set<Reporter::Port>>& known_ports() const; |
863 | + // Getable/observable property holding the state of external outputs. |
864 | + const core::Property<OutputState>& external_output_state() const override; |
865 | + |
866 | +private: |
867 | + struct Private; |
868 | + std::shared_ptr<Private> d; |
869 | +}; |
870 | +} |
871 | +} |
872 | +} |
873 | +} |
874 | + |
875 | +#endif // CORE_UBUNTU_MEDIA_AUDIO_PULSE_AUDIO_OUTPUT_OBSERVER_H_ |
876 | |
877 | === modified file 'src/core/media/gstreamer/playbin.cpp' |
878 | --- src/core/media/gstreamer/playbin.cpp 2015-03-19 00:04:24 +0000 |
879 | +++ src/core/media/gstreamer/playbin.cpp 2015-03-19 00:04:25 +0000 |
880 | @@ -406,6 +406,9 @@ |
881 | }; |
882 | |
883 | auto ret = gst_element_set_state(pipeline, new_state); |
884 | + |
885 | + std::cout << __PRETTY_FUNCTION__ << ": requested state change." << std::endl; |
886 | + |
887 | bool result = false; GstState current, pending; |
888 | switch(ret) |
889 | { |
890 | @@ -419,7 +422,7 @@ |
891 | pipeline, |
892 | ¤t, |
893 | &pending, |
894 | - state_change_timeout.count()); |
895 | + state_change_timeout.count()); |
896 | break; |
897 | } |
898 | |
899 | |
900 | === modified file 'src/core/media/service_implementation.cpp' |
901 | --- src/core/media/service_implementation.cpp 2015-03-19 00:04:24 +0000 |
902 | +++ src/core/media/service_implementation.cpp 2015-03-19 00:04:25 +0000 |
903 | @@ -22,6 +22,7 @@ |
904 | |
905 | #include "service_implementation.h" |
906 | |
907 | +#include "audio/output_observer.h" |
908 | #include "client_death_observer.h" |
909 | #include "call-monitor/call_monitor.h" |
910 | #include "player_configuration.h" |
911 | @@ -57,349 +58,10 @@ |
912 | display_state_lock(power_state_controller->display_state_lock()), |
913 | client_death_observer(media::platform_default_client_death_observer()), |
914 | recorder_observer(media::make_platform_default_recorder_observer()), |
915 | - pulse_mainloop_api(nullptr), |
916 | - pulse_context(nullptr), |
917 | - headphones_connected(false), |
918 | - a2dp_connected(false), |
919 | - primary_idx(-1), |
920 | + audio_output_observer(media::audio::make_platform_default_output_observer()), |
921 | + audio_output_state(media::audio::OutputState::Speaker), |
922 | call_monitor(new CallMonitor) |
923 | { |
924 | - // Spawn pulse watchdog |
925 | - pulse_mainloop = nullptr; |
926 | - pulse_worker = std::move(std::thread([this]() |
927 | - { |
928 | - std::unique_lock<std::mutex> lk(pulse_mutex); |
929 | - pcv.wait(lk, |
930 | - [this]{ |
931 | - if (pulse_mainloop != nullptr || pulse_context != nullptr) |
932 | - { |
933 | - // We come from instance death, destroy and create. |
934 | - if (pulse_context != nullptr) |
935 | - { |
936 | - pa_threaded_mainloop_lock(pulse_mainloop); |
937 | - pa_operation *o; |
938 | - o = pa_context_drain(pulse_context, |
939 | - [](pa_context *context, void *userdata) |
940 | - { |
941 | - (void) context; |
942 | - |
943 | - Private *p = reinterpret_cast<Private*>(userdata); |
944 | - pa_threaded_mainloop_signal(p->mainloop(), 0); |
945 | - }, this); |
946 | - |
947 | - if (o) |
948 | - { |
949 | - while (pa_operation_get_state(o) == PA_OPERATION_RUNNING) |
950 | - pa_threaded_mainloop_wait(pulse_mainloop); |
951 | - |
952 | - pa_operation_unref(o); |
953 | - } |
954 | - |
955 | - pa_context_set_state_callback(pulse_context, NULL, NULL); |
956 | - pa_context_set_subscribe_callback(pulse_context, NULL, NULL); |
957 | - pa_context_disconnect(pulse_context); |
958 | - pa_context_unref(pulse_context); |
959 | - pulse_context = nullptr; |
960 | - pa_threaded_mainloop_unlock(pulse_mainloop); |
961 | - } |
962 | - } |
963 | - |
964 | - if (pulse_mainloop == nullptr) |
965 | - { |
966 | - pulse_mainloop = pa_threaded_mainloop_new(); |
967 | - |
968 | - if (pa_threaded_mainloop_start(pulse_mainloop) != 0) |
969 | - { |
970 | - std::cerr << "Unable to start pulseaudio mainloop, audio output detection will not function" << std::endl; |
971 | - pa_threaded_mainloop_free(pulse_mainloop); |
972 | - pulse_mainloop = nullptr; |
973 | - } |
974 | - } |
975 | - |
976 | - do { |
977 | - create_pulse_context(); |
978 | - } while (pulse_context == nullptr); |
979 | - |
980 | - // Wait for next instance death. |
981 | - return false; |
982 | - }); |
983 | - })); |
984 | - |
985 | - recorder_observer->recording_state().changed().connect([this](media::RecordingState state) |
986 | - { |
987 | - media_recording_state_changed(state); |
988 | - }); |
989 | - } |
990 | - |
991 | - ~Private() |
992 | - { |
993 | - release_pulse_context(); |
994 | - |
995 | - if (pulse_mainloop != nullptr) |
996 | - { |
997 | - pa_threaded_mainloop_stop(pulse_mainloop); |
998 | - pa_threaded_mainloop_free(pulse_mainloop); |
999 | - pulse_mainloop = nullptr; |
1000 | - } |
1001 | - |
1002 | - if (pulse_worker.joinable()) |
1003 | - pulse_worker.join(); |
1004 | - } |
1005 | - |
1006 | - void media_recording_state_changed(media::RecordingState state) |
1007 | - { |
1008 | - if (state == media::RecordingState::started) |
1009 | - { |
1010 | - display_state_lock->request_acquire(media::power::DisplayState::on); |
1011 | - pause_playback(); |
1012 | - } |
1013 | - else if (state == media::RecordingState::stopped) |
1014 | - { |
1015 | - display_state_lock->request_release(media::power::DisplayState::on); |
1016 | - } |
1017 | - } |
1018 | - |
1019 | - pa_threaded_mainloop *mainloop() |
1020 | - { |
1021 | - return pulse_mainloop; |
1022 | - } |
1023 | - |
1024 | - bool is_port_available(pa_card_port_info **ports, uint32_t n_ports, const char *name) |
1025 | - { |
1026 | - bool ret = false; |
1027 | - |
1028 | - if (ports != nullptr && n_ports > 0 && name != nullptr) |
1029 | - { |
1030 | - for (uint32_t i=0; i<n_ports; i++) |
1031 | - { |
1032 | - if (strstr(ports[i]->name, name) != nullptr && ports[i]->available != PA_PORT_AVAILABLE_NO) |
1033 | - { |
1034 | - ret = true; |
1035 | - break; |
1036 | - } |
1037 | - } |
1038 | - } |
1039 | - |
1040 | - return ret; |
1041 | - } |
1042 | - |
1043 | - void update_wired_output() |
1044 | - { |
1045 | - const pa_operation *o = pa_context_get_card_info_by_index(pulse_context, primary_idx, |
1046 | - [](pa_context *context, const pa_card_info *info, int eol, void *userdata) |
1047 | - { |
1048 | - (void) context; |
1049 | - (void) eol; |
1050 | - |
1051 | - if (info == nullptr || userdata == nullptr) |
1052 | - return; |
1053 | - |
1054 | - Private *p = reinterpret_cast<Private*>(userdata); |
1055 | - if (p->is_port_available(info->ports, info->n_ports, "output-wired")) |
1056 | - { |
1057 | - if (!p->headphones_connected) |
1058 | - std::cout << "Wired headphones connected" << std::endl; |
1059 | - p->headphones_connected = true; |
1060 | - } |
1061 | - else if (p->headphones_connected == true) |
1062 | - { |
1063 | - std::cout << "Wired headphones disconnected" << std::endl; |
1064 | - p->headphones_connected = false; |
1065 | - p->pause_playback_if_necessary(std::get<0>(p->active_sink)); |
1066 | - } |
1067 | - }, this); |
1068 | - (void) o; |
1069 | - } |
1070 | - |
1071 | - void pause_playback_if_necessary(int index) |
1072 | - { |
1073 | - // Catch uninitialized case (active index == -1) |
1074 | - if (std::get<0>(active_sink) == -1) |
1075 | - return; |
1076 | - |
1077 | - if (headphones_connected) |
1078 | - return; |
1079 | - |
1080 | - // No headphones/fallback on primary sink? Pause. |
1081 | - if (index == primary_idx) |
1082 | - pause_playback(); |
1083 | - } |
1084 | - |
1085 | - void set_active_sink(const char *name) |
1086 | - { |
1087 | - const pa_operation *o = pa_context_get_sink_info_by_name(pulse_context, name, |
1088 | - [](pa_context *context, const pa_sink_info *i, int eol, void *userdata) |
1089 | - { |
1090 | - (void) context; |
1091 | - |
1092 | - if (eol) |
1093 | - return; |
1094 | - |
1095 | - Private *p = reinterpret_cast<Private*>(userdata); |
1096 | - std::tuple<uint32_t, uint32_t, std::string> new_sink(std::make_tuple(i->index, i->card, i->name)); |
1097 | - |
1098 | - printf("pulsesink: active_sink=('%s',%d,%d) -> ('%s',%d,%d)\n", |
1099 | - std::get<2>(p->active_sink).c_str(), std::get<0>(p->active_sink), |
1100 | - std::get<1>(p->active_sink), i->name, i->index, i->card); |
1101 | - |
1102 | - p->pause_playback_if_necessary(i->index); |
1103 | - p->active_sink = new_sink; |
1104 | - }, this); |
1105 | - |
1106 | - (void) o; |
1107 | - } |
1108 | - |
1109 | - void update_active_sink() |
1110 | - { |
1111 | - const pa_operation *o = pa_context_get_server_info(pulse_context, |
1112 | - [](pa_context *context, const pa_server_info *i, void *userdata) |
1113 | - { |
1114 | - (void) context; |
1115 | - |
1116 | - Private *p = reinterpret_cast<Private*>(userdata); |
1117 | - if (i->default_sink_name != std::get<2>(p->active_sink)) |
1118 | - p->set_active_sink(i->default_sink_name); |
1119 | - p->update_wired_output(); |
1120 | - }, this); |
1121 | - |
1122 | - (void) o; |
1123 | - } |
1124 | - |
1125 | - void create_pulse_context() |
1126 | - { |
1127 | - if (pulse_context != nullptr) |
1128 | - return; |
1129 | - |
1130 | - active_sink = std::make_tuple(-1, -1, ""); |
1131 | - |
1132 | - bool keep_going = true, ok = true; |
1133 | - |
1134 | - pulse_mainloop_api = pa_threaded_mainloop_get_api(pulse_mainloop); |
1135 | - pa_threaded_mainloop_lock(pulse_mainloop); |
1136 | - |
1137 | - pulse_context = pa_context_new(pulse_mainloop_api, "MediaHubPulseContext"); |
1138 | - pa_context_set_state_callback(pulse_context, |
1139 | - [](pa_context *context, void *userdata) |
1140 | - { |
1141 | - (void) context; |
1142 | - Private *p = reinterpret_cast<Private*>(userdata); |
1143 | - // Signals the pa_threaded_mainloop_wait below to proceed |
1144 | - pa_threaded_mainloop_signal(p->mainloop(), 0); |
1145 | - }, this); |
1146 | - |
1147 | - if (pulse_context == nullptr) |
1148 | - { |
1149 | - std::cerr << "Unable to create new pulseaudio context" << std::endl; |
1150 | - pa_threaded_mainloop_unlock(pulse_mainloop); |
1151 | - return; |
1152 | - } |
1153 | - |
1154 | - pa_context_connect(pulse_context, nullptr, pa_context_flags_t((int) PA_CONTEXT_NOAUTOSPAWN | (int) PA_CONTEXT_NOFAIL), nullptr); |
1155 | - pa_threaded_mainloop_wait(pulse_mainloop); |
1156 | - |
1157 | - while (keep_going) |
1158 | - { |
1159 | - switch (pa_context_get_state(pulse_context)) |
1160 | - { |
1161 | - case PA_CONTEXT_CONNECTING: // Wait for service to be available |
1162 | - case PA_CONTEXT_AUTHORIZING: |
1163 | - case PA_CONTEXT_SETTING_NAME: |
1164 | - break; |
1165 | - |
1166 | - case PA_CONTEXT_READY: |
1167 | - std::cout << "Pulseaudio connection established." << std::endl; |
1168 | - keep_going = false; |
1169 | - break; |
1170 | - |
1171 | - case PA_CONTEXT_FAILED: |
1172 | - case PA_CONTEXT_TERMINATED: |
1173 | - keep_going = false; |
1174 | - ok = false; |
1175 | - break; |
1176 | - |
1177 | - default: |
1178 | - std::cerr << "Pulseaudio connection failure: " << pa_strerror(pa_context_errno(pulse_context)); |
1179 | - keep_going = false; |
1180 | - ok = false; |
1181 | - } |
1182 | - |
1183 | - if (keep_going) |
1184 | - pa_threaded_mainloop_wait(pulse_mainloop); |
1185 | - } |
1186 | - |
1187 | - if (ok) |
1188 | - { |
1189 | - pa_context_set_state_callback(pulse_context, |
1190 | - [](pa_context *context, void *userdata) |
1191 | - { |
1192 | - (void) context; |
1193 | - (void) userdata; |
1194 | - Private *p = reinterpret_cast<Private*>(userdata); |
1195 | - std::unique_lock<std::mutex> lk(p->pulse_mutex); |
1196 | - switch (pa_context_get_state(context)) |
1197 | - { |
1198 | - case PA_CONTEXT_FAILED: |
1199 | - case PA_CONTEXT_TERMINATED: |
1200 | - p->pcv.notify_all(); |
1201 | - break; |
1202 | - default: |
1203 | - break; |
1204 | - } |
1205 | - }, this); |
1206 | - |
1207 | - //FIXME: Get index for "sink.primary", the default onboard card on Touch. |
1208 | - pa_context_get_sink_info_by_name(pulse_context, "sink.primary", |
1209 | - [](pa_context *context, const pa_sink_info *i, int eol, void *userdata) |
1210 | - { |
1211 | - (void) context; |
1212 | - |
1213 | - if (eol) |
1214 | - return; |
1215 | - |
1216 | - Private *p = reinterpret_cast<Private*>(userdata); |
1217 | - p->primary_idx = i->index; |
1218 | - p->update_wired_output(); |
1219 | - }, this); |
1220 | - |
1221 | - update_active_sink(); |
1222 | - |
1223 | - pa_context_set_subscribe_callback(pulse_context, |
1224 | - [](pa_context *context, pa_subscription_event_type_t t, uint32_t idx, void *userdata) |
1225 | - { |
1226 | - (void) context; |
1227 | - (void) idx; |
1228 | - |
1229 | - if (userdata == nullptr) |
1230 | - return; |
1231 | - |
1232 | - Private *p = reinterpret_cast<Private*>(userdata); |
1233 | - if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK) |
1234 | - { |
1235 | - p->update_active_sink(); |
1236 | - } |
1237 | - }, this); |
1238 | - pa_context_subscribe(pulse_context, PA_SUBSCRIPTION_MASK_SINK, nullptr, this); |
1239 | - } |
1240 | - else |
1241 | - { |
1242 | - std::cerr << "Connection to pulseaudio failed or was dropped." << std::endl; |
1243 | - pa_context_unref(pulse_context); |
1244 | - pulse_context = nullptr; |
1245 | - } |
1246 | - |
1247 | - pa_threaded_mainloop_unlock(pulse_mainloop); |
1248 | - } |
1249 | - |
1250 | - void release_pulse_context() |
1251 | - { |
1252 | - if (pulse_context != nullptr) |
1253 | - { |
1254 | - pa_threaded_mainloop_lock(pulse_mainloop); |
1255 | - pa_context_disconnect(pulse_context); |
1256 | - pa_context_unref(pulse_context); |
1257 | - pa_threaded_mainloop_unlock(pulse_mainloop); |
1258 | - pulse_context = nullptr; |
1259 | - } |
1260 | } |
1261 | |
1262 | media::ServiceImplementation::Configuration configuration; |
1263 | @@ -411,22 +73,9 @@ |
1264 | media::power::StateController::Lock<media::power::DisplayState>::Ptr display_state_lock; |
1265 | media::ClientDeathObserver::Ptr client_death_observer; |
1266 | media::RecorderObserver::Ptr recorder_observer; |
1267 | - // Pulse-specific |
1268 | - pa_mainloop_api *pulse_mainloop_api; |
1269 | - pa_threaded_mainloop *pulse_mainloop; |
1270 | - pa_context *pulse_context; |
1271 | - std::thread pulse_worker; |
1272 | - std::mutex pulse_mutex; |
1273 | - std::condition_variable pcv; |
1274 | - bool headphones_connected; |
1275 | - bool a2dp_connected; |
1276 | - std::tuple<int, int, std::string> active_sink; |
1277 | - int primary_idx; |
1278 | + media::audio::OutputObserver::Ptr audio_output_observer; |
1279 | + media::audio::OutputState audio_output_state; |
1280 | |
1281 | - // Gets signaled when both the headphone jack is removed or an A2DP device is |
1282 | - // disconnected and playback needs pausing. Also gets signaled when recording |
1283 | - // begins. |
1284 | - core::Signal<void> pause_playback; |
1285 | std::unique_ptr<CallMonitor> call_monitor; |
1286 | std::list<media::Player::PlayerKey> paused_sessions; |
1287 | }; |
1288 | @@ -456,10 +105,22 @@ |
1289 | resume_multimedia_session(); |
1290 | }); |
1291 | |
1292 | - d->pause_playback.connect([this]() |
1293 | + d->audio_output_observer->external_output_state().changed().connect([this](audio::OutputState state) |
1294 | { |
1295 | - std::cout << "Got pause_playback signal, pausing all multimedia sessions" << std::endl; |
1296 | - pause_all_multimedia_sessions(); |
1297 | + switch (state) |
1298 | + { |
1299 | + case audio::OutputState::Earpiece: |
1300 | + std::cout << "AudioOutputObserver reports that output is now Headphones/Headset." << std::endl; |
1301 | + break; |
1302 | + case audio::OutputState::Speaker: |
1303 | + std::cout << "AudioOutputObserver reports that output is now Speaker." << std::endl; |
1304 | + pause_all_multimedia_sessions(); |
1305 | + break; |
1306 | + case audio::OutputState::External: |
1307 | + std::cout << "AudioOutputObserver reports that output is now External." << std::endl; |
1308 | + break; |
1309 | + } |
1310 | + d->audio_output_state = state; |
1311 | }); |
1312 | |
1313 | d->call_monitor->on_change([this](CallMonitor::State state) { |
1314 | @@ -475,6 +136,19 @@ |
1315 | break; |
1316 | } |
1317 | }); |
1318 | + |
1319 | + d->recorder_observer->recording_state().changed().connect([this](RecordingState state) |
1320 | + { |
1321 | + if (state == media::RecordingState::started) |
1322 | + { |
1323 | + d->display_state_lock->request_acquire(media::power::DisplayState::on); |
1324 | + pause_all_multimedia_sessions(); |
1325 | + } |
1326 | + else if (state == media::RecordingState::stopped) |
1327 | + { |
1328 | + d->display_state_lock->request_release(media::power::DisplayState::on); |
1329 | + } |
1330 | + }); |
1331 | } |
1332 | |
1333 | media::ServiceImplementation::~ServiceImplementation() |
FAILED: Continuous integration, rev:107 jenkins. qa.ubuntu. com/job/ media-hub- ci/183/ jenkins. qa.ubuntu. com/job/ media-hub- vivid-amd64- ci/23/console jenkins. qa.ubuntu. com/job/ media-hub- vivid-armhf- ci/23/console jenkins. qa.ubuntu. com/job/ media-hub- vivid-i386- ci/23/console
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/media- hub-ci/ 183/rebuild
http://