Skip to content

Data tracks support#586

Merged
ladvoc merged 52 commits intomainfrom
jacobgelman/bot-242-python-client-implementation
Apr 2, 2026
Merged

Data tracks support#586
ladvoc merged 52 commits intomainfrom
jacobgelman/bot-242-python-client-implementation

Conversation

@ladvoc
Copy link
Copy Markdown
Contributor

@ladvoc ladvoc commented Mar 5, 2026

No description provided.

@pblazej
Copy link
Copy Markdown

pblazej commented Mar 16, 2026

Some things from talking to claude (maybe you find it useful):

  • SubscribeDataTrackError / PublishDataTrackError not exported
  • existing streams use aclose() (async), while DataTrackSubscription uses close() (sync); all of them can probably leverage https://www.geeksforgeeks.org/python/aenter-in-python/
  • no remote_data_track_unpublished event - just mirroring rust comment here
  • try_push should probably be push in python
  • remote_data_track_published should be data_track_published as other tracks
  • DataTrackFrame(payload=...) may validate the input type (what happens if you pass a string etc.)

def _timestamp_now() -> int:
return int(time.time() * 1000)

def with_user_timestamp_now(self) -> DataTrackFrame:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could it be static instead?

DataTrackFrame.now(payload=..)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I would probably stick with this since we may add additional timestamp fields in the future (e.g., a single frame could have both a capture and sensor timestamp).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm kind of wondering if this utility is needed at all then? It would be even more complicated if we have multiple timestamps. They don't really help much IMO

self._closed = True
FfiClient.instance.queue.unsubscribe(self._queue)

def close(self) -> None:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's make sure we have aclose (to be future proof)

Comment on lines +23 to +25
track.try_push(frame)
except rtc.PushFrameError as e:
logging.error("Failed to push frame: %s", e)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

From an API perspective, I feel like it's kind of weird that "pushing" can fail? Since it looks like a queue?

Copy link
Copy Markdown
Contributor Author

@ladvoc ladvoc Apr 2, 2026

Choose a reason for hiding this comment

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

V1 only supports lossy delivery, and queuing on the publisher side is minimal; this will error if attempting to push frames too fast (rather than queuing stale data).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should this be named send?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There was a discussion about this back during the planning phase; originally it was going to be called "publish," but it was changed to "push" to disambiguate from publishing the track itself. The "try" prefix is to denote this is not guaranteed to succeed and to give us a path to introduce an async variant just called "push" to support reliable delivery in a future version.

Comment on lines +153 to +157
def unpublish(self) -> None:
"""Unpublishes the track."""
req = proto_ffi.FfiRequest()
req.local_data_track_unpublish.track_handle = self._ffi_handle.handle
FfiClient.instance.request(req)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should this be async as well? Or maybe this shouldnt' even be here. But in LocalParticipant instead? for consistency

Copy link
Copy Markdown
Contributor Author

@ladvoc ladvoc Apr 2, 2026

Choose a reason for hiding this comment

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

Based on how applications work with data tracks, we found it is more convenient to have here. This is the same API in the other SDKs. Also, currently not async in the Rust impl.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I see, tho isn't it better to be consistent with audio/video?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Even if the API is better, it introduces more cognitive load to have different APIs for similar purposes

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

ladvoc added 4 commits April 2, 2026 14:21
SFU used in CI testing doesn't currently have data tracks enabled. Tested locally and verified this case passes.
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 14 additional findings in Devin Review.

Open in Devin Review

frame = rtc.DataTrackFrame(payload=data, user_timestamp=int(time.time() * 1000))
track.try_push(frame)
except rtc.PushFrameError as e:
logging.error("Failed to push frame: %s", e)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Publisher example logs empty string for PushFrameError because exception doesn't pass message to super().init

PushFrameError.__init__ at livekit-rtc/livekit/rtc/data_track.py:42-43 stores the message as self.message but never calls super().__init__(message). When the publisher example formats the exception with %s, Python calls str(e) which returns str(self.args) — an empty tuple () renders as empty string. The log output will be "Failed to push frame: " with no actual error information. The subscriber example at examples/data_tracks/subscriber.py:27 correctly uses e.message, demonstrating the intended access pattern.

Suggested change
logging.error("Failed to push frame: %s", e)
logging.error("Failed to push frame: %s", e.message)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@ladvoc ladvoc merged commit 8064880 into main Apr 2, 2026
27 of 28 checks passed
@ladvoc ladvoc deleted the jacobgelman/bot-242-python-client-implementation branch April 2, 2026 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants