diff --git a/cmvr-es/devices/microphone/ffmpeg_microphone/CMakeLists.txt b/cmvr-es/devices/microphone/ffmpeg_microphone/CMakeLists.txt index 4185d31a..93901401 100644 --- a/cmvr-es/devices/microphone/ffmpeg_microphone/CMakeLists.txt +++ b/cmvr-es/devices/microphone/ffmpeg_microphone/CMakeLists.txt @@ -1,4 +1,7 @@ -add_library(ffmpeg_microphone SHARED src/ffmpeg_microphone.cpp) +add_library(ffmpeg_microphone SHARED + src/ffmpeg_microphone.cpp + src/microphone_device_discovery.cpp +) target_include_directories(ffmpeg_microphone PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 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 new file mode 100644 index 00000000..99fa3adc --- /dev/null +++ b/cmvr-es/devices/microphone/ffmpeg_microphone/src/microphone_device_discovery.cpp @@ -0,0 +1,74 @@ +#include "devices/microphone/microphone_device_discovery.h" + +#include +#include +#include + +extern "C" { +#include +#include +} + +namespace cmvr::device { +namespace { + +std::string avErrorToString(int error) +{ + char buffer[AV_ERROR_MAX_STRING_SIZE] = {0}; + av_strerror(error, buffer, sizeof(buffer)); + return buffer; +} + +struct DeviceListDeleter { + void operator()(AVDeviceInfoList* list) const + { + avdevice_free_list_devices(&list); + } +}; + +} // namespace + +std::vector listAvailableMicrophoneInputDevices() +{ + constexpr const char* kBackend = "alsa"; + + avdevice_register_all(); + const AVInputFormat* input_format = av_find_input_format(kBackend); + if (!input_format) { + throw std::runtime_error("FFmpeg ALSA input backend is unavailable"); + } + + AVDeviceInfoList* raw_device_list = nullptr; + const int result = avdevice_list_input_sources( + input_format, nullptr, nullptr, &raw_device_list); + std::unique_ptr device_list(raw_device_list); + if (result < 0) { + throw std::runtime_error( + "Failed to list ALSA microphone devices: " + avErrorToString(result)); + } + + std::vector devices; + if (!device_list) { + return devices; + } + devices.reserve(static_cast(device_list->nb_devices)); + + for (int index = 0; index < device_list->nb_devices; ++index) { + const AVDeviceInfo* source = device_list->devices[index]; + if (!source || !source->device_name || source->device_name[0] == '\0') { + continue; + } + + MicrophoneInputDeviceInfo device; + device.input_device = source->device_name; + device.display_name = source->device_description && source->device_description[0] != '\0' + ? source->device_description + : source->device_name; + device.backend = kBackend; + device.is_default = index == device_list->default_device; + devices.push_back(std::move(device)); + } + return devices; +} + +} // namespace cmvr::device diff --git a/cmvr-es/devices/microphone/microphone_device_discovery.h b/cmvr-es/devices/microphone/microphone_device_discovery.h new file mode 100644 index 00000000..32e1ff99 --- /dev/null +++ b/cmvr-es/devices/microphone/microphone_device_discovery.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace cmvr::device { + +struct MicrophoneInputDeviceInfo { + std::string input_device; + std::string display_name; + std::string backend; + bool is_default{false}; +}; + +// Returns the capture devices that can be used as microphone inputs. +// Throws std::runtime_error when the platform backend cannot enumerate devices. +std::vector listAvailableMicrophoneInputDevices(); + +} // namespace cmvr::device diff --git a/cmvr-es/service/CMakeLists.txt b/cmvr-es/service/CMakeLists.txt index 24fad685..6e430a18 100644 --- a/cmvr-es/service/CMakeLists.txt +++ b/cmvr-es/service/CMakeLists.txt @@ -18,6 +18,7 @@ target_link_libraries(service PRIVATE cmvr_es::proto osqp cmvr_es::device_manager + cmvr_es::device::microphone cmvr_es::task_manager cmvr_es::algorithms::controller cmvr_es::task diff --git a/cmvr-es/service/grpc/include/grpc_microphone_service.h b/cmvr-es/service/grpc/include/grpc_microphone_service.h index 1e4eac04..39eb731f 100644 --- a/cmvr-es/service/grpc/include/grpc_microphone_service.h +++ b/cmvr-es/service/grpc/include/grpc_microphone_service.h @@ -17,6 +17,10 @@ namespace cmvr::service public: gRPCMicroPhoneServiceImpl(); ~gRPCMicroPhoneServiceImpl() override = default; + grpc::Status ListDevices( + grpc::ServerContext* context, + const api::ListMicrophoneDevicesCommand_Request* request, + api::ListMicrophoneDevicesCommand_Feedback* response) override; grpc::Status GetStatus(grpc::ServerContext* context, const api::GetMicStateCommand_Request* request,api::GetMicStateCommand_Feedback* response) override; grpc::Status StartRecord(grpc::ServerContext* context, const api::StartMicRecordingCommand_Request* request,api::StartMicRecordingCommand_Feedback* response) override; grpc::Status StopRecord(grpc::ServerContext* context, const api::StopMicRecordingCommand_Request* request,api::StopMicRecordingCommand_Feedback* response) override; diff --git a/cmvr-es/service/grpc/src/grpc_microphone_service.cpp b/cmvr-es/service/grpc/src/grpc_microphone_service.cpp index 63f07cb2..5f6f00a9 100644 --- a/cmvr-es/service/grpc/src/grpc_microphone_service.cpp +++ b/cmvr-es/service/grpc/src/grpc_microphone_service.cpp @@ -1,4 +1,5 @@ #include "common/base/logging/logger.h" +#include "devices/microphone/microphone_device_discovery.h" #include "manager/media_source_hub/include/device_media_source_adapter.h" #include #include @@ -28,6 +29,31 @@ grpc::Status failResponse(ResponseT* response, const std::string& message) { gRPCMicroPhoneServiceImpl::gRPCMicroPhoneServiceImpl(): dmgr_(DeviceManager::getInstance()) {} +grpc::Status gRPCMicroPhoneServiceImpl::ListDevices( + grpc::ServerContext* context, + const api::ListMicrophoneDevicesCommand_Request* request, + api::ListMicrophoneDevicesCommand_Feedback* response) { + (void)context; + (void)request; + try { + const auto devices = listAvailableMicrophoneInputDevices(); + for (const auto& device : devices) { + auto* item = response->add_devices(); + item->set_input_device(device.input_device); + item->set_display_name(device.display_name); + item->set_backend(device.backend); + item->set_is_default(device.is_default); + } + response->mutable_header()->set_success(true); + setCurrentTimestamp(response->mutable_header()->mutable_timestamp()); + CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (ListDevices): found " + << devices.size() << " microphone input device(s)"; + return grpc::Status::OK; + } catch (const std::exception& e) { + return failResponse(response, e.what()); + } +} + grpc::Status gRPCMicroPhoneServiceImpl::GetStatus(grpc::ServerContext* context, const api::GetMicStateCommand_Request* request, api::GetMicStateCommand_Feedback* response) { try { diff --git a/protos/cmvr/api/microphone_command.proto b/protos/cmvr/api/microphone_command.proto index 71a6f4ec..add491d9 100644 --- a/protos/cmvr/api/microphone_command.proto +++ b/protos/cmvr/api/microphone_command.proto @@ -12,6 +12,22 @@ message MicState { string error_message = 5; } +message MicrophoneInputDevice { + string input_device = 1; + string display_name = 2; + string backend = 3; + bool is_default = 4; +} + +message ListMicrophoneDevicesCommand { + message Request {} + + message Feedback { + CommandHeader.Feedback header = 1; + repeated MicrophoneInputDevice devices = 2; + } +} + message GetMicStateCommand { message Request { CommandHeader.Request header = 1; diff --git a/protos/cmvr/api/microphone_service.proto b/protos/cmvr/api/microphone_service.proto index d9cea818..8778c6f7 100644 --- a/protos/cmvr/api/microphone_service.proto +++ b/protos/cmvr/api/microphone_service.proto @@ -5,6 +5,7 @@ import "cmvr/api/microphone_command.proto"; package cmvr.api; service MicPhoneService { + rpc ListDevices(ListMicrophoneDevicesCommand.Request) returns (ListMicrophoneDevicesCommand.Feedback); rpc GetStatus(GetMicStateCommand.Request) returns (GetMicStateCommand.Feedback); rpc StartRecord(StartMicRecordingCommand.Request) returns (StartMicRecordingCommand.Feedback); rpc StopRecord(StopMicRecordingCommand.Request) returns (StopMicRecordingCommand.Feedback);