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 { @@ -113,10 +113,10 @@ dependencies {
113 113
114 testImplementation 'junit:junit:4.13.2' 114 testImplementation 'junit:junit:4.13.2'
115 testImplementation 'org.robolectric:robolectric:4.6' 115 testImplementation 'org.robolectric:robolectric:4.6'
116 - testImplementation 'org.mockito:mockito-core:3.8.0'  
117 - testImplementation "org.mockito.kotlin:mockito-kotlin:3.1.0" 116 + testImplementation 'org.mockito:mockito-core:4.0.0'
  117 + testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
118 testImplementation 'androidx.test:core:1.4.0' 118 testImplementation 'androidx.test:core:1.4.0'
119 - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.3" 119 + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0"
120 kaptTest 'com.google.dagger:dagger-compiler:2.38' 120 kaptTest 'com.google.dagger:dagger-compiler:2.38'
121 androidTestImplementation 'androidx.test.ext:junit:1.1.3' 121 androidTestImplementation 'androidx.test.ext:junit:1.1.3'
122 androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 122 androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
@@ -379,6 +379,7 @@ internal constructor( @@ -379,6 +379,7 @@ internal constructor(
379 fun onActiveSpeakersUpdate(speakers: List<LivekitModels.SpeakerInfo>) 379 fun onActiveSpeakersUpdate(speakers: List<LivekitModels.SpeakerInfo>)
380 fun onRemoteMuteChanged(trackSid: String, muted: Boolean) 380 fun onRemoteMuteChanged(trackSid: String, muted: Boolean)
381 fun onRoomUpdate(update: LivekitModels.Room) 381 fun onRoomUpdate(update: LivekitModels.Room)
  382 + fun onConnectionQuality(updates: List<LivekitRtc.ConnectionQualityInfo>)
382 fun onSpeakersChanged(speakers: List<LivekitModels.SpeakerInfo>) 383 fun onSpeakersChanged(speakers: List<LivekitModels.SpeakerInfo>)
383 fun onDisconnect(reason: String) 384 fun onDisconnect(reason: String)
384 fun onFailToConnect(error: Exception) 385 fun onFailToConnect(error: Exception)
@@ -507,6 +508,10 @@ internal constructor( @@ -507,6 +508,10 @@ internal constructor(
507 listener?.onRoomUpdate(update) 508 listener?.onRoomUpdate(update)
508 } 509 }
509 510
  511 + override fun onConnectionQuality(updates: List<LivekitRtc.ConnectionQualityInfo>) {
  512 + listener?.onConnectionQuality(updates)
  513 + }
  514 +
510 override fun onLeave() { 515 override fun onLeave() {
511 close() 516 close()
512 listener?.onDisconnect("") 517 listener?.onDisconnect("")
@@ -11,18 +11,12 @@ import dagger.assisted.AssistedInject @@ -11,18 +11,12 @@ import dagger.assisted.AssistedInject
11 import io.livekit.android.ConnectOptions 11 import io.livekit.android.ConnectOptions
12 import io.livekit.android.Version 12 import io.livekit.android.Version
13 import io.livekit.android.renderer.TextureViewRenderer 13 import io.livekit.android.renderer.TextureViewRenderer
14 -import io.livekit.android.room.participant.LocalParticipant  
15 -import io.livekit.android.room.participant.Participant  
16 -import io.livekit.android.room.participant.ParticipantListener  
17 -import io.livekit.android.room.participant.RemoteParticipant 14 +import io.livekit.android.room.participant.*
18 import io.livekit.android.room.track.* 15 import io.livekit.android.room.track.*
19 import io.livekit.android.util.LKLog 16 import io.livekit.android.util.LKLog
20 import livekit.LivekitModels 17 import livekit.LivekitModels
21 import livekit.LivekitRtc 18 import livekit.LivekitRtc
22 import org.webrtc.* 19 import org.webrtc.*
23 -import kotlin.coroutines.Continuation  
24 -import kotlin.coroutines.resume  
25 -import kotlin.coroutines.suspendCoroutine  
26 20
27 class Room 21 class Room
28 @AssistedInject 22 @AssistedInject
@@ -325,6 +319,20 @@ constructor( @@ -325,6 +319,20 @@ constructor(
325 metadata = update.metadata 319 metadata = update.metadata
326 } 320 }
327 321
  322 + override fun onConnectionQuality(updates: List<LivekitRtc.ConnectionQualityInfo>) {
  323 + updates.forEach { info ->
  324 + val quality = ConnectionQuality.fromProto(info.quality)
  325 + if (info.participantSid == this.localParticipant.sid) {
  326 + this.localParticipant.connectionQuality = quality
  327 + listener?.onConnectionQualityChanged(localParticipant, quality)
  328 + } else {
  329 + val participant = remoteParticipants[info.participantSid] ?: return@forEach
  330 + participant.connectionQuality = quality
  331 + listener?.onConnectionQualityChanged(participant, quality)
  332 + }
  333 + }
  334 + }
  335 +
328 /** 336 /**
329 * @suppress 337 * @suppress
330 */ 338 */
@@ -543,6 +551,14 @@ interface RoomListener { @@ -543,6 +551,14 @@ interface RoomListener {
543 * Received data published by another participant 551 * Received data published by another participant
544 */ 552 */
545 fun onDataReceived(data: ByteArray, participant: RemoteParticipant, room: Room) {} 553 fun onDataReceived(data: ByteArray, participant: RemoteParticipant, room: Room) {}
  554 +
  555 + /**
  556 + * The connection quality for a participant has changed.
  557 + *
  558 + * @param participant Either a remote participant or [Room.localParticipant]
  559 + * @param quality the new connection quality
  560 + */
  561 + fun onConnectionQualityChanged(participant: Participant, quality: ConnectionQuality) {}
546 } 562 }
547 563
548 sealed class RoomException(message: String? = null, cause: Throwable? = null) : 564 sealed class RoomException(message: String? = null, cause: Throwable? = null) :
@@ -398,7 +398,7 @@ constructor( @@ -398,7 +398,7 @@ constructor(
398 listener?.onRoomUpdate(response.roomUpdate.room) 398 listener?.onRoomUpdate(response.roomUpdate.room)
399 } 399 }
400 LivekitRtc.SignalResponse.MessageCase.CONNECTION_QUALITY -> { 400 LivekitRtc.SignalResponse.MessageCase.CONNECTION_QUALITY -> {
401 - // TODO: listener?.onConnectionQuality(response.connectionQuality.updatesList) 401 + listener?.onConnectionQuality(response.connectionQuality.updatesList)
402 } 402 }
403 LivekitRtc.SignalResponse.MessageCase.MESSAGE_NOT_SET, 403 LivekitRtc.SignalResponse.MessageCase.MESSAGE_NOT_SET,
404 null -> { 404 null -> {
@@ -423,7 +423,7 @@ constructor( @@ -423,7 +423,7 @@ constructor(
423 fun onClose(reason: String, code: Int) 423 fun onClose(reason: String, code: Int)
424 fun onRemoteMuteChanged(trackSid: String, muted: Boolean) 424 fun onRemoteMuteChanged(trackSid: String, muted: Boolean)
425 fun onRoomUpdate(update: LivekitModels.Room) 425 fun onRoomUpdate(update: LivekitModels.Room)
426 - // TODO: fun onConnectionQuality(updates: List<LivekitRtc.ConnectionQualityInfo>) 426 + fun onConnectionQuality(updates: List<LivekitRtc.ConnectionQualityInfo>)
427 fun onLeave() 427 fun onLeave()
428 fun onError(error: Exception) 428 fun onError(error: Exception)
429 } 429 }
1 package io.livekit.android.room.participant 1 package io.livekit.android.room.participant
2 2
3 -import io.livekit.android.room.track.* 3 +import io.livekit.android.room.track.LocalTrackPublication
  4 +import io.livekit.android.room.track.RemoteTrackPublication
  5 +import io.livekit.android.room.track.Track
  6 +import io.livekit.android.room.track.TrackPublication
4 import livekit.LivekitModels 7 import livekit.LivekitModels
5 8
6 open class Participant(var sid: String, identity: String? = null) { 9 open class Participant(var sid: String, identity: String? = null) {
@@ -28,6 +31,8 @@ open class Participant(var sid: String, identity: String? = null) { @@ -28,6 +31,8 @@ open class Participant(var sid: String, identity: String? = null) {
28 internalListener?.onMetadataChanged(this, prevMetadata) 31 internalListener?.onMetadataChanged(this, prevMetadata)
29 } 32 }
30 } 33 }
  34 + var connectionQuality: ConnectionQuality = ConnectionQuality.UNKNOWN
  35 + internal set
31 36
32 /** 37 /**
33 * Listener for when participant properties change 38 * Listener for when participant properties change
@@ -161,4 +166,22 @@ interface ParticipantListener { @@ -161,4 +166,22 @@ interface ParticipantListener {
161 * Received data published by another participant 166 * Received data published by another participant
162 */ 167 */
163 fun onDataReceived(data: ByteArray, participant: RemoteParticipant) {} 168 fun onDataReceived(data: ByteArray, participant: RemoteParticipant) {}
  169 +}
  170 +
  171 +enum class ConnectionQuality {
  172 + EXCELLENT,
  173 + GOOD,
  174 + POOR,
  175 + UNKNOWN;
  176 +
  177 + companion object {
  178 + fun fromProto(proto: LivekitModels.ConnectionQuality): ConnectionQuality {
  179 + return when (proto) {
  180 + LivekitModels.ConnectionQuality.EXCELLENT -> EXCELLENT
  181 + LivekitModels.ConnectionQuality.GOOD -> GOOD
  182 + LivekitModels.ConnectionQuality.POOR -> POOR
  183 + LivekitModels.ConnectionQuality.UNRECOGNIZED -> UNKNOWN
  184 + }
  185 + }
  186 + }
164 } 187 }
  1 +package io.livekit.android.coroutines
  2 +
  3 +import kotlinx.coroutines.ExperimentalCoroutinesApi
  4 +import kotlinx.coroutines.test.TestCoroutineDispatcher
  5 +import kotlinx.coroutines.test.TestCoroutineScope
  6 +import org.junit.rules.TestRule
  7 +import org.junit.runner.Description
  8 +import org.junit.runners.model.Statement
  9 +
  10 +@OptIn(ExperimentalCoroutinesApi::class)
  11 +class TestCoroutineRule : TestRule {
  12 + val dispatcher = TestCoroutineDispatcher()
  13 + val scope = TestCoroutineScope(dispatcher)
  14 +
  15 + override fun apply(base: Statement, description: Description?) = object : Statement() {
  16 + @Throws(Throwable::class)
  17 + override fun evaluate() {
  18 + base.evaluate()
  19 + scope.cleanupTestCoroutines()
  20 + }
  21 + }
  22 +}
@@ -2,11 +2,13 @@ package io.livekit.android.room @@ -2,11 +2,13 @@ package io.livekit.android.room
2 2
3 import android.content.Context 3 import android.content.Context
4 import androidx.test.core.app.ApplicationProvider 4 import androidx.test.core.app.ApplicationProvider
  5 +import io.livekit.android.coroutines.TestCoroutineRule
5 import io.livekit.android.mock.MockWebsocketFactory 6 import io.livekit.android.mock.MockWebsocketFactory
6 import io.livekit.android.mock.dagger.DaggerTestLiveKitComponent 7 import io.livekit.android.mock.dagger.DaggerTestLiveKitComponent
  8 +import io.livekit.android.room.participant.ConnectionQuality
7 import io.livekit.android.util.toOkioByteString 9 import io.livekit.android.util.toOkioByteString
8 -import kotlinx.coroutines.CoroutineExceptionHandler  
9 import kotlinx.coroutines.ExperimentalCoroutinesApi 10 import kotlinx.coroutines.ExperimentalCoroutinesApi
  11 +import kotlinx.coroutines.Job
10 import kotlinx.coroutines.launch 12 import kotlinx.coroutines.launch
11 import kotlinx.coroutines.test.TestCoroutineScope 13 import kotlinx.coroutines.test.TestCoroutineScope
12 import kotlinx.coroutines.test.runBlockingTest 14 import kotlinx.coroutines.test.runBlockingTest
@@ -15,6 +17,7 @@ import org.junit.Before @@ -15,6 +17,7 @@ import org.junit.Before
15 import org.junit.Rule 17 import org.junit.Rule
16 import org.junit.Test 18 import org.junit.Test
17 import org.junit.runner.RunWith 19 import org.junit.runner.RunWith
  20 +import org.mockito.Mockito
18 import org.mockito.junit.MockitoJUnit 21 import org.mockito.junit.MockitoJUnit
19 import org.robolectric.RobolectricTestRunner 22 import org.robolectric.RobolectricTestRunner
20 23
@@ -25,6 +28,9 @@ class RoomMockE2ETest { @@ -25,6 +28,9 @@ class RoomMockE2ETest {
25 @get:Rule 28 @get:Rule
26 var mockitoRule = MockitoJUnit.rule() 29 var mockitoRule = MockitoJUnit.rule()
27 30
  31 + @get:Rule
  32 + var coroutineRule = TestCoroutineRule()
  33 +
28 lateinit var context: Context 34 lateinit var context: Context
29 lateinit var room: Room 35 lateinit var room: Room
30 lateinit var wsFactory: MockWebsocketFactory 36 lateinit var wsFactory: MockWebsocketFactory
@@ -41,9 +47,8 @@ class RoomMockE2ETest { @@ -41,9 +47,8 @@ class RoomMockE2ETest {
41 wsFactory = component.websocketFactory() 47 wsFactory = component.websocketFactory()
42 } 48 }
43 49
44 - @Test  
45 - fun connectTest() {  
46 - val job = TestCoroutineScope().launch { 50 + fun connect() {
  51 + val job = coroutineRule.scope.launch {
47 room.connect( 52 room.connect(
48 url = "http://www.example.com", 53 url = "http://www.example.com",
49 token = "", 54 token = "",
@@ -59,24 +64,13 @@ class RoomMockE2ETest { @@ -59,24 +64,13 @@ class RoomMockE2ETest {
59 } 64 }
60 65
61 @Test 66 @Test
62 - fun roomUpdateTest() {  
63 - val handler = CoroutineExceptionHandler { _, exception ->  
64 - println("CoroutineExceptionHandler got $exception")  
65 - exception.printStackTrace()  
66 - }  
67 - val job = TestCoroutineScope().launch(handler) {  
68 - room.connect(  
69 - url = "http://www.example.com",  
70 - token = "",  
71 - options = null  
72 - )  
73 - }  
74 -  
75 - wsFactory.listener.onMessage(wsFactory.ws, SignalClientTest.JOIN.toOkioByteString()) 67 + fun connectTest() {
  68 + connect()
  69 + }
76 70
77 - runBlockingTest {  
78 - job.join()  
79 - } 71 + @Test
  72 + fun roomUpdateTest() {
  73 + connect()
80 wsFactory.listener.onMessage(wsFactory.ws, SignalClientTest.ROOM_UPDATE.toOkioByteString()) 74 wsFactory.listener.onMessage(wsFactory.ws, SignalClientTest.ROOM_UPDATE.toOkioByteString())
81 75
82 Assert.assertEquals( 76 Assert.assertEquals(
@@ -84,4 +78,23 @@ class RoomMockE2ETest { @@ -84,4 +78,23 @@ class RoomMockE2ETest {
84 room.metadata 78 room.metadata
85 ) 79 )
86 } 80 }
  81 +
  82 + @Test
  83 + fun connectionQualityUpdateTest() {
  84 + val roomListener = Mockito.mock(RoomListener::class.java)
  85 + room.listener = roomListener
  86 +
  87 + connect()
  88 + wsFactory.listener.onMessage(
  89 + wsFactory.ws,
  90 + SignalClientTest.CONNECTION_QUALITY.toOkioByteString()
  91 + )
  92 +
  93 + Assert.assertEquals(
  94 + ConnectionQuality.EXCELLENT,
  95 + room.localParticipant.connectionQuality
  96 + )
  97 + Mockito.verify(roomListener)
  98 + .onConnectionQualityChanged(room.localParticipant, ConnectionQuality.EXCELLENT)
  99 + }
87 } 100 }
@@ -2,11 +2,11 @@ package io.livekit.android.room @@ -2,11 +2,11 @@ package io.livekit.android.room
2 2
3 import android.content.Context 3 import android.content.Context
4 import androidx.test.core.app.ApplicationProvider 4 import androidx.test.core.app.ApplicationProvider
  5 +import io.livekit.android.coroutines.TestCoroutineRule
5 import io.livekit.android.mock.MockEglBase 6 import io.livekit.android.mock.MockEglBase
6 import io.livekit.android.room.participant.LocalParticipant 7 import io.livekit.android.room.participant.LocalParticipant
7 import kotlinx.coroutines.ExperimentalCoroutinesApi 8 import kotlinx.coroutines.ExperimentalCoroutinesApi
8 import kotlinx.coroutines.launch 9 import kotlinx.coroutines.launch
9 -import kotlinx.coroutines.test.TestCoroutineScope  
10 import kotlinx.coroutines.test.runBlockingTest 10 import kotlinx.coroutines.test.runBlockingTest
11 import livekit.LivekitModels 11 import livekit.LivekitModels
12 import org.junit.Before 12 import org.junit.Before
@@ -16,6 +16,10 @@ import org.junit.runner.RunWith @@ -16,6 +16,10 @@ import org.junit.runner.RunWith
16 import org.mockito.Mock 16 import org.mockito.Mock
17 import org.mockito.Mockito 17 import org.mockito.Mockito
18 import org.mockito.junit.MockitoJUnit 18 import org.mockito.junit.MockitoJUnit
  19 +import org.mockito.kotlin.any
  20 +import org.mockito.kotlin.anyOrNull
  21 +import org.mockito.kotlin.doReturn
  22 +import org.mockito.kotlin.stub
19 import org.robolectric.RobolectricTestRunner 23 import org.robolectric.RobolectricTestRunner
20 import org.webrtc.EglBase 24 import org.webrtc.EglBase
21 25
@@ -25,6 +29,8 @@ class RoomTest { @@ -25,6 +29,8 @@ class RoomTest {
25 29
26 @get:Rule 30 @get:Rule
27 var mockitoRule = MockitoJUnit.rule() 31 var mockitoRule = MockitoJUnit.rule()
  32 + @get:Rule
  33 + var coroutineRule = TestCoroutineRule()
28 34
29 lateinit var context: Context 35 lateinit var context: Context
30 36
@@ -54,14 +60,17 @@ class RoomTest { @@ -54,14 +60,17 @@ class RoomTest {
54 60
55 @Test 61 @Test
56 fun connectTest() { 62 fun connectTest() {
57 - val job = TestCoroutineScope().launch { 63 + rtcEngine.stub {
  64 + onBlocking { rtcEngine.join(any(), any(), anyOrNull()) }
  65 + .doReturn(SignalClientTest.JOIN.join)
  66 + }
  67 + val job = coroutineRule.scope.launch {
58 room.connect( 68 room.connect(
59 url = "http://www.example.com", 69 url = "http://www.example.com",
60 token = "", 70 token = "",
61 options = null 71 options = null
62 ) 72 )
63 } 73 }
64 - room.onIceConnected()  
65 runBlockingTest { 74 runBlockingTest {
66 job.join() 75 job.join()
67 } 76 }
@@ -9,6 +9,7 @@ import kotlinx.coroutines.test.TestCoroutineDispatcher @@ -9,6 +9,7 @@ import kotlinx.coroutines.test.TestCoroutineDispatcher
9 import kotlinx.coroutines.test.TestCoroutineScope 9 import kotlinx.coroutines.test.TestCoroutineScope
10 import kotlinx.coroutines.test.runBlockingTest 10 import kotlinx.coroutines.test.runBlockingTest
11 import kotlinx.serialization.json.Json 11 import kotlinx.serialization.json.Json
  12 +import livekit.LivekitModels
12 import livekit.LivekitRtc 13 import livekit.LivekitRtc
13 import okhttp3.* 14 import okhttp3.*
14 import org.junit.After 15 import org.junit.After
@@ -134,7 +135,12 @@ class SignalClientTest { @@ -134,7 +135,12 @@ class SignalClientTest {
134 join = with(joinBuilder) { 135 join = with(joinBuilder) {
135 room = with(roomBuilder) { 136 room = with(roomBuilder) {
136 name = "roomname" 137 name = "roomname"
137 - sid = "sid" 138 + sid = "room_sid"
  139 + build()
  140 + }
  141 + participant = with(participantBuilder) {
  142 + sid = "participant_sid"
  143 + identity = "participant_identity"
138 build() 144 build()
139 } 145 }
140 build() 146 build()
@@ -161,5 +167,17 @@ class SignalClientTest { @@ -161,5 +167,17 @@ class SignalClientTest {
161 } 167 }
162 build() 168 build()
163 } 169 }
  170 +
  171 + val CONNECTION_QUALITY = with(LivekitRtc.SignalResponse.newBuilder()) {
  172 + connectionQuality = with(connectionQualityBuilder) {
  173 + addUpdates(with(LivekitRtc.ConnectionQualityInfo.newBuilder()){
  174 + participantSid = JOIN.join.participant.sid
  175 + quality = LivekitModels.ConnectionQuality.EXCELLENT
  176 + build()
  177 + })
  178 + build()
  179 + }
  180 + build()
  181 + }
164 } 182 }
165 } 183 }