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 |
| @@ -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> |
-
请 注册 或 登录 后发表评论