diff --git a/cmvr-es/devices/microphone/ffmpeg_microphone/CMakeLists.txt b/cmvr-es/devices/microphone/ffmpeg_microphone/CMakeLists.txt index 93901401..85be85a0 100644 --- a/cmvr-es/devices/microphone/ffmpeg_microphone/CMakeLists.txt +++ b/cmvr-es/devices/microphone/ffmpeg_microphone/CMakeLists.txt @@ -10,6 +10,8 @@ add_library(cmvr_es::device::ffmpeg_microphone ALIAS ffmpeg_microphone) target_link_libraries(ffmpeg_microphone PRIVATE avfilter avdevice + -lasound + -lpulse cmvr_es::proto ) diff --git a/cmvr-es/devices/microphone/ffmpeg_microphone/src/microphone_device_discovery.cpp b/cmvr-es/devices/microphone/ffmpeg_microphone/src/microphone_device_discovery.cpp index df7f9e7b..6bb9417d 100644 --- a/cmvr-es/devices/microphone/ffmpeg_microphone/src/microphone_device_discovery.cpp +++ b/cmvr-es/devices/microphone/ffmpeg_microphone/src/microphone_device_discovery.cpp @@ -1,13 +1,21 @@ #include "devices/microphone/microphone_device_discovery.h" +#include +#include +#include #include +#include +#include #include +#include #include #include extern "C" { +#include #include #include +#include } namespace cmvr::device { @@ -27,15 +35,148 @@ struct DeviceListDeleter { } }; -bool isFfmpegCaptureEndpoint(const char* device_name) +using AlsaEndpoint = std::pair; + +std::optional parseNonNegativeInt(const char* value) +{ + if (!value || value[0] == '\0') { + return std::nullopt; + } + + char* end = nullptr; + errno = 0; + const long parsed = std::strtol(value, &end, 10); + if (errno != 0 || end == value || *end != '\0' || parsed < 0 || + parsed > std::numeric_limits::max()) { + return std::nullopt; + } + return static_cast(parsed); +} + +std::optional parseFfmpegCaptureEndpoint(const char* device_name) { constexpr std::string_view kPlugHardwarePrefix = "plughw:"; if (!device_name) { - return false; + return std::nullopt; } const std::string_view name(device_name); - return name.compare(0, kPlugHardwarePrefix.size(), kPlugHardwarePrefix) == 0; + if (name.compare(0, kPlugHardwarePrefix.size(), kPlugHardwarePrefix) != 0) { + return std::nullopt; + } + + constexpr std::string_view kCardKey = "CARD="; + constexpr std::string_view kDeviceKey = "DEV="; + const std::size_t card_key = name.find(kCardKey, kPlugHardwarePrefix.size()); + const std::size_t card_end = name.find(',', card_key); + const std::size_t device_key = name.find(kDeviceKey, card_end); + if (card_key == std::string_view::npos || card_end == std::string_view::npos || + device_key == std::string_view::npos) { + return std::nullopt; + } + + const std::string card_name(name.substr(card_key + kCardKey.size(), + card_end - card_key - kCardKey.size())); + const std::string device_text(name.substr(device_key + kDeviceKey.size())); + const int card_index = snd_card_get_index(card_name.c_str()); + const auto device_index = parseNonNegativeInt(device_text.c_str()); + if (card_index < 0 || !device_index) { + return std::nullopt; + } + return AlsaEndpoint{card_index, *device_index}; +} + +struct PulseSourceQuery { + pa_mainloop* mainloop = nullptr; + std::set available_endpoints; + bool complete = false; + bool failed = false; +}; + +void finishPulseSourceQuery(PulseSourceQuery* query, bool failed) +{ + query->complete = !failed; + query->failed = failed; + pa_mainloop_quit(query->mainloop, failed ? 1 : 0); +} + +void pulseSourceInfoCallback( + pa_context*, const pa_source_info* source, int end_of_list, void* userdata) +{ + auto* query = static_cast(userdata); + if (end_of_list != 0) { + finishPulseSourceQuery(query, end_of_list < 0); + return; + } + if (!source || source->monitor_of_sink != PA_INVALID_INDEX || + (source->active_port && source->active_port->available == PA_PORT_AVAILABLE_NO)) { + return; + } + + const auto card = parseNonNegativeInt(pa_proplist_gets(source->proplist, "alsa.card")); + const auto device = parseNonNegativeInt(pa_proplist_gets(source->proplist, "alsa.device")); + if (card && device) { + query->available_endpoints.emplace(*card, *device); + } +} + +void pulseContextStateCallback(pa_context* context, void* userdata) +{ + auto* query = static_cast(userdata); + switch (pa_context_get_state(context)) { + case PA_CONTEXT_READY: { + pa_operation* operation = pa_context_get_source_info_list( + context, pulseSourceInfoCallback, query); + if (!operation) { + finishPulseSourceQuery(query, true); + return; + } + pa_operation_unref(operation); + return; + } + case PA_CONTEXT_FAILED: + case PA_CONTEXT_TERMINATED: + finishPulseSourceQuery(query, true); + return; + default: + return; + } +} + +std::set listAvailablePulseAlsaSources() +{ + std::unique_ptr mainloop( + pa_mainloop_new(), &pa_mainloop_free); + if (!mainloop) { + throw std::runtime_error("Failed to create PulseAudio main loop"); + } + + std::unique_ptr context( + pa_context_new(pa_mainloop_get_api(mainloop.get()), "cmvr-es microphone discovery"), + &pa_context_unref); + if (!context) { + throw std::runtime_error("Failed to create PulseAudio context"); + } + + PulseSourceQuery query; + query.mainloop = mainloop.get(); + pa_context_set_state_callback(context.get(), pulseContextStateCallback, &query); + if (pa_context_connect(context.get(), nullptr, PA_CONTEXT_NOFLAGS, nullptr) < 0) { + throw std::runtime_error( + "Failed to connect to PulseAudio: " + + std::string(pa_strerror(pa_context_errno(context.get())))); + } + + int result = 0; + const int run_result = pa_mainloop_run(mainloop.get(), &result); + pa_context_set_state_callback(context.get(), nullptr, nullptr); + pa_context_disconnect(context.get()); + if (run_result < 0 || result != 0 || query.failed || !query.complete) { + throw std::runtime_error( + "Failed to list PulseAudio input sources: " + + std::string(pa_strerror(pa_context_errno(context.get())))); + } + return query.available_endpoints; } } // namespace @@ -50,6 +191,8 @@ std::vector listAvailableMicrophoneInputDevices() throw std::runtime_error("FFmpeg ALSA input backend is unavailable"); } + const auto available_endpoints = listAvailablePulseAlsaSources(); + AVDeviceInfoList* raw_device_list = nullptr; const int result = avdevice_list_input_sources( input_format, nullptr, nullptr, &raw_device_list); @@ -67,7 +210,11 @@ std::vector listAvailableMicrophoneInputDevices() for (int index = 0; index < device_list->nb_devices; ++index) { const AVDeviceInfo* source = device_list->devices[index]; - if (!source || !isFfmpegCaptureEndpoint(source->device_name)) { + if (!source) { + continue; + } + const auto endpoint = parseFfmpegCaptureEndpoint(source->device_name); + if (!endpoint || available_endpoints.find(*endpoint) == available_endpoints.end()) { continue; }