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 
34 namespace
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.
41 namespace pa
42 {
43 typedef std::shared_ptr<pa_threaded_mainloop> ThreadedMainLoopPtr;
44 ThreadedMainLoopPtr 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 
57 void start_main_loop(ThreadedMainLoopPtr ml)
58 {
59  pa_threaded_mainloop_start(ml.get());
60 }
61 
62 typedef std::shared_ptr<pa_context> ContextPtr;
63 ContextPtr 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 
72 void 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 
77 void 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 
82 void 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 
91 void 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 
99 void 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 
105 void 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 
111 void 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 
117 void 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 
123 void 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 
128 bool 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)},
241  primary_sink_index(-1),
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 == 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};
403  } properties;
404 };
405 
407 {
408  return name == rhs.name;
409 }
410 
412 {
413  return name < rhs.name;
414 }
415 
416 audio::PulseAudioOutputObserver::Reporter::~Reporter()
417 {
418 }
419 
420 void audio::PulseAudioOutputObserver::Reporter::connected_to_pulse_audio()
421 {
422 }
423 
424 void audio::PulseAudioOutputObserver::Reporter::query_for_default_sink_failed()
425 {
426 }
427 
428 void audio::PulseAudioOutputObserver::Reporter::query_for_default_sink_finished(const std::string&)
429 {
430 }
431 
432 void audio::PulseAudioOutputObserver::Reporter::query_for_sink_info_finished(const std::string&, std::uint32_t, const std::set<Port>&)
433 {
434 }
435 
436 void 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.
442 audio::PulseAudioOutputObserver::PulseAudioOutputObserver(const Configuration& config) : d{new Private{config}}
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.
453 const 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.
460 const 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.
466 const core::Property<audio::OutputState>& audio::PulseAudioOutputObserver::external_output_state() const
467 {
468  return d->properties.external_output_state;
469 }
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
#define MH_INFO(...)
Definition: logger.h:125
bool operator==(IntWrapper< Tag, IntegerType > const &lhs, IntWrapper< Tag, IntegerType > const &rhs)
Definition: dimensions.h:97
PulseAudioOutputObserver::Configuration config
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
#define MH_DEBUG(...)
Definition: logger.h:123
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
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