davidliu
Committed by GitHub

Handle reconnect response (#202)

* Update protos

* Handle reconnect response
... ... @@ -11,7 +11,7 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.18'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.19'
classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.30.0"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
... ...
... ... @@ -30,7 +30,15 @@ android {
}
sourceSets {
main.java.srcDirs += "${protobuf.generatedFilesBaseDir}/main/javalite"
main {
proto {
srcDir generated.protoSrc
exclude '*/*.proto' // only use top-level protos.
}
java {
srcDir "${protobuf.generatedFilesBaseDir}/main/javalite"
}
}
}
testOptions {
... ... @@ -112,7 +120,6 @@ dokkaHtml {
}
dependencies {
protobuf files(generated.protoSrc)
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation deps.coroutines.lib
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0'
... ...
... ... @@ -17,9 +17,9 @@ data class ConnectOptions(
/**
* A user-provided RTCConfiguration to override options.
*
* Note: LiveKit requires [PeerConnection.SdpSemantics.UNIFIED_PLAN]
* and a mutable list should be provided for iceServers constructor.
* */
* Note: LiveKit requires [PeerConnection.SdpSemantics.UNIFIED_PLAN] and
* [PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY].
*/
val rtcConfig: PeerConnection.RTCConfiguration? = null,
/**
* capture and publish audio track on connect, defaults to false
... ...
... ... @@ -15,6 +15,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.webrtc.*
import org.webrtc.PeerConnection.RTCConfiguration
import javax.inject.Named
/**
... ... @@ -137,6 +138,10 @@ constructor(
peerConnection.close()
}
fun updateRTCConfig(config: RTCConfiguration) {
peerConnection.setConfiguration(config)
}
@AssistedFactory
interface Factory {
fun create(
... ...
... ... @@ -15,6 +15,7 @@ import io.livekit.android.room.util.setLocalDescription
import io.livekit.android.util.CloseableCoroutineScope
import io.livekit.android.util.Either
import io.livekit.android.util.LKLog
import io.livekit.android.webrtc.copy
import io.livekit.android.webrtc.isConnected
import io.livekit.android.webrtc.isDisconnected
import io.livekit.android.webrtc.toProtoSessionDescription
... ... @@ -22,7 +23,10 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import livekit.LivekitModels
import livekit.LivekitRtc
import livekit.LivekitRtc.JoinResponse
import livekit.LivekitRtc.ReconnectResponse
import org.webrtc.*
import org.webrtc.PeerConnection.RTCConfiguration
import java.net.ConnectException
import java.nio.ByteBuffer
import javax.inject.Inject
... ... @@ -127,7 +131,7 @@ internal constructor(
token: String,
options: ConnectOptions,
roomOptions: RoomOptions
): LivekitRtc.JoinResponse {
): JoinResponse {
coroutineScope.close()
coroutineScope = CloseableCoroutineScope(SupervisorJob() + ioDispatcher)
sessionUrl = url
... ... @@ -142,7 +146,7 @@ internal constructor(
token: String,
options: ConnectOptions,
roomOptions: RoomOptions
): LivekitRtc.JoinResponse {
): JoinResponse {
val joinResponse = client.join(url, token, options, roomOptions)
listener?.onJoinResponse(joinResponse)
isClosed = false
... ... @@ -160,7 +164,7 @@ internal constructor(
return joinResponse
}
private fun configure(joinResponse: LivekitRtc.JoinResponse, connectOptions: ConnectOptions?) {
private fun configure(joinResponse: JoinResponse, connectOptions: ConnectOptions) {
if (_publisher != null && _subscriber != null) {
// already configured
return
... ... @@ -172,60 +176,8 @@ internal constructor(
null
}
// update ICE servers before creating PeerConnection
val iceServers = run {
val servers = mutableListOf<PeerConnection.IceServer>()
for (serverInfo in joinResponse.iceServersList) {
val username = serverInfo.username ?: ""
val credential = serverInfo.credential ?: ""
servers.add(
PeerConnection.IceServer
.builder(serverInfo.urlsList)
.setUsername(username)
.setPassword(credential)
.createIceServer()
)
}
if (servers.isEmpty()) {
servers.addAll(SignalClient.DEFAULT_ICE_SERVERS)
}
servers
}
// Setup peer connections
val rtcConfig = connectOptions?.rtcConfig?.apply {
val mergedServers = this.iceServers
if (connectOptions.iceServers != null) {
connectOptions.iceServers.forEach { server ->
if (!mergedServers.contains(server)) {
mergedServers.add(server)
}
}
}
// Only use server-provided servers if user doesn't provide any.
if (mergedServers.isEmpty()) {
iceServers.forEach { server ->
if (!mergedServers.contains(server)) {
mergedServers.add(server)
}
}
}
}
?: PeerConnection.RTCConfiguration(iceServers).apply {
sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
continualGatheringPolicy =
PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY
}
if (joinResponse.hasClientConfiguration()) {
val clientConfig = joinResponse.clientConfiguration
if (clientConfig.forceRelay == LivekitModels.ClientConfigSetting.ENABLED) {
rtcConfig.iceTransportsType = PeerConnection.IceTransportsType.RELAY
}
}
val rtcConfig = makeRTCConfig(Either.Left(joinResponse), connectOptions)
_publisher?.close()
_publisher = pctFactory.create(
... ... @@ -398,12 +350,13 @@ internal constructor(
ReconnectType.FORCE_FULL_RECONNECT -> true
}
val connectOptions = connectOptions ?: ConnectOptions()
if (isFullReconnect) {
LKLog.v { "Attempting full reconnect." }
try {
closeResources("Full Reconnecting")
listener?.onFullReconnecting()
joinImpl(url, token, connectOptions ?: ConnectOptions(), lastRoomOptions ?: RoomOptions())
joinImpl(url, token, connectOptions, lastRoomOptions ?: RoomOptions())
} catch (e: Exception) {
LKLog.w(e) { "Error during reconnection." }
// reconnect failed, retry.
... ... @@ -413,8 +366,13 @@ internal constructor(
LKLog.v { "Attempting soft reconnect." }
subscriber.prepareForIceRestart()
try {
client.reconnect(url, token, participantSid)
// no join response for regular reconnects
val response = client.reconnect(url, token, participantSid)
if (response is Either.Left) {
val reconnectResponse = response.value
val rtcConfig = makeRTCConfig(Either.Right(reconnectResponse), connectOptions)
_subscriber?.updateRTCConfig(rtcConfig)
_publisher?.updateRTCConfig(rtcConfig)
}
client.onReadyForResponses()
} catch (e: Exception) {
LKLog.w(e) { "Error during reconnection." }
... ... @@ -573,13 +531,87 @@ internal constructor(
}
}
private fun makeRTCConfig(
serverResponse: Either<JoinResponse, ReconnectResponse>,
connectOptions: ConnectOptions
): RTCConfiguration {
// Convert protobuf ice servers
val serverIceServers = run {
val servers = mutableListOf<PeerConnection.IceServer>()
val responseServers = when (serverResponse) {
is Either.Left -> serverResponse.value.iceServersList
is Either.Right -> serverResponse.value.iceServersList
}
for (serverInfo in responseServers) {
servers.add(serverInfo.toWebrtc())
}
if (servers.isEmpty()) {
servers.addAll(SignalClient.DEFAULT_ICE_SERVERS)
}
servers
}
val rtcConfig = connectOptions.rtcConfig?.copy()?.apply {
val mergedServers = iceServers.toMutableList()
if (connectOptions.iceServers != null) {
connectOptions.iceServers.forEach { server ->
if (!mergedServers.contains(server)) {
mergedServers.add(server)
}
}
}
// Only use server-provided servers if user doesn't provide any.
if (mergedServers.isEmpty()) {
iceServers.forEach { server ->
if (!mergedServers.contains(server)) {
mergedServers.add(server)
}
}
}
iceServers = mergedServers
}
?: RTCConfiguration(serverIceServers).apply {
sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
continualGatheringPolicy =
PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY
}
val clientConfig = when (serverResponse) {
is Either.Left -> {
if (serverResponse.value.hasClientConfiguration()) {
serverResponse.value.clientConfiguration
} else {
null
}
}
is Either.Right -> {
if (serverResponse.value.hasClientConfiguration()) {
serverResponse.value.clientConfiguration
} else {
null
}
}
}
if (clientConfig != null) {
if (clientConfig.forceRelay == LivekitModels.ClientConfigSetting.ENABLED) {
rtcConfig.iceTransportsType = PeerConnection.IceTransportsType.RELAY
}
}
return rtcConfig
}
internal interface Listener {
fun onEngineConnected()
fun onEngineReconnected()
fun onEngineReconnecting()
fun onEngineDisconnected(reason: DisconnectReason)
fun onFailToConnect(error: Throwable)
fun onJoinResponse(response: LivekitRtc.JoinResponse)
fun onJoinResponse(response: JoinResponse)
fun onAddTrack(track: MediaStreamTrack, streams: Array<out MediaStream>)
fun onUpdateParticipants(updates: List<LivekitModels.ParticipantInfo>)
fun onActiveSpeakersUpdate(speakers: List<LivekitModels.SpeakerInfo>)
... ... @@ -838,3 +870,10 @@ enum class ReconnectType {
FORCE_SOFT_RECONNECT,
FORCE_FULL_RECONNECT;
}
internal fun LivekitRtc.ICEServer.toWebrtc() = PeerConnection.IceServer.builder(urlsList)
.setUsername(username ?: "")
.setPassword(credential ?: "")
.setTlsAlpnProtocols(emptyList())
.setTlsEllipticCurves(emptyList())
.createIceServer()
\ No newline at end of file
... ...
... ... @@ -19,6 +19,8 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import livekit.LivekitModels
import livekit.LivekitRtc
import livekit.LivekitRtc.JoinResponse
import livekit.LivekitRtc.ReconnectResponse
import okhttp3.*
import okio.ByteString
import okio.ByteString.Companion.toByteString
... ... @@ -56,7 +58,12 @@ constructor(
private var lastOptions: ConnectOptions? = null
private var lastRoomOptions: RoomOptions? = null
private var joinContinuation: CancellableContinuation<Either<LivekitRtc.JoinResponse, Unit>>? = null
// join will always return a JoinResponse.
// reconnect will return a ReconnectResponse or a Unit if a different response was received.
private var joinContinuation: CancellableContinuation<
Either<
JoinResponse,
Either<ReconnectResponse, Unit>>>? = null
private lateinit var coroutineScope: CloseableCoroutineScope
private val requestFlowJobLock = Object()
... ... @@ -80,7 +87,7 @@ constructor(
token: String,
options: ConnectOptions = ConnectOptions(),
roomOptions: RoomOptions = RoomOptions(),
): LivekitRtc.JoinResponse {
): JoinResponse {
val joinResponse = connect(url, token, options, roomOptions)
return (joinResponse as Either.Left).value
}
... ... @@ -88,8 +95,8 @@ constructor(
/**
* @throws Exception if fails to connect.
*/
suspend fun reconnect(url: String, token: String, participantSid: String?) {
connect(
suspend fun reconnect(url: String, token: String, participantSid: String?): Either<ReconnectResponse, Unit> {
val reconnectResponse = connect(
url,
token,
(lastOptions ?: ConnectOptions()).copy()
... ... @@ -99,14 +106,15 @@ constructor(
},
lastRoomOptions ?: RoomOptions()
)
return (reconnectResponse as Either.Right).value
}
suspend fun connect(
private suspend fun connect(
url: String,
token: String,
options: ConnectOptions,
roomOptions: RoomOptions
): Either<LivekitRtc.JoinResponse, Unit> {
): Either<JoinResponse, Either<ReconnectResponse, Unit>> {
// Clean up any pre-existing connection.
close(reason = "Starting new connection")
... ... @@ -210,18 +218,6 @@ constructor(
}
//--------------------------------- WebSocket Listener --------------------------------------//
override fun onOpen(webSocket: WebSocket, response: Response) {
if (isReconnecting) {
// no need to wait for join response on reconnection.
isReconnecting = false
isConnected = true
joinContinuation?.resumeWith(Result.success(Either.Right(Unit)))
// Restart ping job with old settings.
startPingJob()
}
response.body?.close()
}
override fun onMessage(webSocket: WebSocket, text: String) {
LKLog.w { "received JSON message, unsupported in this version." }
}
... ... @@ -484,7 +480,9 @@ constructor(
LKLog.v { "response: $response" }
if (!isConnected) {
// Only handle joins if not connected.
var shouldProcessMessage = false
// Only handle certain messages if not connected.
if (response.hasJoin()) {
isConnected = true
startRequestQueue()
... ... @@ -500,11 +498,29 @@ constructor(
} else if (response.hasLeave()) {
// Some reconnects may immediately send leave back without a join response first.
handleSignalResponseImpl(response)
} else if (isReconnecting) {
// When reconnecting, any message received means signal reconnected.
// Newer servers will send a reconnect response first
isReconnecting = false
isConnected = true
// Restart ping job with old settings.
startPingJob()
if (response.hasReconnect()) {
joinContinuation?.resumeWith(Result.success(Either.Right(Either.Left(response.reconnect))))
} else {
joinContinuation?.resumeWith(Result.success(Either.Right(Either.Right(Unit))))
// Non-reconnect response, handle normally
shouldProcessMessage = true
}
} else {
LKLog.e { "Received response while not connected. $response" }
}
if (!shouldProcessMessage) {
return
}
}
responseFlow.tryEmit(response)
}
... ... @@ -715,6 +731,7 @@ constructor(
}
}
@Suppress("EnumEntryName", "unused")
enum class ProtocolVersion(val value: Int) {
v1(1),
v2(2),
... ...
package io.livekit.android.webrtc
import org.webrtc.PeerConnection
import org.webrtc.PeerConnection.RTCConfiguration
/**
* Completed state is a valid state for a connected connection, so this should be used
... ... @@ -25,3 +26,55 @@ internal fun PeerConnection.PeerConnectionState.isDisconnected(): Boolean {
else -> false
}
}
internal fun RTCConfiguration.copy(): RTCConfiguration {
val newConfig = RTCConfiguration(emptyList())
newConfig.copyFrom(this)
return newConfig
}
internal fun RTCConfiguration.copyFrom(config: RTCConfiguration) {
iceTransportsType = config.iceTransportsType
iceServers = config.iceServers
bundlePolicy = config.bundlePolicy
certificate = config.certificate
rtcpMuxPolicy = config.rtcpMuxPolicy
tcpCandidatePolicy = config.tcpCandidatePolicy
candidateNetworkPolicy = config.candidateNetworkPolicy
audioJitterBufferMaxPackets = config.audioJitterBufferMaxPackets
audioJitterBufferFastAccelerate = config.audioJitterBufferFastAccelerate
iceConnectionReceivingTimeout = config.iceConnectionReceivingTimeout
iceBackupCandidatePairPingInterval = config.iceBackupCandidatePairPingInterval
keyType = config.keyType
continualGatheringPolicy = config.continualGatheringPolicy
iceCandidatePoolSize = config.iceCandidatePoolSize
pruneTurnPorts = config.pruneTurnPorts
turnPortPrunePolicy = config.turnPortPrunePolicy
presumeWritableWhenFullyRelayed = config.presumeWritableWhenFullyRelayed
surfaceIceCandidatesOnIceTransportTypeChanged = config.surfaceIceCandidatesOnIceTransportTypeChanged
iceCheckIntervalStrongConnectivityMs = config.iceCheckIntervalStrongConnectivityMs
iceCheckIntervalWeakConnectivityMs = config.iceCheckIntervalWeakConnectivityMs
iceCheckMinInterval = config.iceCheckMinInterval
iceUnwritableTimeMs = config.iceUnwritableTimeMs
iceUnwritableMinChecks = config.iceUnwritableMinChecks
stunCandidateKeepaliveIntervalMs = config.stunCandidateKeepaliveIntervalMs
stableWritableConnectionPingIntervalMs = config.stableWritableConnectionPingIntervalMs
disableIPv6OnWifi = config.disableIPv6OnWifi
maxIPv6Networks = config.maxIPv6Networks
disableIpv6 = config.disableIpv6
enableDscp = config.enableDscp
enableCpuOveruseDetection = config.enableCpuOveruseDetection
suspendBelowMinBitrate = config.suspendBelowMinBitrate
screencastMinBitrate = config.screencastMinBitrate
combinedAudioVideoBwe = config.combinedAudioVideoBwe
networkPreference = config.networkPreference
sdpSemantics = config.sdpSemantics
turnCustomizer = config.turnCustomizer
activeResetSrtpParams = config.activeResetSrtpParams
allowCodecSwitching = config.allowCodecSwitching
cryptoOptions = config.cryptoOptions
turnLoggingId = config.turnLoggingId
enableImplicitRollback = config.enableImplicitRollback
offerExtmapAllowMixed = config.offerExtmapAllowMixed
}
\ No newline at end of file
... ...
... ... @@ -7,7 +7,7 @@ private class MockNativePeerConnectionFactory : NativePeerConnectionFactory {
}
class MockPeerConnection(
val rtcConfig: RTCConfiguration,
var rtcConfig: RTCConfiguration,
val observer: Observer?
) : PeerConnection(MockNativePeerConnectionFactory()) {
... ... @@ -51,7 +51,8 @@ class MockPeerConnection(
override fun setAudioRecording(recording: Boolean) {
}
override fun setConfiguration(config: RTCConfiguration?): Boolean {
override fun setConfiguration(config: RTCConfiguration): Boolean {
this.rtcConfig = config
return true
}
... ...
... ... @@ -28,6 +28,16 @@ class RTCEngineMockE2ETest : MockE2ETest() {
}
@Test
fun iceServersSetOnJoin() = runTest {
connect()
val sentIceServers = SignalClientTest.JOIN.join.iceServersList
.map { it.toWebrtc() }
val subPeerConnection = rtcEngine.subscriber.peerConnection as MockPeerConnection
assertEquals(sentIceServers, subPeerConnection.rtcConfig.iceServers)
}
@Test
fun iceSubscriberConnect() = runTest {
connect()
assertEquals(
... ... @@ -111,8 +121,8 @@ class RTCEngineMockE2ETest : MockE2ETest() {
build()
})
val pubPeerConnection = rtcEngine.subscriber.peerConnection as MockPeerConnection
assertEquals(PeerConnection.IceTransportsType.RELAY, pubPeerConnection.rtcConfig.iceTransportsType)
val subPeerConnection = rtcEngine.subscriber.peerConnection as MockPeerConnection
assertEquals(PeerConnection.IceTransportsType.RELAY, subPeerConnection.rtcConfig.iceTransportsType)
}
fun participantIdOnReconnect() = runTest {
... ...
package io.livekit.android.room
import io.livekit.android.MockE2ETest
import io.livekit.android.assert.assertIsClassList
import io.livekit.android.events.EventCollector
import io.livekit.android.events.FlowCollector
import io.livekit.android.events.RoomEvent
import io.livekit.android.mock.MockAudioStreamTrack
import io.livekit.android.mock.MockPeerConnection
import io.livekit.android.room.track.LocalAudioTrack
import io.livekit.android.util.flow
import io.livekit.android.util.toPBByteString
import junit.framework.Assert.assertEquals
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import livekit.LivekitRtc
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.webrtc.PeerConnection
/**
* For tests that only target one reconnection type.
... ... @@ -37,6 +34,8 @@ class RoomReconnectionMockE2ETest : MockE2ETest() {
if (softReconnectParam == 0) {
simulateMessageFromServer(SignalClientTest.JOIN)
} else {
simulateMessageFromServer(SignalClientTest.RECONNECT)
}
}
}
... ... @@ -66,6 +65,27 @@ class RoomReconnectionMockE2ETest : MockE2ETest() {
}
@Test
fun softReconnectConfiguration() = runTest {
room.setReconnectionType(ReconnectType.FORCE_SOFT_RECONNECT)
connect()
prepareForReconnect()
disconnectPeerConnection()
// Wait so that the reconnect job properly starts first.
testScheduler.advanceTimeBy(1000)
connectPeerConnection()
val rtcEngine = component.rtcEngine()
val rtcConfig = (rtcEngine.subscriber.peerConnection as MockPeerConnection).rtcConfig
assertEquals(PeerConnection.IceTransportsType.RELAY, rtcConfig.iceTransportsType)
val sentIceServers = SignalClientTest.RECONNECT.reconnect.iceServersList
.map { server -> server.toWebrtc() }
assertEquals(sentIceServers, rtcConfig.iceServers)
}
@Test
fun fullReconnectRepublishesTracks() = runTest {
room.setReconnectionType(ReconnectType.FORCE_FULL_RECONNECT)
connect()
... ...
... ... @@ -39,6 +39,8 @@ class RoomReconnectionTypesMockE2ETest(
if (softReconnectParam == 0) {
simulateMessageFromServer(SignalClientTest.JOIN)
} else {
simulateMessageFromServer(SignalClientTest.RECONNECT)
}
}
}
... ...
... ... @@ -11,7 +11,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.serialization.json.Json
import livekit.LivekitModels
import livekit.LivekitModels.ClientConfiguration
import livekit.LivekitRtc
import livekit.LivekitRtc.ICEServer
import okhttp3.*
import org.junit.Assert.*
import org.junit.Before
... ... @@ -86,6 +88,7 @@ class SignalClientTest : BaseTest() {
}
client.onOpen(wsFactory.ws, createOpenResponse(wsFactory.request))
client.onMessage(wsFactory.ws, RECONNECT.toOkioByteString())
job.await()
assertEquals(true, client.isConnected)
... ... @@ -199,6 +202,7 @@ class SignalClientTest : BaseTest() {
val job = async { client.reconnect(EXAMPLE_URL, "", "participant_sid") }
client.onOpen(wsFactory.ws, createOpenResponse(wsFactory.request))
client.onMessage(wsFactory.ws, RECONNECT.toOkioByteString())
job.await()
val ws = wsFactory.ws
... ... @@ -291,12 +295,35 @@ class SignalClientTest : BaseTest() {
}
participant = TestData.LOCAL_PARTICIPANT
subscriberPrimary = true
addIceServers(with(ICEServer.newBuilder()) {
addUrls("stun:stun.join.com:19302")
username = "username"
credential = "credential"
build()
})
serverVersion = "0.15.2"
build()
}
build()
}
val RECONNECT = with(LivekitRtc.SignalResponse.newBuilder()) {
reconnect = with(LivekitRtc.ReconnectResponse.newBuilder()) {
addIceServers(with(ICEServer.newBuilder()) {
addUrls("stun:stun.reconnect.com:19302")
username = "username"
credential = "credential"
build()
})
clientConfiguration = with(ClientConfiguration.newBuilder()) {
forceRelay = LivekitModels.ClientConfigSetting.ENABLED
build()
}
build()
}
build()
}
val OFFER = with(LivekitRtc.SignalResponse.newBuilder()) {
offer = with(LivekitRtc.SessionDescription.newBuilder()) {
sdp = "remote_offer"
... ...
package io.livekit.android.webrtc
import io.livekit.android.BaseTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mockito.Mockito
import org.webrtc.PeerConnection.RTCConfiguration
class RTCConfigurationTest : BaseTest() {
@Test
fun copyTest() {
val originalConfig = RTCConfiguration(mutableListOf())
fillWithMockData(originalConfig)
val newConfig = originalConfig.copy()
newConfig::class.java
.declaredFields
.forEach { field ->
assertEquals("Failed on ${field.name}", field.get(originalConfig), field.get(newConfig))
}
}
// Test to make sure the copy test is actually checking properly
@Test
fun copyFailureCheckTest() {
val originalConfig = RTCConfiguration(mutableListOf())
fillWithMockData(originalConfig)
val newConfig = originalConfig.copy()
newConfig.activeResetSrtpParams = false
var caughtError = false
try {
newConfig::class.java
.declaredFields
.forEach { field ->
assertEquals("Failed on ${field.name}", field.get(originalConfig), field.get(newConfig))
}
} catch (e: java.lang.AssertionError) {
// Error expected
caughtError = true
}
assertTrue(caughtError)
}
private fun fillWithMockData(config: RTCConfiguration) {
config::class.java
.declaredFields
.forEach { field ->
// Ignore iceServers.
if (field.name == "iceServers") {
return@forEach
}
val value = field.get(config)
val newValue = if (value == null) {
when (field.type) {
Byte::class.javaObjectType -> 1.toByte()
Short::class.javaObjectType -> 1.toShort()
Int::class.javaObjectType -> 1
Long::class.javaObjectType -> 1.toLong()
Float::class.javaObjectType -> 1.toFloat()
Double::class.javaObjectType -> 1.toDouble()
Boolean::class.javaObjectType -> true
Char::class.javaObjectType -> 1.toChar()
String::class.javaObjectType -> "mock string"
else -> Mockito.mock(field.type)
}
} else {
when (value::class.javaObjectType) {
Byte::class.javaObjectType -> ((value as Byte) + 1).toByte()
Short::class.javaObjectType -> ((value as Short) + 1).toShort()
Int::class.javaObjectType -> (value as Int) + 1
Long::class.javaObjectType -> (value as Long) + 1
Float::class.javaObjectType -> (value as Float) + 1
Double::class.javaObjectType -> (value as Double) + 1
Boolean::class.javaObjectType -> !(value as Boolean)
Char::class.javaObjectType -> (value as Char) + 1
String::class.javaObjectType -> "mock string"
else -> Mockito.mock(field.type)
}
}
field.set(config, newValue)
}
}
}
\ No newline at end of file
... ...
Subproject commit ec5189c9fd40d61cbc0b0ca43da6c71e7284ebcb
Subproject commit a1819deeabe143b1af1bf375d84e52b152f784ac
... ...