Music Hub ..
A session-wide music playback service
Loading...
Searching...
No Matches
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
40namespace
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.
45namespace pa
46{
47typedef std::shared_ptr<pa_glib_mainloop> MainLoopPtr;
48MainLoopPtr 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
60typedef std::shared_ptr<pa_context> ContextPtr;
61ContextPtr 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
70void 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
75void 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
80void 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
88void 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
94void 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
100void 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
106void 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
112void 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
117bool 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
137struct audio::PulseAudioOutputObserver::Private
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,
229 PulseAudioOutputObserver *q):
230 main_loop{pa::make_main_loop()},
231 context{pa::make_context(main_loop)},
233 active_sink(std::make_tuple(-1, "")),
234 m_requestedSink(sink),
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),
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)
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 {
326 continue;
327 }
328
329 audio::OutputState state;
330 if (info->index == static_cast<std::uint32_t>(primary_sink_index))
332 else
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;
399 PulseAudioOutputObserver *q;
400};
401
406
411
415
419
423
427
428void audio::PulseAudioOutputObserver::Reporter::query_for_sink_info_finished(const std::string&, std::uint32_t, const std::set<Port>&)
429{
430}
431
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
453std::set<audio::PulseAudioOutputObserver::Reporter::Port>& audio::PulseAudioOutputObserver::knownPorts() const
454{
455 return d->m_knownPorts;
456}
PulseAudioOutputObserver(const QString &sink, const QStringList &outputPortPatterns, Reporter::Ptr reporter, OutputObserver *q)
#define MH_INFO(...)
Definition logging.h:39
#define MH_DEBUG(...)
Definition logging.h:38
Private(const QString &sink, const QStringList &outputPortPatterns, Reporter::Ptr reporter, PulseAudioOutputObserver *q)
std::set< audio::PulseAudioOutputObserver::Reporter::Port > m_knownPorts
static void query_for_active_sink_finished(pa_context *ctxt, const pa_sink_info *si, int eol, void *cookie)
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)
void setOutputState(std::tuple< QRegularExpression, audio::OutputState > &element, audio::OutputState state)
static void context_notification_cb(pa_context *ctxt, void *cookie)
std::vector< std::tuple< QRegularExpression, audio::OutputState > > outputs
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)
virtual void query_for_sink_info_finished(const std::string &name, std::uint32_t index, const std::set< Port > &known_ports)