Skip to content

Data track subscribing - #992

Open
ladvoc wants to merge 6 commits into
ladvoc/data-track-publishingfrom
ladvoc/data-track-subscribing
Open

Data track subscribing#992
ladvoc wants to merge 6 commits into
ladvoc/data-track-publishingfrom
ladvoc/data-track-subscribing

Conversation

@ladvoc

@ladvoc ladvoc commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Adds support for subscribing to data tracks.

Architecture and public API closely match Rust and JS clients:

  • When a remote participant publishes a data track, the user receives an OnDataTrackPublished event to receive the track object which they can then use to subscribe if desired.
    • Multiple application-level subscriptions are supported for a single track; internally, packets are still only received and processed once and will be fanned out.
  • A RemoteManager is owned by the room to manage internal state and transitions for all remote data tracks.

Areas to review:

  • Same as in previous PR; some APIs meant for internal use (e.g., RemoteManager) are currently public. Would like to understand if there is a better way to organize packages to avoid this.
  • The RemoteManager is currently owned by LocalParticipant, but I am not sure this is the right place for it to live.

Closes BOT-541

@ladvoc
ladvoc force-pushed the ladvoc/data-track-subscribing branch from ce38d1d to a1d1643 Compare September 4, 2026 16:04
@ladvoc
ladvoc force-pushed the ladvoc/data-track-subscribing branch from a1d1643 to bee8224 Compare September 4, 2026 22:57
@ladvoc
ladvoc force-pushed the ladvoc/data-track-subscribing branch from bee8224 to 2fd1c9b Compare September 8, 2026 21:55
@ladvoc
ladvoc requested a review from boks1971 September 8, 2026 22:15
@ladvoc
ladvoc marked this pull request as ready for review September 8, 2026 22:15
@ladvoc
ladvoc requested a review from a team as a code owner September 8, 2026 22:15

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 5 potential issues.

2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment thread room.go
r.LocalParticipant = newLocalParticipant(r.engine, r.callback, r.serverInfo, r.log)
r.localDataTracks = datatrack.NewLocalManager(datatrack.LocalManagerParams{Transport: localDataTrackTransport{engine: r.engine}, Logger: r.log})
r.LocalParticipant.dataTracks = r.localDataTracks
r.remoteDataTracks = datatrack.NewRemoteManager(datatrack.RemoteManagerParams{Transport: remoteDataTrackTransport{room: r}, Logger: r.log})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Encrypted tracks return ciphertext

With WithDataEncryption, remoteDataTracks still receives no Decryptor. processPacket then returns ciphertext as a valid frame. Subscribers cannot consume encrypted publications.

Prompt for agents
Wire remote data-track decryption into Room session encryption. The manager created in room.go receives no Decryptor even when WithDataEncryption configures engine.dataCryptor. Add an adapter from the session DataCryptor/key provider to datatrack.Decryptor, resolve it for the current session, and ensure encrypted tracks fail clearly rather than yielding ciphertext when encryption is unavailable. Cover encrypted subscriptions through the Room API, including reconnects and key rotation.
Devin Review

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

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.

This is addressed by follow-up PRs (#993 and #994) to keep the scope of this PR limited.

Comment thread datatrack/remote.go
Comment on lines +572 to +573
if withdraw != nil {
m.sendSubscriptionUpdate(*withdraw)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Concurrent resubscribe gets cancelled

When the last stream closes during Subscribe, the new subscribe can send before this deferred unsubscribe. The stale unsubscribe cancels the new request, leaving Subscribe pending until timeout.

Prompt for agents
Serialize subscription state transitions with their outgoing UpdateDataSubscription messages. In datatrack/remote.go, removeStream marks the track unsubscribed under manager.mu but sends the unsubscribe after unlocking. A concurrent Subscribe can therefore send a newer subscribe before the older unsubscribe. Introduce ordered outbound updates or generation-aware reconciliation so stale withdrawals cannot overtake newer subscriptions. Apply the same ordering protection to timeout withdrawals and reconnect resubscriptions, then add a deterministic concurrent Close/Subscribe test.
Devin Review

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

Comment thread room.go
r.runParticipantDefers(newSid, rp)
}
}
r.remoteDataTracks.HandleParticipantUpdate(participants, r.LocalParticipant.Identity())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Disconnect loses participant callback

A disconnected publisher is removed before HandleParticipantUpdate emits data-track removals. OnTrackUnpublished then skips its participant callback and passes nil to the room callback.

Prompt for agents
Preserve the RemoteParticipant through data-track unpublication on participant disconnect. Room.OnParticipantUpdate currently calls OnParticipantDisconnect, which removes the participant, before RemoteManager.HandleParticipantUpdate emits removals. Reorder or carry the participant reference so RemoteParticipant.Callback.OnDataTrackUnpublished runs and RoomCallback receives the documented participant. Keep participant and room callback ordering consistent with media-track unpublication.
Devin Review

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

Comment thread remotedatatrack.go
// Subscribe, without stalling signal handling.
func (t remoteDataTrackTransport) OnTrackPublished(track *datatrack.RemoteTrack) {
rp := t.room.GetParticipantByIdentity(track.PublisherIdentity())
go func() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Publication callbacks arrive reversed

Each event starts an independent goroutine, so rapid publication and removal can run OnDataTrackUnpublished first. Applications observe removal before discovery or subscribe after removal.

Prompt for agents
Dispatch data-track lifecycle callbacks asynchronously but in publication order. The two remoteDataTrackTransport methods currently launch independent goroutines, allowing publish and unpublish events for the same track to overtake each other. Use a serialized room-level or per-track callback queue that does not block signal handling, while retaining participant-before-room callback ordering.
Devin Review

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

Comment thread datatrack/remote.go
Comment on lines +507 to +509
for _, stream := range *t.streamList.Load() {
stream.push(frame)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Resubscribe leaks old frames

A closed pipeline can drain buffered packets after a new subscription replaces streamList. Its old worker then pushes pre-subscription frames into the new streams.

Prompt for agents
Prevent workers from delivering packets across subscription generations. runPipeline reads the track-wide current streamList, while deactivateLocked only closes the old packet channel and does not wait for its worker. After resubscription, that worker can drain old buffered packets into newly installed streams. Tie each worker to its subscription generation and stream snapshot, or synchronously stop it before exposing new streams. Add a test that pauses an old worker, closes the final stream, resubscribes, then releases the worker.
Devin Review

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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant