Skip to content

DataTracks integration through FFI#66

Open
stephen-derosa wants to merge 26 commits intolivekit:mainfrom
stephen-derosa:sderosa/data_tracks
Open

DataTracks integration through FFI#66
stephen-derosa wants to merge 26 commits intolivekit:mainfrom
stephen-derosa:sderosa/data_tracks

Conversation

@stephen-derosa
Copy link
Contributor

@stephen-derosa stephen-derosa commented Feb 26, 2026

Overview

Integrate DataTracks into the CPP SDK through the FFI

Building

This library is attached to the build system of the core C++ SDK library. Use build.sh as is.

Testing

bridge/tests/ implements new tests for stress, integration, and unit. These closely follow the testing format of the base SDK.

Examples

  • examples/bridge_human_robot/ has been updated to include a DataTrack. The Robot sends a string with a time and count, the human prints it.
  • examples/realsense-livekit/ has examples for getting data from a realsense camera, serializing, compressing and sending it. It also shows writing receiving participant data and writing it to an mcap which can be visualized in foxglove.

Unit tests

  • bridge/tests/unit/test_bridge_data_track.cpp

Limitations

  • No E2EE configuration
  • RPC (incoming PR)
  • Simulcast tuning
  • Video format selection (RGBA is the default; no format option yet)

Blocked By

Rust SDKs PR

Resolves BOT-268

@stephen-derosa stephen-derosa self-assigned this Feb 26, 2026
@stephen-derosa stephen-derosa added the enhancement New feature or request label Feb 26, 2026
Copy link
Contributor

@ladvoc ladvoc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial comments, but I will make another pass. Looking clean so far!


proto::FfiResponse resp = FfiClient::instance().sendRequest(req);
const auto &r = resp.local_data_track_try_push();
return !r.has_error();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Should we be exposing the error reason to the caller? There are several reasons why pushing a frame could fail (e.g., the track is unpublished, buffer full, etc.). For the ROS bridge, a boolean might be fine if there is logging, but for the core SDK, I lean towards we should report the error reason.

Related question: the FFI interface currently flattens all errors to strings for simplicty, however, on the Rust side there are proper error enums that describe the reason for the error. Should I expose these over FFI rather than using strings? Can an enum describing the error reason map nicely into a C++ exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great question -- I need to sync with @xianshijing-lk on what the client facing applications should do. I think in general Livekit should never cause an application to crash (other than assert calls), so the sdk should catch any exceptions. Of course we still want to flow up the root cause (which we are currently doing). I could be way off here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for SDKs used in real-time systems, in general, exceptions are discouraged for "expected" failures like "buffer full" or "track unpublished".

I think the best practice here might be returning an result, which will include the success or error codes/Enums ?

Copy link
Contributor

@ladvoc ladvoc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some additional comments.

@stephen-derosa stephen-derosa force-pushed the sderosa/data_tracks branch 3 times, most recently from 8647582 to 74e4b99 Compare March 6, 2026 00:49
@stephen-derosa stephen-derosa force-pushed the sderosa/data_tracks branch 2 times, most recently from 6413736 to 2b792db Compare March 19, 2026 22:15
realsense-livekit: Examples of using DataTracks, realsense and mcap
…ich deprecates bridge. Add helper set/clearDataTrackCallback(), and publishDataTrack helper functions
…ame, assert(since that breaks cpp<->ffi<->rust agreement of rust handling buffering
* The proto field is a bare uint64 with no prescribed unit.
* By convention the SDK examples use microseconds since the Unix epoch.
*/
std::optional<std::uint64_t> user_timestamp;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, suggested adding things like

DataFrame() = default;
DataFrame(DataFrame&&) = default;
DataFrame& operator=(DataFrame&&) = default;

These constructors will help compiler optimize those move, assignment use cases


proto::FfiResponse resp = FfiClient::instance().sendRequest(req);
const auto &r = resp.local_data_track_try_push();
return !r.has_error();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for SDKs used in real-time systems, in general, exceptions are discouraged for "expected" failures like "buffer full" or "track unpublished".

I think the best practice here might be returning an result, which will include the success or error codes/Enums ?

* // process frame.payload
* }
*/
class DataTrackSubscription {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe I asked this question before, I wonder if should be called data_track_stream ?
or remote_data_track_stream ?

so it will be aligned with audio_stream / video_stream


const auto &dts = event.data_track_subscription_event();
if (dts.subscription_handle() !=
static_cast<std::uint64_t>(subscription_handle_.get())) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this worries me a bit, onFfiEvent is a callback. If DataTrackSubscription is moved to a new memory location, the lambda captured this.

take a step back, why do we need to support the move operator ? do we see real use cases from it ?

auto *msg = req.mutable_data_track_subscription_read();
msg->set_subscription_handle(
static_cast<uint64_t>(subscription_handle_.get()));
FfiClient::instance().sendRequest(req);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are some races there.
like one thread calls this read(), and pass this section under std::lock_guardstd::mutex lock(mutex_); and get to this section.

meanwhile, close() is being called, and reset the subscription_handle_

}

out = std::move(frame_.value());
frame_.reset();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, If the DataFrame have move constructor, you don't need this frame_.reset()

* @return true on success, false if the push failed (e.g. back-pressure
* or the track has been unpublished).
*/
bool tryPush(const std::uint8_t *data, std::size_t size,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curiously, do we need all these 3 overloaded methods ? or we should push for one instead?

@xianshijing-lk xianshijing-lk dismissed their stale review March 26, 2026 21:50

Sorry, I think I missed some details and have some more comments to address before landing this PR.

}

std::shared_ptr<LocalDataTrack>
LocalParticipant::publishDataTrack(const std::string &name) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curiously, what will happen if calling publishDataTrack with the same |name| more than once ?

bool LocalDataTrack::tryPush(const std::vector<std::uint8_t> &payload,
std::optional<std::uint64_t> user_timestamp) {
DataFrame frame;
frame.payload = payload;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this makes me wondering if we should allow developers to pass things like
bool LocalDataTrack::tryPush(std::vectorstd::uint8_t&& payload,
std::optionalstd::uint64_t user_timestamp) { ... }

So that we can use the move constructor for the payload ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants