fix(microphone): filter unavailable capture sources
This commit is contained in:
parent
05ab64cf76
commit
41b112efb7
@ -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
|
||||
)
|
||||
|
||||
|
||||
@ -1,13 +1,21 @@
|
||||
#include "devices/microphone/microphone_device_discovery.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
extern "C" {
|
||||
#include <alsa/asoundlib.h>
|
||||
#include <libavdevice/avdevice.h>
|
||||
#include <libavutil/error.h>
|
||||
#include <pulse/pulseaudio.h>
|
||||
}
|
||||
|
||||
namespace cmvr::device {
|
||||
@ -27,15 +35,148 @@ struct DeviceListDeleter {
|
||||
}
|
||||
};
|
||||
|
||||
bool isFfmpegCaptureEndpoint(const char* device_name)
|
||||
using AlsaEndpoint = std::pair<int, int>;
|
||||
|
||||
std::optional<int> 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<int>::max()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return static_cast<int>(parsed);
|
||||
}
|
||||
|
||||
std::optional<AlsaEndpoint> 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<AlsaEndpoint> 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<PulseSourceQuery*>(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<PulseSourceQuery*>(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<AlsaEndpoint> listAvailablePulseAlsaSources()
|
||||
{
|
||||
std::unique_ptr<pa_mainloop, decltype(&pa_mainloop_free)> mainloop(
|
||||
pa_mainloop_new(), &pa_mainloop_free);
|
||||
if (!mainloop) {
|
||||
throw std::runtime_error("Failed to create PulseAudio main loop");
|
||||
}
|
||||
|
||||
std::unique_ptr<pa_context, decltype(&pa_context_unref)> 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<MicrophoneInputDeviceInfo> 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<MicrophoneInputDeviceInfo> 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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user