/*
* Copyright 2026 LiveKit
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "livekit/platform_audio.h"
#include
#include "ffi.pb.h"
#include "ffi_client.h"
namespace livekit {
struct PlatformAudioState {
FfiHandle handle;
};
namespace {
std::uint64_t requireHandle(const std::shared_ptr& state) {
if (!state || !state->handle) {
throw PlatformAudioError("PlatformAudio has no valid FFI handle");
}
return static_cast(state->handle.get());
}
AudioDeviceInfo fromProto(const proto::AudioDeviceInfo& device) {
AudioDeviceInfo out;
out.index = device.index();
out.name = device.name();
out.id = device.has_guid() ? device.guid() : std::string();
return out;
}
std::vector convertDevices(const google::protobuf::RepeatedPtrField& devices) {
std::vector out;
out.reserve(static_cast(devices.size()));
for (const auto& device : devices) {
out.push_back(fromProto(device));
}
return out;
}
proto::AudioSourceOptions toProto(const PlatformAudioOptions& options) {
proto::AudioSourceOptions out;
out.set_echo_cancellation(options.echo_cancellation);
out.set_noise_suppression(options.noise_suppression);
out.set_auto_gain_control(options.auto_gain_control);
out.set_prefer_hardware(options.prefer_hardware);
return out;
}
proto::GetAudioDevicesResponse getAudioDevices(const std::shared_ptr& state) {
proto::FfiRequest req;
req.mutable_get_audio_devices()->set_platform_audio_handle(requireHandle(state));
const proto::FfiResponse resp = FfiClient::instance().sendRequest(req);
if (!resp.has_get_audio_devices()) {
throw PlatformAudioError("FfiResponse missing get_audio_devices");
}
const auto& devices = resp.get_audio_devices();
if (devices.has_error() && !devices.error().empty()) {
throw PlatformAudioError(devices.error());
}
return devices;
}
} // namespace
PlatformAudioSource::PlatformAudioSource(FfiHandle handle, std::shared_ptr platform_audio) noexcept
: handle_(std::move(handle)), platform_audio_(std::move(platform_audio)) {}
PlatformAudio::PlatformAudio() {
proto::FfiRequest req;
req.mutable_new_platform_audio();
const proto::FfiResponse resp = FfiClient::instance().sendRequest(req);
if (!resp.has_new_platform_audio()) {
throw PlatformAudioError("Failed to construct PlatformAudio: FfiResponse missing new_platform_audio");
}
const auto& platform_audio = resp.new_platform_audio();
if (platform_audio.has_error()) {
throw PlatformAudioError(platform_audio.error());
}
if (!platform_audio.has_platform_audio()) {
throw PlatformAudioError("Failed to construct PlatformAudio: NewPlatformAudioResponse missing platform_audio");
}
const auto& owned = platform_audio.platform_audio();
state_ = std::make_shared();
state_->handle = FfiHandle(static_cast(owned.handle().id()));
}
std::int32_t PlatformAudio::recordingDeviceCount() const {
return static_cast(getAudioDevices(state_).recording_devices_size());
}
std::int32_t PlatformAudio::playoutDeviceCount() const {
return static_cast(getAudioDevices(state_).playout_devices_size());
}
std::vector PlatformAudio::recordingDevices() const {
return convertDevices(getAudioDevices(state_).recording_devices());
}
std::vector PlatformAudio::playoutDevices() const {
return convertDevices(getAudioDevices(state_).playout_devices());
}
void PlatformAudio::setRecordingDevice(const std::string& device_id) const {
proto::FfiRequest req;
auto* msg = req.mutable_set_recording_device();
msg->set_platform_audio_handle(requireHandle(state_));
msg->set_device_id(device_id);
const proto::FfiResponse resp = FfiClient::instance().sendRequest(req);
if (!resp.has_set_recording_device()) {
throw PlatformAudioError("FfiResponse missing set_recording_device");
}
const auto& set_device = resp.set_recording_device();
if (set_device.has_error() && !set_device.error().empty()) {
throw PlatformAudioError(set_device.error());
}
}
void PlatformAudio::setPlayoutDevice(const std::string& device_id) const {
proto::FfiRequest req;
auto* msg = req.mutable_set_playout_device();
msg->set_platform_audio_handle(requireHandle(state_));
msg->set_device_id(device_id);
const proto::FfiResponse resp = FfiClient::instance().sendRequest(req);
if (!resp.has_set_playout_device()) {
throw PlatformAudioError("FfiResponse missing set_playout_device");
}
const auto& set_device = resp.set_playout_device();
if (set_device.has_error() && !set_device.error().empty()) {
throw PlatformAudioError(set_device.error());
}
}
std::shared_ptr PlatformAudio::createAudioSource(const PlatformAudioOptions& options) const {
proto::FfiRequest req;
auto* msg = req.mutable_new_audio_source();
msg->set_type(proto::AudioSourceType::AUDIO_SOURCE_PLATFORM);
msg->set_platform_audio_handle(requireHandle(state_));
*msg->mutable_options() = toProto(options);
const proto::FfiResponse resp = FfiClient::instance().sendRequest(req);
if (!resp.has_new_audio_source()) {
throw PlatformAudioError("FfiResponse missing new_audio_source");
}
const auto& new_source = resp.new_audio_source();
if (!new_source.has_source() || !new_source.source().has_handle()) {
throw PlatformAudioError("NewAudioSourceResponse missing source handle");
}
const auto& source = new_source.source();
const std::uint64_t handle_id = source.handle().id();
if (handle_id == 0) {
throw PlatformAudioError("NewAudioSourceResponse returned an invalid (null) source handle");
}
FfiHandle handle(static_cast(handle_id));
return std::shared_ptr(new PlatformAudioSource(std::move(handle), state_));
}
} // namespace livekit