Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
8e7a63e
feat(token): add agentAttributes to TokenRequestOptions
davidliu Sep 14, 2026
fdfc41c
test(token): assert agent attributes reach the request body
davidliu Sep 14, 2026
5663e9c
chore(protocol): bump protocol submodule to 2172178d
davidliu Sep 14, 2026
619d256
IMPORTANT(build): depend on livekit-uniffi-android 0.1.9
davidliu Sep 14, 2026
1c2ab18
feat(datatrack): add DataTrackSid and DataTrackFrame
davidliu Sep 14, 2026
7038841
feat(datatrack): add schema and frame encoding types
davidliu Sep 14, 2026
0e0d9d9
test(datatrack): cover schema blob key encoding
davidliu Sep 14, 2026
a643461
feat(datatrack): add DataTrackInfo and DataTrackPublishOptions
davidliu Sep 14, 2026
be921d4
feat(datatrack): add publish, push, subscribe, and schema exception t…
davidliu Sep 14, 2026
05f8041
feat(datatrack): add factory seam for the UniFFI data track managers
davidliu Sep 14, 2026
b08210d
test(datatrack): add mock UniFFI local and remote data track managers
davidliu Sep 14, 2026
8624b3b
test(datatrack): bind the mock factories in the test component
davidliu Sep 14, 2026
5963caa
IMPORTANT(datatrack): add DataTrackFrameSender drop-oldest outbound d…
davidliu Sep 14, 2026
b7f155f
test(datatrack): pin DataTrackFrameSender drain semantics
davidliu Sep 14, 2026
82dc9d9
IMPORTANT(datatrack): open a publisher _data_track channel and drain …
davidliu Sep 14, 2026
ac3063f
IMPORTANT(datatrack): wait for the publisher channel before publishing
davidliu Sep 14, 2026
1928876
refactor(signal): keep the encoded websocket bytes alongside decoded …
davidliu Sep 14, 2026
45e93ad
feat(signal): dispatch data track SFU responses to the listener
davidliu Sep 14, 2026
1ed9875
feat(signal): forward UniFFI-built signal requests
davidliu Sep 14, 2026
4c26a5a
test(signal): cover encoded request forwarding and retained join bytes
davidliu Sep 14, 2026
ac9a7fe
IMPORTANT(datatrack): add LocalDataTrack wrapper
davidliu Sep 14, 2026
8cb39b0
IMPORTANT(datatrack): add OutgoingDataTrackManager bridging the UniFF…
davidliu Sep 14, 2026
68d1265
feat(datatrack): wire OutgoingDataTrackManager into RTCEngine
davidliu Sep 14, 2026
e0415ba
IMPORTANT(datatrack): add LocalParticipant.publishDataTrack and withD…
davidliu Sep 14, 2026
84214e3
test(datatrack): cover publishing through the mock local manager
davidliu Sep 14, 2026
f29d062
feat(datatrack): add LocalDataTrack.send for streaming frames from a …
davidliu Sep 14, 2026
69435bf
test(datatrack): cover send drop and fail policies
davidliu Sep 14, 2026
b727dfb
IMPORTANT(signal): add id-correlated StoreDataBlob and GetDataBlob re…
davidliu Sep 14, 2026
7b66194
test(signal): cover blob store, get, and rejection
davidliu Sep 14, 2026
6c99f2e
feat(datatrack): add LocalParticipant.defineSchema and getSchema
davidliu Sep 14, 2026
a42d590
test(datatrack): cover defineSchema when disconnected
davidliu Sep 14, 2026
0a74726
IMPORTANT(datatrack): add DataTrackStream with a shared multi-collect…
davidliu Sep 14, 2026
c655a79
test(datatrack): cover DataTrackStream collector semantics
davidliu Sep 14, 2026
d0ed4e8
feat(datatrack): add RemoteDataTrack wrapper
davidliu Sep 14, 2026
cd5361b
IMPORTANT(datatrack): add IncomingDataTrackManager bridging the UniFF…
davidliu Sep 14, 2026
c6e7fbe
IMPORTANT(datatrack): route the subscriber channel and SFU updates in…
davidliu Sep 14, 2026
ea8e72f
test(datatrack): cover incoming transport and join forwarding
davidliu Sep 14, 2026
7b6128e
IMPORTANT(datatrack): expose data tracks on RemoteParticipant
davidliu Sep 14, 2026
f438ad8
feat(datatrack): surface remote data track events on Room
davidliu Sep 14, 2026
33df521
test(datatrack): cover attach, park, unpublish, and disconnect
davidliu Sep 14, 2026
3fb442c
IMPORTANT(datatrack): restore data track state across reconnects
davidliu Sep 14, 2026
03fa4b9
test(datatrack): cover publisher reconnect and sync state
davidliu Sep 14, 2026
dbd6516
test(datatrack): cover subscriber reconnect and reattach
davidliu Sep 14, 2026
167704e
IMPORTANT(datatrack): encrypt data track frames through E2EEManager
davidliu Sep 14, 2026
f0c3bce
test(datatrack): cover the E2EE bridge
davidliu Sep 14, 2026
441d2cd
feat(sample): subscribe to remote data tracks in CallViewModel
davidliu Sep 14, 2026
58f0b6c
chore: update detekt baseline and copyright headers
davidliu Sep 14, 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
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.9"

[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.

7 changes: 6 additions & 1 deletion livekit-android-sdk/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
limitations under the License.
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<!-- livekit-uniffi currently declares minSdk 24; keep SDK at 21 until that is aligned. -->
<uses-sdk tools:overrideLibrary="io.livekit.uniffi" />

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
Expand All @@ -31,3 +35,4 @@
android:stopWithTask="true" />
</application>
</manifest>

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,65 @@
/*
* 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.
*
* @suppress
*/
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