// Copyright 2022 Benjamin Barenblat // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy of // the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. #include "pulse.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace pulse { // Adapters for libpulse callbacks. // // libpulse presents a callback-driven C API--callbacks must be C function // pointers, and each callback can be passed a user-controlled void*. This isn't // a great API for a C++ library, though, so this library accepts std::functions // and pass them by pointer to statically defined callbacks. // // Even though these functions are defined in an anonymous namespace, the extern // "C" means they need "static" to get the desired internal linkage. namespace { using SignalCallback = std::function; extern "C" { static void CallSignalCallback(pa_mainloop_api*, pa_signal_event*, int /*signal*/, void* real_callback) noexcept { assert(real_callback != nullptr); (*reinterpret_cast(real_callback))(); } static void DeleteSignalCallback(pa_mainloop_api*, pa_signal_event*, void* real_callback) noexcept { assert(real_callback != nullptr); delete reinterpret_cast(real_callback); } } // extern "C" using ContextNotifyCallback = std::function; extern "C" { static void CallContextNotifyCallback(pa_context* ctx, void* real_callback) noexcept { assert(real_callback != nullptr); (*reinterpret_cast(real_callback))( pa_context_get_state(ctx)); } } // extern "C" struct SinkInfoCallbacks { std::function on_failure; std::function on_data; }; extern "C" { static void CallOrDeleteSinkInfoCallbacks(pa_context*, const pa_sink_info* info, int status, void* real_callbacks) noexcept { assert(real_callbacks != nullptr); auto* sink_info_callbacks = reinterpret_cast(real_callbacks); if (status == 0) { // The RPC succeeded, and info contains sink information. Tell the user. assert(info != nullptr); sink_info_callbacks->on_data(*info); } else { // No sink information came through... if (status < 0) { // ...because of an error. Tell the user. sink_info_callbacks->on_failure(); } else { // ...because no more data is available from the PulseAudio server. The // user doesn't need to hear about that. } // In any case, no more data are coming, so don't hang onto the callbacks // anymore. delete sink_info_callbacks; } } } // extern "C" struct RpcResultCallbacks { std::function on_failure; std::function on_success; }; extern "C" { static void CallAndDeleteRpcResultCallbacks(pa_context*, int success, void* real_callbacks) noexcept { assert(real_callbacks != nullptr); auto* rpc_result_callbacks = reinterpret_cast(real_callbacks); if (success) { rpc_result_callbacks->on_success(); } else { rpc_result_callbacks->on_failure(); } delete rpc_result_callbacks; } } // extern "C" using SubscribeCallback = std::function; extern "C" { static void CallSubscribeCallback(pa_context*, pa_subscription_event_type_t type, uint32_t /*index*/, void* real_callback) noexcept { assert(real_callback != nullptr); (*reinterpret_cast(real_callback))(type); } } // extern "C" } // namespace PropertyList::PropertyList() : list_(pa_proplist_new()) { if (list_ == nullptr) { throw Error::InFunction("pa_proplist_new"); } } PropertyList::PropertyList(const PropertyList& other) : list_(pa_proplist_copy(other.list_)) { if (list_ == nullptr) { throw Error::InFunction("pa_proplist_copy"); } } PropertyList& PropertyList::operator=(const PropertyList& other) { if (this != &other) { PropertyList other2(other); swap(*this, other2); } return *this; } PropertyList::~PropertyList() noexcept { if (list_ != nullptr) { pa_proplist_free(list_); } } void PropertyList::Set(const char key[], std::string_view value) { if (pa_proplist_set(list_, key, value.data(), value.size())) { throw Error::InFunction("pa_proplist_set"); } } PollMainLoop::PollMainLoop() : loop_(pa_mainloop_new()), handling_signals_(false) { if (loop_ == nullptr) { throw Error::InFunction("pa_mainloop_new"); } api_ = pa_mainloop_get_api(loop_); } PollMainLoop::~PollMainLoop() noexcept { if (handling_signals_) { StopHandlingSignals(); } if (loop_ != nullptr) { pa_mainloop_free(loop_); } } void PollMainLoop::HandleSignals() { if (pa_signal_init(api_)) { throw Error::InFunction("pa_signal_init"); } handling_signals_ = true; } int PollMainLoop::Run() { int r; if (pa_mainloop_run(loop_, &r) < 0) { throw Error::InFunction("pa_mainloop_run"); } return r; } SignalEventSource::SignalEventSource(int signal, std::function real_callback) { // Construct source_ using CallSignalCallback as the C callback and // real_callback as the real callback. real_callback is going to get passed by // pointer, so that address needs to continue to point to real_callback until // pa_signal_free. Happily, libpulse provides a mechanism to run an arbitrary // callback during pa_signal_free, so move the callback onto the heap and // delete it during pa_signal_free. This function is still responsible for // deleting it if pa_signal_new fails, though, so stash it inside a unique_ptr // and release the unique_ptr if pa_signal_new succeeds. auto heap_callback = std::make_unique(std::move(real_callback)); source_ = pa_signal_new(signal, &CallSignalCallback, heap_callback.get()); if (source_ == nullptr) { throw Error::InFunction("pa_signal_new"); } pa_signal_set_destroy(source_, &DeleteSignalCallback); heap_callback.release(); } SignalEventSource::~SignalEventSource() noexcept { if (source_ != nullptr) { pa_signal_free(source_); } } Context::Context(const char application_name[], PropertyList& proplist, PollMainLoop& loop) : ctx_(pa_context_new_with_proplist(pa_mainloop_get_api(loop.get()), application_name, proplist.get())) { if (ctx_ == nullptr) { throw Error::InFunction("pa_context_new_with_proplist"); } } Context::~Context() noexcept { if (state_callback_ != nullptr) { Disconnect(); } if (ctx_ != nullptr) { pa_context_unref(ctx_); } } void Context::Connect( pa_context_flags_t flags, std::function on_state_change) noexcept { // Set Up CallContextNotifyCallback to call on_state_change. on_state_change // is going to get passed by pointer, so its address needs to continue to // point to on_state_change until pa_context_disconnect. Saving the callback // as a member ensures it stays alive long enough, but it doesn't ensure // pointer stability; to get that, save it on the heap and keep a unique_ptr // to it as a member. state_callback_ = std::make_unique(std::move(on_state_change)); pa_context_set_state_callback(ctx_, &CallContextNotifyCallback, state_callback_.get()); if (pa_context_connect(ctx_, /*server=*/nullptr, flags, /*spawn_api=*/nullptr)) { // The RPC never even went out. libpulse won't call the callback in this // case, so do it manually. (*state_callback_)(PA_CONTEXT_FAILED); // The callback will never be called again at this point, and hanging onto // it could cause resource leakage (e.g., if it captured a std::shared_ptr). // Get rid of it. state_callback_ = nullptr; } } void Context::GetSinkInfo( const char name[], std::function on_failure, std::function on_data) const noexcept { // Set up CallOrDeleteSinkInfoCallbacks to call on_failure or on_data as // appropriate. These callbacks are going to get passed by pointer, so they // need to live on the heap. If constructing the get_sink_info_by_name RPC // fails, though, libpulse won't call on_failure; to allow calling it // manually, stash the callbacks inside a unique_ptr and release the // unique_ptr if the RPC actually gets dispatched. auto callbacks = std::make_unique(); callbacks->on_failure = std::move(on_failure); callbacks->on_data = std::move(on_data); pa_operation* op = pa_context_get_sink_info_by_name( ctx_, name, &CallOrDeleteSinkInfoCallbacks, callbacks.get()); if (op == nullptr) { // The RPC never even went out. libpulse won't call on_failure in this case, // so do it manually. callbacks->on_failure(); } else { pa_operation_unref(op); // The RPC handle is no longer needed. // The RPC is running, which means CallOrDeleteSinkInfoCallbacks will be // called. (In fact, it's possible that CallOrDeleteSinkInfoCallbacks has // already been called and the callbacks struct has already been deleted!) // Stop managing the callbacks struct. callbacks.release(); } } void Context::Subscribe( pa_subscription_mask_t desired_events, std::function on_failure, std::function on_event) const noexcept { // Set up CallSubscribeCallback to call on_event. This callback is going to // get passed by pointer, so its address needs to continue to point to // on_event until pa_context_disconnect. Saving the callback as a member // ensures it stays alive long enough, but it doesn't ensure pointer // stability; to get that, save it on the heap and keep a unique_ptr to it as // a member. subscribe_callback_ = std::make_unique(std::move(on_event)); pa_context_set_subscribe_callback(ctx_, &CallSubscribeCallback, subscribe_callback_.get()); // Set up CallAndDeleteRpcResultCallbacks to handle the result of the // subscribe RPC. The real callbacks struct is going to get passed by pointer, // so it needs to live on the heap. If constructing the subscribe RPC fails, // though, libpulse won't call on_failure; to allow calling it manually, stash // the callbacks inside a unique_ptr and release the unique_ptr if the RPC // actually gets dispatched. auto subscribe_completion_callbacks = std::make_unique(); subscribe_completion_callbacks->on_failure = std::move(on_failure); // We don't need to tell the user the RPC succeded; the event callback can // tell them that. subscribe_completion_callbacks->on_success = []() noexcept {}; pa_operation* op = pa_context_subscribe(ctx_, desired_events, &CallAndDeleteRpcResultCallbacks, subscribe_completion_callbacks.get()); if (op == nullptr) { // The RPC never even went out. libpulse won't call // CallAndDeleteRpcResultCallbacks in this case, call on_failure manually. subscribe_completion_callbacks->on_failure(); } else { pa_operation_unref(op); // The RPC handle is no longer needed. // The RPC is running, which means CallAndDeleteRpcResultCallbacks will be // called. (In fact, it's possible that CallAndDeleteRpcResultCallbacks has // already been called and the result callbacks struct has already been // deleted!) Stop managing the result callbacks struct. subscribe_completion_callbacks.release(); } } } // namespace pulse