22 #include <pulse/pulseaudio.h> 43 typedef std::shared_ptr<pa_threaded_mainloop> ThreadedMainLoopPtr;
44 ThreadedMainLoopPtr make_threaded_main_loop()
46 return ThreadedMainLoopPtr
48 pa_threaded_mainloop_new(),
49 [](pa_threaded_mainloop* ml)
51 pa_threaded_mainloop_stop(ml);
52 pa_threaded_mainloop_free(ml);
57 void start_main_loop(ThreadedMainLoopPtr ml)
59 pa_threaded_mainloop_start(ml.get());
62 typedef std::shared_ptr<pa_context> ContextPtr;
63 ContextPtr make_context(ThreadedMainLoopPtr main_loop)
67 pa_context_new(pa_threaded_mainloop_get_api(main_loop.get()),
"MediaHubPulseContext"),
72 void set_state_callback(ContextPtr ctxt, pa_context_notify_cb_t cb,
void* cookie)
74 pa_context_set_state_callback(ctxt.get(), cb, cookie);
77 void set_subscribe_callback(ContextPtr ctxt, pa_context_subscribe_cb_t cb,
void* cookie)
79 pa_context_set_subscribe_callback(ctxt.get(), cb, cookie);
82 void throw_if_not_on_main_loop(ThreadedMainLoopPtr ml)
84 if (not pa_threaded_mainloop_in_thread(ml.get()))
throw std::logic_error
86 "Attempted to call into a pulseaudio object from another" 87 "thread than the pulseaudio mainloop thread." 91 void throw_if_not_connected(ContextPtr ctxt)
93 if (pa_context_get_state(ctxt.get()) != PA_CONTEXT_READY )
throw std::logic_error
95 "Attempted to issue a call against pulseaudio via a non-connected context." 99 void get_server_info_async(ContextPtr ctxt, ThreadedMainLoopPtr ml, pa_server_info_cb_t cb,
void* cookie)
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));
105 void subscribe_to_events(ContextPtr ctxt, ThreadedMainLoopPtr ml, pa_subscription_mask mask)
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));
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)
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));
117 void get_sink_info_by_index_async(ContextPtr ctxt, ThreadedMainLoopPtr ml, std::int32_t index, pa_sink_info_cb_t cb,
void* cookie)
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));
123 void connect_async(ContextPtr ctxt)
125 pa_context_connect(ctxt.get(),
nullptr,
static_cast<pa_context_flags_t
>(PA_CONTEXT_NOAUTOSPAWN | PA_CONTEXT_NOFAIL),
nullptr);
128 bool is_port_available_on_sink(
const pa_sink_info* info,
const std::regex& port_pattern)
133 for (std::uint32_t i = 0; i < info->n_ports; i++)
135 if (info->ports[i]->available == PA_PORT_AVAILABLE_NO ||
136 info->ports[i]->available == PA_PORT_AVAILABLE_UNKNOWN)
139 if (std::regex_match(std::string{info->ports[i]->name}, port_pattern))
152 if (
auto thiz = static_cast<Private*>(cookie))
156 if (thiz->context.get() != ctxt)
159 switch (pa_context_get_state(ctxt))
161 case PA_CONTEXT_READY:
162 thiz->on_context_ready();
164 case PA_CONTEXT_FAILED:
165 thiz->on_context_failed();
177 if (
auto thiz = static_cast<Private*>(cookie))
181 if (thiz->context.get() != ctxt)
184 if ((ev & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK)
185 thiz->on_sink_event_with_index(idx);
194 if (
auto thiz = static_cast<Private*>(cookie))
198 if (thiz->context.get() != ctxt)
201 thiz->on_query_for_active_sink_finished(si);
210 if (
auto thiz = static_cast<Private*>(cookie))
214 if (thiz->context.get() != ctxt)
217 thiz->on_query_for_primary_sink_finished(si);
226 if (
auto thiz = static_cast<Private*>(cookie))
230 if (thiz->context.get() != ctxt)
233 thiz->on_query_for_server_info_finished(si);
237 Private(
const audio::PulseAudioOutputObserver::Configuration& 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,
""))
244 for (
const auto& pattern : config.output_port_patterns)
246 outputs.emplace_back(pattern, core::Property<media::audio::OutputState>{media::audio::OutputState::Speaker});
247 std::get<1>(outputs.back()) | properties.external_output_state;
250 MH_DEBUG(
"Connection state for port changed to: %s", state);
254 pa::set_state_callback(context, Private::context_notification_cb,
this);
255 pa::set_subscribe_callback(context, Private::context_subscription_cb,
this);
257 pa::connect_async(context);
258 pa::start_main_loop(main_loop);
265 config.reporter->connected_to_pulse_audio();
267 pa::subscribe_to_events(context, main_loop, PA_SUBSCRIPTION_MASK_SINK);
269 if (config.sink ==
"query.from.server")
271 pa::get_server_info_async(context, main_loop, Private::query_for_server_info_finished,
this);
275 properties.sink = config.sink;
277 pa::get_index_of_sink_by_name_async(context, main_loop, config.sink, Private::query_for_primary_sink_finished,
this);
279 pa::get_server_info_async(context, main_loop, Private::query_for_server_info_finished,
this);
287 pa::connect_async(context);
293 config.reporter->sink_event_with_index(index);
296 pa::get_server_info_async(context, main_loop, Private::query_for_server_info_finished,
this);
303 if (std::get<0>(active_sink) != info->index)
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;
316 for (
auto& element : outputs)
319 if (std::get<0>(active_sink) != info->index)
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));
327 std::get<1>(element) = audio::OutputState::Earpiece;
332 if (info->index == static_cast<std::uint32_t>(primary_sink_index))
333 state = audio::OutputState::Speaker;
335 state = audio::OutputState::External;
337 std::get<1>(element) = state;
340 std::set<Reporter::Port> known_ports;
341 for (std::uint32_t i = 0; i < info->n_ports; i++)
343 bool is_monitored =
false;
345 for (
auto& element : outputs)
346 is_monitored |= std::regex_match(info->ports[i]->name, std::get<0>(element));
348 known_ports.insert(Reporter::Port
350 info->ports[i]->name,
351 info->ports[i]->description,
352 info->ports[i]->available == PA_PORT_AVAILABLE_YES,
357 properties.known_ports = known_ports;
360 if (primary_sink_index == -1)
361 primary_sink_index = info->index;
363 config.reporter->query_for_sink_info_finished(info->name, info->index, known_ports);
370 if (not info->default_sink_name)
372 config.reporter->query_for_default_sink_failed();
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);
381 pa::get_sink_info_by_index_async(context, main_loop, primary_sink_index, Private::query_for_primary_sink_finished,
this);
383 if (properties.sink.get() != config.sink)
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);
391 PulseAudioOutputObserver::Configuration
config;
396 std::vector<std::tuple<std::regex, core::Property<media::audio::OutputState>>>
outputs;
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};
408 return name == rhs.
name;
413 return name < rhs.
name;
416 audio::PulseAudioOutputObserver::Reporter::~Reporter()
420 void audio::PulseAudioOutputObserver::Reporter::connected_to_pulse_audio()
424 void audio::PulseAudioOutputObserver::Reporter::query_for_default_sink_failed()
428 void audio::PulseAudioOutputObserver::Reporter::query_for_default_sink_finished(
const std::string&)
432 void audio::PulseAudioOutputObserver::Reporter::query_for_sink_info_finished(
const std::string&, std::uint32_t,
const std::set<Port>&)
436 void audio::PulseAudioOutputObserver::Reporter::sink_event_with_index(std::uint32_t)
444 if (not d->config.reporter)
throw std::runtime_error
446 "PulseAudioOutputObserver: Cannot construct for invalid reporter instance." 455 return d->properties.sink;
462 return d->properties.known_ports;
468 return d->properties.external_output_state;
static void query_for_active_sink_finished(pa_context *ctxt, const pa_sink_info *si, int eol, void *cookie)
pa::ThreadedMainLoopPtr main_loop
PulseAudioOutputObserver::Configuration config
void on_query_for_primary_sink_finished(const pa_sink_info *info)
std::vector< std::tuple< std::regex, core::Property< media::audio::OutputState > > > outputs
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)
core::Property< std::string > sink
void on_query_for_active_sink_finished(const pa_sink_info *info)
void on_sink_event_with_index(std::int32_t index)
std::int32_t primary_sink_index
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)