Music Hub ..
A session-wide music playback service
pulse_audio_output_observer.cpp
Go to the documentation of this file.
1/*
2 * Copyright © 2014 Canonical Ltd.
3 *
4 * This program is free software: you can redistribute it and/or modify it
5 * under the terms of the GNU Lesser General Public License version 3,
6 * as published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU Lesser General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authored by: Thomas Voß <thomas.voss@canonical.com>
17 * Ricardo Mendoza <ricardo.mendoza@canonical.com>
18 */
19
21
22#include <pulse/pulseaudio.h>
23
25
26#include <cstdint>
27
28#include <map>
29#include <regex>
30#include <string>
31
33
34namespace
35{
36// We wrap calls to the pulseaudio client api into its
37// own namespace and make sure that only managed types
38// can be passed to calls to pulseaudio. In addition,
39// we add guards to the function calls to ensure that
40// they are conly called on the correct thread.
41namespace pa
42{
43typedef std::shared_ptr<pa_threaded_mainloop> ThreadedMainLoopPtr;
44ThreadedMainLoopPtr make_threaded_main_loop()
45{
46 return ThreadedMainLoopPtr
47 {
48 pa_threaded_mainloop_new(),
49 [](pa_threaded_mainloop* ml)
50 {
51 pa_threaded_mainloop_stop(ml);
52 pa_threaded_mainloop_free(ml);
53 }
54 };
55}
56
57void start_main_loop(ThreadedMainLoopPtr ml)
58{
59 pa_threaded_mainloop_start(ml.get());
60}
61
62typedef std::shared_ptr<pa_context> ContextPtr;
63ContextPtr make_context(ThreadedMainLoopPtr main_loop)
64{
65 return ContextPtr
66 {
67 pa_context_new(pa_threaded_mainloop_get_api(main_loop.get()), "MediaHubPulseContext"),
68 pa_context_unref
69 };
70}
71
72void set_state_callback(ContextPtr ctxt, pa_context_notify_cb_t cb, void* cookie)
73{
74 pa_context_set_state_callback(ctxt.get(), cb, cookie);
75}
76
77void set_subscribe_callback(ContextPtr ctxt, pa_context_subscribe_cb_t cb, void* cookie)
78{
79 pa_context_set_subscribe_callback(ctxt.get(), cb, cookie);
80}
81
82void throw_if_not_on_main_loop(ThreadedMainLoopPtr ml)
83{
84 if (not pa_threaded_mainloop_in_thread(ml.get())) throw std::logic_error
85 {
86 "Attempted to call into a pulseaudio object from another"
87 "thread than the pulseaudio mainloop thread."
88 };
89}
90
91void throw_if_not_connected(ContextPtr ctxt)
92{
93 if (pa_context_get_state(ctxt.get()) != PA_CONTEXT_READY ) throw std::logic_error
94 {
95 "Attempted to issue a call against pulseaudio via a non-connected context."
96 };
97}
98
99void get_server_info_async(ContextPtr ctxt, ThreadedMainLoopPtr ml, pa_server_info_cb_t cb, void* cookie)
100{
101 throw_if_not_on_main_loop(ml); throw_if_not_connected(ctxt);
102 pa_operation_unref(pa_context_get_server_info(ctxt.get(), cb, cookie));
103}
104
105void subscribe_to_events(ContextPtr ctxt, ThreadedMainLoopPtr ml, pa_subscription_mask mask)
106{
107 throw_if_not_on_main_loop(ml); throw_if_not_connected(ctxt);
108 pa_operation_unref(pa_context_subscribe(ctxt.get(), mask, nullptr, nullptr));
109}
110
111void get_index_of_sink_by_name_async(ContextPtr ctxt, ThreadedMainLoopPtr ml, const std::string& name, pa_sink_info_cb_t cb, void* cookie)
112{
113 throw_if_not_on_main_loop(ml); throw_if_not_connected(ctxt);
114 pa_operation_unref(pa_context_get_sink_info_by_name(ctxt.get(), name.c_str(), cb, cookie));
115}
116
117void get_sink_info_by_index_async(ContextPtr ctxt, ThreadedMainLoopPtr ml, std::int32_t index, pa_sink_info_cb_t cb, void* cookie)
118{
119 throw_if_not_on_main_loop(ml); throw_if_not_connected(ctxt);
120 pa_operation_unref(pa_context_get_sink_info_by_index(ctxt.get(), index, cb, cookie));
121}
122
123void connect_async(ContextPtr ctxt)
124{
125 pa_context_connect(ctxt.get(), nullptr, static_cast<pa_context_flags_t>(PA_CONTEXT_NOAUTOSPAWN | PA_CONTEXT_NOFAIL), nullptr);
126}
127
128bool is_port_available_on_sink(const pa_sink_info* info, const std::regex& port_pattern)
129{
130 if (not info)
131 return false;
132
133 for (std::uint32_t i = 0; i < info->n_ports; i++)
134 {
135 if (info->ports[i]->available == PA_PORT_AVAILABLE_NO ||
136 info->ports[i]->available == PA_PORT_AVAILABLE_UNKNOWN)
137 continue;
138
139 if (std::regex_match(std::string{info->ports[i]->name}, port_pattern))
140 return true;
141 }
142
143 return false;
144}
145}
146}
147
149{
150 static void context_notification_cb(pa_context* ctxt, void* cookie)
151 {
152 if (auto thiz = static_cast<Private*>(cookie))
153 {
154 // Better safe than sorry: Check if we got signaled for the
155 // context we are actually interested in.
156 if (thiz->context.get() != ctxt)
157 return;
158
159 switch (pa_context_get_state(ctxt))
160 {
161 case PA_CONTEXT_READY:
162 thiz->on_context_ready();
163 break;
164 case PA_CONTEXT_FAILED:
165 thiz->on_context_failed();
166 break;
167 default:
168 break;
169 }
170 }
171 }
172
173 static void context_subscription_cb(pa_context* ctxt, pa_subscription_event_type_t ev, uint32_t idx, void* cookie)
174 {
175 (void) idx;
176
177 if (auto thiz = static_cast<Private*>(cookie))
178 {
179 // Better safe than sorry: Check if we got signaled for the
180 // context we are actually interested in.
181 if (thiz->context.get() != ctxt)
182 return;
183
184 if ((ev & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK)
185 thiz->on_sink_event_with_index(idx);
186 }
187 }
188
189 static void query_for_active_sink_finished(pa_context* ctxt, const pa_sink_info* si, int eol, void* cookie)
190 {
191 if (eol)
192 return;
193
194 if (auto thiz = static_cast<Private*>(cookie))
195 {
196 // Better safe than sorry: Check if we got signaled for the
197 // context we are actually interested in.
198 if (thiz->context.get() != ctxt)
199 return;
200
201 thiz->on_query_for_active_sink_finished(si);
202 }
203 }
204
205 static void query_for_primary_sink_finished(pa_context* ctxt, const pa_sink_info* si, int eol, void* cookie)
206 {
207 if (eol)
208 return;
209
210 if (auto thiz = static_cast<Private*>(cookie))
211 {
212 // Better safe than sorry: Check if we got signaled for the
213 // context we are actually interested in.
214 if (thiz->context.get() != ctxt)
215 return;
216
217 thiz->on_query_for_primary_sink_finished(si);
218 }
219 }
220
221 static void query_for_server_info_finished(pa_context* ctxt, const pa_server_info* si, void* cookie)
222 {
223 if (not si)
224 return;
225
226 if (auto thiz = static_cast<Private*>(cookie))
227 {
228 // Better safe than sorry: Check if we got signaled for the
229 // context we are actually interested in.
230 if (thiz->context.get() != ctxt)
231 return;
232
233 thiz->on_query_for_server_info_finished(si);
234 }
235 }
236
237 Private(const audio::PulseAudioOutputObserver::Configuration& config)
238 : config(config),
239 main_loop{pa::make_threaded_main_loop()},
240 context{pa::make_context(main_loop)},
242 active_sink(std::make_tuple(-1, ""))
243 {
244 for (const auto& pattern : config.output_port_patterns)
245 {
246 outputs.emplace_back(pattern, core::Property<media::audio::OutputState>{media::audio::OutputState::Speaker});
247 std::get<1>(outputs.back()) | properties.external_output_state;
248 std::get<1>(outputs.back()).changed().connect([](media::audio::OutputState state)
249 {
250 MH_DEBUG("Connection state for port changed to: %s", state);
251 });
252 }
253
254 pa::set_state_callback(context, Private::context_notification_cb, this);
255 pa::set_subscribe_callback(context, Private::context_subscription_cb, this);
256
257 pa::connect_async(context);
258 pa::start_main_loop(main_loop);
259 }
260
261 // The connection attempt has been successful and we are connected
262 // to pulseaudio now.
264 {
265 config.reporter->connected_to_pulse_audio();
266
267 pa::subscribe_to_events(context, main_loop, PA_SUBSCRIPTION_MASK_SINK);
268
269 if (config.sink == "query.from.server")
270 {
271 pa::get_server_info_async(context, main_loop, Private::query_for_server_info_finished, this);
272 }
273 else
274 {
275 properties.sink = config.sink;
276 // Get primary sink index (default)
277 pa::get_index_of_sink_by_name_async(context, main_loop, config.sink, Private::query_for_primary_sink_finished, this);
278 // Update active sink (could be == default)
279 pa::get_server_info_async(context, main_loop, Private::query_for_server_info_finished, this);
280 }
281 }
282
283 // Either a connection attempt failed, or an existing connection
284 // was unexpectedly terminated.
286 {
287 pa::connect_async(context);
288 }
289
290 // Something changed on the sink with index idx.
291 void on_sink_event_with_index(std::int32_t index)
292 {
293 config.reporter->sink_event_with_index(index);
294
295 // Update server info (active sink)
296 pa::get_server_info_async(context, main_loop, Private::query_for_server_info_finished, this);
297
298 }
299
300 void on_query_for_active_sink_finished(const pa_sink_info* info)
301 {
302 // Update active sink if a change is registered.
303 if (std::get<0>(active_sink) != info->index)
304 {
305 std::get<0>(active_sink) = info->index;
306 std::get<1>(active_sink) = info->name;
307 if (info->index != static_cast<std::uint32_t>(primary_sink_index))
308 for (auto& element : outputs)
309 std::get<1>(element) = audio::OutputState::External;
310 }
311 }
312
313 // Query for primary sink finished.
314 void on_query_for_primary_sink_finished(const pa_sink_info* info)
315 {
316 for (auto& element : outputs)
317 {
318 // Only issue state change if the change happened on the active index.
319 if (std::get<0>(active_sink) != info->index)
320 continue;
321
322 MH_INFO("Checking if port is available -> %s",
323 pa::is_port_available_on_sink(info, std::get<0>(element)));
324 const bool available = pa::is_port_available_on_sink(info, std::get<0>(element));
325 if (available)
326 {
327 std::get<1>(element) = audio::OutputState::Earpiece;
328 continue;
329 }
330
331 audio::OutputState state;
332 if (info->index == static_cast<std::uint32_t>(primary_sink_index))
333 state = audio::OutputState::Speaker;
334 else
335 state = audio::OutputState::External;
336
337 std::get<1>(element) = state;
338 }
339
340 std::set<Reporter::Port> known_ports;
341 for (std::uint32_t i = 0; i < info->n_ports; i++)
342 {
343 bool is_monitored = false;
344
345 for (auto& element : outputs)
346 is_monitored |= std::regex_match(info->ports[i]->name, std::get<0>(element));
347
348 known_ports.insert(Reporter::Port
349 {
350 info->ports[i]->name,
351 info->ports[i]->description,
352 info->ports[i]->available == PA_PORT_AVAILABLE_YES,
353 is_monitored
354 });
355 }
356
357 properties.known_ports = known_ports;
358
359 // Initialize sink of primary index (onboard)
360 if (primary_sink_index == -1)
361 primary_sink_index = info->index;
362
363 config.reporter->query_for_sink_info_finished(info->name, info->index, known_ports);
364 }
365
366 void on_query_for_server_info_finished(const pa_server_info* info)
367 {
368 // We bail out if we could not determine the default sink name.
369 // In this case, we are not able to carry out audio output observation.
370 if (not info->default_sink_name)
371 {
372 config.reporter->query_for_default_sink_failed();
373 return;
374 }
375
376 // Update active sink
377 if (info->default_sink_name != std::get<1>(active_sink))
378 pa::get_index_of_sink_by_name_async(context, main_loop, info->default_sink_name, Private::query_for_active_sink_finished, this);
379
380 // Update wired output for primary sink (onboard)
381 pa::get_sink_info_by_index_async(context, main_loop, primary_sink_index, Private::query_for_primary_sink_finished, this);
382
383 if (properties.sink.get() != config.sink)
384 {
385 config.reporter->query_for_default_sink_finished(info->default_sink_name);
386 properties.sink = config.sink = info->default_sink_name;
387 pa::get_index_of_sink_by_name_async(context, main_loop, config.sink, Private::query_for_primary_sink_finished, this);
388 }
389 }
390
391 PulseAudioOutputObserver::Configuration config;
392 pa::ThreadedMainLoopPtr main_loop;
393 pa::ContextPtr context;
394 std::int32_t primary_sink_index;
395 std::tuple<uint32_t, std::string> active_sink;
396 std::vector<std::tuple<std::regex, core::Property<media::audio::OutputState>>> outputs;
397
398 struct
399 {
400 core::Property<std::string> sink;
401 core::Property<std::set<audio::PulseAudioOutputObserver::Reporter::Port>> known_ports;
402 core::Property<audio::OutputState> external_output_state{audio::OutputState::Speaker};
404};
405
407{
408 return name == rhs.name;
409}
410
412{
413 return name < rhs.name;
414}
415
416audio::PulseAudioOutputObserver::Reporter::~Reporter()
417{
418}
419
420void audio::PulseAudioOutputObserver::Reporter::connected_to_pulse_audio()
421{
422}
423
424void audio::PulseAudioOutputObserver::Reporter::query_for_default_sink_failed()
425{
426}
427
428void audio::PulseAudioOutputObserver::Reporter::query_for_default_sink_finished(const std::string&)
429{
430}
431
432void audio::PulseAudioOutputObserver::Reporter::query_for_sink_info_finished(const std::string&, std::uint32_t, const std::set<Port>&)
433{
434}
435
436void audio::PulseAudioOutputObserver::Reporter::sink_event_with_index(std::uint32_t)
437{
438}
439
440// Constructs a new instance, or throws std::runtime_error
441// if connection to pulseaudio fails.
443{
444 if (not d->config.reporter) throw std::runtime_error
445 {
446 "PulseAudioOutputObserver: Cannot construct for invalid reporter instance."
447 };
448}
449
450// We provide the name of the sink we are connecting to as a
451// getable/observable property. This is specifically meant for
452// consumption by test code.
453const core::Property<std::string>& audio::PulseAudioOutputObserver::sink() const
454{
455 return d->properties.sink;
456}
457
458// The set of ports that have been identified on the configured sink.
459// Specifically meant for consumption by test code.
460const core::Property<std::set<audio::PulseAudioOutputObserver::Reporter::Port>>& audio::PulseAudioOutputObserver::known_ports() const
461{
462 return d->properties.known_ports;
463}
464
465// Getable/observable property holding the state of external outputs.
466const core::Property<audio::OutputState>& audio::PulseAudioOutputObserver::external_output_state() const
467{
468 return d->properties.external_output_state;
469}
const core::Property< std::set< Reporter::Port > > & known_ports() const
const core::Property< std::string > & sink() const
const core::Property< OutputState > & external_output_state() const override
#define MH_INFO(...)
Definition: logger.h:125
#define MH_DEBUG(...)
Definition: logger.h:123
bool operator<(IntWrapper< Tag, IntegerType > const &lhs, IntWrapper< Tag, IntegerType > const &rhs)
Definition: dimensions.h:121
bool operator==(IntWrapper< Tag, IntegerType > const &lhs, IntWrapper< Tag, IntegerType > const &rhs)
Definition: dimensions.h:97
core::Property< audio::OutputState > external_output_state
PulseAudioOutputObserver::Configuration config
struct audio::PulseAudioOutputObserver::Private::@0 properties
static void query_for_active_sink_finished(pa_context *ctxt, const pa_sink_info *si, int eol, void *cookie)
Private(const audio::PulseAudioOutputObserver::Configuration &config)
void on_query_for_server_info_finished(const pa_server_info *info)
std::vector< std::tuple< std::regex, core::Property< media::audio::OutputState > > > outputs
std::tuple< uint32_t, std::string > active_sink
static void query_for_server_info_finished(pa_context *ctxt, const pa_server_info *si, void *cookie)
core::Property< std::set< audio::PulseAudioOutputObserver::Reporter::Port > > known_ports
static void context_notification_cb(pa_context *ctxt, void *cookie)
void on_query_for_primary_sink_finished(const pa_sink_info *info)
static void query_for_primary_sink_finished(pa_context *ctxt, const pa_sink_info *si, int eol, void *cookie)
static void context_subscription_cb(pa_context *ctxt, pa_subscription_event_type_t ev, uint32_t idx, void *cookie)
void on_query_for_active_sink_finished(const pa_sink_info *info)