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
|
target_link_libraries(ffmpeg_microphone PRIVATE
|
||||||
avfilter
|
avfilter
|
||||||
avdevice
|
avdevice
|
||||||
|
-lasound
|
||||||
|
-lpulse
|
||||||
cmvr_es::proto
|
cmvr_es::proto
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,21 @@
|
|||||||
#include "devices/microphone/microphone_device_discovery.h"
|
#include "devices/microphone/microphone_device_discovery.h"
|
||||||
|
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <limits>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
#include <alsa/asoundlib.h>
|
||||||
#include <libavdevice/avdevice.h>
|
#include <libavdevice/avdevice.h>
|
||||||
#include <libavutil/error.h>
|
#include <libavutil/error.h>
|
||||||
|
#include <pulse/pulseaudio.h>
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace cmvr::device {
|
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:";
|
constexpr std::string_view kPlugHardwarePrefix = "plughw:";
|
||||||
if (!device_name) {
|
if (!device_name) {
|
||||||
return false;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string_view name(device_name);
|
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
|
} // namespace
|
||||||
@ -50,6 +191,8 @@ std::vector<MicrophoneInputDeviceInfo> listAvailableMicrophoneInputDevices()
|
|||||||
throw std::runtime_error("FFmpeg ALSA input backend is unavailable");
|
throw std::runtime_error("FFmpeg ALSA input backend is unavailable");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto available_endpoints = listAvailablePulseAlsaSources();
|
||||||
|
|
||||||
AVDeviceInfoList* raw_device_list = nullptr;
|
AVDeviceInfoList* raw_device_list = nullptr;
|
||||||
const int result = avdevice_list_input_sources(
|
const int result = avdevice_list_input_sources(
|
||||||
input_format, nullptr, nullptr, &raw_device_list);
|
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) {
|
for (int index = 0; index < device_list->nb_devices; ++index) {
|
||||||
const AVDeviceInfo* source = device_list->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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user