davidliu
Committed by GitHub

connection quality api (#20)

* connection quality api

* fix room test

* add in listener for connection quality
... ... @@ -113,10 +113,10 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.6'
testImplementation 'org.mockito:mockito-core:3.8.0'
testImplementation "org.mockito.kotlin:mockito-kotlin:3.1.0"
testImplementation 'org.mockito:mockito-core:4.0.0'
testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
testImplementation 'androidx.test:core:1.4.0'
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.3"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0"
kaptTest 'com.google.dagger:dagger-compiler:2.38'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
... ...
... ... @@ -379,6 +379,7 @@ internal constructor(
fun onActiveSpeakersUpdate(speakers: List<LivekitModels.SpeakerInfo>)
fun onRemoteMuteChanged(trackSid: String, muted: Boolean)
fun onRoomUpdate(update: LivekitModels.Room)
fun onConnectionQuality(updates: List<LivekitRtc.ConnectionQualityInfo>)
fun onSpeakersChanged(speakers: List<LivekitModels.SpeakerInfo>)
fun onDisconnect(reason: String)
fun onFailToConnect(error: Exception)
... ... @@ -507,6 +508,10 @@ internal constructor(
listener?.onRoomUpdate(update)
}
override fun onConnectionQuality(updates: List<LivekitRtc.ConnectionQualityInfo>) {
listener?.onConnectionQuality(updates)
}
override fun onLeave() {
close()
listener?.onDisconnect("")
... ...
... ... @@ -11,18 +11,12 @@ import dagger.assisted.AssistedInject
import io.livekit.android.ConnectOptions
import io.livekit.android.Version
import io.livekit.android.renderer.TextureViewRenderer
import io.livekit.android.room.participant.LocalParticipant
import io.livekit.android.room.participant.Participant
import io.livekit.android.room.participant.ParticipantListener
import io.livekit.android.room.participant.RemoteParticipant
import io.livekit.android.room.participant.*
import io.livekit.android.room.track.*
import io.livekit.android.util.LKLog
import livekit.LivekitModels
import livekit.LivekitRtc
import org.webrtc.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class Room
@AssistedInject
... ... @@ -325,6 +319,20 @@ constructor(
metadata = update.metadata
}
override fun onConnectionQuality(updates: List<LivekitRtc.ConnectionQualityInfo>) {
updates.forEach { info ->
val quality = ConnectionQuality.fromProto(info.quality)
if (info.participantSid == this.localParticipant.sid) {
this.localParticipant.connectionQuality = quality
listener?.onConnectionQualityChanged(localParticipant, quality)
} else {
val participant = remoteParticipants[info.participantSid] ?: return@forEach
participant.connectionQuality = quality
listener?.onConnectionQualityChanged(participant, quality)
}
}
}
/**
* @suppress
*/
... ... @@ -543,6 +551,14 @@ interface RoomListener {
* Received data published by another participant
*/
fun onDataReceived(data: ByteArray, participant: RemoteParticipant, room: Room) {}
/**
* The connection quality for a participant has changed.
*
* @param participant Either a remote participant or [Room.localParticipant]
* @param quality the new connection quality
*/
fun onConnectionQualityChanged(participant: Participant, quality: ConnectionQuality) {}
}
sealed class RoomException(message: String? = null, cause: Throwable? = null) :
... ...
... ... @@ -398,7 +398,7 @@ constructor(
listener?.onRoomUpdate(response.roomUpdate.room)
}
LivekitRtc.SignalResponse.MessageCase.CONNECTION_QUALITY -> {
// TODO: listener?.onConnectionQuality(response.connectionQuality.updatesList)
listener?.onConnectionQuality(response.connectionQuality.updatesList)
}
LivekitRtc.SignalResponse.MessageCase.MESSAGE_NOT_SET,
null -> {
... ... @@ -423,7 +423,7 @@ constructor(
fun onClose(reason: String, code: Int)
fun onRemoteMuteChanged(trackSid: String, muted: Boolean)
fun onRoomUpdate(update: LivekitModels.Room)
// TODO: fun onConnectionQuality(updates: List<LivekitRtc.ConnectionQualityInfo>)
fun onConnectionQuality(updates: List<LivekitRtc.ConnectionQualityInfo>)
fun onLeave()
fun onError(error: Exception)
}
... ...
package io.livekit.android.room.participant
import io.livekit.android.room.track.*
import io.livekit.android.room.track.LocalTrackPublication
import io.livekit.android.room.track.RemoteTrackPublication
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.TrackPublication
import livekit.LivekitModels
open class Participant(var sid: String, identity: String? = null) {
... ... @@ -28,6 +31,8 @@ open class Participant(var sid: String, identity: String? = null) {
internalListener?.onMetadataChanged(this, prevMetadata)
}
}
var connectionQuality: ConnectionQuality = ConnectionQuality.UNKNOWN
internal set
/**
* Listener for when participant properties change
... ... @@ -161,4 +166,22 @@ interface ParticipantListener {
* Received data published by another participant
*/
fun onDataReceived(data: ByteArray, participant: RemoteParticipant) {}
}
enum class ConnectionQuality {
EXCELLENT,
GOOD,
POOR,
UNKNOWN;
companion object {
fun fromProto(proto: LivekitModels.ConnectionQuality): ConnectionQuality {
return when (proto) {
LivekitModels.ConnectionQuality.EXCELLENT -> EXCELLENT
LivekitModels.ConnectionQuality.GOOD -> GOOD
LivekitModels.ConnectionQuality.POOR -> POOR
LivekitModels.ConnectionQuality.UNRECOGNIZED -> UNKNOWN
}
}
}
}
\ No newline at end of file
... ...
package io.livekit.android.coroutines
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
@OptIn(ExperimentalCoroutinesApi::class)
class TestCoroutineRule : TestRule {
val dispatcher = TestCoroutineDispatcher()
val scope = TestCoroutineScope(dispatcher)
override fun apply(base: Statement, description: Description?) = object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
base.evaluate()
scope.cleanupTestCoroutines()
}
}
}
\ No newline at end of file
... ...
... ... @@ -2,11 +2,13 @@ package io.livekit.android.room
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import io.livekit.android.coroutines.TestCoroutineRule
import io.livekit.android.mock.MockWebsocketFactory
import io.livekit.android.mock.dagger.DaggerTestLiveKitComponent
import io.livekit.android.room.participant.ConnectionQuality
import io.livekit.android.util.toOkioByteString
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
... ... @@ -15,6 +17,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.mockito.junit.MockitoJUnit
import org.robolectric.RobolectricTestRunner
... ... @@ -25,6 +28,9 @@ class RoomMockE2ETest {
@get:Rule
var mockitoRule = MockitoJUnit.rule()
@get:Rule
var coroutineRule = TestCoroutineRule()
lateinit var context: Context
lateinit var room: Room
lateinit var wsFactory: MockWebsocketFactory
... ... @@ -41,9 +47,8 @@ class RoomMockE2ETest {
wsFactory = component.websocketFactory()
}
@Test
fun connectTest() {
val job = TestCoroutineScope().launch {
fun connect() {
val job = coroutineRule.scope.launch {
room.connect(
url = "http://www.example.com",
token = "",
... ... @@ -59,24 +64,13 @@ class RoomMockE2ETest {
}
@Test
fun roomUpdateTest() {
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
exception.printStackTrace()
}
val job = TestCoroutineScope().launch(handler) {
room.connect(
url = "http://www.example.com",
token = "",
options = null
)
}
wsFactory.listener.onMessage(wsFactory.ws, SignalClientTest.JOIN.toOkioByteString())
fun connectTest() {
connect()
}
runBlockingTest {
job.join()
}
@Test
fun roomUpdateTest() {
connect()
wsFactory.listener.onMessage(wsFactory.ws, SignalClientTest.ROOM_UPDATE.toOkioByteString())
Assert.assertEquals(
... ... @@ -84,4 +78,23 @@ class RoomMockE2ETest {
room.metadata
)
}
@Test
fun connectionQualityUpdateTest() {
val roomListener = Mockito.mock(RoomListener::class.java)
room.listener = roomListener
connect()
wsFactory.listener.onMessage(
wsFactory.ws,
SignalClientTest.CONNECTION_QUALITY.toOkioByteString()
)
Assert.assertEquals(
ConnectionQuality.EXCELLENT,
room.localParticipant.connectionQuality
)
Mockito.verify(roomListener)
.onConnectionQualityChanged(room.localParticipant, ConnectionQuality.EXCELLENT)
}
}
\ No newline at end of file
... ...
... ... @@ -2,11 +2,11 @@ package io.livekit.android.room
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import io.livekit.android.coroutines.TestCoroutineRule
import io.livekit.android.mock.MockEglBase
import io.livekit.android.room.participant.LocalParticipant
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
import livekit.LivekitModels
import org.junit.Before
... ... @@ -16,6 +16,10 @@ import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.junit.MockitoJUnit
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.stub
import org.robolectric.RobolectricTestRunner
import org.webrtc.EglBase
... ... @@ -25,6 +29,8 @@ class RoomTest {
@get:Rule
var mockitoRule = MockitoJUnit.rule()
@get:Rule
var coroutineRule = TestCoroutineRule()
lateinit var context: Context
... ... @@ -54,14 +60,17 @@ class RoomTest {
@Test
fun connectTest() {
val job = TestCoroutineScope().launch {
rtcEngine.stub {
onBlocking { rtcEngine.join(any(), any(), anyOrNull()) }
.doReturn(SignalClientTest.JOIN.join)
}
val job = coroutineRule.scope.launch {
room.connect(
url = "http://www.example.com",
token = "",
options = null
)
}
room.onIceConnected()
runBlockingTest {
job.join()
}
... ...
... ... @@ -9,6 +9,7 @@ import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.serialization.json.Json
import livekit.LivekitModels
import livekit.LivekitRtc
import okhttp3.*
import org.junit.After
... ... @@ -134,7 +135,12 @@ class SignalClientTest {
join = with(joinBuilder) {
room = with(roomBuilder) {
name = "roomname"
sid = "sid"
sid = "room_sid"
build()
}
participant = with(participantBuilder) {
sid = "participant_sid"
identity = "participant_identity"
build()
}
build()
... ... @@ -161,5 +167,17 @@ class SignalClientTest {
}
build()
}
val CONNECTION_QUALITY = with(LivekitRtc.SignalResponse.newBuilder()) {
connectionQuality = with(connectionQualityBuilder) {
addUpdates(with(LivekitRtc.ConnectionQualityInfo.newBuilder()){
participantSid = JOIN.join.participant.sid
quality = LivekitModels.ConnectionQuality.EXCELLENT
build()
})
build()
}
build()
}
}
}
\ No newline at end of file
... ...