22 #include <pulse/pulseaudio.h>
41 typedef std::shared_ptr<pa_threaded_mainloop> ThreadedMainLoopPtr;
42 ThreadedMainLoopPtr make_threaded_main_loop()
44 return ThreadedMainLoopPtr
46 pa_threaded_mainloop_new(),
47 [](pa_threaded_mainloop* ml)
49 pa_threaded_mainloop_stop(ml);
50 pa_threaded_mainloop_free(ml);
55 void start_main_loop(ThreadedMainLoopPtr ml)
57 pa_threaded_mainloop_start(ml.get());
60 typedef std::shared_ptr<pa_context> ContextPtr;
61 ContextPtr make_context(ThreadedMainLoopPtr main_loop)
65 pa_context_new(pa_threaded_mainloop_get_api(main_loop.get()),
"MediaHubPulseContext"),
70 void set_state_callback(ContextPtr ctxt, pa_context_notify_cb_t cb,
void* cookie)
72 pa_context_set_state_callback(ctxt.get(), cb, cookie);
75 void set_subscribe_callback(ContextPtr ctxt, pa_context_subscribe_cb_t cb,
void* cookie)
77 pa_context_set_subscribe_callback(ctxt.get(), cb, cookie);
80 void throw_if_not_on_main_loop(ThreadedMainLoopPtr ml)
82 if (not pa_threaded_mainloop_in_thread(ml.get()))
throw std::logic_error
84 "Attempted to call into a pulseaudio object from another"
85 "thread than the pulseaudio mainloop thread."
89 void throw_if_not_connected(ContextPtr ctxt)
91 if (pa_context_get_state(ctxt.get()) != PA_CONTEXT_READY )
throw std::logic_error
93 "Attempted to issue a call against pulseaudio via a non-connected context."
97 void get_server_info_async(ContextPtr ctxt, ThreadedMainLoopPtr ml, pa_server_info_cb_t cb,
void* cookie)
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));
103 void subscribe_to_events(ContextPtr ctxt, ThreadedMainLoopPtr ml, pa_subscription_mask mask)
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));
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)
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));
115 void get_sink_info_by_index_async(ContextPtr ctxt, ThreadedMainLoopPtr ml, std::int32_t index, pa_sink_info_cb_t cb,
void* cookie)
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));
121 void connect_async(ContextPtr ctxt)
123 pa_context_connect(ctxt.get(),
nullptr,
static_cast<pa_context_flags_t
>(PA_CONTEXT_NOAUTOSPAWN | PA_CONTEXT_NOFAIL),
nullptr);
126 bool is_port_available_on_sink(
const pa_sink_info* info,
const std::regex& port_pattern)
131 for (std::uint32_t i = 0; i < info->n_ports; i++)
133 if (info->ports[i]->available == PA_PORT_AVAILABLE_NO ||
134 info->ports[i]->available == PA_PORT_AVAILABLE_UNKNOWN)
137 if (std::regex_match(std::string{info->ports[i]->name}, port_pattern))
150 if (
auto thiz = static_cast<Private*>(cookie))
154 if (thiz->context.get() != ctxt)
157 switch (pa_context_get_state(ctxt))
159 case PA_CONTEXT_READY:
160 thiz->on_context_ready();
162 case PA_CONTEXT_FAILED:
163 thiz->on_context_failed();
175 if (
auto thiz = static_cast<Private*>(cookie))
179 if (thiz->context.get() != ctxt)
182 if ((ev & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK)
183 thiz->on_sink_event_with_index(idx);
192 if (
auto thiz = static_cast<Private*>(cookie))
196 if (thiz->context.get() != ctxt)
199 thiz->on_query_for_active_sink_finished(si);
208 if (
auto thiz = static_cast<Private*>(cookie))
212 if (thiz->context.get() != ctxt)
215 thiz->on_query_for_primary_sink_finished(si);
224 if (
auto thiz = static_cast<Private*>(cookie))
228 if (thiz->context.get() != ctxt)
231 thiz->on_query_for_server_info_finished(si);
237 main_loop{pa::make_threaded_main_loop()},
242 for (
const auto& pattern :
config.output_port_patterns)
244 outputs.emplace_back(pattern, core::Property<media::audio::OutputState>{media::audio::OutputState::Speaker});
248 std::cout <<
"Connection state for port changed to: " << state << std::endl;
263 config.reporter->connected_to_pulse_audio();
267 if (
config.sink ==
"query.from.server")
291 config.reporter->sink_event_with_index(index);
307 std::get<1>(element) = audio::OutputState::External;
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));
325 std::get<1>(element) = audio::OutputState::Earpiece;
331 state = audio::OutputState::Speaker;
333 state = audio::OutputState::External;
335 std::get<1>(element) = state;
339 for (std::uint32_t i = 0; i < info->n_ports; i++)
341 bool is_monitored =
false;
343 for (
auto& element : outputs)
344 is_monitored |= std::regex_match(info->ports[i]->name, std::get<0>(element));
346 known_ports.insert(Reporter::Port
348 info->ports[i]->name,
349 info->ports[i]->description,
350 info->ports[i]->available == PA_PORT_AVAILABLE_YES,
361 config.reporter->query_for_sink_info_finished(info->name, info->index, known_ports);
368 if (not info->default_sink_name)
370 config.reporter->query_for_default_sink_failed();
375 if (info->default_sink_name != std::get<1>(
active_sink))
383 config.reporter->query_for_default_sink_finished(info->default_sink_name);
389 PulseAudioOutputObserver::Configuration
config;
394 std::vector<std::tuple<std::regex, core::Property<media::audio::OutputState>>>
outputs;
398 core::Property<std::string>
sink;
399 core::Property<std::set<audio::PulseAudioOutputObserver::Reporter::Port>>
known_ports;
406 return name == rhs.
name;
411 return name < rhs.
name;
414 audio::PulseAudioOutputObserver::Reporter::~Reporter()
418 void audio::PulseAudioOutputObserver::Reporter::connected_to_pulse_audio()
422 void audio::PulseAudioOutputObserver::Reporter::query_for_default_sink_failed()
426 void audio::PulseAudioOutputObserver::Reporter::query_for_default_sink_finished(
const std::string&)
430 void audio::PulseAudioOutputObserver::Reporter::query_for_sink_info_finished(
const std::string&, std::uint32_t,
const std::set<Port>&)
434 void audio::PulseAudioOutputObserver::Reporter::sink_event_with_index(std::uint32_t)
442 if (not d->config.reporter)
throw std::runtime_error
444 "PulseAudioOutputObserver: Cannot construct for invalid reporter instance."
453 return d->properties.sink;
460 return d->properties.known_ports;
466 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
core::Property< audio::OutputState > external_output_state
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
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)