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  * Copyright © 2022 UBports Foundation.
4  *
5  * Contact: Alberto Mardegan <mardy@users.sourceforge.net>
6  *
7  * This program is free software: you can redistribute it and/or modify it
8  * under the terms of the GNU Lesser General Public License version 3,
9  * as published by the Free Software Foundation.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this program. If not, see <http://www.gnu.org/licenses/>.
18  *
19  * Authored by: Thomas Voß <thomas.voss@canonical.com>
20  * Ricardo Mendoza <ricardo.mendoza@canonical.com>
21  */
22 
24 
25 #include "logging.h"
26 
27 #include <pulse/pulseaudio.h>
28 #include <pulse/glib-mainloop.h>
29 
30 #include <QString>
31 #include <QStringList>
32 #include <QRegularExpression>
33 
34 #include <cstdint>
35 #include <map>
36 #include <string>
37 
39 
40 namespace
41 {
42 // We wrap calls to the pulseaudio client api into its
43 // own namespace and make sure that only managed types
44 // can be passed to calls to pulseaudio.
45 namespace pa
46 {
47 typedef std::shared_ptr<pa_glib_mainloop> MainLoopPtr;
48 MainLoopPtr make_main_loop()
49 {
50  return MainLoopPtr
51  {
52  pa_glib_mainloop_new(g_main_context_default()),
53  [](pa_glib_mainloop *ml)
54  {
55  pa_glib_mainloop_free(ml);
56  }
57  };
58 }
59 
60 typedef std::shared_ptr<pa_context> ContextPtr;
61 ContextPtr make_context(MainLoopPtr main_loop)
62 {
63  return ContextPtr
64  {
65  pa_context_new(pa_glib_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_connected(ContextPtr ctxt)
81 {
82  if (pa_context_get_state(ctxt.get()) != PA_CONTEXT_READY ) throw std::logic_error
83  {
84  "Attempted to issue a call against pulseaudio via a non-connected context."
85  };
86 }
87 
88 void get_server_info_async(ContextPtr ctxt, pa_server_info_cb_t cb, void* cookie)
89 {
90  throw_if_not_connected(ctxt);
91  pa_operation_unref(pa_context_get_server_info(ctxt.get(), cb, cookie));
92 }
93 
94 void subscribe_to_events(ContextPtr ctxt, pa_subscription_mask mask)
95 {
96  throw_if_not_connected(ctxt);
97  pa_operation_unref(pa_context_subscribe(ctxt.get(), mask, nullptr, nullptr));
98 }
99 
100 void get_index_of_sink_by_name_async(ContextPtr ctxt, const QString &name, pa_sink_info_cb_t cb, void* cookie)
101 {
102  throw_if_not_connected(ctxt);
103  pa_operation_unref(pa_context_get_sink_info_by_name(ctxt.get(), qUtf8Printable(name), cb, cookie));
104 }
105 
106 void get_sink_info_by_index_async(ContextPtr ctxt, std::int32_t index, pa_sink_info_cb_t cb, void* cookie)
107 {
108  throw_if_not_connected(ctxt);
109  pa_operation_unref(pa_context_get_sink_info_by_index(ctxt.get(), index, cb, cookie));
110 }
111 
112 void connect_async(ContextPtr ctxt)
113 {
114  pa_context_connect(ctxt.get(), nullptr, static_cast<pa_context_flags_t>(PA_CONTEXT_NOAUTOSPAWN | PA_CONTEXT_NOFAIL), nullptr);
115 }
116 
117 bool is_port_available_on_sink(const pa_sink_info* info, const QRegularExpression& port_pattern)
118 {
119  if (not info)
120  return false;
121 
122  for (std::uint32_t i = 0; i < info->n_ports; i++)
123  {
124  if (info->ports[i]->available == PA_PORT_AVAILABLE_NO ||
125  info->ports[i]->available == PA_PORT_AVAILABLE_UNKNOWN)
126  continue;
127 
128  if (port_pattern.match(QString(info->ports[i]->name)).hasMatch())
129  return true;
130  }
131 
132  return false;
133 }
134 }
135 }
136 
138 {
139  static void context_notification_cb(pa_context* ctxt, void* cookie)
140  {
141  if (auto thiz = static_cast<Private*>(cookie))
142  {
143  // Better safe than sorry: Check if we got signaled for the
144  // context we are actually interested in.
145  if (thiz->context.get() != ctxt)
146  return;
147 
148  switch (pa_context_get_state(ctxt))
149  {
150  case PA_CONTEXT_READY:
151  thiz->on_context_ready();
152  break;
153  case PA_CONTEXT_FAILED:
154  thiz->on_context_failed();
155  break;
156  default:
157  break;
158  }
159  }
160  }
161 
162  static void context_subscription_cb(pa_context* ctxt, pa_subscription_event_type_t ev, uint32_t idx, void* cookie)
163  {
164  (void) idx;
165 
166  if (auto thiz = static_cast<Private*>(cookie))
167  {
168  // Better safe than sorry: Check if we got signaled for the
169  // context we are actually interested in.
170  if (thiz->context.get() != ctxt)
171  return;
172 
173  if ((ev & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK)
174  thiz->on_sink_event_with_index(idx);
175  }
176  }
177 
178  static void query_for_active_sink_finished(pa_context* ctxt, const pa_sink_info* si, int eol, void* cookie)
179  {
180  if (eol)
181  return;
182 
183  if (auto thiz = static_cast<Private*>(cookie))
184  {
185  // Better safe than sorry: Check if we got signaled for the
186  // context we are actually interested in.
187  if (thiz->context.get() != ctxt)
188  return;
189 
190  thiz->on_query_for_active_sink_finished(si);
191  }
192  }
193 
194  static void query_for_primary_sink_finished(pa_context* ctxt, const pa_sink_info* si, int eol, void* cookie)
195  {
196  if (eol)
197  return;
198 
199  if (auto thiz = static_cast<Private*>(cookie))
200  {
201  // Better safe than sorry: Check if we got signaled for the
202  // context we are actually interested in.
203  if (thiz->context.get() != ctxt)
204  return;
205 
206  thiz->on_query_for_primary_sink_finished(si);
207  }
208  }
209 
210  static void query_for_server_info_finished(pa_context* ctxt, const pa_server_info* si, void* cookie)
211  {
212  if (not si)
213  return;
214 
215  if (auto thiz = static_cast<Private*>(cookie))
216  {
217  // Better safe than sorry: Check if we got signaled for the
218  // context we are actually interested in.
219  if (thiz->context.get() != ctxt)
220  return;
221 
222  thiz->on_query_for_server_info_finished(si);
223  }
224  }
225 
226  Private(const QString &sink,
227  const QStringList &outputPortPatterns,
228  Reporter::Ptr reporter,
230  main_loop{pa::make_main_loop()},
231  context{pa::make_context(main_loop)},
232  primary_sink_index(-1),
233  active_sink(std::make_tuple(-1, "")),
235  m_reporter(reporter),
236  q(q)
237  {
238  q->setOutputState(audio::OutputState::Speaker);
239  for (const auto& pattern : outputPortPatterns)
240  {
241  outputs.emplace_back(QRegularExpression(pattern),
242  audio::OutputState::Speaker);
243  }
244 
245  pa::set_state_callback(context, Private::context_notification_cb, this);
246  pa::set_subscribe_callback(context, Private::context_subscription_cb, this);
247 
248  pa::connect_async(context);
249  }
250 
251  void setOutputState(std::tuple<QRegularExpression, audio::OutputState> &element,
252  audio::OutputState state)
253  {
254  MH_DEBUG("Connection state for port changed to: %d", state);
255  std::get<1>(element) = state;
256  q->setOutputState(state);
257  }
258 
259  // The connection attempt has been successful and we are connected
260  // to pulseaudio now.
262  {
263  m_reporter->connected_to_pulse_audio();
264 
265  pa::subscribe_to_events(context, PA_SUBSCRIPTION_MASK_SINK);
266 
267  if (m_requestedSink == "query.from.server")
268  {
269  pa::get_server_info_async(context, Private::query_for_server_info_finished, this);
270  }
271  else
272  {
273  // Get primary sink index (default)
274  pa::get_index_of_sink_by_name_async(context, m_requestedSink,
276  // Update active sink (could be == default)
277  pa::get_server_info_async(context, 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  m_reporter->sink_event_with_index(index);
292 
293  // Update server info (active sink)
294  pa::get_server_info_async(context, 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  setOutputState(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  MH_INFO("Checking if port is available -> %d",
321  pa::is_port_available_on_sink(info, std::get<0>(element)));
322  const bool available = pa::is_port_available_on_sink(info, std::get<0>(element));
323  if (available)
324  {
325  setOutputState(element, audio::OutputState::Earpiece);
326  continue;
327  }
328 
329  audio::OutputState state;
330  if (info->index == static_cast<std::uint32_t>(primary_sink_index))
331  state = audio::OutputState::Speaker;
332  else
333  state = audio::OutputState::External;
334 
335  setOutputState(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::get<0>(element).match(info->ports[i]->name).hasMatch();
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  m_knownPorts = known_ports;
356 
357  // Initialize sink of primary index (onboard)
358  if (primary_sink_index == -1)
359  primary_sink_index = info->index;
360 
361  m_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  m_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, 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, primary_sink_index, Private::query_for_primary_sink_finished, this);
380 
381  if (m_sink != m_requestedSink)
382  {
383  m_reporter->query_for_default_sink_finished(info->default_sink_name);
384  m_sink = m_requestedSink = info->default_sink_name;
385  pa::get_index_of_sink_by_name_async(context, m_sink, Private::query_for_primary_sink_finished, this);
386  }
387  }
388 
389  pa::MainLoopPtr main_loop;
390  pa::ContextPtr context;
391  std::int32_t primary_sink_index;
392  std::tuple<uint32_t, std::string> active_sink;
393  std::vector<std::tuple<QRegularExpression, audio::OutputState>> outputs;
394 
395  QString m_sink;
397  Reporter::Ptr m_reporter;
398  std::set<audio::PulseAudioOutputObserver::Reporter::Port> m_knownPorts;
400 };
401 
402 bool audio::PulseAudioOutputObserver::Reporter::Port::operator==(const audio::PulseAudioOutputObserver::Reporter::Port& rhs) const
403 {
404  return name == rhs.name;
405 }
406 
407 bool audio::PulseAudioOutputObserver::Reporter::Port::operator<(const audio::PulseAudioOutputObserver::Reporter::Port& rhs) const
408 {
409  return name < rhs.name;
410 }
411 
412 audio::PulseAudioOutputObserver::Reporter::~Reporter()
413 {
414 }
415 
416 void audio::PulseAudioOutputObserver::Reporter::connected_to_pulse_audio()
417 {
418 }
419 
420 void audio::PulseAudioOutputObserver::Reporter::query_for_default_sink_failed()
421 {
422 }
423 
424 void audio::PulseAudioOutputObserver::Reporter::query_for_default_sink_finished(const std::string&)
425 {
426 }
427 
428 void audio::PulseAudioOutputObserver::Reporter::query_for_sink_info_finished(const std::string&, std::uint32_t, const std::set<Port>&)
429 {
430 }
431 
432 void audio::PulseAudioOutputObserver::Reporter::sink_event_with_index(std::uint32_t)
433 {
434 }
435 
436 // Constructs a new instance, or throws std::runtime_error
437 // if connection to pulseaudio fails.
439  const QString &sink,
440  const QStringList &outputPortPatterns,
441  Reporter::Ptr reporter,
442  OutputObserver *q):
444  d(new Private(sink, outputPortPatterns, reporter, this))
445 {
446 }
447 
449 {
450  return d->m_sink;
451 }
452 
453 std::set<audio::PulseAudioOutputObserver::Reporter::Port>& audio::PulseAudioOutputObserver::knownPorts() const
454 {
455  return d->m_knownPorts;
456 }
lomiri::MediaHubService::audio::OutputObserverPrivate
Definition: output_observer_p.h:28
lomiri::MediaHubService::audio::PulseAudioOutputObserver::PulseAudioOutputObserver
PulseAudioOutputObserver(const QString &sink, const QStringList &outputPortPatterns, Reporter::Ptr reporter, OutputObserver *q)
lomiri::MediaHubService::audio
Definition: ostream_reporter.h:33
audio::PulseAudioOutputObserver::Private::Private
Private(const QString &sink, const QStringList &outputPortPatterns, Reporter::Ptr reporter, PulseAudioOutputObserver *q)
Definition: pulse_audio_output_observer.cpp:226
lomiri::MediaHubService::audio::PulseAudioOutputObserver::Reporter::Port::name
std::string name
Definition: pulse_audio_output_observer.h:61
audio::PulseAudioOutputObserver::Private
Definition: pulse_audio_output_observer.cpp:137
audio::PulseAudioOutputObserver::Private::m_requestedSink
QString m_requestedSink
Definition: pulse_audio_output_observer.cpp:396
audio::PulseAudioOutputObserver::Private::on_sink_event_with_index
void on_sink_event_with_index(std::int32_t index)
Definition: pulse_audio_output_observer.cpp:289
lomiri::MediaHubService::audio::OutputObserver
Definition: output_observer.h:50
audio::PulseAudioOutputObserver::Private::m_knownPorts
std::set< audio::PulseAudioOutputObserver::Reporter::Port > m_knownPorts
Definition: pulse_audio_output_observer.cpp:398
audio::PulseAudioOutputObserver::Private::q
PulseAudioOutputObserver * q
Definition: pulse_audio_output_observer.cpp:399
audio::PulseAudioOutputObserver::Private::context_subscription_cb
static void context_subscription_cb(pa_context *ctxt, pa_subscription_event_type_t ev, uint32_t idx, void *cookie)
Definition: pulse_audio_output_observer.cpp:162
pa
Definition: pulse_audio_output_observer.cpp:45
audio::PulseAudioOutputObserver::Private::on_query_for_primary_sink_finished
void on_query_for_primary_sink_finished(const pa_sink_info *info)
Definition: pulse_audio_output_observer.cpp:312
audio::PulseAudioOutputObserver::Private::outputs
std::vector< std::tuple< QRegularExpression, audio::OutputState > > outputs
Definition: pulse_audio_output_observer.cpp:393
pulse_audio_output_observer.h
audio::PulseAudioOutputObserver::Private::query_for_primary_sink_finished
static void query_for_primary_sink_finished(pa_context *ctxt, const pa_sink_info *si, int eol, void *cookie)
Definition: pulse_audio_output_observer.cpp:194
audio::PulseAudioOutputObserver::Private::on_query_for_active_sink_finished
void on_query_for_active_sink_finished(const pa_sink_info *info)
Definition: pulse_audio_output_observer.cpp:298
audio::PulseAudioOutputObserver::Private::main_loop
pa::MainLoopPtr main_loop
Definition: pulse_audio_output_observer.cpp:389
lomiri::MediaHubService::audio::PulseAudioOutputObserver::Reporter::Port
Definition: pulse_audio_output_observer.h:54
lomiri::MediaHubService::audio::OutputState
OutputState
Definition: output_observer.h:35
audio::PulseAudioOutputObserver::Private::m_sink
QString m_sink
Definition: pulse_audio_output_observer.cpp:395
audio::PulseAudioOutputObserver::Private::on_context_ready
void on_context_ready()
Definition: pulse_audio_output_observer.cpp:261
audio::PulseAudioOutputObserver::Private::context_notification_cb
static void context_notification_cb(pa_context *ctxt, void *cookie)
Definition: pulse_audio_output_observer.cpp:139
audio::PulseAudioOutputObserver::Private::primary_sink_index
std::int32_t primary_sink_index
Definition: pulse_audio_output_observer.cpp:391
lomiri::MediaHubService::audio::PulseAudioOutputObserver::sink
QString sink() const
lomiri::MediaHubService::audio::PulseAudioOutputObserver::knownPorts
std::set< Reporter::Port > & knownPorts() const
audio::PulseAudioOutputObserver::Private::context
pa::ContextPtr context
Definition: pulse_audio_output_observer.cpp:390
audio::PulseAudioOutputObserver::Private::active_sink
std::tuple< uint32_t, std::string > active_sink
Definition: pulse_audio_output_observer.cpp:392
MH_DEBUG
#define MH_DEBUG(...)
Definition: logging.h:38
MH_INFO
#define MH_INFO(...)
Definition: logging.h:39
audio::PulseAudioOutputObserver::Private::on_context_failed
void on_context_failed()
Definition: pulse_audio_output_observer.cpp:283
lomiri::MediaHubService::audio::PulseAudioOutputObserver::Reporter::Ptr
std::shared_ptr< Reporter > Ptr
Definition: pulse_audio_output_observer.h:51
audio::PulseAudioOutputObserver::Private::on_query_for_server_info_finished
void on_query_for_server_info_finished(const pa_server_info *info)
Definition: pulse_audio_output_observer.cpp:364
audio::PulseAudioOutputObserver::Private::m_reporter
Reporter::Ptr m_reporter
Definition: pulse_audio_output_observer.cpp:397
audio::PulseAudioOutputObserver::Private::query_for_active_sink_finished
static void query_for_active_sink_finished(pa_context *ctxt, const pa_sink_info *si, int eol, void *cookie)
Definition: pulse_audio_output_observer.cpp:178
audio::PulseAudioOutputObserver::Private::query_for_server_info_finished
static void query_for_server_info_finished(pa_context *ctxt, const pa_server_info *si, void *cookie)
Definition: pulse_audio_output_observer.cpp:210
audio::PulseAudioOutputObserver::Private::setOutputState
void setOutputState(std::tuple< QRegularExpression, audio::OutputState > &element, audio::OutputState state)
Definition: pulse_audio_output_observer.cpp:251