davidliu
Committed by GitHub

Support local participant name and metadata update (#210)

* protocol update

* support and handle name/metadata update

* wait for server to return updates since we don't know if we have perms to update

* change function names to better indicate that these are not direct setter functions

* fix tests
@@ -17,6 +17,11 @@ sealed class ParticipantEvent(open val participant: Participant) : Event() { @@ -17,6 +17,11 @@ sealed class ParticipantEvent(open val participant: Participant) : Event() {
17 class MetadataChanged(participant: Participant, val prevMetadata: String?) : ParticipantEvent(participant) 17 class MetadataChanged(participant: Participant, val prevMetadata: String?) : ParticipantEvent(participant)
18 18
19 /** 19 /**
  20 + * When a participant's display name is changed, fired for all participants
  21 + */
  22 + class NameChanged(participant: Participant, val name: String?) : ParticipantEvent(participant)
  23 +
  24 + /**
20 * Fired when the current participant's isSpeaking property changes. (including LocalParticipant) 25 * Fired when the current participant's isSpeaking property changes. (including LocalParticipant)
21 */ 26 */
22 class SpeakingChanged(participant: Participant, val isSpeaking: Boolean) : ParticipantEvent(participant) 27 class SpeakingChanged(participant: Participant, val isSpeaking: Boolean) : ParticipantEvent(participant)
@@ -60,6 +60,12 @@ sealed class RoomEvent(val room: Room) : Event() { @@ -60,6 +60,12 @@ sealed class RoomEvent(val room: Room) : Event() {
60 val prevMetadata: String? 60 val prevMetadata: String?
61 ) : RoomEvent(room) 61 ) : RoomEvent(room)
62 62
  63 + class ParticipantNameChanged(
  64 + room: Room,
  65 + val participant: Participant,
  66 + val name: String?
  67 + ) : RoomEvent(room)
  68 +
63 /** 69 /**
64 * The participant was muted. 70 * The participant was muted.
65 * 71 *
@@ -220,6 +220,15 @@ constructor( @@ -220,6 +220,15 @@ constructor(
220 ) 220 )
221 ) 221 )
222 } 222 }
  223 + is ParticipantEvent.NameChanged -> {
  224 + emitWhenConnected(
  225 + RoomEvent.ParticipantNameChanged(
  226 + this@Room,
  227 + it.participant,
  228 + it.name
  229 + )
  230 + )
  231 + }
223 else -> { 232 else -> {
224 /* do nothing */ 233 /* do nothing */
225 } 234 }
@@ -371,6 +380,15 @@ constructor( @@ -371,6 +380,15 @@ constructor(
371 ) 380 )
372 ) 381 )
373 } 382 }
  383 + is ParticipantEvent.NameChanged -> {
  384 + emitWhenConnected(
  385 + RoomEvent.ParticipantNameChanged(
  386 + this@Room,
  387 + it.participant,
  388 + it.name,
  389 + )
  390 + )
  391 + }
374 is ParticipantEvent.ParticipantPermissionsChanged -> eventBus.postEvent( 392 is ParticipantEvent.ParticipantPermissionsChanged -> eventBus.postEvent(
375 RoomEvent.ParticipantPermissionsChanged( 393 RoomEvent.ParticipantPermissionsChanged(
376 room = this@Room, 394 room = this@Room,
@@ -436,6 +436,18 @@ constructor( @@ -436,6 +436,18 @@ constructor(
436 sendRequest(request) 436 sendRequest(request)
437 } 437 }
438 438
  439 + fun sendUpdateLocalMetadata(metadata: String?, name: String?) {
  440 + val update = LivekitRtc.UpdateParticipantMetadata.newBuilder()
  441 + .setMetadata(metadata ?: "")
  442 + .setName(name ?: "")
  443 +
  444 + val request = LivekitRtc.SignalRequest.newBuilder()
  445 + .setUpdateMetadata(update)
  446 + .build()
  447 +
  448 + sendRequest(request)
  449 + }
  450 +
439 fun sendSyncState(syncState: LivekitRtc.SyncState) { 451 fun sendSyncState(syncState: LivekitRtc.SyncState) {
440 val request = LivekitRtc.SignalRequest.newBuilder() 452 val request = LivekitRtc.SignalRequest.newBuilder()
441 .setSyncState(syncState) 453 .setSyncState(syncState)
@@ -491,6 +491,26 @@ internal constructor( @@ -491,6 +491,26 @@ internal constructor(
491 } 491 }
492 } 492 }
493 493
  494 + /**
  495 + * Updates the metadata of the local participant. Changes will not be reflected until the
  496 + * server responds confirming the update.
  497 + * Note: this requires `CanUpdateOwnMetadata` permission encoded in the token.
  498 + * @param metadata
  499 + */
  500 + fun updateMetadata(metadata: String) {
  501 + this.engine.client.sendUpdateLocalMetadata(metadata, name)
  502 + }
  503 +
  504 + /**
  505 + * Updates the name of the local participant. Changes will not be reflected until the
  506 + * server responds confirming the update.
  507 + * Note: this requires `CanUpdateOwnMetadata` permission encoded in the token.
  508 + * @param name
  509 + */
  510 + fun updateName(name: String) {
  511 + this.engine.client.sendUpdateLocalMetadata(metadata, name)
  512 + }
  513 +
494 internal fun onRemoteMuteChanged(trackSid: String, muted: Boolean) { 514 internal fun onRemoteMuteChanged(trackSid: String, muted: Boolean) {
495 val pub = tracks[trackSid] 515 val pub = tracks[trackSid]
496 pub?.muted = muted 516 pub?.muted = muted
@@ -75,7 +75,12 @@ open class Participant( @@ -75,7 +75,12 @@ open class Participant(
75 75
76 @FlowObservable 76 @FlowObservable
77 @get:FlowObservable 77 @get:FlowObservable
78 - var name by flowDelegate<String?>(null) 78 + var name by flowDelegate<String?>(null) { newValue, oldValue ->
  79 + if (newValue != oldValue) {
  80 + eventBus.postEvent(ParticipantEvent.NameChanged(this, newValue), scope)
  81 + }
  82 + }
  83 + internal set
79 84
80 /** 85 /**
81 * Changes can be observed by using [io.livekit.android.util.flow] 86 * Changes can be observed by using [io.livekit.android.util.flow]
@@ -187,26 +187,6 @@ class RoomMockE2ETest : MockE2ETest() { @@ -187,26 +187,6 @@ class RoomMockE2ETest : MockE2ETest() {
187 } 187 }
188 188
189 @Test 189 @Test
190 - fun participantMetadataChanged() = runTest {  
191 - connect()  
192 -  
193 - wsFactory.listener.onMessage(  
194 - wsFactory.ws,  
195 - SignalClientTest.PARTICIPANT_JOIN.toOkioByteString()  
196 - )  
197 -  
198 - val eventCollector = EventCollector(room.events, coroutineRule.scope)  
199 - wsFactory.listener.onMessage(  
200 - wsFactory.ws,  
201 - SignalClientTest.PARTICIPANT_METADATA_CHANGED.toOkioByteString()  
202 - )  
203 - val events = eventCollector.stopCollecting()  
204 -  
205 - Assert.assertEquals(1, events.size)  
206 - Assert.assertEquals(true, events[0] is RoomEvent.ParticipantMetadataChanged)  
207 - }  
208 -  
209 - @Test  
210 fun trackStreamStateChanged() = runTest { 190 fun trackStreamStateChanged() = runTest {
211 connect() 191 connect()
212 192
@@ -435,10 +435,24 @@ class SignalClientTest : BaseTest() { @@ -435,10 +435,24 @@ class SignalClientTest : BaseTest() {
435 build() 435 build()
436 } 436 }
437 437
438 - val PARTICIPANT_METADATA_CHANGED = with(LivekitRtc.SignalResponse.newBuilder()) { 438 + val LOCAL_PARTICIPANT_METADATA_CHANGED = with(LivekitRtc.SignalResponse.newBuilder()) {
  439 + update = with(LivekitRtc.ParticipantUpdate.newBuilder()) {
  440 + val participantMetadataChanged = TestData.LOCAL_PARTICIPANT.toBuilder()
  441 + .setMetadata("changed_metadata")
  442 + .setName("changed_name")
  443 + .build()
  444 +
  445 + addParticipants(participantMetadataChanged)
  446 + build()
  447 + }
  448 + build()
  449 + }
  450 +
  451 + val REMOTE_PARTICIPANT_METADATA_CHANGED = with(LivekitRtc.SignalResponse.newBuilder()) {
439 update = with(LivekitRtc.ParticipantUpdate.newBuilder()) { 452 update = with(LivekitRtc.ParticipantUpdate.newBuilder()) {
440 val participantMetadataChanged = TestData.REMOTE_PARTICIPANT.toBuilder() 453 val participantMetadataChanged = TestData.REMOTE_PARTICIPANT.toBuilder()
441 .setMetadata("changed_metadata") 454 .setMetadata("changed_metadata")
  455 + .setName("changed_name")
442 .build() 456 .build()
443 457
444 addParticipants(participantMetadataChanged) 458 addParticipants(participantMetadataChanged)
1 package io.livekit.android.room.participant 1 package io.livekit.android.room.participant
2 2
3 import io.livekit.android.MockE2ETest 3 import io.livekit.android.MockE2ETest
  4 +import io.livekit.android.assert.assertIsClassList
  5 +import io.livekit.android.events.EventCollector
  6 +import io.livekit.android.events.ParticipantEvent
  7 +import io.livekit.android.events.RoomEvent
4 import io.livekit.android.mock.MockAudioStreamTrack 8 import io.livekit.android.mock.MockAudioStreamTrack
5 import io.livekit.android.room.SignalClientTest 9 import io.livekit.android.room.SignalClientTest
6 import io.livekit.android.room.track.LocalAudioTrack 10 import io.livekit.android.room.track.LocalAudioTrack
7 import io.livekit.android.util.toOkioByteString 11 import io.livekit.android.util.toOkioByteString
  12 +import io.livekit.android.util.toPBByteString
8 import kotlinx.coroutines.ExperimentalCoroutinesApi 13 import kotlinx.coroutines.ExperimentalCoroutinesApi
9 import kotlinx.coroutines.launch 14 import kotlinx.coroutines.launch
10 -import kotlinx.coroutines.test.advanceUntilIdle  
11 -import kotlinx.coroutines.test.runCurrent 15 +import livekit.LivekitRtc
12 import org.junit.Assert.* 16 import org.junit.Assert.*
13 import org.junit.Test 17 import org.junit.Test
14 import org.junit.runner.RunWith 18 import org.junit.runner.RunWith
@@ -20,7 +24,6 @@ class LocalParticipantMockE2ETest : MockE2ETest() { @@ -20,7 +24,6 @@ class LocalParticipantMockE2ETest : MockE2ETest() {
20 24
21 @Test 25 @Test
22 fun disconnectCleansLocalParticipant() = runTest { 26 fun disconnectCleansLocalParticipant() = runTest {
23 -  
24 connect() 27 connect()
25 28
26 val publishJob = launch { 29 val publishJob = launch {
@@ -49,4 +52,75 @@ class LocalParticipantMockE2ETest : MockE2ETest() { @@ -49,4 +52,75 @@ class LocalParticipantMockE2ETest : MockE2ETest() {
49 assertEquals(0, room.localParticipant.audioTracks.size) 52 assertEquals(0, room.localParticipant.audioTracks.size)
50 assertEquals(0, room.localParticipant.videoTracks.size) 53 assertEquals(0, room.localParticipant.videoTracks.size)
51 } 54 }
  55 +
  56 + @Test
  57 + fun updateName() = runTest {
  58 + connect()
  59 + val newName = "new_name"
  60 + wsFactory.ws.clearRequests()
  61 +
  62 + room.localParticipant.updateName(newName)
  63 +
  64 + val requestString = wsFactory.ws.sentRequests.first().toPBByteString()
  65 + val sentRequest = LivekitRtc.SignalRequest.newBuilder()
  66 + .mergeFrom(requestString)
  67 + .build()
  68 +
  69 + assertTrue(sentRequest.hasUpdateMetadata())
  70 + assertEquals(newName, sentRequest.updateMetadata.name)
  71 + }
  72 +
  73 + @Test
  74 + fun updateMetadata() = runTest {
  75 + connect()
  76 + val newMetadata = "new_metadata"
  77 + wsFactory.ws.clearRequests()
  78 +
  79 + room.localParticipant.updateMetadata(newMetadata)
  80 +
  81 + val requestString = wsFactory.ws.sentRequests.first().toPBByteString()
  82 + val sentRequest = LivekitRtc.SignalRequest.newBuilder()
  83 + .mergeFrom(requestString)
  84 + .build()
  85 +
  86 + assertTrue(sentRequest.hasUpdateMetadata())
  87 + assertEquals(newMetadata, sentRequest.updateMetadata.metadata)
  88 + }
  89 +
  90 + @Test
  91 + fun participantMetadataChanged() = runTest {
  92 + connect()
  93 +
  94 + val roomEventsCollector = EventCollector(room.events, coroutineRule.scope)
  95 + val participantEventsCollector = EventCollector(room.localParticipant.events, coroutineRule.scope)
  96 +
  97 + wsFactory.listener.onMessage(
  98 + wsFactory.ws,
  99 + SignalClientTest.LOCAL_PARTICIPANT_METADATA_CHANGED.toOkioByteString()
  100 + )
  101 +
  102 + val roomEvents = roomEventsCollector.stopCollecting()
  103 + val participantEvents = participantEventsCollector.stopCollecting()
  104 +
  105 + val localParticipant = room.localParticipant
  106 + val updateData = SignalClientTest.REMOTE_PARTICIPANT_METADATA_CHANGED.update.getParticipants(0)
  107 + assertEquals(updateData.metadata, localParticipant.metadata)
  108 + assertEquals(updateData.name, localParticipant.name)
  109 +
  110 + assertIsClassList(
  111 + listOf(
  112 + RoomEvent.ParticipantMetadataChanged::class.java,
  113 + RoomEvent.ParticipantNameChanged::class.java,
  114 + ),
  115 + roomEvents
  116 + )
  117 +
  118 + assertIsClassList(
  119 + listOf(
  120 + ParticipantEvent.MetadataChanged::class.java,
  121 + ParticipantEvent.NameChanged::class.java,
  122 + ),
  123 + participantEvents
  124 + )
  125 + }
52 } 126 }
1 package io.livekit.android.room.participant 1 package io.livekit.android.room.participant
2 2
3 import io.livekit.android.MockE2ETest 3 import io.livekit.android.MockE2ETest
  4 +import io.livekit.android.assert.assertIsClassList
4 import io.livekit.android.events.EventCollector 5 import io.livekit.android.events.EventCollector
  6 +import io.livekit.android.events.ParticipantEvent
5 import io.livekit.android.events.RoomEvent 7 import io.livekit.android.events.RoomEvent
6 import io.livekit.android.mock.MockAudioStreamTrack 8 import io.livekit.android.mock.MockAudioStreamTrack
7 import io.livekit.android.room.SignalClientTest 9 import io.livekit.android.room.SignalClientTest
8 import io.livekit.android.room.track.LocalAudioTrack 10 import io.livekit.android.room.track.LocalAudioTrack
  11 +import io.livekit.android.util.toOkioByteString
9 import kotlinx.coroutines.ExperimentalCoroutinesApi 12 import kotlinx.coroutines.ExperimentalCoroutinesApi
10 import kotlinx.coroutines.launch 13 import kotlinx.coroutines.launch
11 -import org.junit.Assert 14 +import org.junit.Assert.assertEquals
12 import org.junit.Test 15 import org.junit.Test
13 import org.junit.runner.RunWith 16 import org.junit.runner.RunWith
14 import org.robolectric.RobolectricTestRunner 17 import org.robolectric.RobolectricTestRunner
@@ -39,9 +42,9 @@ class ParticipantMockE2ETest : MockE2ETest() { @@ -39,9 +42,9 @@ class ParticipantMockE2ETest : MockE2ETest() {
39 simulateMessageFromServer(SignalClientTest.LOCAL_TRACK_UNPUBLISHED) 42 simulateMessageFromServer(SignalClientTest.LOCAL_TRACK_UNPUBLISHED)
40 val events = eventCollector.stopCollecting() 43 val events = eventCollector.stopCollecting()
41 44
42 - Assert.assertEquals(1, events.size)  
43 - Assert.assertEquals(true, events[0] is RoomEvent.TrackUnpublished)  
44 - Assert.assertEquals(0, room.localParticipant.tracks.size) 45 + assertEquals(1, events.size)
  46 + assertEquals(true, events[0] is RoomEvent.TrackUnpublished)
  47 + assertEquals(0, room.localParticipant.tracks.size)
45 } 48 }
46 49
47 @Test 50 @Test
@@ -52,7 +55,48 @@ class ParticipantMockE2ETest : MockE2ETest() { @@ -52,7 +55,48 @@ class ParticipantMockE2ETest : MockE2ETest() {
52 simulateMessageFromServer(SignalClientTest.PERMISSION_CHANGE) 55 simulateMessageFromServer(SignalClientTest.PERMISSION_CHANGE)
53 val events = eventCollector.stopCollecting() 56 val events = eventCollector.stopCollecting()
54 57
55 - Assert.assertEquals(1, events.size)  
56 - Assert.assertEquals(true, events[0] is RoomEvent.ParticipantPermissionsChanged) 58 + assertEquals(1, events.size)
  59 + assertEquals(true, events[0] is RoomEvent.ParticipantPermissionsChanged)
57 } 60 }
  61 +
  62 + @Test
  63 + fun participantMetadataChanged() = runTest {
  64 + connect()
  65 +
  66 + wsFactory.listener.onMessage(
  67 + wsFactory.ws,
  68 + SignalClientTest.PARTICIPANT_JOIN.toOkioByteString()
  69 + )
  70 +
  71 + val remoteParticipant = room.remoteParticipants.values.first()
  72 + val roomEventsCollector = EventCollector(room.events, coroutineRule.scope)
  73 + val participantEventsCollector = EventCollector(remoteParticipant.events, coroutineRule.scope)
  74 + wsFactory.listener.onMessage(
  75 + wsFactory.ws,
  76 + SignalClientTest.REMOTE_PARTICIPANT_METADATA_CHANGED.toOkioByteString()
  77 + )
  78 + val roomEvents = roomEventsCollector.stopCollecting()
  79 + val participantEvents = participantEventsCollector.stopCollecting()
  80 +
  81 + val updateData = SignalClientTest.REMOTE_PARTICIPANT_METADATA_CHANGED.update.getParticipants(0)
  82 + assertEquals(updateData.metadata, remoteParticipant.metadata)
  83 + assertEquals(updateData.name, remoteParticipant.name)
  84 +
  85 + assertIsClassList(
  86 + listOf(
  87 + RoomEvent.ParticipantMetadataChanged::class.java,
  88 + RoomEvent.ParticipantNameChanged::class.java,
  89 + ),
  90 + roomEvents
  91 + )
  92 +
  93 + assertIsClassList(
  94 + listOf(
  95 + ParticipantEvent.MetadataChanged::class.java,
  96 + ParticipantEvent.NameChanged::class.java,
  97 + ),
  98 + participantEvents
  99 + )
  100 + }
  101 +
58 } 102 }
1 -Subproject commit a1819deeabe143b1af1bf375d84e52b152f784ac 1 +Subproject commit e78c5b18c0f3a18bae5d5cec44d30bb6358b9bc4