Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ add_subdirectory(logging_levels/basic_usage logging_levels_basic_usage)
add_subdirectory(logging_levels/custom_sinks logging_levels_custom_sinks)
add_subdirectory(hello_livekit/sender hello_livekit_sender)
add_subdirectory(hello_livekit/receiver hello_livekit_receiver)
add_subdirectory(platform_audio)
add_subdirectory(ping_pong/ping ping_pong_ping)
add_subdirectory(ping_pong/pong ping_pong_pong)
add_subdirectory(user_timestamped_video)
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,21 @@ For example:
./build/basic_room/basic_room --url <ws-url> --token <token>
```

### PlatformAudio

The `platform_audio` examples show microphone capture and speaker playout using
WebRTC's platform Audio Device Module:

```bash
./build/platform_audio/player/PlatformAudioPlayer <ws-url> <player-token>
./build/platform_audio/sender/PlatformAudioSender <ws-url> <sender-token>
```

### Supported platforms

Prebuilt SDKs are downloaded automatically for:
* Windows: x64
* macOS: x64, arm64 (Apple Silicon)
* Linux: x64

If no matching SDK is available for your platform, CMake configuration will fail with a clear error.
If no matching SDK is available for your platform, CMake configuration will fail with a clear error.
16 changes: 16 additions & 0 deletions platform_audio/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2026 LiveKit, Inc.
#
# 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.

add_subdirectory(sender)
add_subdirectory(player)
44 changes: 44 additions & 0 deletions platform_audio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# PlatformAudio

These examples demonstrate the platform Audio Device Module path:

- `PlatformAudioSender` publishes microphone audio with echo cancellation, noise suppression, and auto gain control.
- `PlatformAudioPlayer` joins the same room and plays subscribed remote audio through the platform output device.

Build the collection, then run the player and sender with different participant tokens for the same room:

```bash
./build/platform_audio/player/PlatformAudioPlayer <ws-url> <player-token>
./build/platform_audio/sender/PlatformAudioSender <ws-url> <sender-token>
```

Environment fallbacks:

```bash
export LIVEKIT_URL=wss://your-livekit-host
export LIVEKIT_PLAYER_TOKEN=<player-token>
export LIVEKIT_SENDER_TOKEN=<sender-token>
```

## Test environments

The sender captures the mic and runs the AEC/NS/AGC front-end; the player drives
hardware playout. The acoustic echo cancellation (AEC) reference is per Audio
Device Module (ADM), so the sender can only cancel playout from *its own* ADM.
Pick an environment based on what you want to prove out.

| Environment | What it proves | Notes |
|-------------|----------------|-------|
| **Two machines** (sender on A, player on B) | End-to-end capture → publish → subscribe → hardware playout over the network, like a real call. | Closest to a real LiveKit call. For full duplex (both apps on both boxes), use headphones or separate rooms to avoid feedback. |
| **One machine + headphones** | Mic capture and ADM playout both work on one box, with no acoustic feedback path. | Best for confirming device selection and round-trip latency in isolation. |
| **Noise suppression (NS)** | Steady background noise is attenuated while speech passes. | Add a fan / AC hum / typing near the mic. A/B by setting `noise_suppression = false` in `sender/main.cpp`. |
| **Auto gain control (AGC)** | Quiet vs. loud / near vs. far speech is normalized on the player side. | A/B by setting `auto_gain_control = false` in `sender/main.cpp`. |
| **`prefer_hardware = true`** | Platform hardware voice processing engages (e.g. macOS voice-processing I/O). | Set in the sender options; compare CPU and audio character vs. the software path. |
| **Device / hot-plug sanity** | `recordingDevices()` / `playoutDevices()` reflect attached hardware and route correctly. | Plug/unplug a USB or Bluetooth mic/headset before launch and check the startup device logs. |

> **AEC caveat:** these split sender/player apps cannot demonstrate AEC against
> each other on one machine over open speakers — the sender's AEC has no
> reference to the player's separate ADM, so the speaker output is treated as
> external sound and you get an echo/feedback loop. Genuine AEC requires a
> single application that both plays remote audio and captures the mic through
> the *same* ADM. Use headphones to avoid feedback with these examples.
22 changes: 22 additions & 0 deletions platform_audio/player/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2026 LiveKit, Inc.
#
# 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.

add_executable(PlatformAudioPlayer
main.cpp
)

target_include_directories(PlatformAudioPlayer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(PlatformAudioPlayer PRIVATE ${LIVEKIT_CORE_TARGET})

livekit_copy_windows_runtime_dlls(PlatformAudioPlayer)
152 changes: 152 additions & 0 deletions platform_audio/player/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* Copyright 2026 LiveKit, Inc.
*
* 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.
*/

/// Plays subscribed room audio using PlatformAudio.
///
/// Usage:
/// PlatformAudioPlayer <ws-url> <player-token>
///
/// Or via environment variables:
/// LIVEKIT_URL, LIVEKIT_PLAYER_TOKEN

#include <atomic>
#include <chrono>
#include <csignal>
#include <cstdlib>
#include <exception>
#include <iostream>
#include <memory>
#include <string>
#include <thread>

#include "livekit/livekit.h"

using namespace livekit;

namespace {

std::atomic<bool> g_running{true};

void handleSignal(int) { g_running.store(false); }

std::string getenvOrEmpty(const char* name) {
const char* value = std::getenv(name);
return value ? std::string(value) : std::string{};
}

void printUsage() {
std::cerr << "[error] Usage: PlatformAudioPlayer <ws-url> <player-token>\n"
<< " or set LIVEKIT_URL and LIVEKIT_PLAYER_TOKEN\n";
}

class PlayerDelegate final : public RoomDelegate {
public:
void onParticipantConnected(Room&, const ParticipantConnectedEvent& event) override {
if (event.participant) {
std::cout << "[info] [platform-audio-player] Participant connected identity='" << event.participant->identity()
<< "'\n";
}
}

void onTrackSubscribed(Room&, const TrackSubscribedEvent& event) override {
if (!event.track || event.track->kind() != TrackKind::KIND_AUDIO) {
return;
}

const std::string participant_identity = event.participant ? event.participant->identity() : std::string("unknown");
const std::string publication_name = event.publication ? event.publication->name() : event.track->name();
std::cout << "[info] [platform-audio-player] Playing audio track '" << publication_name
<< "' from participant identity='" << participant_identity << "'\n";
}

void onTrackSubscriptionFailed(Room&, const TrackSubscriptionFailedEvent& event) override {
const std::string participant_identity = event.participant ? event.participant->identity() : std::string("unknown");
std::cerr << "[warn] [platform-audio-player] Audio subscription failed for participant identity='"
<< participant_identity << "' track_sid='" << event.track_sid << "'\n";
}
};

} // namespace

int main(int argc, char* argv[]) {
std::string url = getenvOrEmpty("LIVEKIT_URL");
std::string player_token = getenvOrEmpty("LIVEKIT_PLAYER_TOKEN");

if (argc >= 3) {
url = argv[1];
player_token = argv[2];
}

if (url.empty() || player_token.empty()) {
printUsage();
return 1;
}

std::signal(SIGINT, handleSignal);
#ifdef SIGTERM
std::signal(SIGTERM, handleSignal);
#endif

livekit::initialize(livekit::LogLevel::Info);

try {
PlatformAudio platform_audio;

auto playout_devices = platform_audio.playoutDevices();
std::cout << "[info] [platform-audio-player] Playout devices: " << playout_devices.size() << "\n";
for (const auto& device : playout_devices) {
std::cout << " [" << device.index << "] " << device.name << " id=" << device.id << "\n";
}

auto room = std::make_unique<Room>();
PlayerDelegate delegate;
room->setDelegate(&delegate);

RoomOptions options;
options.auto_subscribe = true;
options.dynacast = false;

if (!room->connect(url, player_token, options)) {
std::cerr << "[error] [platform-audio-player] Failed to connect\n";
room.reset();
livekit::shutdown();
return 1;
}

if (auto local_participant = room->localParticipant().lock()) {
std::cout << "[info] [platform-audio-player] Connected as identity='" << local_participant->identity()
<< "' room='" << room->roomInfo().name << "'\n";
} else {
throw std::runtime_error("unable to lock local participant");
}

std::cout << "[info] [platform-audio-player] Waiting for remote audio; Ctrl-C to exit\n";
while (g_running.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}

std::cout << "[info] [platform-audio-player] Disconnecting\n";
room->setDelegate(nullptr);
room.reset();
} catch (const std::exception& error) {
std::cerr << "[error] [platform-audio-player] " << error.what() << "\n";
livekit::shutdown();
return 1;
}

livekit::shutdown();
return 0;
}
22 changes: 22 additions & 0 deletions platform_audio/sender/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2026 LiveKit, Inc.
#
# 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.

add_executable(PlatformAudioSender
main.cpp
)

target_include_directories(PlatformAudioSender PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(PlatformAudioSender PRIVATE ${LIVEKIT_CORE_TARGET})

livekit_copy_windows_runtime_dlls(PlatformAudioSender)
Loading
Loading