CloudWebRTC
Committed by GitHub

feat: E2EE. (#238)

* feat: E2EE.

* chore: more changes.

* chore: reduce code.

* revert unnecessary changes.

* update.

* update.

* add e2ee to sample-app.

* update.

* fix build.

* Fix preferences save/get e2ee settings.

* update.

* update.

* update.

* revert changes.

* Update livekit-android-sdk/src/main/java/io/livekit/android/e2ee/KeyProvider.kt

Co-authored-by: davidliu <davidliu@deviange.net>

* Update livekit-android-sdk/src/main/java/io/livekit/android/e2ee/KeyProvider.kt

Co-authored-by: davidliu <davidliu@deviange.net>

* Update sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt

Co-authored-by: davidliu <davidliu@deviange.net>

* Update livekit-android-sdk/src/main/java/io/livekit/android/room/Room.kt

Co-authored-by: davidliu <davidliu@deviange.net>

* improve code

* update.

* Update livekit-android-sdk/src/main/java/io/livekit/android/room/track/LocalAudioTrack.kt

Co-authored-by: davidliu <davidliu@deviange.net>

* Update livekit-android-sdk/src/main/java/io/livekit/android/room/track/LocalVideoTrack.kt

Co-authored-by: davidliu <davidliu@deviange.net>

* Update livekit-android-sdk/src/main/java/io/livekit/android/room/track/RemoteAudioTrack.kt

Co-authored-by: davidliu <davidliu@deviange.net>

* update.

* update.

---------

Co-authored-by: davidliu <davidliu@deviange.net>
正在显示 24 个修改的文件 包含 452 行增加18 行删除
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 16
17 package io.livekit.android 17 package io.livekit.android
18 18
  19 +import io.livekit.android.e2ee.E2EEOptions
19 import io.livekit.android.room.Room 20 import io.livekit.android.room.Room
20 import io.livekit.android.room.participant.AudioTrackPublishDefaults 21 import io.livekit.android.room.participant.AudioTrackPublishDefaults
21 import io.livekit.android.room.participant.VideoTrackPublishDefaults 22 import io.livekit.android.room.participant.VideoTrackPublishDefaults
@@ -33,6 +34,11 @@ data class RoomOptions( @@ -33,6 +34,11 @@ data class RoomOptions(
33 */ 34 */
34 val dynacast: Boolean = false, 35 val dynacast: Boolean = false,
35 36
  37 + /**
  38 + * Options for end-to-end encryption.
  39 + */
  40 + var e2eeOptions: E2EEOptions? = null,
  41 +
36 val audioTrackCaptureDefaults: LocalAudioTrackOptions? = null, 42 val audioTrackCaptureDefaults: LocalAudioTrackOptions? = null,
37 val videoTrackCaptureDefaults: LocalVideoTrackOptions? = null, 43 val videoTrackCaptureDefaults: LocalVideoTrackOptions? = null,
38 val audioTrackPublishDefaults: AudioTrackPublishDefaults? = null, 44 val audioTrackPublishDefaults: AudioTrackPublishDefaults? = null,
  1 +package io.livekit.android.e2ee
  2 +
  3 +import io.livekit.android.events.RoomEvent
  4 +import io.livekit.android.room.Room
  5 +import io.livekit.android.room.participant.*
  6 +import io.livekit.android.room.track.LocalAudioTrack
  7 +import io.livekit.android.room.track.LocalVideoTrack
  8 +import io.livekit.android.room.track.RemoteAudioTrack
  9 +import io.livekit.android.room.track.RemoteVideoTrack
  10 +import io.livekit.android.room.track.Track
  11 +import io.livekit.android.room.track.TrackPublication
  12 +import io.livekit.android.util.LKLog
  13 +import org.webrtc.FrameCryptor
  14 +import org.webrtc.FrameCryptor.FrameCryptionState
  15 +import org.webrtc.FrameCryptorAlgorithm
  16 +import org.webrtc.FrameCryptorFactory
  17 +import org.webrtc.RtpReceiver
  18 +import org.webrtc.RtpSender
  19 +
  20 +class E2EEManager
  21 +constructor(keyProvider: KeyProvider) {
  22 + private var room: Room? = null
  23 + private var keyProvider: KeyProvider
  24 + private var frameCryptors = mutableMapOf<String, FrameCryptor>()
  25 + private var algorithm: FrameCryptorAlgorithm = FrameCryptorAlgorithm.AES_GCM
  26 + private lateinit var emitEvent: (roomEvent: RoomEvent) -> Unit?
  27 + var enabled: Boolean = false
  28 + init {
  29 + this.keyProvider = keyProvider
  30 + }
  31 +
  32 + suspend fun setup(room: Room, emitEvent: (roomEvent: RoomEvent) -> Unit) {
  33 + if (this.room != room) {
  34 + // E2EEManager already setup, clean up first
  35 + cleanUp()
  36 + }
  37 + this.enabled = true
  38 + this.room = room
  39 + this.emitEvent = emitEvent
  40 + this.room?.localParticipant?.tracks?.forEach() { item ->
  41 + var participant = this.room!!.localParticipant
  42 + var publication = item.value
  43 + if (publication.track != null) {
  44 + addPublishedTrack(publication.track!!, publication, participant, room)
  45 + }
  46 + }
  47 + this.room?.remoteParticipants?.forEach() { item ->
  48 + var participant = item.value
  49 + participant.tracks.forEach() { item ->
  50 + var publication = item.value
  51 + if (publication.track != null) {
  52 + addSubscribedTrack(publication.track!!, publication, participant, room)
  53 + }
  54 + }
  55 + }
  56 + }
  57 +
  58 + public fun addSubscribedTrack(track: Track, publication: TrackPublication, participant: RemoteParticipant, room: Room) {
  59 + var trackId = publication.sid
  60 + var participantId = participant.sid
  61 + var rtpReceiver: RtpReceiver? = when (publication.track!!) {
  62 + is RemoteAudioTrack -> (publication.track!! as RemoteAudioTrack).receiver
  63 + is RemoteVideoTrack -> (publication.track!! as RemoteVideoTrack).receiver
  64 + else -> {
  65 + throw IllegalArgumentException("unsupported track type")
  66 + }
  67 + }
  68 + var frameCryptor = addRtpReceiver(rtpReceiver!!, participantId, trackId, publication.track!!.kind.name.lowercase())
  69 + frameCryptor.setObserver { trackId, state ->
  70 + LKLog.i { "Receiver::onFrameCryptionStateChanged: $trackId, state: $state" }
  71 + emitEvent(
  72 + RoomEvent.TrackE2EEStateEvent(
  73 + room!!, publication.track!!, publication,
  74 + participant,
  75 + state = e2eeStateFromFrameCryptoState(state)
  76 + )
  77 + )
  78 + }
  79 + }
  80 +
  81 + public fun addPublishedTrack(track: Track, publication: TrackPublication, participant: LocalParticipant, room: Room) {
  82 + var trackId = publication.sid
  83 + var participantId = participant.sid
  84 + var rtpSender: RtpSender? = when (publication.track!!) {
  85 + is LocalAudioTrack -> (publication.track!! as LocalAudioTrack)?.sender
  86 + is LocalVideoTrack -> (publication.track!! as LocalVideoTrack)?.sender
  87 + else -> {
  88 + throw IllegalArgumentException("unsupported track type")
  89 + }
  90 + } ?: throw IllegalArgumentException("rtpSender is null")
  91 +
  92 + var frameCryptor = addRtpSender(rtpSender!!, participantId, trackId, publication.track!!.kind.name.lowercase())
  93 + frameCryptor.setObserver { trackId, state ->
  94 + LKLog.i { "Sender::onFrameCryptionStateChanged: $trackId, state: $state" }
  95 + emitEvent(
  96 + RoomEvent.TrackE2EEStateEvent(
  97 + room!!, publication.track!!, publication,
  98 + participant,
  99 + state = e2eeStateFromFrameCryptoState(state)
  100 + )
  101 + )
  102 + }
  103 + }
  104 +
  105 + private fun e2eeStateFromFrameCryptoState(state: FrameCryptionState?): E2EEState {
  106 + return when (state) {
  107 + FrameCryptionState.NEW -> E2EEState.NEW
  108 + FrameCryptionState.OK -> E2EEState.OK
  109 + FrameCryptionState.KEYRATCHETED -> E2EEState.KEY_RATCHETED
  110 + FrameCryptionState.MISSINGKEY -> E2EEState.MISSING_KEY
  111 + FrameCryptionState.ENCRYPTIONFAILED -> E2EEState.ENCRYPTION_FAILED
  112 + FrameCryptionState.DECRYPTIONFAILED -> E2EEState.DECRYPTION_FAILED
  113 + FrameCryptionState.INTERNALERROR -> E2EEState.INTERNAL_ERROR
  114 + else -> { E2EEState.INTERNAL_ERROR}
  115 + }
  116 + }
  117 +
  118 + private fun addRtpSender(sender: RtpSender, participantId: String, trackId: String , kind: String): FrameCryptor {
  119 + var pid = "$kind-sender-$participantId-$trackId"
  120 + var frameCryptor = FrameCryptorFactory.createFrameCryptorForRtpSender(
  121 + sender, pid, algorithm, keyProvider.rtcKeyProvider)
  122 +
  123 + frameCryptors[trackId] = frameCryptor
  124 + frameCryptor.setEnabled(enabled)
  125 + if(keyProvider.enableSharedKey) {
  126 + keyProvider.rtcKeyProvider?.setKey(pid, 0, keyProvider?.sharedKey)
  127 + frameCryptor.setKeyIndex(0)
  128 + }
  129 +
  130 + return frameCryptor
  131 + }
  132 +
  133 + private fun addRtpReceiver(receiver: RtpReceiver, participantId: String, trackId: String, kind: String): FrameCryptor {
  134 + var pid = "$kind-receiver-$participantId-$trackId"
  135 + var frameCryptor = FrameCryptorFactory.createFrameCryptorForRtpReceiver(
  136 + receiver, pid, algorithm, keyProvider.rtcKeyProvider)
  137 +
  138 + frameCryptors[trackId] = frameCryptor
  139 + frameCryptor.setEnabled(enabled)
  140 +
  141 + if(keyProvider.enableSharedKey) {
  142 + keyProvider.rtcKeyProvider?.setKey(pid, 0, keyProvider?.sharedKey)
  143 + frameCryptor.setKeyIndex(0)
  144 + }
  145 +
  146 + return frameCryptor
  147 + }
  148 +
  149 + /**
  150 + * Enable or disable E2EE
  151 + * @param enabled
  152 + */
  153 + public fun enableE2EE(enabled: Boolean) {
  154 + this.enabled = enabled
  155 + for (item in frameCryptors.entries) {
  156 + var frameCryptor = item.value
  157 + var participantId = item.key
  158 + frameCryptor.setEnabled(enabled)
  159 + if(keyProvider.enableSharedKey) {
  160 + keyProvider.rtcKeyProvider?.setKey(participantId, 0, keyProvider?.sharedKey)
  161 + frameCryptor.setKeyIndex(0)
  162 + }
  163 + }
  164 + }
  165 +
  166 + /**
  167 + * Ratchet key for local participant
  168 + */
  169 + fun ratchetKey() {
  170 + for (participantId in frameCryptors.keys) {
  171 + var newKey = keyProvider.rtcKeyProvider?.ratchetKey(participantId, 0)
  172 + LKLog.d{ "ratchetKey: newKey: $newKey" }
  173 + }
  174 + }
  175 +
  176 + fun cleanUp() {
  177 + for (frameCryptor in frameCryptors.values) {
  178 + frameCryptor.dispose()
  179 + }
  180 + frameCryptors.clear()
  181 + }
  182 +}
  1 +package io.livekit.android.e2ee
  2 +
  3 +import livekit.LivekitModels.Encryption
  4 +
  5 +var defaultRatchetSalt = "LKFrameEncryptionKey"
  6 +var defaultMagicBytes = "LK-ROCKS"
  7 +var defaultRatchetWindowSize = 16
  8 +
  9 +class E2EEOptions
  10 +constructor(keyProvider: KeyProvider = BaseKeyProvider(
  11 + defaultRatchetSalt,
  12 + defaultMagicBytes,
  13 + defaultRatchetWindowSize,
  14 + true,
  15 +), encryptionType: Encryption.Type = Encryption.Type.GCM) {
  16 + var keyProvider: KeyProvider
  17 + var encryptionType: Encryption.Type = Encryption.Type.NONE
  18 + init {
  19 + this.keyProvider = keyProvider
  20 + this.encryptionType = encryptionType
  21 + }
  22 +}
  1 +package io.livekit.android.e2ee
  2 +
  3 +enum class E2EEState {
  4 + NEW, // initial state
  5 + OK, // encryption or decryption succeeded
  6 + KEY_RATCHETED, // key ratcheted
  7 + MISSING_KEY, // missing key
  8 + ENCRYPTION_FAILED, // encryption failed
  9 + DECRYPTION_FAILED, // decryption failed
  10 + INTERNAL_ERROR // internal error
  11 +}
  1 +package io.livekit.android.e2ee
  2 +
  3 +import io.livekit.android.util.LKLog
  4 +import org.webrtc.FrameCryptorFactory
  5 +import org.webrtc.FrameCryptorKeyProvider
  6 +
  7 +class KeyInfo
  8 +constructor(var participantId: String, var keyIndex: Int, var key: String ) {
  9 + override fun toString(): String {
  10 + return "KeyInfo(participantId='$participantId', keyIndex=$keyIndex)"
  11 + }
  12 +}
  13 +
  14 + public interface KeyProvider {
  15 + fun setKey(key: String, participantId: String?, keyIndex: Int? = 0)
  16 + fun ratchetKey(participantId: String, index: Int): ByteArray
  17 +
  18 + val rtcKeyProvider: FrameCryptorKeyProvider
  19 +
  20 + var sharedKey: ByteArray?
  21 +
  22 + var enableSharedKey: Boolean
  23 +}
  24 +
  25 +class BaseKeyProvider
  26 +constructor(private var ratchetSalt: String, private var uncryptedMagicBytes: String, private var ratchetWindowSize: Int, override var enableSharedKey: Boolean = true) :
  27 + KeyProvider {
  28 + override var sharedKey: ByteArray? = null
  29 + private var keys: MutableMap<String, MutableMap<Int, String>> = mutableMapOf()
  30 +
  31 + /**
  32 + * Set a key for a participant
  33 + * @param key
  34 + * @param participantId
  35 + * @param keyIndex
  36 + */
  37 + override fun setKey(key: String, participantId: String?, keyIndex: Int?) {
  38 + if (enableSharedKey) {
  39 + sharedKey = key.toByteArray()
  40 + return
  41 + }
  42 +
  43 + if(participantId == null) {
  44 + LKLog.d{ "Please provide valid participantId for non-SharedKey mode." }
  45 + return
  46 + }
  47 +
  48 + var keyInfo = KeyInfo(participantId, keyIndex ?: 0, key)
  49 +
  50 + if (!keys.containsKey(keyInfo.participantId)) {
  51 + keys[keyInfo.participantId] = mutableMapOf()
  52 + }
  53 + keys[keyInfo.participantId]!![keyInfo.keyIndex] = keyInfo.key
  54 + rtcKeyProvider.setKey(participantId, keyInfo.keyIndex, key.toByteArray())
  55 + }
  56 +
  57 + override fun ratchetKey(participantId: String, index: Int): ByteArray {
  58 + return rtcKeyProvider.ratchetKey(participantId, index)
  59 + }
  60 +
  61 + override val rtcKeyProvider: FrameCryptorKeyProvider
  62 +
  63 + init {
  64 + this.ratchetSalt = ratchetSalt
  65 + this.uncryptedMagicBytes = uncryptedMagicBytes
  66 + this.ratchetWindowSize = ratchetWindowSize
  67 + this.enableSharedKey = enableSharedKey
  68 + this.rtcKeyProvider = FrameCryptorFactory.createFrameCryptorKeyProvider(
  69 + enableSharedKey,
  70 + ratchetSalt.toByteArray(),
  71 + ratchetWindowSize,
  72 + uncryptedMagicBytes.toByteArray(),
  73 + )
  74 + }
  75 +}
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 16
17 package io.livekit.android.events 17 package io.livekit.android.events
18 18
  19 +import io.livekit.android.e2ee.E2EEState
19 import io.livekit.android.room.Room 20 import io.livekit.android.room.Room
20 import io.livekit.android.room.participant.* 21 import io.livekit.android.room.participant.*
21 import io.livekit.android.room.track.LocalTrackPublication 22 import io.livekit.android.room.track.LocalTrackPublication
@@ -197,6 +198,17 @@ sealed class RoomEvent(val room: Room) : Event() { @@ -197,6 +198,17 @@ sealed class RoomEvent(val room: Room) : Event() {
197 * The recording of a room has started/stopped. 198 * The recording of a room has started/stopped.
198 */ 199 */
199 class RecordingStatusChanged(room: Room, isRecording: Boolean) : RoomEvent(room) 200 class RecordingStatusChanged(room: Room, isRecording: Boolean) : RoomEvent(room)
  201 +
  202 + /**
  203 + * The E2EE state of a track has changed.
  204 + */
  205 + class TrackE2EEStateEvent(
  206 + room: Room,
  207 + val track: Track,
  208 + val publication: TrackPublication,
  209 + val participant: Participant,
  210 + var state: E2EEState
  211 + ) : RoomEvent(room)
200 } 212 }
201 213
202 enum class DisconnectReason { 214 enum class DisconnectReason {
@@ -21,6 +21,7 @@ import com.google.protobuf.ByteString @@ -21,6 +21,7 @@ import com.google.protobuf.ByteString
21 import io.livekit.android.ConnectOptions 21 import io.livekit.android.ConnectOptions
22 import io.livekit.android.RoomOptions 22 import io.livekit.android.RoomOptions
23 import io.livekit.android.dagger.InjectionNames 23 import io.livekit.android.dagger.InjectionNames
  24 +import io.livekit.android.e2ee.E2EEOptions
24 import io.livekit.android.events.DisconnectReason 25 import io.livekit.android.events.DisconnectReason
25 import io.livekit.android.events.convert 26 import io.livekit.android.events.convert
26 import io.livekit.android.room.participant.ParticipantTrackPermission 27 import io.livekit.android.room.participant.ParticipantTrackPermission
@@ -38,6 +39,7 @@ import io.livekit.android.webrtc.toProtoSessionDescription @@ -38,6 +39,7 @@ import io.livekit.android.webrtc.toProtoSessionDescription
38 import kotlinx.coroutines.* 39 import kotlinx.coroutines.*
39 import kotlinx.coroutines.sync.Mutex 40 import kotlinx.coroutines.sync.Mutex
40 import livekit.LivekitModels 41 import livekit.LivekitModels
  42 +import livekit.LivekitModels.Encryption
41 import livekit.LivekitRtc 43 import livekit.LivekitRtc
42 import livekit.LivekitRtc.JoinResponse 44 import livekit.LivekitRtc.JoinResponse
43 import livekit.LivekitRtc.ReconnectResponse 45 import livekit.LivekitRtc.ReconnectResponse
@@ -271,7 +273,6 @@ internal constructor( @@ -271,7 +273,6 @@ internal constructor(
271 if (pendingTrackResolvers[cid] != null) { 273 if (pendingTrackResolvers[cid] != null) {
272 throw TrackException.DuplicateTrackException("Track with same ID $cid has already been published!") 274 throw TrackException.DuplicateTrackException("Track with same ID $cid has already been published!")
273 } 275 }
274 -  
275 // Suspend until signal client receives message confirming track publication. 276 // Suspend until signal client receives message confirming track publication.
276 return suspendCoroutine { cont -> 277 return suspendCoroutine { cont ->
277 pendingTrackResolvers[cid] = cont 278 pendingTrackResolvers[cid] = cont
@@ -32,6 +32,7 @@ import io.livekit.android.RoomOptions @@ -32,6 +32,7 @@ import io.livekit.android.RoomOptions
32 import io.livekit.android.Version 32 import io.livekit.android.Version
33 import io.livekit.android.audio.AudioHandler 33 import io.livekit.android.audio.AudioHandler
34 import io.livekit.android.dagger.InjectionNames 34 import io.livekit.android.dagger.InjectionNames
  35 +import io.livekit.android.e2ee.E2EEManager
35 import io.livekit.android.events.* 36 import io.livekit.android.events.*
36 import io.livekit.android.memory.CloseableManager 37 import io.livekit.android.memory.CloseableManager
37 import io.livekit.android.renderer.TextureViewRenderer 38 import io.livekit.android.renderer.TextureViewRenderer
@@ -131,6 +132,11 @@ constructor( @@ -131,6 +132,11 @@ constructor(
131 private set 132 private set
132 133
133 /** 134 /**
  135 + * end-to-end encryption manager
  136 + */
  137 + var e2eeManager: E2EEManager? = null
  138 +
  139 + /**
134 * Automatically manage quality of subscribed video tracks, subscribe to the 140 * Automatically manage quality of subscribed video tracks, subscribe to the
135 * an appropriate resolution based on the size of the video elements that tracks 141 * an appropriate resolution based on the size of the video elements that tracks
136 * are attached to. 142 * are attached to.
@@ -199,9 +205,10 @@ constructor( @@ -199,9 +205,10 @@ constructor(
199 videoTrackCaptureDefaults = videoTrackCaptureDefaults, 205 videoTrackCaptureDefaults = videoTrackCaptureDefaults,
200 audioTrackPublishDefaults = audioTrackPublishDefaults, 206 audioTrackPublishDefaults = audioTrackPublishDefaults,
201 videoTrackPublishDefaults = videoTrackPublishDefaults, 207 videoTrackPublishDefaults = videoTrackPublishDefaults,
  208 + e2eeOptions = null,
202 ) 209 )
203 210
204 - suspend fun connect(url: String, token: String, options: ConnectOptions = ConnectOptions()) { 211 + suspend fun connect(url: String, token: String, options: ConnectOptions = ConnectOptions(), roomOptions: RoomOptions = getCurrentRoomOptions()) {
205 if (this::coroutineScope.isInitialized) { 212 if (this::coroutineScope.isInitialized) {
206 coroutineScope.cancel() 213 coroutineScope.cancel()
207 } 214 }
@@ -259,7 +266,17 @@ constructor( @@ -259,7 +266,17 @@ constructor(
259 266
260 state = State.CONNECTING 267 state = State.CONNECTING
261 connectOptions = options 268 connectOptions = options
262 - engine.join(url, token, options, getCurrentRoomOptions()) 269 +
  270 + if(roomOptions.e2eeOptions != null) {
  271 + e2eeManager = E2EEManager(roomOptions!!.e2eeOptions!!.keyProvider)
  272 + e2eeManager!!.setup(this, {event ->
  273 + coroutineScope.launch {
  274 + emitWhenConnected(event)
  275 + }
  276 + })
  277 + }
  278 +
  279 + engine.join(url, token, options, roomOptions)
263 280
264 val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 281 val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
265 val networkRequest = NetworkRequest.Builder() 282 val networkRequest = NetworkRequest.Builder()
@@ -510,6 +527,7 @@ constructor( @@ -510,6 +527,7 @@ constructor(
510 * Removes all participants and tracks from the room. 527 * Removes all participants and tracks from the room.
511 */ 528 */
512 private fun cleanupRoom() { 529 private fun cleanupRoom() {
  530 + e2eeManager?.cleanUp()
513 localParticipant.cleanup() 531 localParticipant.cleanup()
514 remoteParticipants.keys.toMutableSet() // copy keys to avoid concurrent modifications. 532 remoteParticipants.keys.toMutableSet() // copy keys to avoid concurrent modifications.
515 .forEach { sid -> handleParticipantDisconnect(sid) } 533 .forEach { sid -> handleParticipantDisconnect(sid) }
@@ -684,7 +702,8 @@ constructor( @@ -684,7 +702,8 @@ constructor(
684 track, 702 track,
685 trackSid!!, 703 trackSid!!,
686 autoManageVideo = adaptiveStream, 704 autoManageVideo = adaptiveStream,
687 - statsGetter = statsGetter 705 + statsGetter = statsGetter,
  706 + receiver = receiver
688 ) 707 )
689 } 708 }
690 709
@@ -907,6 +926,16 @@ constructor( @@ -907,6 +926,16 @@ constructor(
907 /** 926 /**
908 * @suppress 927 * @suppress
909 */ 928 */
  929 + override fun onTrackPublished(publication: LocalTrackPublication, participant: LocalParticipant) {
  930 + listener?.onTrackPublished(publication, participant, this)
  931 + if(e2eeManager != null) {
  932 + e2eeManager!!.addPublishedTrack(publication.track!!, publication, participant, this)
  933 + }
  934 + eventBus.postEvent(RoomEvent.TrackPublished(this, publication, participant), coroutineScope)
  935 + }
  936 + /**
  937 + * @suppress
  938 + */
910 override fun onTrackUnpublished(publication: LocalTrackPublication, participant: LocalParticipant) { 939 override fun onTrackUnpublished(publication: LocalTrackPublication, participant: LocalParticipant) {
911 listener?.onTrackUnpublished(publication, participant, this) 940 listener?.onTrackUnpublished(publication, participant, this)
912 eventBus.postEvent(RoomEvent.TrackUnpublished(this, publication, participant), coroutineScope) 941 eventBus.postEvent(RoomEvent.TrackUnpublished(this, publication, participant), coroutineScope)
@@ -917,6 +946,9 @@ constructor( @@ -917,6 +946,9 @@ constructor(
917 */ 946 */
918 override fun onTrackSubscribed(track: Track, publication: RemoteTrackPublication, participant: RemoteParticipant) { 947 override fun onTrackSubscribed(track: Track, publication: RemoteTrackPublication, participant: RemoteParticipant) {
919 listener?.onTrackSubscribed(track, publication, participant, this) 948 listener?.onTrackSubscribed(track, publication, participant, this)
  949 + if(e2eeManager != null) {
  950 + e2eeManager!!.addSubscribedTrack(track, publication, participant, this)
  951 + }
920 eventBus.postEvent(RoomEvent.TrackSubscribed(this, track, publication, participant), coroutineScope) 952 eventBus.postEvent(RoomEvent.TrackSubscribed(this, track, publication, participant), coroutineScope)
921 } 953 }
922 954
@@ -34,6 +34,7 @@ import kotlinx.serialization.decodeFromString @@ -34,6 +34,7 @@ import kotlinx.serialization.decodeFromString
34 import kotlinx.serialization.encodeToString 34 import kotlinx.serialization.encodeToString
35 import kotlinx.serialization.json.Json 35 import kotlinx.serialization.json.Json
36 import livekit.LivekitModels 36 import livekit.LivekitModels
  37 +import livekit.LivekitModels.Encryption
37 import livekit.LivekitRtc 38 import livekit.LivekitRtc
38 import livekit.LivekitRtc.JoinResponse 39 import livekit.LivekitRtc.JoinResponse
39 import livekit.LivekitRtc.ReconnectResponse 40 import livekit.LivekitRtc.ReconnectResponse
@@ -376,10 +377,12 @@ constructor( @@ -376,10 +377,12 @@ constructor(
376 type: LivekitModels.TrackType, 377 type: LivekitModels.TrackType,
377 builder: LivekitRtc.AddTrackRequest.Builder = LivekitRtc.AddTrackRequest.newBuilder() 378 builder: LivekitRtc.AddTrackRequest.Builder = LivekitRtc.AddTrackRequest.newBuilder()
378 ) { 379 ) {
  380 + var encryptionType = lastRoomOptions?.e2eeOptions?.encryptionType ?: LivekitModels.Encryption.Type.NONE
379 val addTrackRequest = builder 381 val addTrackRequest = builder
380 .setCid(cid) 382 .setCid(cid)
381 .setName(name) 383 .setName(name)
382 .setType(type) 384 .setType(type)
  385 + .setEncryption(encryptionType)
383 val request = LivekitRtc.SignalRequest.newBuilder() 386 val request = LivekitRtc.SignalRequest.newBuilder()
384 .setAddTrack(addTrackRequest) 387 .setAddTrack(addTrackRequest)
385 .build() 388 .build()
@@ -650,6 +653,10 @@ constructor( @@ -650,6 +653,10 @@ constructor(
650 // TODO 653 // TODO
651 } 654 }
652 655
  656 + LivekitRtc.SignalResponse.MessageCase.SUBSCRIPTION_RESPONSE -> {
  657 + // TODO
  658 + }
  659 +
653 LivekitRtc.SignalResponse.MessageCase.MESSAGE_NOT_SET, 660 LivekitRtc.SignalResponse.MessageCase.MESSAGE_NOT_SET,
654 null -> { 661 null -> {
655 LKLog.v { "empty messageCase!" } 662 LKLog.v { "empty messageCase!" }
@@ -25,6 +25,7 @@ import dagger.assisted.AssistedFactory @@ -25,6 +25,7 @@ import dagger.assisted.AssistedFactory
25 import dagger.assisted.AssistedInject 25 import dagger.assisted.AssistedInject
26 import io.livekit.android.dagger.CapabilitiesGetter 26 import io.livekit.android.dagger.CapabilitiesGetter
27 import io.livekit.android.dagger.InjectionNames 27 import io.livekit.android.dagger.InjectionNames
  28 +import io.livekit.android.e2ee.E2EEOptions
28 import io.livekit.android.events.ParticipantEvent 29 import io.livekit.android.events.ParticipantEvent
29 import io.livekit.android.room.ConnectionState 30 import io.livekit.android.room.ConnectionState
30 import io.livekit.android.room.DefaultsManager 31 import io.livekit.android.room.DefaultsManager
@@ -30,6 +30,8 @@ import livekit.LivekitModels @@ -30,6 +30,8 @@ import livekit.LivekitModels
30 import livekit.LivekitRtc 30 import livekit.LivekitRtc
31 import org.webrtc.AudioTrack 31 import org.webrtc.AudioTrack
32 import org.webrtc.MediaStreamTrack 32 import org.webrtc.MediaStreamTrack
  33 +import org.webrtc.RtpReceiver
  34 +import org.webrtc.RtpTransceiver
33 import org.webrtc.VideoTrack 35 import org.webrtc.VideoTrack
34 36
35 class RemoteParticipant( 37 class RemoteParticipant(
@@ -114,8 +116,9 @@ class RemoteParticipant( @@ -114,8 +116,9 @@ class RemoteParticipant(
114 mediaTrack: MediaStreamTrack, 116 mediaTrack: MediaStreamTrack,
115 sid: String, 117 sid: String,
116 statsGetter: RTCStatsGetter, 118 statsGetter: RTCStatsGetter,
  119 + receiver: RtpReceiver,
117 autoManageVideo: Boolean = false, 120 autoManageVideo: Boolean = false,
118 - triesLeft: Int = 20 121 + triesLeft: Int = 20,
119 ) { 122 ) {
120 val publication = getTrackPublication(sid) 123 val publication = getTrackPublication(sid)
121 124
@@ -132,19 +135,20 @@ class RemoteParticipant( @@ -132,19 +135,20 @@ class RemoteParticipant(
132 } else { 135 } else {
133 coroutineScope.launch { 136 coroutineScope.launch {
134 delay(150) 137 delay(150)
135 - addSubscribedMediaTrack(mediaTrack, sid, statsGetter, autoManageVideo, triesLeft - 1) 138 + addSubscribedMediaTrack(mediaTrack, sid, statsGetter,receiver = receiver, autoManageVideo, triesLeft - 1)
136 } 139 }
137 } 140 }
138 return 141 return
139 } 142 }
140 143
141 val track: Track = when (val kind = mediaTrack.kind()) { 144 val track: Track = when (val kind = mediaTrack.kind()) {
142 - KIND_AUDIO -> RemoteAudioTrack(rtcTrack = mediaTrack as AudioTrack, name = "") 145 + KIND_AUDIO -> RemoteAudioTrack(rtcTrack = mediaTrack as AudioTrack, name = "", receiver = receiver)
143 KIND_VIDEO -> RemoteVideoTrack( 146 KIND_VIDEO -> RemoteVideoTrack(
144 rtcTrack = mediaTrack as VideoTrack, 147 rtcTrack = mediaTrack as VideoTrack,
145 name = "", 148 name = "",
146 autoManageVideo = autoManageVideo, 149 autoManageVideo = autoManageVideo,
147 - dispatcher = ioDispatcher 150 + dispatcher = ioDispatcher,
  151 + receiver = receiver
148 ) 152 )
149 153
150 else -> throw TrackException.InvalidTrackTypeException("invalid track type: $kind") 154 else -> throw TrackException.InvalidTrackTypeException("invalid track type: $kind")
@@ -156,6 +160,7 @@ class RemoteParticipant( @@ -156,6 +160,7 @@ class RemoteParticipant(
156 publication.subscriptionAllowed = true 160 publication.subscriptionAllowed = true
157 track.name = publication.name 161 track.name = publication.name
158 track.sid = publication.sid 162 track.sid = publication.sid
  163 +
159 addTrackPublication(publication) 164 addTrackPublication(publication)
160 track.start() 165 track.start()
161 166
@@ -42,7 +42,7 @@ class LocalAudioTrack( @@ -42,7 +42,7 @@ class LocalAudioTrack(
42 } 42 }
43 43
44 internal var transceiver: RtpTransceiver? = null 44 internal var transceiver: RtpTransceiver? = null
45 - private val sender: RtpSender? 45 + internal val sender: RtpSender?
46 get() = transceiver?.sender 46 get() = transceiver?.sender
47 47
48 companion object { 48 companion object {
@@ -77,7 +77,7 @@ constructor( @@ -77,7 +77,7 @@ constructor(
77 } 77 }
78 78
79 internal var transceiver: RtpTransceiver? = null 79 internal var transceiver: RtpTransceiver? = null
80 - private val sender: RtpSender? 80 + internal val sender: RtpSender?
81 get() = transceiver?.sender 81 get() = transceiver?.sender
82 82
83 private val closeableManager = CloseableManager() 83 private val closeableManager = CloseableManager()
@@ -17,5 +17,6 @@ @@ -17,5 +17,6 @@
17 package io.livekit.android.room.track 17 package io.livekit.android.room.track
18 18
19 import org.webrtc.AudioTrack 19 import org.webrtc.AudioTrack
  20 +import org.webrtc.RtpReceiver
20 21
21 -class RemoteAudioTrack(name: String, rtcTrack: AudioTrack) : io.livekit.android.room.track.AudioTrack(name, rtcTrack) 22 +class RemoteAudioTrack(name: String, rtcTrack: AudioTrack, internal val receiver: RtpReceiver) : io.livekit.android.room.track.AudioTrack(name, rtcTrack)
@@ -24,6 +24,8 @@ import io.livekit.android.room.track.video.VideoSinkVisibility @@ -24,6 +24,8 @@ import io.livekit.android.room.track.video.VideoSinkVisibility
24 import io.livekit.android.room.track.video.ViewVisibility 24 import io.livekit.android.room.track.video.ViewVisibility
25 import io.livekit.android.util.LKLog 25 import io.livekit.android.util.LKLog
26 import kotlinx.coroutines.* 26 import kotlinx.coroutines.*
  27 +import org.webrtc.RtpReceiver
  28 +import org.webrtc.RtpTransceiver
27 import org.webrtc.VideoSink 29 import org.webrtc.VideoSink
28 import javax.inject.Named 30 import javax.inject.Named
29 import kotlin.math.max 31 import kotlin.math.max
@@ -34,6 +36,7 @@ class RemoteVideoTrack( @@ -34,6 +36,7 @@ class RemoteVideoTrack(
34 val autoManageVideo: Boolean = false, 36 val autoManageVideo: Boolean = false,
35 @Named(InjectionNames.DISPATCHER_DEFAULT) 37 @Named(InjectionNames.DISPATCHER_DEFAULT)
36 private val dispatcher: CoroutineDispatcher, 38 private val dispatcher: CoroutineDispatcher,
  39 + receiver: RtpReceiver,
37 ) : VideoTrack(name, rtcTrack) { 40 ) : VideoTrack(name, rtcTrack) {
38 41
39 private var coroutineScope = CoroutineScope(dispatcher + SupervisorJob()) 42 private var coroutineScope = CoroutineScope(dispatcher + SupervisorJob())
@@ -45,6 +48,12 @@ class RemoteVideoTrack( @@ -45,6 +48,12 @@ class RemoteVideoTrack(
45 internal var lastDimensions: Dimensions = Dimensions(0, 0) 48 internal var lastDimensions: Dimensions = Dimensions(0, 0)
46 private set 49 private set
47 50
  51 + internal var receiver: RtpReceiver
  52 +
  53 + init {
  54 + this.receiver = receiver
  55 + }
  56 +
48 /** 57 /**
49 * If `autoManageVideo` is enabled, a VideoSinkVisibility should be passed, using 58 * If `autoManageVideo` is enabled, a VideoSinkVisibility should be passed, using
50 * [ViewVisibility] if using a traditional View layout, or [ComposeVisibility] 59 * [ViewVisibility] if using a traditional View layout, or [ComposeVisibility]
@@ -39,6 +39,11 @@ open class TrackPublication( @@ -39,6 +39,11 @@ open class TrackPublication(
39 var kind: Track.Kind 39 var kind: Track.Kind
40 private set 40 private set
41 41
  42 + open val encryptionType: LivekitModels.Encryption.Type
  43 + get() {
  44 + return trackInfo?.encryption ?: LivekitModels.Encryption.Type.NONE
  45 + }
  46 +
42 @FlowObservable 47 @FlowObservable
43 @get:FlowObservable 48 @get:FlowObservable
44 open var muted: Boolean by flowDelegate(false) 49 open var muted: Boolean by flowDelegate(false)
@@ -19,6 +19,7 @@ package io.livekit.android.room.track @@ -19,6 +19,7 @@ package io.livekit.android.room.track
19 import io.livekit.android.BaseTest 19 import io.livekit.android.BaseTest
20 import io.livekit.android.events.EventCollector 20 import io.livekit.android.events.EventCollector
21 import io.livekit.android.events.TrackEvent 21 import io.livekit.android.events.TrackEvent
  22 +import io.livekit.android.mock.MockRtpReceiver
22 import io.livekit.android.mock.MockVideoStreamTrack 23 import io.livekit.android.mock.MockVideoStreamTrack
23 import io.livekit.android.room.track.video.VideoSinkVisibility 24 import io.livekit.android.room.track.video.VideoSinkVisibility
24 import kotlinx.coroutines.ExperimentalCoroutinesApi 25 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -39,7 +40,8 @@ class RemoteVideoTrackTest : BaseTest() { @@ -39,7 +40,8 @@ class RemoteVideoTrackTest : BaseTest() {
39 name = "track", 40 name = "track",
40 rtcTrack = MockVideoStreamTrack(), 41 rtcTrack = MockVideoStreamTrack(),
41 autoManageVideo = true, 42 autoManageVideo = true,
42 - dispatcher = coroutineRule.dispatcher 43 + dispatcher = coroutineRule.dispatcher,
  44 + receiver = MockRtpReceiver.create()
43 ) 45 )
44 } 46 }
45 47
1 -Subproject commit ac74d1e920384ac3972b6017d1514ca983450f0d 1 +Subproject commit 519c96683da8b98f214d42810c1608ddb794cf2e
@@ -13,6 +13,7 @@ import io.livekit.android.LiveKit @@ -13,6 +13,7 @@ import io.livekit.android.LiveKit
13 import io.livekit.android.LiveKitOverrides 13 import io.livekit.android.LiveKitOverrides
14 import io.livekit.android.RoomOptions 14 import io.livekit.android.RoomOptions
15 import io.livekit.android.audio.AudioSwitchHandler 15 import io.livekit.android.audio.AudioSwitchHandler
  16 +import io.livekit.android.e2ee.E2EEOptions
16 import io.livekit.android.events.RoomEvent 17 import io.livekit.android.events.RoomEvent
17 import io.livekit.android.events.collect 18 import io.livekit.android.events.collect
18 import io.livekit.android.room.Room 19 import io.livekit.android.room.Room
@@ -37,10 +38,23 @@ import kotlinx.coroutines.launch @@ -37,10 +38,23 @@ import kotlinx.coroutines.launch
37 class CallViewModel( 38 class CallViewModel(
38 val url: String, 39 val url: String,
39 val token: String, 40 val token: String,
40 - application: Application 41 + application: Application,
  42 + val e2ee: Boolean = false,
  43 + val e2eeKey: String? = "",
41 ) : AndroidViewModel(application) { 44 ) : AndroidViewModel(application) {
42 -  
43 val audioHandler = AudioSwitchHandler(application) 45 val audioHandler = AudioSwitchHandler(application)
  46 +
  47 + private fun getE2EEOptions(): E2EEOptions? {
  48 + var e2eeOptions: E2EEOptions? = null
  49 + if(e2ee && e2eeKey != null) {
  50 + e2eeOptions = E2EEOptions()
  51 + }
  52 + e2eeOptions?.keyProvider?.setKey(e2eeKey!!, null, 0)
  53 + return e2eeOptions
  54 + }
  55 +
  56 +
  57 +
44 val room = LiveKit.create( 58 val room = LiveKit.create(
45 appContext = application, 59 appContext = application,
46 options = RoomOptions(adaptiveStream = true, dynacast = true), 60 options = RoomOptions(adaptiveStream = true, dynacast = true),
@@ -162,6 +176,7 @@ class CallViewModel( @@ -162,6 +176,7 @@ class CallViewModel(
162 room.connect( 176 room.connect(
163 url = url, 177 url = url,
164 token = token, 178 token = token,
  179 + roomOptions = RoomOptions(e2eeOptions = getE2EEOptions())
165 ) 180 )
166 181
167 // Create and publish audio/video tracks 182 // Create and publish audio/video tracks
@@ -12,6 +12,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { @@ -12,6 +12,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
12 12
13 fun getSavedUrl() = preferences.getString(PREFERENCES_KEY_URL, URL) as String 13 fun getSavedUrl() = preferences.getString(PREFERENCES_KEY_URL, URL) as String
14 fun getSavedToken() = preferences.getString(PREFERENCES_KEY_TOKEN, TOKEN) as String 14 fun getSavedToken() = preferences.getString(PREFERENCES_KEY_TOKEN, TOKEN) as String
  15 + fun getE2EEOptionsOn() = preferences.getBoolean(PREFERENCES_KEY_E2EE_ON, false)
  16 + fun getSavedE2EEKey() = preferences.getString(PREFERENCES_KEY_E2EE_KEY, "12345678") as String
15 17
16 fun setSavedUrl(url: String) { 18 fun setSavedUrl(url: String) {
17 preferences.edit { 19 preferences.edit {
@@ -25,6 +27,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { @@ -25,6 +27,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
25 } 27 }
26 } 28 }
27 29
  30 + fun setSavedE2EEOn(yesno: Boolean) {
  31 + preferences.edit {
  32 + putBoolean(PREFERENCES_KEY_E2EE_ON, yesno)
  33 + }
  34 + }
  35 +
  36 + fun setSavedE2EEKey(key: String) {
  37 + preferences.edit {
  38 + putString(PREFERENCES_KEY_E2EE_KEY, key)
  39 + }
  40 + }
  41 +
28 fun reset() { 42 fun reset() {
29 preferences.edit { clear() } 43 preferences.edit { clear() }
30 } 44 }
@@ -32,6 +46,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { @@ -32,6 +46,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
32 companion object { 46 companion object {
33 private const val PREFERENCES_KEY_URL = "url" 47 private const val PREFERENCES_KEY_URL = "url"
34 private const val PREFERENCES_KEY_TOKEN = "token" 48 private const val PREFERENCES_KEY_TOKEN = "token"
  49 + private const val PREFERENCES_KEY_E2EE_ON = "enable_e2ee"
  50 + private const val PREFERENCES_KEY_E2EE_KEY = "e2ee_key"
35 51
36 const val URL = BuildConfig.DEFAULT_URL 52 const val URL = BuildConfig.DEFAULT_URL
37 const val TOKEN = BuildConfig.DEFAULT_TOKEN 53 const val TOKEN = BuildConfig.DEFAULT_TOKEN
@@ -13,6 +13,7 @@ import androidx.appcompat.app.AppCompatActivity @@ -13,6 +13,7 @@ import androidx.appcompat.app.AppCompatActivity
13 import androidx.lifecycle.lifecycleScope 13 import androidx.lifecycle.lifecycleScope
14 import androidx.recyclerview.widget.LinearLayoutManager 14 import androidx.recyclerview.widget.LinearLayoutManager
15 import com.xwray.groupie.GroupieAdapter 15 import com.xwray.groupie.GroupieAdapter
  16 +import io.livekit.android.e2ee.E2EEOptions
16 import io.livekit.android.sample.databinding.CallActivityBinding 17 import io.livekit.android.sample.databinding.CallActivityBinding
17 import io.livekit.android.sample.dialog.showDebugMenuDialog 18 import io.livekit.android.sample.dialog.showDebugMenuDialog
18 import io.livekit.android.sample.dialog.showSelectAudioDeviceDialog 19 import io.livekit.android.sample.dialog.showSelectAudioDeviceDialog
@@ -24,7 +25,7 @@ class CallActivity : AppCompatActivity() { @@ -24,7 +25,7 @@ class CallActivity : AppCompatActivity() {
24 val viewModel: CallViewModel by viewModelByFactory { 25 val viewModel: CallViewModel by viewModelByFactory {
25 val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS) 26 val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS)
26 ?: throw NullPointerException("args is null!") 27 ?: throw NullPointerException("args is null!")
27 - CallViewModel(args.url, args.token, application) 28 + CallViewModel(args.url, args.token, application, args.e2ee, args.e2eeKey)
28 } 29 }
29 lateinit var binding: CallActivityBinding 30 lateinit var binding: CallActivityBinding
30 private val screenCaptureIntentLauncher = 31 private val screenCaptureIntentLauncher =
@@ -176,5 +177,5 @@ class CallActivity : AppCompatActivity() { @@ -176,5 +177,5 @@ class CallActivity : AppCompatActivity() {
176 } 177 }
177 178
178 @Parcelize 179 @Parcelize
179 - data class BundleArgs(val url: String, val token: String) : Parcelable 180 + data class BundleArgs(val url: String, val token: String, val e2ee: Boolean, val e2eeKey: String) : Parcelable
180 } 181 }
@@ -25,16 +25,22 @@ class MainActivity : AppCompatActivity() { @@ -25,16 +25,22 @@ class MainActivity : AppCompatActivity() {
25 25
26 val urlString = viewModel.getSavedUrl() 26 val urlString = viewModel.getSavedUrl()
27 val tokenString = viewModel.getSavedToken() 27 val tokenString = viewModel.getSavedToken()
  28 + val e2EEOn = viewModel.getE2EEOptionsOn()
  29 + val e2EEKey = viewModel.getSavedE2EEKey()
28 binding.run { 30 binding.run {
29 url.editText?.text = SpannableStringBuilder(urlString) 31 url.editText?.text = SpannableStringBuilder(urlString)
30 token.editText?.text = SpannableStringBuilder(tokenString) 32 token.editText?.text = SpannableStringBuilder(tokenString)
  33 + e2eeEnabled.isChecked = e2EEOn
  34 + e2eeKey.editText?.text = SpannableStringBuilder(e2EEKey)
31 connectButton.setOnClickListener { 35 connectButton.setOnClickListener {
32 val intent = Intent(this@MainActivity, CallActivity::class.java).apply { 36 val intent = Intent(this@MainActivity, CallActivity::class.java).apply {
33 putExtra( 37 putExtra(
34 CallActivity.KEY_ARGS, 38 CallActivity.KEY_ARGS,
35 CallActivity.BundleArgs( 39 CallActivity.BundleArgs(
36 url.editText?.text.toString(), 40 url.editText?.text.toString(),
37 - token.editText?.text.toString() 41 + token.editText?.text.toString(),
  42 + e2eeEnabled.isChecked,
  43 + e2eeKey.editText?.text.toString()
38 ) 44 )
39 ) 45 )
40 } 46 }
@@ -46,6 +52,8 @@ class MainActivity : AppCompatActivity() { @@ -46,6 +52,8 @@ class MainActivity : AppCompatActivity() {
46 52
47 viewModel.setSavedUrl(url.editText?.text?.toString() ?: "") 53 viewModel.setSavedUrl(url.editText?.text?.toString() ?: "")
48 viewModel.setSavedToken(token.editText?.text?.toString() ?: "") 54 viewModel.setSavedToken(token.editText?.text?.toString() ?: "")
  55 + viewModel.setSavedE2EEOn(e2eeEnabled.isChecked)
  56 + viewModel.setSavedE2EEKey(e2eeKey.editText?.text?.toString() ?: "")
49 57
50 Toast.makeText( 58 Toast.makeText(
51 this@MainActivity, 59 this@MainActivity,
@@ -58,6 +66,8 @@ class MainActivity : AppCompatActivity() { @@ -58,6 +66,8 @@ class MainActivity : AppCompatActivity() {
58 viewModel.reset() 66 viewModel.reset()
59 url.editText?.text = SpannableStringBuilder(MainViewModel.URL) 67 url.editText?.text = SpannableStringBuilder(MainViewModel.URL)
60 token.editText?.text = SpannableStringBuilder(MainViewModel.TOKEN) 68 token.editText?.text = SpannableStringBuilder(MainViewModel.TOKEN)
  69 + e2eeEnabled.isChecked = false
  70 + e2eeKey.editText?.text = SpannableStringBuilder("")
61 71
62 Toast.makeText( 72 Toast.makeText(
63 this@MainActivity, 73 this@MainActivity,
@@ -38,6 +38,25 @@ @@ -38,6 +38,25 @@
38 38
39 </com.google.android.material.textfield.TextInputLayout> 39 </com.google.android.material.textfield.TextInputLayout>
40 40
  41 + <com.google.android.material.textfield.TextInputLayout
  42 + android:id="@+id/e2ee_key"
  43 + android:layout_width="match_parent"
  44 + android:layout_height="wrap_content"
  45 + android:layout_marginTop="20dp"
  46 + android:hint="@string/e2ee_key_str">
  47 +
  48 + <com.google.android.material.textfield.TextInputEditText
  49 + android:layout_width="match_parent"
  50 + android:layout_height="wrap_content" />
  51 +
  52 + </com.google.android.material.textfield.TextInputLayout>
  53 +
  54 + <Switch
  55 + android:id="@+id/e2ee_enabled"
  56 + android:layout_width="match_parent"
  57 + android:layout_height="wrap_content"
  58 + android:text="@string/e2ee_enabled_str" />
  59 +
41 <Button 60 <Button
42 android:id="@+id/connect_button" 61 android:id="@+id/connect_button"
43 android:layout_width="match_parent" 62 android:layout_width="match_parent"
@@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
2 <string name="app_name">livekit-android</string> 2 <string name="app_name">livekit-android</string>
3 <string name="connect">Connect</string> 3 <string name="connect">Connect</string>
4 <string name="token">Token</string> 4 <string name="token">Token</string>
  5 + <string name="e2ee_key_str">E2EE Key</string>
  6 + <string name="e2ee_enabled_str">Enable E2EE</string>
5 <string name="url">URL</string> 7 <string name="url">URL</string>
6 <string name="save_values">Save Values</string> 8 <string name="save_values">Save Values</string>
7 <string name="reset_values">Reset Values</string> 9 <string name="reset_values">Reset Values</string>