fix(microphone): filter unavailable capture sources

This commit is contained in:
linbo 2026-08-25 16:05:31 +08:00
parent 05ab64cf76
commit 41b112efb7
2 changed files with 153 additions and 4 deletions

View File

@ -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
)

View File

@ -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;
}