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