diff --git a/CMakeLists.txt b/CMakeLists.txt
index 39d06fa..e64fae0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -77,6 +77,7 @@ set(FFI_PROTO_FILES
${FFI_PROTO_DIR}/e2ee.proto
${FFI_PROTO_DIR}/stats.proto
${FFI_PROTO_DIR}/data_stream.proto
+ ${FFI_PROTO_DIR}/data_track.proto
${FFI_PROTO_DIR}/rpc.proto
${FFI_PROTO_DIR}/track_publication.proto
)
@@ -325,6 +326,7 @@ add_library(livekit SHARED
src/audio_source.cpp
src/audio_stream.cpp
src/data_stream.cpp
+ src/data_track_subscription.cpp
src/e2ee.cpp
src/ffi_handle.cpp
src/ffi_client.cpp
@@ -332,7 +334,9 @@ add_library(livekit SHARED
src/livekit.cpp
src/logging.cpp
src/local_audio_track.cpp
+ src/local_data_track.cpp
src/remote_audio_track.cpp
+ src/remote_data_track.cpp
src/room.cpp
src/room_proto_converter.cpp
src/room_proto_converter.h
@@ -682,10 +686,6 @@ install(FILES
# Build the LiveKit C++ bridge before examples (human_robot depends on it)
add_subdirectory(bridge)
-# ---- Examples ----
-# add_subdirectory(examples)
-
-
if(LIVEKIT_BUILD_EXAMPLES)
add_subdirectory(examples)
endif()
diff --git a/README.md b/README.md
index eb473f2..97fc77f 100644
--- a/README.md
+++ b/README.md
@@ -447,6 +447,37 @@ CPP SDK is using clang C++ format
brew install clang-format
```
+
+#### Memory Checks
+Run valgrind on various examples or tests to check for memory leaks and other issues.
+```bash
+valgrind --leak-check=full ./build-debug/bin/BridgeRobot
+valgrind --leak-check=full ./build-debug/bin/BridgeHuman
+valgrind --leak-check=full ./build-debug/bin/livekit_integration_tests
+valgrind --leak-check=full ./build-debug/bin/livekit_stress_tests
+```
+
+# Running locally
+1. Install the livekit-server
+https://docs.livekit.io/transport/self-hosting/local/
+
+Start the livekit-server with data tracks enabled:
+```bash
+LIVEKIT_CONFIG="enable_data_tracks: true" livekit-server --dev
+```
+
+```bash
+# generate tokens, do for all participants
+lk token create \
+ --api-key devkey \
+ --api-secret secret \
+ -i robot \
+ --join \
+ --valid-for 99999h \
+ --room robo_room \
+ --grant '{"canPublish":true,"canSubscribe":true,"canPublishData":true}'
+```
+
| LiveKit Ecosystem |
diff --git a/bridge/CMakeLists.txt b/bridge/CMakeLists.txt
index c1b2369..996b82e 100644
--- a/bridge/CMakeLists.txt
+++ b/bridge/CMakeLists.txt
@@ -8,6 +8,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(livekit_bridge SHARED
src/livekit_bridge.cpp
src/bridge_audio_track.cpp
+ src/bridge_data_track.cpp
src/bridge_video_track.cpp
src/bridge_room_delegate.cpp
src/bridge_room_delegate.h
diff --git a/bridge/README.md b/bridge/README.md
index 76b4f2b..f77da63 100644
--- a/bridge/README.md
+++ b/bridge/README.md
@@ -206,10 +206,7 @@ The human will print periodic summaries like:
## Testing
-The bridge includes a unit test suite built with [Google Test](https://github.com/google/googletest). Tests cover
-1. `CallbackKey` hashing/equality,
-2. `BridgeAudioTrack`/`BridgeVideoTrack` state management, and
-3. `LiveKitBridge` pre-connection behaviour (callback registration, error handling).
+The bridge includes a unit test suite built with [Google Test](https://github.com/google/googletest).
### Building and running tests
@@ -222,18 +219,46 @@ Bridge tests are automatically included when you build with the `debug-tests` or
Then run them directly:
```bash
-./build-debug/bin/livekit_bridge_tests
+./build-debug/bin/livekit_bridge_*_tests
```
-### Standalone bridge tests only
+### Bridge Tests
-If you want to build bridge tests independently (without the parent SDK tests), set `LIVEKIT_BRIDGE_BUILD_TESTS=ON`:
+The bridge layer (`bridge/tests/`) has its own integration and stress tests
+that exercise the full `LiveKitBridge` API over a real LiveKit server.
+They use the **same environment variables** as the SDK tests above.
```bash
-cmake --preset macos-debug -DLIVEKIT_BRIDGE_BUILD_TESTS=ON
-cmake --build build-debug --target livekit_bridge_tests
+# Run bridge tests via CTest
+cd build-debug && ctest -L "bridge_integration" --output-on-failure
+cd build-debug && ctest -L "bridge_stress" --output-on-failure
+
+# Or run executables directly
+./build-debug/bin/livekit_bridge_integration_tests
+./build-debug/bin/livekit_bridge_stress_tests
+
+# Run specific test suites
+./build-debug/bin/livekit_bridge_integration_tests --gtest_filter="*AudioFrameRoundTrip*"
+./build-debug/bin/livekit_bridge_stress_tests --gtest_filter="*HighThroughput*"
```
+| Executable | Description |
+|------------|-------------|
+| `livekit_bridge_unit_tests` | Unit tests (no server required) |
+| `livekit_bridge_integration_tests` | Audio & data round-trip, latency, connect/disconnect cycles |
+| `livekit_bridge_stress_tests` | Sustained push, lifecycle, callback churn, multi-track concurrency |
+
+#### Stress test suites
+
+| Test file | Tests | What it exercises |
+|-----------|-------|-------------------|
+| `test_bridge_audio_stress` | SustainedAudioPush, ReleaseUnderActivePush, RapidConnectDisconnectWithCallback | Audio push at real-time pace, release/push race, abrupt teardown |
+| `test_bridge_data_stress` | HighThroughput, LargePayloadStress, CallbackChurn | Data throughput, 64 KB frames, data callback register/clear churn |
+| `test_bridge_lifecycle_stress` | DisconnectUnderLoad, TrackReleaseWhileReceiving, FullLifecycleSoak | Disconnect with active pushers+receivers, mid-stream track release, repeated full create→push→release→destroy |
+| `test_bridge_callback_stress` | AudioCallbackChurn, MixedCallbackChurn, CallbackReplacement | Audio set/clear churn, mixed audio+data churn, rapid callback overwrite |
+| `test_bridge_multi_track_stress` | ConcurrentMultiTrackPush, ConcurrentCreateRelease, FullDuplexMultiTrack | 4 tracks pushed concurrently, create/release cycles under contention, bidirectional audio+data |
+
+
## Limitations
The bridge is designed for simplicity and currently only supports limited audio and video features. It does not expose:
diff --git a/bridge/include/livekit_bridge/bridge_audio_track.h b/bridge/include/livekit_bridge/bridge_audio_track.h
index 5683e06..c313c0a 100644
--- a/bridge/include/livekit_bridge/bridge_audio_track.h
+++ b/bridge/include/livekit_bridge/bridge_audio_track.h
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LiveKit
+ * 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.
@@ -55,6 +55,10 @@ class BridgeAudioTrackTest;
* one thread while another calls mute()/unmute()/release(), or to call
* pushFrame() concurrently from multiple threads.
*
+ * All public methods are thread-safe: it is safe to call pushFrame() from
+ * one thread while another calls mute()/unmute()/release(), or to call
+ * pushFrame() concurrently from multiple threads.
+ *
* Usage:
* auto mic = bridge.createAudioTrack("mic", 48000, 2,
* livekit::TrackSource::SOURCE_MICROPHONE);
diff --git a/bridge/include/livekit_bridge/bridge_data_track.h b/bridge/include/livekit_bridge/bridge_data_track.h
new file mode 100644
index 0000000..e009f95
--- /dev/null
+++ b/bridge/include/livekit_bridge/bridge_data_track.h
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace livekit {
+class LocalDataTrack;
+class LocalParticipant;
+} // namespace livekit
+
+namespace livekit_bridge {
+
+namespace test {
+class BridgeDataTrackTest;
+} // namespace test
+
+/**
+ * Handle to a published local data track.
+ *
+ * Created via LiveKitBridge::createDataTrack(). The bridge retains a
+ * reference to every track it creates and will automatically release all
+ * tracks when disconnect() is called. To unpublish a track mid-session,
+ * call release() explicitly.
+ *
+ * Unlike BridgeAudioTrack / BridgeVideoTrack, data tracks have no
+ * Source, Publication, mute(), or unmute(). They carry arbitrary binary
+ * frames via pushFrame().
+ *
+ * All public methods are thread-safe.
+ *
+ * Usage:
+ * auto dt = bridge.createDataTrack("sensor-data");
+ * dt->pushFrame({0x01, 0x02, 0x03});
+ * dt->release(); // unpublishes mid-session
+ */
+class BridgeDataTrack {
+public:
+ ~BridgeDataTrack();
+
+ BridgeDataTrack(const BridgeDataTrack &) = delete;
+ BridgeDataTrack &operator=(const BridgeDataTrack &) = delete;
+
+ /**
+ * Push a binary frame to all subscribers of this data track.
+ *
+ * @param payload Raw bytes to send.
+ * @param user_timestamp Optional application-defined timestamp.
+ * @return true if the frame was pushed, false if the track has been
+ * released or the push failed (e.g. back-pressure).
+ */
+ bool pushFrame(const std::vector &payload,
+ std::optional user_timestamp = std::nullopt);
+
+ /**
+ * Push a binary frame from a raw pointer.
+ *
+ * @param data Pointer to raw bytes.
+ * @param size Number of bytes.
+ * @param user_timestamp Optional application-defined timestamp.
+ * @return true on success, false if released or push failed.
+ */
+ bool pushFrame(const std::uint8_t *data, std::size_t size,
+ std::optional user_timestamp = std::nullopt);
+
+ /// Track name as provided at creation.
+ const std::string &name() const noexcept { return name_; }
+
+ /// Whether the track is still published in the room.
+ bool isPublished() const;
+
+ /// Whether this track has been released / unpublished.
+ bool isReleased() const noexcept;
+
+ /**
+ * Explicitly unpublish the track and release underlying SDK resources.
+ *
+ * After this call, pushFrame() returns false. Called automatically by the
+ * destructor and by LiveKitBridge::disconnect(). Safe to call multiple
+ * times (idempotent).
+ */
+ void release();
+
+private:
+ friend class LiveKitBridge;
+ friend class test::BridgeDataTrackTest;
+
+ BridgeDataTrack(std::string name,
+ std::shared_ptr track,
+ livekit::LocalParticipant *participant);
+
+ /** Protects released_ and track_ for thread-safe access. */
+ mutable std::mutex mutex_;
+
+ /** Publisher-assigned track name (immutable after construction). */
+ std::string name_;
+
+ /** True after release() or disconnect(); prevents further pushFrame(). */
+ bool released_ = false;
+
+ /** Underlying SDK data track handle. Nulled on release(). */
+ std::shared_ptr track_;
+
+ /** Participant that published this track; used for unpublish. Not owned. */
+ livekit::LocalParticipant *participant_ = nullptr;
+};
+
+} // namespace livekit_bridge
diff --git a/bridge/include/livekit_bridge/bridge_video_track.h b/bridge/include/livekit_bridge/bridge_video_track.h
index 8057b7a..350c852 100644
--- a/bridge/include/livekit_bridge/bridge_video_track.h
+++ b/bridge/include/livekit_bridge/bridge_video_track.h
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LiveKit
+ * 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.
@@ -55,6 +55,10 @@ class BridgeVideoTrackTest;
* one thread while another calls mute()/unmute()/release(), or to call
* pushFrame() concurrently from multiple threads.
*
+ * All public methods are thread-safe: it is safe to call pushFrame() from
+ * one thread while another calls mute()/unmute()/release(), or to call
+ * pushFrame() concurrently from multiple threads.
+ *
* Usage:
* auto cam = bridge.createVideoTrack("cam", 1280, 720,
* livekit::TrackSource::SOURCE_CAMERA);
diff --git a/bridge/include/livekit_bridge/livekit_bridge.h b/bridge/include/livekit_bridge/livekit_bridge.h
index 3d27dea..536b1f1 100644
--- a/bridge/include/livekit_bridge/livekit_bridge.h
+++ b/bridge/include/livekit_bridge/livekit_bridge.h
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LiveKit
+ * 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.
@@ -20,6 +20,7 @@
#pragma once
#include "livekit_bridge/bridge_audio_track.h"
+#include "livekit_bridge/bridge_data_track.h"
#include "livekit_bridge/bridge_video_track.h"
#include "livekit/room.h"
@@ -28,6 +29,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -39,6 +41,8 @@ class AudioFrame;
class VideoFrame;
class AudioStream;
class VideoStream;
+class DataTrackSubscription;
+class RemoteDataTrack;
class Track;
enum class TrackSource;
} // namespace livekit
@@ -63,6 +67,14 @@ using AudioFrameCallback = std::function;
using VideoFrameCallback = std::function;
+/// Callback type for incoming data track frames.
+/// Called on a background reader thread.
+/// @param payload Raw binary data received.
+/// @param user_timestamp Optional application-defined timestamp from sender.
+using DataFrameCallback =
+ std::function &payload,
+ std::optional user_timestamp)>;
+
/**
* High-level bridge to the LiveKit C++ SDK.
*
@@ -165,6 +177,9 @@ class LiveKitBridge {
* @pre The bridge must be connected (via connect()). Calling this on a
* disconnected bridge is a programming error.
*
+ * @pre The bridge must be connected (via connect()). Calling this on a
+ * disconnected bridge is a programming error.
+ *
* @param name Human-readable track name.
* @param sample_rate Sample rate in Hz (e.g. 48000).
* @param num_channels Number of audio channels (1 = mono, 2 = stereo).
@@ -189,6 +204,9 @@ class LiveKitBridge {
* @pre The bridge must be connected (via connect()). Calling this on a
* disconnected bridge is a programming error.
*
+ * @pre The bridge must be connected (via connect()). Calling this on a
+ * disconnected bridge is a programming error.
+ *
* @param name Human-readable track name.
* @param width Video width in pixels.
* @param height Video height in pixels.
@@ -203,6 +221,19 @@ class LiveKitBridge {
createVideoTrack(const std::string &name, int width, int height,
livekit::TrackSource source);
+ /**
+ * Create and publish a local data track.
+ *
+ * Data tracks carry arbitrary binary frames and are independent of the
+ * audio/video track lifecycle. The bridge retains a reference and will
+ * automatically release on disconnect().
+ *
+ * @param name Unique track name visible to other participants.
+ * @return Shared pointer to the published data track handle (never null).
+ * @throws std::runtime_error if the bridge is not connected.
+ */
+ std::shared_ptr createDataTrack(const std::string &name);
+
// ---------------------------------------------------------------
// Incoming frame callbacks
// ---------------------------------------------------------------
@@ -264,6 +295,37 @@ class LiveKitBridge {
void clearOnVideoFrameCallback(const std::string &participant_identity,
livekit::TrackSource source);
+ /**
+ * Set the callback for data frames from a specific remote participant's
+ * data track.
+ *
+ * The callback fires on a background thread whenever a new data frame is
+ * received. If the remote data track has not yet been published, the
+ * callback is stored and auto-wired when the track is published (via
+ * onRemoteDataTrackPublished). If the track was already published, the
+ * reader is started immediately—mirroring the onTrackSubscribed behavior
+ * for audio/video.
+ *
+ * Data tracks are keyed by (participant_identity, track_name) rather
+ * than TrackSource, since data tracks don't have a TrackSource enum.
+ *
+ * @param participant_identity Identity of the remote participant.
+ * @param track_name Name of the remote data track.
+ * @param callback Function to invoke per data frame.
+ */
+ void setOnDataFrameCallback(const std::string &participant_identity,
+ const std::string &track_name,
+ DataFrameCallback callback);
+
+ /**
+ * Clear the data frame callback for a specific remote participant + track
+ * name.
+ *
+ * If a reader thread is active, it is stopped and joined.
+ */
+ void clearOnDataFrameCallback(const std::string &participant_identity,
+ const std::string &track_name);
+
private:
friend class BridgeRoomDelegate;
friend class test::CallbackKeyTest;
@@ -290,7 +352,42 @@ class LiveKitBridge {
bool is_audio = false;
};
- /// Called by BridgeRoomDelegate when a remote track is subscribed.
+ /**
+ * Composite key for data track callbacks: (participant_identity, track_name).
+ *
+ * Data tracks are identified by name rather than TrackSource because they
+ * don't belong to the standard Source/Publication hierarchy used by
+ * audio/video tracks.
+ */
+ struct DataCallbackKey {
+ /** Remote participant identity string. */
+ std::string identity;
+
+ /** Publisher-assigned data track name. */
+ std::string track_name;
+
+ bool operator==(const DataCallbackKey &o) const;
+ };
+
+ struct DataCallbackKeyHash {
+ std::size_t operator()(const DataCallbackKey &k) const;
+ };
+
+ /** Active reader thread + subscription for an incoming data track. */
+ struct ActiveDataReader {
+ /** The remote track must stay alive for the subscription to receive frames.
+ * Dropping the RemoteDataTrack handle tells the Rust FFI we no longer care
+ * about this track, which may cause it to stop forwarding frames. */
+ std::shared_ptr remote_track;
+
+ /** Underlying SDK subscription that delivers frames via read(). */
+ std::shared_ptr subscription;
+
+ /** Background thread running the blocking read loop. */
+ std::thread thread;
+ };
+
+ // Called by BridgeRoomDelegate when a remote track is subscribed
void onTrackSubscribed(const std::string &participant_identity,
livekit::TrackSource source,
const std::shared_ptr &track);
@@ -299,9 +396,17 @@ class LiveKitBridge {
void onTrackUnsubscribed(const std::string &participant_identity,
livekit::TrackSource source);
- /// Extract the thread for the given callback key.
- /// @pre Caller must hold @c mutex_.
+ // Called by BridgeRoomDelegate when a remote data track is published.
+ // If a callback is registered for (identity, track_name), starts the data
+ // reader thread (like onTrackSubscribed for audio/video); otherwise stores
+ // the track as pending until setOnDataFrameCallback is called.
+ void
+ onRemoteDataTrackPublished(std::shared_ptr track);
+
+ /// Close the stream and extract the thread for the caller to join
+ /// (caller must hold mutex_)
std::thread extractReaderThread(const CallbackKey &key);
+ std::thread extractDataReaderThread(const DataCallbackKey &key);
/// Start a reader thread for a subscribed track.
/// @return The reader thread for this track.
@@ -313,6 +418,10 @@ class LiveKitBridge {
std::thread startVideoReader(const CallbackKey &key,
const std::shared_ptr &track,
VideoFrameCallback cb);
+ std::thread
+ startDataReader(const DataCallbackKey &key,
+ const std::shared_ptr &track,
+ DataFrameCallback cb);
mutable std::mutex mutex_;
bool connected_;
@@ -330,10 +439,22 @@ class LiveKitBridge {
/// @copydoc audio_callbacks_
std::unordered_map
video_callbacks_;
+ std::unordered_map
+ data_callbacks_;
+
+ /// Remote data tracks published before a frame callback was registered;
+ /// when setOnDataFrameCallback is called for a matching key, we start the
+ /// reader and remove the track from here.
+ std::unordered_map,
+ DataCallbackKeyHash>
+ pending_remote_data_tracks_;
- /// Active reader threads for subscribed tracks.
+ /// Active reader threads for subscribed audio and video tracks.
std::unordered_map
- active_readers_;
+ active_av_readers_;
+ /// Active reader threads for subscribed data tracks.
+ std::unordered_map
+ active_data_readers_;
/// All tracks created by this bridge. The bridge retains a shared_ptr so
/// it can force-release every track on disconnect() before the room is
@@ -341,6 +462,7 @@ class LiveKitBridge {
std::vector> published_audio_tracks_;
/// @copydoc published_audio_tracks_
std::vector> published_video_tracks_;
+ std::vector> published_data_tracks_;
};
} // namespace livekit_bridge
diff --git a/bridge/src/bridge_audio_track.cpp b/bridge/src/bridge_audio_track.cpp
index 654129c..230b733 100644
--- a/bridge/src/bridge_audio_track.cpp
+++ b/bridge/src/bridge_audio_track.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LiveKit
+ * 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.
diff --git a/bridge/src/bridge_data_track.cpp b/bridge/src/bridge_data_track.cpp
new file mode 100644
index 0000000..66f4887
--- /dev/null
+++ b/bridge/src/bridge_data_track.cpp
@@ -0,0 +1,106 @@
+/*
+ * 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_bridge/bridge_data_track.h"
+
+#include "lk_log.h"
+
+#include "livekit/data_frame.h"
+#include "livekit/local_data_track.h"
+#include "livekit/local_participant.h"
+
+namespace livekit_bridge {
+
+BridgeDataTrack::BridgeDataTrack(std::string name,
+ std::shared_ptr track,
+ livekit::LocalParticipant *participant)
+ : name_(std::move(name)), track_(std::move(track)),
+ participant_(participant) {}
+
+BridgeDataTrack::~BridgeDataTrack() { release(); }
+
+bool BridgeDataTrack::pushFrame(const std::vector &payload,
+ std::optional user_timestamp) {
+ std::lock_guard lock(mutex_);
+ if (released_ || !track_) {
+ return false;
+ }
+
+ livekit::DataFrame frame;
+ frame.payload = payload;
+ frame.user_timestamp = user_timestamp;
+
+ try {
+ return track_->tryPush(frame);
+ } catch (const std::exception &e) {
+ LK_LOG_ERROR("[BridgeDataTrack] tryPush error: {}", e.what());
+ return false;
+ }
+}
+
+bool BridgeDataTrack::pushFrame(const std::uint8_t *data, std::size_t size,
+ std::optional user_timestamp) {
+ std::lock_guard lock(mutex_);
+ if (released_ || !track_) {
+ return false;
+ }
+
+ livekit::DataFrame frame;
+ frame.payload.assign(data, data + size);
+ frame.user_timestamp = user_timestamp;
+
+ try {
+ return track_->tryPush(frame);
+ } catch (const std::exception &e) {
+ LK_LOG_ERROR("[BridgeDataTrack] tryPush error: {}", e.what());
+ return false;
+ }
+}
+
+bool BridgeDataTrack::isPublished() const {
+ std::lock_guard lock(mutex_);
+ if (released_ || !track_) {
+ return false;
+ }
+ return track_->isPublished();
+}
+
+bool BridgeDataTrack::isReleased() const noexcept {
+ std::lock_guard lock(mutex_);
+ return released_;
+}
+
+void BridgeDataTrack::release() {
+ std::lock_guard lock(mutex_);
+ if (released_) {
+ return;
+ }
+ released_ = true;
+
+ if (participant_ && track_) {
+ try {
+ participant_->unpublishDataTrack(track_);
+ } catch (...) {
+ LK_LOG_ERROR("[BridgeDataTrack] unpublishDataTrack error, continuing "
+ "with cleanup");
+ }
+ }
+
+ track_.reset();
+ participant_ = nullptr;
+}
+
+} // namespace livekit_bridge
diff --git a/bridge/src/bridge_room_delegate.cpp b/bridge/src/bridge_room_delegate.cpp
index 118627c..2f563eb 100644
--- a/bridge/src/bridge_room_delegate.cpp
+++ b/bridge/src/bridge_room_delegate.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LiveKit
+ * 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.
@@ -19,6 +19,9 @@
#include "bridge_room_delegate.h"
+#include "lk_log.h"
+
+#include "livekit/remote_data_track.h"
#include "livekit/remote_participant.h"
#include "livekit/remote_track_publication.h"
#include "livekit/track.h"
@@ -50,4 +53,19 @@ void BridgeRoomDelegate::onTrackUnsubscribed(
bridge_.onTrackUnsubscribed(identity, source);
}
+void BridgeRoomDelegate::onRemoteDataTrackPublished(
+ livekit::Room & /*room*/,
+ const livekit::RemoteDataTrackPublishedEvent &ev) {
+ if (!ev.track) {
+ LK_LOG_ERROR("[BridgeRoomDelegate] onRemoteDataTrackPublished called "
+ "with null track.");
+ return;
+ }
+
+ LK_LOG_INFO("[BridgeRoomDelegate] onRemoteDataTrackPublished: \"{}\" from "
+ "\"{}\"",
+ ev.track->info().name, ev.track->publisherIdentity());
+ bridge_.onRemoteDataTrackPublished(ev.track);
+}
+
} // namespace livekit_bridge
diff --git a/bridge/src/bridge_room_delegate.h b/bridge/src/bridge_room_delegate.h
index 8f5dd31..9c5d0da 100644
--- a/bridge/src/bridge_room_delegate.h
+++ b/bridge/src/bridge_room_delegate.h
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LiveKit
+ * 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.
@@ -44,6 +44,10 @@ class BridgeRoomDelegate : public livekit::RoomDelegate {
void onTrackUnsubscribed(livekit::Room &room,
const livekit::TrackUnsubscribedEvent &ev) override;
+ void onRemoteDataTrackPublished(
+ livekit::Room &room,
+ const livekit::RemoteDataTrackPublishedEvent &ev) override;
+
private:
LiveKitBridge &bridge_;
};
diff --git a/bridge/src/bridge_video_track.cpp b/bridge/src/bridge_video_track.cpp
index d78d232..0034612 100644
--- a/bridge/src/bridge_video_track.cpp
+++ b/bridge/src/bridge_video_track.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LiveKit
+ * 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.
diff --git a/bridge/src/livekit_bridge.cpp b/bridge/src/livekit_bridge.cpp
index bae25c1..abf54fd 100644
--- a/bridge/src/livekit_bridge.cpp
+++ b/bridge/src/livekit_bridge.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LiveKit
+ * 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.
@@ -23,11 +23,15 @@
#include "livekit/audio_frame.h"
#include "livekit/audio_source.h"
#include "livekit/audio_stream.h"
+#include "livekit/data_frame.h"
+#include "livekit/data_track_subscription.h"
#include "livekit/livekit.h"
#include "livekit/local_audio_track.h"
+#include "livekit/local_data_track.h"
#include "livekit/local_participant.h"
#include "livekit/local_track_publication.h"
#include "livekit/local_video_track.h"
+#include "livekit/remote_data_track.h"
#include "livekit/room.h"
#include "livekit/track.h"
#include "livekit/video_frame.h"
@@ -56,6 +60,18 @@ LiveKitBridge::CallbackKeyHash::operator()(const CallbackKey &k) const {
return h1 ^ (h2 << 1);
}
+bool LiveKitBridge::DataCallbackKey::operator==(
+ const DataCallbackKey &o) const {
+ return identity == o.identity && track_name == o.track_name;
+}
+
+std::size_t
+LiveKitBridge::DataCallbackKeyHash::operator()(const DataCallbackKey &k) const {
+ std::size_t h1 = std::hash{}(k.identity);
+ std::size_t h2 = std::hash{}(k.track_name);
+ return h1 ^ (h2 << 1);
+}
+
// ---------------------------------------------------------------
// Construction / Destruction
// ---------------------------------------------------------------
@@ -92,12 +108,18 @@ bool LiveKitBridge::connect(const std::string &url, const std::string &token,
}
}
- // ---- Phase 2: create room and connect without holding the lock ----
- // This avoids blocking other threads during the network handshake and
- // eliminates the risk of deadlock if the SDK delivers delegate callbacks
+ // ---- Phase 2: create room, attach delegate, and connect without holding the
+ // lock ---- This avoids blocking other threads during the network handshake
+ // and eliminates the risk of deadlock if the SDK delivers delegate callbacks
// synchronously during Connect().
+ // Set the delegate before Connect() so that events (e.g.
+ // RemoteDataTrackPublished) that may be delivered during or immediately after
+ // Connect() are not dropped.
+ auto delegate = std::make_unique(*this);
+ assert(delegate != nullptr);
auto room = std::make_unique();
assert(room != nullptr);
+ room->setDelegate(delegate.get());
bool result = room->Connect(url, token, options);
if (!result) {
@@ -106,14 +128,8 @@ bool LiveKitBridge::connect(const std::string &url, const std::string &token,
return false;
}
- // ---- Phase 3: commit and attach delegate under lock ----
- // Setting the delegate here (after Connect) ensures that any queued
- // onTrackSubscribed events are delivered only after
- // room_/delegate_/connected_ are all in a consistent state.
-
- auto delegate = std::make_unique(*this);
- assert(delegate != nullptr);
- room->setDelegate(delegate.get());
+ // ---- Phase 3: commit under lock ----
+ // room_/delegate_/connected_ are now in a consistent state.
{
std::lock_guard lock(mutex_);
room_ = std::move(room);
@@ -141,18 +157,21 @@ void LiveKitBridge::disconnect() {
connecting_ = false;
// Release all published tracks while the room/participant are still alive.
- // This calls unpublishTrack() on each, ensuring participant_ is valid.
for (auto &track : published_audio_tracks_) {
track->release();
}
for (auto &track : published_video_tracks_) {
track->release();
}
+ for (auto &track : published_data_tracks_) {
+ track->release();
+ }
published_audio_tracks_.clear();
published_video_tracks_.clear();
+ published_data_tracks_.clear();
// Close all streams (unblocks read loops) and collect threads
- for (auto &[key, reader] : active_readers_) {
+ for (auto &[key, reader] : active_av_readers_) {
if (reader.audio_stream) {
reader.audio_stream->close();
}
@@ -163,11 +182,23 @@ void LiveKitBridge::disconnect() {
threads_to_join.emplace_back(std::move(reader.thread));
}
}
- active_readers_.clear();
+ active_av_readers_.clear();
+
+ for (auto &[key, reader] : active_data_readers_) {
+ if (reader.subscription) {
+ reader.subscription->close();
+ }
+ if (reader.thread.joinable()) {
+ threads_to_join.emplace_back(std::move(reader.thread));
+ }
+ }
+ active_data_readers_.clear();
- // Clear callback registrations
+ // Clear callback registrations and pending data tracks
audio_callbacks_.clear();
video_callbacks_.clear();
+ data_callbacks_.clear();
+ pending_remote_data_tracks_.clear();
// Tear down the room
if (room_) {
@@ -267,6 +298,26 @@ LiveKitBridge::createVideoTrack(const std::string &name, int width, int height,
return bridge_track;
}
+std::shared_ptr
+LiveKitBridge::createDataTrack(const std::string &name) {
+ std::lock_guard lock(mutex_);
+
+ if (!connected_ || !room_) {
+ throw std::runtime_error(
+ "LiveKitBridge::createDataTrack: not connected to a room");
+ }
+
+ LK_LOG_INFO("[LiveKitBridge] Publishing data track \"{}\"...", name);
+ auto track = room_->localParticipant()->publishDataTrack(name);
+ LK_LOG_INFO("[LiveKitBridge] Data track \"{}\" published (sid={}).", name,
+ track->info().sid);
+
+ auto bridge_track = std::shared_ptr(
+ new BridgeDataTrack(name, std::move(track), room_->localParticipant()));
+ published_data_tracks_.emplace_back(bridge_track);
+ return bridge_track;
+}
+
// ---------------------------------------------------------------
// Incoming frame callbacks
// ---------------------------------------------------------------
@@ -300,6 +351,35 @@ void LiveKitBridge::setOnVideoFrameCallback(
video_callbacks_[key] = std::move(callback);
}
+void LiveKitBridge::setOnDataFrameCallback(
+ const std::string &participant_identity, const std::string &track_name,
+ DataFrameCallback callback) {
+ std::thread old_thread;
+ {
+ std::lock_guard lock(mutex_);
+ LK_LOG_INFO(
+ "[LiveKitBridge] Registered data callback for (\"{}\", \"{}\").",
+ participant_identity, track_name);
+ DataCallbackKey key{participant_identity, track_name};
+ data_callbacks_[key] = std::move(callback);
+
+ // If this track was already published and stored as pending, start the
+ // reader now (mirrors late registration for audio/video).
+ auto pending_it = pending_remote_data_tracks_.find(key);
+ if (pending_it != pending_remote_data_tracks_.end()) {
+ auto track = std::move(pending_it->second);
+ pending_remote_data_tracks_.erase(pending_it);
+ auto cb_it = data_callbacks_.find(key);
+ if (cb_it != data_callbacks_.end()) {
+ old_thread = startDataReader(key, track, cb_it->second);
+ }
+ }
+ }
+ if (old_thread.joinable()) {
+ old_thread.join();
+ }
+}
+
void LiveKitBridge::clearOnAudioFrameCallback(
const std::string &participant_identity, livekit::TrackSource source) {
std::thread thread_to_join;
@@ -328,6 +408,20 @@ void LiveKitBridge::clearOnVideoFrameCallback(
}
}
+void LiveKitBridge::clearOnDataFrameCallback(
+ const std::string &participant_identity, const std::string &track_name) {
+ std::thread thread_to_join;
+ {
+ std::lock_guard lock(mutex_);
+ DataCallbackKey key{participant_identity, track_name};
+ data_callbacks_.erase(key);
+ thread_to_join = extractDataReaderThread(key);
+ }
+ if (thread_to_join.joinable()) {
+ thread_to_join.join();
+ }
+}
+
// ---------------------------------------------------------------
// Internal: track subscribe / unsubscribe from delegate
// ---------------------------------------------------------------
@@ -351,6 +445,10 @@ void LiveKitBridge::onTrackSubscribed(
if (it != video_callbacks_.end()) {
old_thread = startVideoReader(key, track, it->second);
}
+ } else {
+ LK_LOG_INFO(
+ "[LiveKitBridge] Track subscribed: \"{}\" from \"{}\" (sid={})",
+ track->name(), participant_identity, track->sid());
}
}
// If this key already had a reader (e.g. track was re-subscribed), the old
@@ -375,6 +473,43 @@ void LiveKitBridge::onTrackUnsubscribed(const std::string &participant_identity,
}
}
+void LiveKitBridge::onRemoteDataTrackPublished(
+ std::shared_ptr track) {
+ LK_LOG_INFO("[LiveKitBridge] Remote data track published: \"{}\" from \"{}\" "
+ "(sid={})",
+ track->info().name, track->publisherIdentity(),
+ track->info().sid);
+
+ std::thread old_thread;
+ std::string identity;
+ std::string track_name;
+
+ {
+ std::lock_guard lock(mutex_);
+
+ DataCallbackKey key{track->publisherIdentity(), track->info().name};
+ identity = key.identity;
+ track_name = key.track_name;
+
+ auto it = data_callbacks_.find(key);
+ if (it != data_callbacks_.end()) {
+ LK_LOG_INFO("[LiveKitBridge] Found matching callback for ({}, {}), "
+ "starting data reader.",
+ key.identity, key.track_name);
+ old_thread = startDataReader(key, track, it->second);
+ } else {
+ LK_LOG_INFO("[LiveKitBridge] No callback registered yet for ({}, {}); "
+ "storing as pending (will start when callback is set).",
+ key.identity, key.track_name);
+ pending_remote_data_tracks_[key] = track;
+ }
+ }
+
+ if (old_thread.joinable()) {
+ old_thread.join();
+ }
+}
+
// ---------------------------------------------------------------
// Internal: reader thread management
// ---------------------------------------------------------------
@@ -382,8 +517,8 @@ void LiveKitBridge::onTrackUnsubscribed(const std::string &participant_identity,
std::thread LiveKitBridge::extractReaderThread(const CallbackKey &key) {
// Caller must hold mutex_.
// Closes the stream and extracts the thread for the caller to join.
- auto it = active_readers_.find(key);
- if (it == active_readers_.end()) {
+ auto it = active_av_readers_.find(key);
+ if (it == active_av_readers_.end()) {
return {};
}
@@ -398,7 +533,7 @@ std::thread LiveKitBridge::extractReaderThread(const CallbackKey &key) {
}
auto thread = std::move(reader.thread);
- active_readers_.erase(it);
+ active_av_readers_.erase(it);
return thread;
}
@@ -434,10 +569,10 @@ LiveKitBridge::startAudioReader(const CallbackKey &key,
}
});
- active_readers_[key] = std::move(reader);
- if (active_readers_.size() > kMaxActiveReaders) {
- LK_LOG_WARN("More than expected active readers. Need to evaluate how much "
- "to expect/support.");
+ active_av_readers_[key] = std::move(reader);
+ if (active_av_readers_.size() > kMaxActiveReaders) {
+ LK_LOG_ERROR("[LiveKitBridge] More than expected active readers. Need to "
+ "evaluate how much to expect/support.");
}
return old_thread;
}
@@ -475,12 +610,79 @@ LiveKitBridge::startVideoReader(const CallbackKey &key,
}
});
- active_readers_[key] = std::move(reader);
- if (active_readers_.size() > kMaxActiveReaders) {
- LK_LOG_WARN("More than expected active readers. Need to evaluate how much "
- "to expect/support.");
+ active_av_readers_[key] = std::move(reader);
+ if (active_av_readers_.size() > kMaxActiveReaders) {
+ LK_LOG_ERROR("[LiveKitBridge] More than expected active readers. Need to "
+ "evaluate how much to expect/support.");
}
return old_thread;
}
+std::thread LiveKitBridge::startDataReader(
+ const DataCallbackKey &key,
+ const std::shared_ptr &track,
+ DataFrameCallback cb) {
+ auto old_thread = extractDataReaderThread(key);
+
+ LK_LOG_INFO("[LiveKitBridge] Subscribing to data track \"{}\" from \"{}\"...",
+ key.track_name, key.identity);
+
+ std::shared_ptr subscription;
+ try {
+ subscription = track->subscribe();
+ } catch (const std::exception &e) {
+ LK_LOG_ERROR("[LiveKitBridge] Failed to subscribe to data track \"{}\" "
+ "from \"{}\": {}",
+ key.track_name, key.identity, e.what());
+ return old_thread;
+ }
+
+ LK_LOG_INFO("[LiveKitBridge] Subscribed to data track \"{}\"; starting "
+ "reader thread.",
+ key.track_name);
+
+ auto sub_copy = subscription;
+ auto track_name = key.track_name;
+ auto identity = key.identity;
+
+ ActiveDataReader reader;
+ reader.remote_track = track;
+ reader.subscription = std::move(subscription);
+ reader.thread = std::thread([sub_copy, cb, track_name, identity]() {
+ LK_LOG_INFO(
+ "[LiveKitBridge] Data reader thread running for \"{}\" from \"{}\".",
+ track_name, identity);
+ livekit::DataFrame frame;
+ while (sub_copy->read(frame)) {
+ try {
+ cb(frame.payload, frame.user_timestamp);
+ } catch (const std::exception &e) {
+ LK_LOG_ERROR("[LiveKitBridge] Data callback exception: {}", e.what());
+ }
+ }
+ LK_LOG_INFO(
+ "[LiveKitBridge] Data reader thread exiting for \"{}\" from \"{}\".",
+ track_name, identity);
+ });
+
+ active_data_readers_[key] = std::move(reader);
+ return old_thread;
+}
+
+std::thread LiveKitBridge::extractDataReaderThread(const DataCallbackKey &key) {
+ auto it = active_data_readers_.find(key);
+ if (it == active_data_readers_.end()) {
+ return {};
+ }
+
+ auto &reader = it->second;
+
+ if (reader.subscription) {
+ reader.subscription->close();
+ }
+
+ auto thread = std::move(reader.thread);
+ active_data_readers_.erase(it);
+ return thread;
+}
} // namespace livekit_bridge
diff --git a/bridge/tests/CMakeLists.txt b/bridge/tests/CMakeLists.txt
index 258681e..f5114cf 100644
--- a/bridge/tests/CMakeLists.txt
+++ b/bridge/tests/CMakeLists.txt
@@ -25,70 +25,154 @@ enable_testing()
include(GoogleTest)
# ============================================================================
-# Bridge Unit Tests
+# Helper: copy shared libraries to a test executable's output directory
# ============================================================================
-
-file(GLOB BRIDGE_TEST_SOURCES
- "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp"
-)
-
-if(BRIDGE_TEST_SOURCES)
- add_executable(livekit_bridge_tests
- ${BRIDGE_TEST_SOURCES}
- )
-
- target_link_libraries(livekit_bridge_tests
- PRIVATE
- livekit_bridge
- GTest::gtest_main
- )
-
- # Copy shared libraries to test executable directory
+function(bridge_test_copy_libs target)
if(WIN32)
- add_custom_command(TARGET livekit_bridge_tests POST_BUILD
+ add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$
- $
+ $
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$
- $
+ $
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$/livekit_ffi.dll"
- $
- COMMENT "Copying DLLs to bridge test directory"
+ $
+ COMMENT "Copying DLLs to ${target} directory"
)
elseif(APPLE)
- add_custom_command(TARGET livekit_bridge_tests POST_BUILD
+ add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$
- $
+ $
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$
- $
+ $
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$/liblivekit_ffi.dylib"
- $
- COMMENT "Copying dylibs to bridge test directory"
+ $
+ COMMENT "Copying dylibs to ${target} directory"
)
else()
- add_custom_command(TARGET livekit_bridge_tests POST_BUILD
+ add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$
- $
+ $
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$
- $
+ $
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$/liblivekit_ffi.so"
- $
- COMMENT "Copying shared libraries to bridge test directory"
+ $
+ COMMENT "Copying shared libraries to ${target} directory"
)
endif()
+endfunction()
+
+# ============================================================================
+# Bridge Unit Tests (existing — files in tests/ root)
+# ============================================================================
+
+file(GLOB BRIDGE_TEST_SOURCES
+ "${CMAKE_CURRENT_SOURCE_DIR}/unit/*.cpp"
+)
+
+if(BRIDGE_TEST_SOURCES)
+ add_executable(livekit_bridge_unit_tests
+ ${BRIDGE_TEST_SOURCES}
+ )
+
+ target_link_libraries(livekit_bridge_unit_tests
+ PRIVATE
+ livekit_bridge
+ GTest::gtest_main
+ )
+
+ bridge_test_copy_libs(livekit_bridge_unit_tests)
+
+ gtest_discover_tests(livekit_bridge_unit_tests
+ WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
+ PROPERTIES
+ LABELS "bridge_unit_tests"
+ )
+endif()
+
+# ============================================================================
+# Bridge Integration Tests (tests/integration/)
+# ============================================================================
+
+file(GLOB BRIDGE_INTEGRATION_SOURCES
+ "${CMAKE_CURRENT_SOURCE_DIR}/integration/*.cpp"
+)
+
+if(BRIDGE_INTEGRATION_SOURCES)
+ add_executable(livekit_bridge_integration_tests
+ ${BRIDGE_INTEGRATION_SOURCES}
+ )
+
+ target_link_libraries(livekit_bridge_integration_tests
+ PRIVATE
+ livekit_bridge
+ GTest::gtest_main
+ )
+
+ bridge_test_copy_libs(livekit_bridge_integration_tests)
+
+ gtest_discover_tests(livekit_bridge_integration_tests
+ WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
+ PROPERTIES
+ LABELS "bridge_integration"
+ )
+endif()
+
+# ============================================================================
+# Bridge Stress Tests (tests/stress/)
+# ============================================================================
+
+file(GLOB BRIDGE_STRESS_SOURCES
+ "${CMAKE_CURRENT_SOURCE_DIR}/stress/*.cpp"
+)
- # Register tests with CTest
- gtest_discover_tests(livekit_bridge_tests
+if(BRIDGE_STRESS_SOURCES)
+ add_executable(livekit_bridge_stress_tests
+ ${BRIDGE_STRESS_SOURCES}
+ )
+
+ target_link_libraries(livekit_bridge_stress_tests
+ PRIVATE
+ livekit_bridge
+ GTest::gtest_main
+ )
+
+ bridge_test_copy_libs(livekit_bridge_stress_tests)
+
+ gtest_discover_tests(livekit_bridge_stress_tests
WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
PROPERTIES
- LABELS "bridge_unit"
+ LABELS "bridge_stress"
+ TIMEOUT 300
)
endif()
+
+# ============================================================================
+# Combined target
+# ============================================================================
+
+add_custom_target(run_bridge_tests
+ COMMAND ${CMAKE_CTEST_COMMAND} -L "bridge_unit|bridge_integration|bridge_stress" --output-on-failure
+ WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+ COMMENT "Running all bridge tests"
+)
+
+if(TARGET livekit_bridge_unit_tests)
+ add_dependencies(run_bridge_tests livekit_bridge_unit_tests)
+endif()
+
+if(TARGET livekit_bridge_integration_tests)
+ add_dependencies(run_bridge_tests livekit_bridge_integration_tests)
+endif()
+
+if(TARGET livekit_bridge_stress_tests)
+ add_dependencies(run_bridge_tests livekit_bridge_stress_tests)
+endif()
diff --git a/bridge/tests/common/bridge_test_common.h b/bridge/tests/common/bridge_test_common.h
new file mode 100644
index 0000000..9389a50
--- /dev/null
+++ b/bridge/tests/common/bridge_test_common.h
@@ -0,0 +1,311 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include