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 @@
package io.livekit.android
import io.livekit.android.e2ee.E2EEOptions
import io.livekit.android.room.Room
import io.livekit.android.room.participant.AudioTrackPublishDefaults
import io.livekit.android.room.participant.VideoTrackPublishDefaults
... ... @@ -33,6 +34,11 @@ data class RoomOptions(
*/
val dynacast: Boolean = false,
/**
* Options for end-to-end encryption.
*/
var e2eeOptions: E2EEOptions? = null,
val audioTrackCaptureDefaults: LocalAudioTrackOptions? = null,
val videoTrackCaptureDefaults: LocalVideoTrackOptions? = null,
val audioTrackPublishDefaults: AudioTrackPublishDefaults? = null,
... ...
package io.livekit.android.e2ee
import io.livekit.android.events.RoomEvent
import io.livekit.android.room.Room
import io.livekit.android.room.participant.*
import io.livekit.android.room.track.LocalAudioTrack
import io.livekit.android.room.track.LocalVideoTrack
import io.livekit.android.room.track.RemoteAudioTrack
import io.livekit.android.room.track.RemoteVideoTrack
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.TrackPublication
import io.livekit.android.util.LKLog
import org.webrtc.FrameCryptor
import org.webrtc.FrameCryptor.FrameCryptionState
import org.webrtc.FrameCryptorAlgorithm
import org.webrtc.FrameCryptorFactory
import org.webrtc.RtpReceiver
import org.webrtc.RtpSender
class E2EEManager
constructor(keyProvider: KeyProvider) {
private var room: Room? = null
private var keyProvider: KeyProvider
private var frameCryptors = mutableMapOf<String, FrameCryptor>()
private var algorithm: FrameCryptorAlgorithm = FrameCryptorAlgorithm.AES_GCM
private lateinit var emitEvent: (roomEvent: RoomEvent) -> Unit?
var enabled: Boolean = false
init {
this.keyProvider = keyProvider
}
suspend fun setup(room: Room, emitEvent: (roomEvent: RoomEvent) -> Unit) {
if (this.room != room) {
// E2EEManager already setup, clean up first
cleanUp()
}
this.enabled = true
this.room = room
this.emitEvent = emitEvent
this.room?.localParticipant?.tracks?.forEach() { item ->
var participant = this.room!!.localParticipant
var publication = item.value
if (publication.track != null) {
addPublishedTrack(publication.track!!, publication, participant, room)
}
}
this.room?.remoteParticipants?.forEach() { item ->
var participant = item.value
participant.tracks.forEach() { item ->
var publication = item.value
if (publication.track != null) {
addSubscribedTrack(publication.track!!, publication, participant, room)
}
}
}
}
public fun addSubscribedTrack(track: Track, publication: TrackPublication, participant: RemoteParticipant, room: Room) {
var trackId = publication.sid
var participantId = participant.sid
var rtpReceiver: RtpReceiver? = when (publication.track!!) {
is RemoteAudioTrack -> (publication.track!! as RemoteAudioTrack).receiver
is RemoteVideoTrack -> (publication.track!! as RemoteVideoTrack).receiver
else -> {
throw IllegalArgumentException("unsupported track type")
}
}
var frameCryptor = addRtpReceiver(rtpReceiver!!, participantId, trackId, publication.track!!.kind.name.lowercase())
frameCryptor.setObserver { trackId, state ->
LKLog.i { "Receiver::onFrameCryptionStateChanged: $trackId, state: $state" }
emitEvent(
RoomEvent.TrackE2EEStateEvent(
room!!, publication.track!!, publication,
participant,
state = e2eeStateFromFrameCryptoState(state)
)
)
}
}
public fun addPublishedTrack(track: Track, publication: TrackPublication, participant: LocalParticipant, room: Room) {
var trackId = publication.sid
var participantId = participant.sid
var rtpSender: RtpSender? = when (publication.track!!) {
is LocalAudioTrack -> (publication.track!! as LocalAudioTrack)?.sender
is LocalVideoTrack -> (publication.track!! as LocalVideoTrack)?.sender
else -> {
throw IllegalArgumentException("unsupported track type")
}
} ?: throw IllegalArgumentException("rtpSender is null")
var frameCryptor = addRtpSender(rtpSender!!, participantId, trackId, publication.track!!.kind.name.lowercase())
frameCryptor.setObserver { trackId, state ->
LKLog.i { "Sender::onFrameCryptionStateChanged: $trackId, state: $state" }
emitEvent(
RoomEvent.TrackE2EEStateEvent(
room!!, publication.track!!, publication,
participant,
state = e2eeStateFromFrameCryptoState(state)
)
)
}
}
private fun e2eeStateFromFrameCryptoState(state: FrameCryptionState?): E2EEState {
return when (state) {
FrameCryptionState.NEW -> E2EEState.NEW
FrameCryptionState.OK -> E2EEState.OK
FrameCryptionState.KEYRATCHETED -> E2EEState.KEY_RATCHETED
FrameCryptionState.MISSINGKEY -> E2EEState.MISSING_KEY
FrameCryptionState.ENCRYPTIONFAILED -> E2EEState.ENCRYPTION_FAILED
FrameCryptionState.DECRYPTIONFAILED -> E2EEState.DECRYPTION_FAILED
FrameCryptionState.INTERNALERROR -> E2EEState.INTERNAL_ERROR
else -> { E2EEState.INTERNAL_ERROR}
}
}
private fun addRtpSender(sender: RtpSender, participantId: String, trackId: String , kind: String): FrameCryptor {
var pid = "$kind-sender-$participantId-$trackId"
var frameCryptor = FrameCryptorFactory.createFrameCryptorForRtpSender(
sender, pid, algorithm, keyProvider.rtcKeyProvider)
frameCryptors[trackId] = frameCryptor
frameCryptor.setEnabled(enabled)
if(keyProvider.enableSharedKey) {
keyProvider.rtcKeyProvider?.setKey(pid, 0, keyProvider?.sharedKey)
frameCryptor.setKeyIndex(0)
}
return frameCryptor
}
private fun addRtpReceiver(receiver: RtpReceiver, participantId: String, trackId: String, kind: String): FrameCryptor {
var pid = "$kind-receiver-$participantId-$trackId"
var frameCryptor = FrameCryptorFactory.createFrameCryptorForRtpReceiver(
receiver, pid, algorithm, keyProvider.rtcKeyProvider)
frameCryptors[trackId] = frameCryptor
frameCryptor.setEnabled(enabled)
if(keyProvider.enableSharedKey) {
keyProvider.rtcKeyProvider?.setKey(pid, 0, keyProvider?.sharedKey)
frameCryptor.setKeyIndex(0)
}
return frameCryptor
}
/**
* Enable or disable E2EE
* @param enabled
*/
public fun enableE2EE(enabled: Boolean) {
this.enabled = enabled
for (item in frameCryptors.entries) {
var frameCryptor = item.value
var participantId = item.key
frameCryptor.setEnabled(enabled)
if(keyProvider.enableSharedKey) {
keyProvider.rtcKeyProvider?.setKey(participantId, 0, keyProvider?.sharedKey)
frameCryptor.setKeyIndex(0)
}
}
}
/**
* Ratchet key for local participant
*/
fun ratchetKey() {
for (participantId in frameCryptors.keys) {
var newKey = keyProvider.rtcKeyProvider?.ratchetKey(participantId, 0)
LKLog.d{ "ratchetKey: newKey: $newKey" }
}
}
fun cleanUp() {
for (frameCryptor in frameCryptors.values) {
frameCryptor.dispose()
}
frameCryptors.clear()
}
}
... ...
package io.livekit.android.e2ee
import livekit.LivekitModels.Encryption
var defaultRatchetSalt = "LKFrameEncryptionKey"
var defaultMagicBytes = "LK-ROCKS"
var defaultRatchetWindowSize = 16
class E2EEOptions
constructor(keyProvider: KeyProvider = BaseKeyProvider(
defaultRatchetSalt,
defaultMagicBytes,
defaultRatchetWindowSize,
true,
), encryptionType: Encryption.Type = Encryption.Type.GCM) {
var keyProvider: KeyProvider
var encryptionType: Encryption.Type = Encryption.Type.NONE
init {
this.keyProvider = keyProvider
this.encryptionType = encryptionType
}
}
... ...
package io.livekit.android.e2ee
enum class E2EEState {
NEW, // initial state
OK, // encryption or decryption succeeded
KEY_RATCHETED, // key ratcheted
MISSING_KEY, // missing key
ENCRYPTION_FAILED, // encryption failed
DECRYPTION_FAILED, // decryption failed
INTERNAL_ERROR // internal error
}
... ...
package io.livekit.android.e2ee
import io.livekit.android.util.LKLog
import org.webrtc.FrameCryptorFactory
import org.webrtc.FrameCryptorKeyProvider
class KeyInfo
constructor(var participantId: String, var keyIndex: Int, var key: String ) {
override fun toString(): String {
return "KeyInfo(participantId='$participantId', keyIndex=$keyIndex)"
}
}
public interface KeyProvider {
fun setKey(key: String, participantId: String?, keyIndex: Int? = 0)
fun ratchetKey(participantId: String, index: Int): ByteArray
val rtcKeyProvider: FrameCryptorKeyProvider
var sharedKey: ByteArray?
var enableSharedKey: Boolean
}
class BaseKeyProvider
constructor(private var ratchetSalt: String, private var uncryptedMagicBytes: String, private var ratchetWindowSize: Int, override var enableSharedKey: Boolean = true) :
KeyProvider {
override var sharedKey: ByteArray? = null
private var keys: MutableMap<String, MutableMap<Int, String>> = mutableMapOf()
/**
* Set a key for a participant
* @param key
* @param participantId
* @param keyIndex
*/
override fun setKey(key: String, participantId: String?, keyIndex: Int?) {
if (enableSharedKey) {
sharedKey = key.toByteArray()
return
}
if(participantId == null) {
LKLog.d{ "Please provide valid participantId for non-SharedKey mode." }
return
}
var keyInfo = KeyInfo(participantId, keyIndex ?: 0, key)
if (!keys.containsKey(keyInfo.participantId)) {
keys[keyInfo.participantId] = mutableMapOf()
}
keys[keyInfo.participantId]!![keyInfo.keyIndex] = keyInfo.key
rtcKeyProvider.setKey(participantId, keyInfo.keyIndex, key.toByteArray())
}
override fun ratchetKey(participantId: String, index: Int): ByteArray {
return rtcKeyProvider.ratchetKey(participantId, index)
}
override val rtcKeyProvider: FrameCryptorKeyProvider
init {
this.ratchetSalt = ratchetSalt
this.uncryptedMagicBytes = uncryptedMagicBytes
this.ratchetWindowSize = ratchetWindowSize
this.enableSharedKey = enableSharedKey
this.rtcKeyProvider = FrameCryptorFactory.createFrameCryptorKeyProvider(
enableSharedKey,
ratchetSalt.toByteArray(),
ratchetWindowSize,
uncryptedMagicBytes.toByteArray(),
)
}
}
... ...
... ... @@ -16,6 +16,7 @@
package io.livekit.android.events
import io.livekit.android.e2ee.E2EEState
import io.livekit.android.room.Room
import io.livekit.android.room.participant.*
import io.livekit.android.room.track.LocalTrackPublication
... ... @@ -197,6 +198,17 @@ sealed class RoomEvent(val room: Room) : Event() {
* The recording of a room has started/stopped.
*/
class RecordingStatusChanged(room: Room, isRecording: Boolean) : RoomEvent(room)
/**
* The E2EE state of a track has changed.
*/
class TrackE2EEStateEvent(
room: Room,
val track: Track,
val publication: TrackPublication,
val participant: Participant,
var state: E2EEState
) : RoomEvent(room)
}
enum class DisconnectReason {
... ...
... ... @@ -21,6 +21,7 @@ import com.google.protobuf.ByteString
import io.livekit.android.ConnectOptions
import io.livekit.android.RoomOptions
import io.livekit.android.dagger.InjectionNames
import io.livekit.android.e2ee.E2EEOptions
import io.livekit.android.events.DisconnectReason
import io.livekit.android.events.convert
import io.livekit.android.room.participant.ParticipantTrackPermission
... ... @@ -38,6 +39,7 @@ import io.livekit.android.webrtc.toProtoSessionDescription
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import livekit.LivekitModels
import livekit.LivekitModels.Encryption
import livekit.LivekitRtc
import livekit.LivekitRtc.JoinResponse
import livekit.LivekitRtc.ReconnectResponse
... ... @@ -271,7 +273,6 @@ internal constructor(
if (pendingTrackResolvers[cid] != null) {
throw TrackException.DuplicateTrackException("Track with same ID $cid has already been published!")
}
// Suspend until signal client receives message confirming track publication.
return suspendCoroutine { cont ->
pendingTrackResolvers[cid] = cont
... ...
... ... @@ -32,6 +32,7 @@ import io.livekit.android.RoomOptions
import io.livekit.android.Version
import io.livekit.android.audio.AudioHandler
import io.livekit.android.dagger.InjectionNames
import io.livekit.android.e2ee.E2EEManager
import io.livekit.android.events.*
import io.livekit.android.memory.CloseableManager
import io.livekit.android.renderer.TextureViewRenderer
... ... @@ -131,6 +132,11 @@ constructor(
private set
/**
* end-to-end encryption manager
*/
var e2eeManager: E2EEManager? = null
/**
* Automatically manage quality of subscribed video tracks, subscribe to the
* an appropriate resolution based on the size of the video elements that tracks
* are attached to.
... ... @@ -199,9 +205,10 @@ constructor(
videoTrackCaptureDefaults = videoTrackCaptureDefaults,
audioTrackPublishDefaults = audioTrackPublishDefaults,
videoTrackPublishDefaults = videoTrackPublishDefaults,
e2eeOptions = null,
)
suspend fun connect(url: String, token: String, options: ConnectOptions = ConnectOptions()) {
suspend fun connect(url: String, token: String, options: ConnectOptions = ConnectOptions(), roomOptions: RoomOptions = getCurrentRoomOptions()) {
if (this::coroutineScope.isInitialized) {
coroutineScope.cancel()
}
... ... @@ -259,7 +266,17 @@ constructor(
state = State.CONNECTING
connectOptions = options
engine.join(url, token, options, getCurrentRoomOptions())
if(roomOptions.e2eeOptions != null) {
e2eeManager = E2EEManager(roomOptions!!.e2eeOptions!!.keyProvider)
e2eeManager!!.setup(this, {event ->
coroutineScope.launch {
emitWhenConnected(event)
}
})
}
engine.join(url, token, options, roomOptions)
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkRequest = NetworkRequest.Builder()
... ... @@ -510,6 +527,7 @@ constructor(
* Removes all participants and tracks from the room.
*/
private fun cleanupRoom() {
e2eeManager?.cleanUp()
localParticipant.cleanup()
remoteParticipants.keys.toMutableSet() // copy keys to avoid concurrent modifications.
.forEach { sid -> handleParticipantDisconnect(sid) }
... ... @@ -684,7 +702,8 @@ constructor(
track,
trackSid!!,
autoManageVideo = adaptiveStream,
statsGetter = statsGetter
statsGetter = statsGetter,
receiver = receiver
)
}
... ... @@ -907,6 +926,16 @@ constructor(
/**
* @suppress
*/
override fun onTrackPublished(publication: LocalTrackPublication, participant: LocalParticipant) {
listener?.onTrackPublished(publication, participant, this)
if(e2eeManager != null) {
e2eeManager!!.addPublishedTrack(publication.track!!, publication, participant, this)
}
eventBus.postEvent(RoomEvent.TrackPublished(this, publication, participant), coroutineScope)
}
/**
* @suppress
*/
override fun onTrackUnpublished(publication: LocalTrackPublication, participant: LocalParticipant) {
listener?.onTrackUnpublished(publication, participant, this)
eventBus.postEvent(RoomEvent.TrackUnpublished(this, publication, participant), coroutineScope)
... ... @@ -917,6 +946,9 @@ constructor(
*/
override fun onTrackSubscribed(track: Track, publication: RemoteTrackPublication, participant: RemoteParticipant) {
listener?.onTrackSubscribed(track, publication, participant, this)
if(e2eeManager != null) {
e2eeManager!!.addSubscribedTrack(track, publication, participant, this)
}
eventBus.postEvent(RoomEvent.TrackSubscribed(this, track, publication, participant), coroutineScope)
}
... ...
... ... @@ -34,6 +34,7 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import livekit.LivekitModels
import livekit.LivekitModels.Encryption
import livekit.LivekitRtc
import livekit.LivekitRtc.JoinResponse
import livekit.LivekitRtc.ReconnectResponse
... ... @@ -376,10 +377,12 @@ constructor(
type: LivekitModels.TrackType,
builder: LivekitRtc.AddTrackRequest.Builder = LivekitRtc.AddTrackRequest.newBuilder()
) {
var encryptionType = lastRoomOptions?.e2eeOptions?.encryptionType ?: LivekitModels.Encryption.Type.NONE
val addTrackRequest = builder
.setCid(cid)
.setName(name)
.setType(type)
.setEncryption(encryptionType)
val request = LivekitRtc.SignalRequest.newBuilder()
.setAddTrack(addTrackRequest)
.build()
... ... @@ -650,6 +653,10 @@ constructor(
// TODO
}
LivekitRtc.SignalResponse.MessageCase.SUBSCRIPTION_RESPONSE -> {
// TODO
}
LivekitRtc.SignalResponse.MessageCase.MESSAGE_NOT_SET,
null -> {
LKLog.v { "empty messageCase!" }
... ...
... ... @@ -25,6 +25,7 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.livekit.android.dagger.CapabilitiesGetter
import io.livekit.android.dagger.InjectionNames
import io.livekit.android.e2ee.E2EEOptions
import io.livekit.android.events.ParticipantEvent
import io.livekit.android.room.ConnectionState
import io.livekit.android.room.DefaultsManager
... ...
... ... @@ -30,6 +30,8 @@ import livekit.LivekitModels
import livekit.LivekitRtc
import org.webrtc.AudioTrack
import org.webrtc.MediaStreamTrack
import org.webrtc.RtpReceiver
import org.webrtc.RtpTransceiver
import org.webrtc.VideoTrack
class RemoteParticipant(
... ... @@ -114,8 +116,9 @@ class RemoteParticipant(
mediaTrack: MediaStreamTrack,
sid: String,
statsGetter: RTCStatsGetter,
receiver: RtpReceiver,
autoManageVideo: Boolean = false,
triesLeft: Int = 20
triesLeft: Int = 20,
) {
val publication = getTrackPublication(sid)
... ... @@ -132,19 +135,20 @@ class RemoteParticipant(
} else {
coroutineScope.launch {
delay(150)
addSubscribedMediaTrack(mediaTrack, sid, statsGetter, autoManageVideo, triesLeft - 1)
addSubscribedMediaTrack(mediaTrack, sid, statsGetter,receiver = receiver, autoManageVideo, triesLeft - 1)
}
}
return
}
val track: Track = when (val kind = mediaTrack.kind()) {
KIND_AUDIO -> RemoteAudioTrack(rtcTrack = mediaTrack as AudioTrack, name = "")
KIND_AUDIO -> RemoteAudioTrack(rtcTrack = mediaTrack as AudioTrack, name = "", receiver = receiver)
KIND_VIDEO -> RemoteVideoTrack(
rtcTrack = mediaTrack as VideoTrack,
name = "",
autoManageVideo = autoManageVideo,
dispatcher = ioDispatcher
dispatcher = ioDispatcher,
receiver = receiver
)
else -> throw TrackException.InvalidTrackTypeException("invalid track type: $kind")
... ... @@ -156,6 +160,7 @@ class RemoteParticipant(
publication.subscriptionAllowed = true
track.name = publication.name
track.sid = publication.sid
addTrackPublication(publication)
track.start()
... ...
... ... @@ -42,7 +42,7 @@ class LocalAudioTrack(
}
internal var transceiver: RtpTransceiver? = null
private val sender: RtpSender?
internal val sender: RtpSender?
get() = transceiver?.sender
companion object {
... ...
... ... @@ -77,7 +77,7 @@ constructor(
}
internal var transceiver: RtpTransceiver? = null
private val sender: RtpSender?
internal val sender: RtpSender?
get() = transceiver?.sender
private val closeableManager = CloseableManager()
... ...
... ... @@ -17,5 +17,6 @@
package io.livekit.android.room.track
import org.webrtc.AudioTrack
import org.webrtc.RtpReceiver
class RemoteAudioTrack(name: String, rtcTrack: AudioTrack) : io.livekit.android.room.track.AudioTrack(name, rtcTrack)
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
import io.livekit.android.room.track.video.ViewVisibility
import io.livekit.android.util.LKLog
import kotlinx.coroutines.*
import org.webrtc.RtpReceiver
import org.webrtc.RtpTransceiver
import org.webrtc.VideoSink
import javax.inject.Named
import kotlin.math.max
... ... @@ -34,6 +36,7 @@ class RemoteVideoTrack(
val autoManageVideo: Boolean = false,
@Named(InjectionNames.DISPATCHER_DEFAULT)
private val dispatcher: CoroutineDispatcher,
receiver: RtpReceiver,
) : VideoTrack(name, rtcTrack) {
private var coroutineScope = CoroutineScope(dispatcher + SupervisorJob())
... ... @@ -45,6 +48,12 @@ class RemoteVideoTrack(
internal var lastDimensions: Dimensions = Dimensions(0, 0)
private set
internal var receiver: RtpReceiver
init {
this.receiver = receiver
}
/**
* If `autoManageVideo` is enabled, a VideoSinkVisibility should be passed, using
* [ViewVisibility] if using a traditional View layout, or [ComposeVisibility]
... ...
... ... @@ -39,6 +39,11 @@ open class TrackPublication(
var kind: Track.Kind
private set
open val encryptionType: LivekitModels.Encryption.Type
get() {
return trackInfo?.encryption ?: LivekitModels.Encryption.Type.NONE
}
@FlowObservable
@get:FlowObservable
open var muted: Boolean by flowDelegate(false)
... ...
... ... @@ -19,6 +19,7 @@ package io.livekit.android.room.track
import io.livekit.android.BaseTest
import io.livekit.android.events.EventCollector
import io.livekit.android.events.TrackEvent
import io.livekit.android.mock.MockRtpReceiver
import io.livekit.android.mock.MockVideoStreamTrack
import io.livekit.android.room.track.video.VideoSinkVisibility
import kotlinx.coroutines.ExperimentalCoroutinesApi
... ... @@ -39,7 +40,8 @@ class RemoteVideoTrackTest : BaseTest() {
name = "track",
rtcTrack = MockVideoStreamTrack(),
autoManageVideo = true,
dispatcher = coroutineRule.dispatcher
dispatcher = coroutineRule.dispatcher,
receiver = MockRtpReceiver.create()
)
}
... ...
Subproject commit ac74d1e920384ac3972b6017d1514ca983450f0d
Subproject commit 519c96683da8b98f214d42810c1608ddb794cf2e
... ...
... ... @@ -13,6 +13,7 @@ import io.livekit.android.LiveKit
import io.livekit.android.LiveKitOverrides
import io.livekit.android.RoomOptions
import io.livekit.android.audio.AudioSwitchHandler
import io.livekit.android.e2ee.E2EEOptions
import io.livekit.android.events.RoomEvent
import io.livekit.android.events.collect
import io.livekit.android.room.Room
... ... @@ -37,10 +38,23 @@ import kotlinx.coroutines.launch
class CallViewModel(
val url: String,
val token: String,
application: Application
application: Application,
val e2ee: Boolean = false,
val e2eeKey: String? = "",
) : AndroidViewModel(application) {
val audioHandler = AudioSwitchHandler(application)
private fun getE2EEOptions(): E2EEOptions? {
var e2eeOptions: E2EEOptions? = null
if(e2ee && e2eeKey != null) {
e2eeOptions = E2EEOptions()
}
e2eeOptions?.keyProvider?.setKey(e2eeKey!!, null, 0)
return e2eeOptions
}
val room = LiveKit.create(
appContext = application,
options = RoomOptions(adaptiveStream = true, dynacast = true),
... ... @@ -162,6 +176,7 @@ class CallViewModel(
room.connect(
url = url,
token = token,
roomOptions = RoomOptions(e2eeOptions = getE2EEOptions())
)
// Create and publish audio/video tracks
... ...
... ... @@ -12,6 +12,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun getSavedUrl() = preferences.getString(PREFERENCES_KEY_URL, URL) as String
fun getSavedToken() = preferences.getString(PREFERENCES_KEY_TOKEN, TOKEN) as String
fun getE2EEOptionsOn() = preferences.getBoolean(PREFERENCES_KEY_E2EE_ON, false)
fun getSavedE2EEKey() = preferences.getString(PREFERENCES_KEY_E2EE_KEY, "12345678") as String
fun setSavedUrl(url: String) {
preferences.edit {
... ... @@ -25,6 +27,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
}
fun setSavedE2EEOn(yesno: Boolean) {
preferences.edit {
putBoolean(PREFERENCES_KEY_E2EE_ON, yesno)
}
}
fun setSavedE2EEKey(key: String) {
preferences.edit {
putString(PREFERENCES_KEY_E2EE_KEY, key)
}
}
fun reset() {
preferences.edit { clear() }
}
... ... @@ -32,6 +46,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
companion object {
private const val PREFERENCES_KEY_URL = "url"
private const val PREFERENCES_KEY_TOKEN = "token"
private const val PREFERENCES_KEY_E2EE_ON = "enable_e2ee"
private const val PREFERENCES_KEY_E2EE_KEY = "e2ee_key"
const val URL = BuildConfig.DEFAULT_URL
const val TOKEN = BuildConfig.DEFAULT_TOKEN
... ...
... ... @@ -13,6 +13,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.xwray.groupie.GroupieAdapter
import io.livekit.android.e2ee.E2EEOptions
import io.livekit.android.sample.databinding.CallActivityBinding
import io.livekit.android.sample.dialog.showDebugMenuDialog
import io.livekit.android.sample.dialog.showSelectAudioDeviceDialog
... ... @@ -24,7 +25,7 @@ class CallActivity : AppCompatActivity() {
val viewModel: CallViewModel by viewModelByFactory {
val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS)
?: throw NullPointerException("args is null!")
CallViewModel(args.url, args.token, application)
CallViewModel(args.url, args.token, application, args.e2ee, args.e2eeKey)
}
lateinit var binding: CallActivityBinding
private val screenCaptureIntentLauncher =
... ... @@ -176,5 +177,5 @@ class CallActivity : AppCompatActivity() {
}
@Parcelize
data class BundleArgs(val url: String, val token: String) : Parcelable
data class BundleArgs(val url: String, val token: String, val e2ee: Boolean, val e2eeKey: String) : Parcelable
}
\ No newline at end of file
... ...
... ... @@ -25,16 +25,22 @@ class MainActivity : AppCompatActivity() {
val urlString = viewModel.getSavedUrl()
val tokenString = viewModel.getSavedToken()
val e2EEOn = viewModel.getE2EEOptionsOn()
val e2EEKey = viewModel.getSavedE2EEKey()
binding.run {
url.editText?.text = SpannableStringBuilder(urlString)
token.editText?.text = SpannableStringBuilder(tokenString)
e2eeEnabled.isChecked = e2EEOn
e2eeKey.editText?.text = SpannableStringBuilder(e2EEKey)
connectButton.setOnClickListener {
val intent = Intent(this@MainActivity, CallActivity::class.java).apply {
putExtra(
CallActivity.KEY_ARGS,
CallActivity.BundleArgs(
url.editText?.text.toString(),
token.editText?.text.toString()
token.editText?.text.toString(),
e2eeEnabled.isChecked,
e2eeKey.editText?.text.toString()
)
)
}
... ... @@ -46,6 +52,8 @@ class MainActivity : AppCompatActivity() {
viewModel.setSavedUrl(url.editText?.text?.toString() ?: "")
viewModel.setSavedToken(token.editText?.text?.toString() ?: "")
viewModel.setSavedE2EEOn(e2eeEnabled.isChecked)
viewModel.setSavedE2EEKey(e2eeKey.editText?.text?.toString() ?: "")
Toast.makeText(
this@MainActivity,
... ... @@ -58,6 +66,8 @@ class MainActivity : AppCompatActivity() {
viewModel.reset()
url.editText?.text = SpannableStringBuilder(MainViewModel.URL)
token.editText?.text = SpannableStringBuilder(MainViewModel.TOKEN)
e2eeEnabled.isChecked = false
e2eeKey.editText?.text = SpannableStringBuilder("")
Toast.makeText(
this@MainActivity,
... ...
... ... @@ -38,6 +38,25 @@
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/e2ee_key"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:hint="@string/e2ee_key_str">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<Switch
android:id="@+id/e2ee_enabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/e2ee_enabled_str" />
<Button
android:id="@+id/connect_button"
android:layout_width="match_parent"
... ...
... ... @@ -2,6 +2,8 @@
<string name="app_name">livekit-android</string>
<string name="connect">Connect</string>
<string name="token">Token</string>
<string name="e2ee_key_str">E2EE Key</string>
<string name="e2ee_enabled_str">Enable E2EE</string>
<string name="url">URL</string>
<string name="save_values">Save Values</string>
<string name="reset_values">Reset Values</string>
... ...