Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion 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.8"
[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
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,34 @@ 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.
*
* ```
* room.events.collect { event ->
* if (event is RoomEvent.DataTrackPublished) {
* 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