Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
9ea3e26
LocalDataTrackManager impl
davidliu Jul 29, 2026
64a5d4a
Move to OutgoingDataTrackManager
davidliu Jul 29, 2026
fea2afe
more migration
davidliu Jul 29, 2026
76a5964
Remote data track manager
davidliu Jul 30, 2026
713a792
sdk wrappers
davidliu Aug 17, 2026
9c9a673
update uniffi to use mavencentral artifact
davidliu Aug 17, 2026
251d7c1
publishDataTrack api
davidliu Aug 17, 2026
587de90
Use Results instead of throwing exceptions
davidliu Aug 17, 2026
4815193
clean up LocalDataTrack to more similarly match swift api
davidliu Aug 17, 2026
ed3dc29
data track schema
davidliu Aug 17, 2026
a8c99d9
Factories to inject the actual and mock data track manager factories
davidliu Aug 17, 2026
9b103b4
wire up remote participant data tracks
davidliu Aug 17, 2026
424bb14
wiring for UNPUBLISH_DATA_TRACK_RESPONSE
davidliu Aug 17, 2026
e8c8f34
quick reconnect handling
davidliu Aug 17, 2026
b26ca2d
wait until publisher open
davidliu Aug 17, 2026
aff6491
DataTrackFrameSender
davidliu Aug 17, 2026
c0f1fa8
hook up e2ee
davidliu Aug 17, 2026
380b1d7
fix data tracks not properly flowing after reconnect
davidliu Aug 20, 2026
3274297
cleanup
davidliu Aug 26, 2026
602d79e
add attributes to TokenSource due to test failure
davidliu Sep 1, 2026
d40a61f
extract data channel logic and simplification
davidliu Sep 1, 2026
317beaa
handle potential exceptions when loading uniffi
davidliu Sep 3, 2026
88029f7
remove debug logs
davidliu Sep 7, 2026
61e6e95
detekt fixes
davidliu Sep 7, 2026
9e3c3e1
missing commit
davidliu Sep 7, 2026
86d3ae3
Add DataTrackFrameFormat to enforce requiring a frameEncoding for a s…
davidliu Sep 7, 2026
105b8b6
make DataTrackPublished and Unpublished events symmetric
davidliu Sep 7, 2026
26bd6a9
LocalDataTrack doc fix
davidliu Sep 7, 2026
d02cf8f
fix up DataTrackStream not properly sharing frames across collectors
davidliu Sep 7, 2026
d2f13a6
signalclient: guard against sendEncodedRequest with garbage data
davidliu Sep 7, 2026
9f12dcb
remove unneeded copy of packets to be sent
davidliu Sep 7, 2026
476b3af
remove redundant write to hasPublished
davidliu Sep 7, 2026
1ab1bdc
attributes for tokensource (for the protocol upgrade)
davidliu Sep 7, 2026
8714e2a
clean up orphaned code
davidliu Sep 7, 2026
8142908
toml file cleanup
davidliu Sep 7, 2026
35d0d5d
remove redundant uses-sdk
davidliu Sep 7, 2026
4276935
document passing in bad track frame format is an error
davidliu Sep 7, 2026
4691d8a
Call view model doc updates
davidliu Sep 14, 2026
bcc088e
clean up unneeded @Suppress kdoc annotations
davidliu Sep 14, 2026
40991a7
clean up public api by making new classes internal as needed
davidliu Sep 14, 2026
245e720
fix tests
davidliu Sep 14, 2026
0aaa8df
widen exception handling for the data track managers to handle races
davidliu Sep 14, 2026
0deda05
changesets
davidliu Sep 14, 2026
4d93b97
make channelManager in DataTrackPublisherChannel volatile
davidliu Sep 14, 2026
dee0b50
update uniffi to 0.1.12
davidliu Sep 14, 2026
3be789d
re-widen visibility of managers for testing
davidliu Sep 14, 2026
c264705
fix DataTrackStream race
davidliu Sep 14, 2026
f66f9cf
tests for DataTrack race
davidliu Sep 14, 2026
ea22909
clarifying doc for RemoteDataTracks map key
davidliu Sep 14, 2026
0531105
fix hanging flow for late collectors
davidliu Sep 15, 2026
d4c5e7a
more frame dropping fixes
davidliu Sep 15, 2026
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
19 changes: 19 additions & 0 deletions .changeset/data-tracks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"client-sdk-android": minor
---

Add data tracks, a named channel for streaming structured frames to a room.

Publish with `LocalParticipant.publishDataTrack` (or `withDataTrack` to scope a publication to a
block), then push frames with `LocalDataTrack.tryPush` or `send`. Subscribers find published tracks
through `RemoteParticipant.dataTracks` or the new `RoomEvent.DataTrackPublished` /
`DataTrackUnpublished` events, and call `RemoteDataTrack.subscribe` for a `DataTrackStream` of
incoming frames.

Frame formats are declared through `DataTrackPublishOptions` using `DataTrackFrameEncoding`
(protobuf, flatbuffer, CDR, ROS 1, CBOR, msgpack, JSON), and schema definitions can be shared
with `LocalParticipant.defineSchema` / `getSchema`. Frames are end-to-end encrypted when E2EE
is enabled on the room.

This adds a dependency on `io.livekit:livekit-uniffi-android`, containing the shared Rust core also
used by the other native SDKs.
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ noise = "2.0.0"
lifecycleProcess = "2.8.7"
agp = "8.7.2"
kotlin = "1.9.25"
livekit-uniffi = "0.1.12"

[libraries]
livekit-uniffi = { module = "io.livekit:livekit-uniffi-android", version.ref = "livekit-uniffi" }

android-jain-sip-ri = { module = "javax.sip:android-jain-sip-ri", version.ref = "androidJainSipRi" }
androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidx-camera" }
Expand Down
1 change: 1 addition & 0 deletions livekit-android-sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation libs.coroutines.lib
implementation libs.kotlinx.serialization.json
implementation libs.livekit.uniffi
api libs.webrtc
api libs.okhttp.lib
implementation libs.okhttp.coroutines
Expand Down
10 changes: 6 additions & 4 deletions livekit-android-sdk/detekt-baseline-release.xml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions livekit-android-sdk/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@
android:stopWithTask="true" />
</application>
</manifest>

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2025 LiveKit, Inc.
* Copyright 2025-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.
Expand All @@ -22,6 +22,8 @@ import io.livekit.android.room.datastream.incoming.IncomingDataStreamManager
import io.livekit.android.room.datastream.incoming.IncomingDataStreamManagerImpl
import io.livekit.android.room.datastream.outgoing.OutgoingDataStreamManager
import io.livekit.android.room.datastream.outgoing.OutgoingDataStreamManagerImpl
import io.livekit.android.room.datatrack.IncomingDataTrackManager
import io.livekit.android.room.datatrack.IncomingDataTrackManagerImpl

/**
* @suppress
Expand All @@ -33,4 +35,7 @@ abstract class InternalBindsModule {

@Binds
abstract fun outgoingDataStreamManager(manager: OutgoingDataStreamManagerImpl): OutgoingDataStreamManager

@Binds
internal abstract fun incomingDataTrackManager(manager: IncomingDataTrackManagerImpl): IncomingDataTrackManager
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import io.livekit.android.audio.NoAudioRecordPrewarmer
import io.livekit.android.e2ee.DataPacketCryptorManager
import io.livekit.android.e2ee.DataPacketCryptorManagerImpl
import io.livekit.android.memory.CloseableManager
import io.livekit.android.room.datatrack.LocalDataTrackManagerFactory
import io.livekit.android.room.datatrack.RemoteDataTrackManagerFactory
import io.livekit.android.util.LKLog
import io.livekit.android.util.LoggingLevel
import io.livekit.android.webrtc.CustomAudioProcessingFactory
Expand All @@ -46,6 +48,8 @@ import io.livekit.android.webrtc.peerconnection.RTCThreadToken
import io.livekit.android.webrtc.peerconnection.RTCThreadTokenImpl
import io.livekit.android.webrtc.peerconnection.executeBlockingOnRTCThread
import io.livekit.android.webrtc.peerconnection.executeOnRTCThread
import io.livekit.uniffi.LocalDataTrackManager
import io.livekit.uniffi.RemoteDataTrackManager
import livekit.org.webrtc.AudioProcessingFactory
import livekit.org.webrtc.EglBase
import livekit.org.webrtc.Logging
Expand Down Expand Up @@ -384,6 +388,20 @@ internal object RTCModule {
return DataPacketCryptorManagerImpl.Factory
}

@Provides
fun localDataTrackManagerFactory(): LocalDataTrackManagerFactory {
return LocalDataTrackManagerFactory { delegate, encryptionProvider ->
LocalDataTrackManager(delegate, encryptionProvider)
}
}

@Provides
fun remoteDataTrackManagerFactory(): RemoteDataTrackManagerFactory {
return RemoteDataTrackManagerFactory { delegate, decryptionProvider ->
RemoteDataTrackManager(delegate, decryptionProvider)
}
}

@Provides
@Singleton
fun peerConnectionFactory(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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.
*/

package io.livekit.android.e2ee

import io.livekit.android.room.participant.Participant
import uniffi.livekit_datatrack.DecryptionException
import uniffi.livekit_datatrack.DecryptionProvider
import uniffi.livekit_datatrack.EncryptedPayload
import uniffi.livekit_datatrack.EncryptionException
import uniffi.livekit_datatrack.EncryptionProvider

/**
* Bridges UniFFI data-track [EncryptionProvider] / [DecryptionProvider] to [E2EEManager].
*
* Adds no key handling of its own — encryption rides [E2EEManager]'s existing AES-GCM data path
* (the same [DataPacketCryptorManager] used for data-channel payloads). The manager is resolved
* per call so one assigned after connecting still applies.
*/
internal class DataTrackCryptor(
private val e2eeManagerProvider: () -> E2EEManager?,
) : EncryptionProvider, DecryptionProvider {

override fun encrypt(payload: ByteArray): EncryptedPayload {
val manager = requireManager { message -> EncryptionException.Failed(message) }
val packet = manager.encrypt(payload)
?: throw EncryptionException.Failed("Failed to encrypt data track payload")
return EncryptedPayload(
payload = packet.payload,
iv = packet.iv,
keyIndex = packet.keyIndex.toUByte(),
)
}

override fun decrypt(payload: EncryptedPayload, senderIdentity: String): ByteArray {
val manager = requireManager { message -> DecryptionException.Failed(message) }
val packet = EncryptedPacket(
payload = payload.payload,
iv = payload.iv,
keyIndex = payload.keyIndex.toInt(),
)
return manager.decrypt(Participant.Identity(senderIdentity), packet)
?: throw DecryptionException.Failed("Failed to decrypt data track payload")
}

private fun <T : Exception> requireManager(failed: (String) -> T): E2EEManager {
return e2eeManagerProvider()
?: throw failed("Room has no E2EE manager")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import io.livekit.android.room.track.RemoteVideoTrack
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.TrackPublication
import io.livekit.android.util.LKLog
import livekit.LivekitModels.Encryption
import livekit.org.webrtc.FrameCryptor
import livekit.org.webrtc.FrameCryptor.FrameCryptionState
import livekit.org.webrtc.FrameCryptorAlgorithm
Expand Down Expand Up @@ -73,6 +74,16 @@ constructor(
return enabled && dataChannelEncryptionEnabled
}

/**
* Whether data-track frames should be encrypted: the runtime flag plus a configured
* encryption type (unlike the data-channel gate, which also requires
* [dataChannelEncryptionEnabled]).
*/
internal fun isDataTrackEncryptionEnabled(): Boolean {
val type = room?.e2eeOptions?.encryptionType ?: return false
return enabled && type != Encryption.Type.NONE
}

fun keyProvider(): KeyProvider {
return this.keyProvider
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 LiveKit, Inc.
* Copyright 2023-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.
Expand All @@ -16,6 +16,8 @@

package io.livekit.android.events

import io.livekit.android.room.datatrack.DataTrackSid
import io.livekit.android.room.datatrack.RemoteDataTrack
import io.livekit.android.room.participant.LocalParticipant
import io.livekit.android.room.participant.Participant
import io.livekit.android.room.participant.ParticipantPermission
Expand Down Expand Up @@ -120,6 +122,22 @@ sealed class ParticipantEvent(open val participant: Participant) : Event() {
class TrackUnpublished(override val participant: RemoteParticipant, val publication: RemoteTrackPublication) :
ParticipantEvent(participant)

/**
* A [RemoteParticipant] published a data track.
*/
class DataTrackPublished(
override val participant: RemoteParticipant,
val track: RemoteDataTrack,
) : ParticipantEvent(participant)

/**
* A [RemoteParticipant] unpublished a data track.
*/
class DataTrackUnpublished(
override val participant: RemoteParticipant,
val sid: DataTrackSid,
) : ParticipantEvent(participant)

/**
* Subscribed to a new track
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package io.livekit.android.events
import io.livekit.android.annotations.Beta
import io.livekit.android.e2ee.E2EEState
import io.livekit.android.room.Room
import io.livekit.android.room.datatrack.DataTrackSid
import io.livekit.android.room.datatrack.RemoteDataTrack
import io.livekit.android.room.participant.ConnectionQuality
import io.livekit.android.room.participant.LocalParticipant
import io.livekit.android.room.participant.Participant
Expand Down Expand Up @@ -156,6 +158,41 @@ sealed class RoomEvent(val room: Room) : Event() {
class TrackUnpublished(room: Room, val publication: TrackPublication, val participant: Participant) :
RoomEvent(room)

/**
* A [RemoteParticipant] published a data track.
*
* Fires for every track, including those already published when this participant was
* first seen and those reattached after a full reconnect.
*
* Collect frames in a separate coroutine so this event collector is not blocked.
*
* ```
* room.events.collect { event ->
* if (event is RoomEvent.DataTrackPublished) {
* scope.launch {
* event.track.subscribe().onSuccess { stream ->
* stream.flow.collect { frame -> process(frame.payload) }
* }
* }
* }
* }
* ```
*/
class DataTrackPublished(
room: Room,
val participant: RemoteParticipant,
val track: RemoteDataTrack,
) : RoomEvent(room)

/**
* A [RemoteParticipant] unpublished a data track.
*/
class DataTrackUnpublished(
room: Room,
val participant: RemoteParticipant,
val sid: DataTrackSid,
) : RoomEvent(room)

/**
* The [LocalParticipant] has subscribed to a new track. This event will always fire as
* long as new tracks are ready for use.
Expand Down
Loading
Loading