davidliu
Committed by GitHub

Add support for participant attributes (#468)

* Update protocol submodule

* Add support for participant attributes

* spotless
@@ -39,6 +39,21 @@ sealed class ParticipantEvent(open val participant: Participant) : Event() { @@ -39,6 +39,21 @@ sealed class ParticipantEvent(open val participant: Participant) : Event() {
39 class NameChanged(participant: Participant, val name: String?) : ParticipantEvent(participant) 39 class NameChanged(participant: Participant, val name: String?) : ParticipantEvent(participant)
40 40
41 /** 41 /**
  42 + * When a participant's attributes are changed, fired for all participants
  43 + */
  44 + class AttributesChanged(
  45 + participant: Participant,
  46 + /**
  47 + * The attributes that have changed and their new associated values.
  48 + */
  49 + val changedAttributes: Map<String, String>,
  50 + /**
  51 + * The old attributes prior to change.
  52 + */
  53 + val oldAttributes: Map<String, String>,
  54 + ) : ParticipantEvent(participant)
  55 +
  56 + /**
42 * Fired when the current participant's isSpeaking property changes. (including LocalParticipant) 57 * Fired when the current participant's isSpeaking property changes. (including LocalParticipant)
43 */ 58 */
44 class SpeakingChanged(participant: Participant, val isSpeaking: Boolean) : ParticipantEvent(participant) 59 class SpeakingChanged(participant: Participant, val isSpeaking: Boolean) : ParticipantEvent(participant)
@@ -511,10 +511,11 @@ constructor( @@ -511,10 +511,11 @@ constructor(
511 sendRequest(request) 511 sendRequest(request)
512 } 512 }
513 513
514 - fun sendUpdateLocalMetadata(metadata: String?, name: String?) { 514 + fun sendUpdateLocalMetadata(metadata: String?, name: String?, attributes: Map<String, String>? = emptyMap()) {
515 val update = LivekitRtc.UpdateParticipantMetadata.newBuilder() 515 val update = LivekitRtc.UpdateParticipantMetadata.newBuilder()
516 .setMetadata(metadata ?: "") 516 .setMetadata(metadata ?: "")
517 .setName(name ?: "") 517 .setName(name ?: "")
  518 + .putAllAttributes(attributes)
518 519
519 val request = LivekitRtc.SignalRequest.newBuilder() 520 val request = LivekitRtc.SignalRequest.newBuilder()
520 .setUpdateMetadata(update) 521 .setUpdateMetadata(update)
@@ -765,6 +766,10 @@ constructor( @@ -765,6 +766,10 @@ constructor(
765 // TODO 766 // TODO
766 } 767 }
767 768
  769 + LivekitRtc.SignalResponse.MessageCase.ERROR_RESPONSE -> {
  770 + // TODO
  771 + }
  772 +
768 LivekitRtc.SignalResponse.MessageCase.MESSAGE_NOT_SET, 773 LivekitRtc.SignalResponse.MessageCase.MESSAGE_NOT_SET,
769 null, 774 null,
770 -> { 775 -> {
@@ -27,6 +27,7 @@ import io.livekit.android.room.track.RemoteTrackPublication @@ -27,6 +27,7 @@ import io.livekit.android.room.track.RemoteTrackPublication
27 import io.livekit.android.room.track.Track 27 import io.livekit.android.room.track.Track
28 import io.livekit.android.room.track.TrackPublication 28 import io.livekit.android.room.track.TrackPublication
29 import io.livekit.android.util.FlowObservable 29 import io.livekit.android.util.FlowObservable
  30 +import io.livekit.android.util.diffMapChange
30 import io.livekit.android.util.flow 31 import io.livekit.android.util.flow
31 import io.livekit.android.util.flowDelegate 32 import io.livekit.android.util.flowDelegate
32 import kotlinx.coroutines.CoroutineDispatcher 33 import kotlinx.coroutines.CoroutineDispatcher
@@ -82,6 +83,8 @@ open class Participant( @@ -82,6 +83,8 @@ open class Participant(
82 private set 83 private set
83 84
84 /** 85 /**
  86 + * The participant's identity on the server. [name] should be preferred for UI usecases.
  87 + *
85 * Changes can be observed by using [io.livekit.android.util.flow] 88 * Changes can be observed by using [io.livekit.android.util.flow]
86 */ 89 */
87 @FlowObservable 90 @FlowObservable
@@ -99,6 +102,9 @@ open class Participant( @@ -99,6 +102,9 @@ open class Participant(
99 102
100 /** 103 /**
101 * Changes can be observed by using [io.livekit.android.util.flow] 104 * Changes can be observed by using [io.livekit.android.util.flow]
  105 + *
  106 + * A [ParticipantEvent.SpeakingChanged] event is emitted from [events] whenever
  107 + * this changes.
102 */ 108 */
103 @FlowObservable 109 @FlowObservable
104 @get:FlowObservable 110 @get:FlowObservable
@@ -113,6 +119,14 @@ open class Participant( @@ -113,6 +119,14 @@ open class Participant(
113 } 119 }
114 @VisibleForTesting set 120 @VisibleForTesting set
115 121
  122 + /**
  123 + * The participant's name. To be used for user-facing purposes (i.e. when displayed in the UI).
  124 + *
  125 + * Changes can be observed by using [io.livekit.android.util.flow]
  126 + *
  127 + * A [ParticipantEvent.NameChanged] event is emitted from [events] whenever
  128 + * this changes.
  129 + */
116 @FlowObservable 130 @FlowObservable
117 @get:FlowObservable 131 @get:FlowObservable
118 var name by flowDelegate<String?>(null) { newValue, oldValue -> 132 var name by flowDelegate<String?>(null) { newValue, oldValue ->
@@ -123,7 +137,12 @@ open class Participant( @@ -123,7 +137,12 @@ open class Participant(
123 @VisibleForTesting set 137 @VisibleForTesting set
124 138
125 /** 139 /**
  140 + * The metadata for this participant.
  141 + *
126 * Changes can be observed by using [io.livekit.android.util.flow] 142 * Changes can be observed by using [io.livekit.android.util.flow]
  143 + *
  144 + * A [ParticipantEvent.MetadataChanged] event is emitted from [events] whenever
  145 + * this changes.
127 */ 146 */
128 @FlowObservable 147 @FlowObservable
129 @get:FlowObservable 148 @get:FlowObservable
@@ -136,7 +155,33 @@ open class Participant( @@ -136,7 +155,33 @@ open class Participant(
136 @VisibleForTesting set 155 @VisibleForTesting set
137 156
138 /** 157 /**
  158 + * The attributes set on this participant.
  159 + *
  160 + * Changes can be observed by using [io.livekit.android.util.flow]
  161 + *
  162 + * A [ParticipantEvent.AttributesChanged] event is emitted from [events] whenever
  163 + * this changes.
  164 + */
  165 + @FlowObservable
  166 + @get:FlowObservable
  167 + var attributes: Map<String, String> by flowDelegate(emptyMap()) { newAttributes, oldAttributes ->
  168 + if (newAttributes != oldAttributes) {
  169 + val diff = diffMapChange(newAttributes, oldAttributes, "")
  170 +
  171 + if (diff.isNotEmpty()) {
  172 + eventBus.postEvent(ParticipantEvent.AttributesChanged(this, diff, oldAttributes), scope)
  173 + }
  174 + }
  175 + }
  176 + @VisibleForTesting set
  177 +
  178 + /**
  179 + * The permissions for this participant.
  180 + *
  181 + * Changes can be observed by using [io.livekit.android.util.flow]
139 * 182 *
  183 + * A [ParticipantEvent.ParticipantPermissionsChanged] event is emitted from [events] whenever
  184 + * this changes.
140 */ 185 */
141 @FlowObservable 186 @FlowObservable
142 @get:FlowObservable 187 @get:FlowObservable
@@ -331,6 +376,7 @@ open class Participant( @@ -331,6 +376,7 @@ open class Participant(
331 if (info.hasPermission()) { 376 if (info.hasPermission()) {
332 permissions = ParticipantPermission.fromProto(info.permission) 377 permissions = ParticipantPermission.fromProto(info.permission)
333 } 378 }
  379 + attributes = info.attributesMap
334 } 380 }
335 381
336 override fun equals(other: Any?): Boolean { 382 override fun equals(other: Any?): Boolean {
  1 +/*
  2 + * Copyright 2024 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.util
  18 +
  19 +fun <K, V> diffMapChange(newMap: Map<K, V>, oldMap: Map<K, V>, defaultValue: V): MutableMap<K, V> {
  20 + val allKeys = newMap.keys + oldMap.keys
  21 + val diff = mutableMapOf<K, V>()
  22 +
  23 + for (key in allKeys) {
  24 + if (newMap[key] != oldMap[key]) {
  25 + diff[key] = newMap[key] ?: defaultValue
  26 + }
  27 + }
  28 +
  29 + return diff
  30 +}
@@ -57,6 +57,8 @@ class ParticipantTest { @@ -57,6 +57,8 @@ class ParticipantTest {
57 assertEquals(INFO.metadata, participant.metadata) 57 assertEquals(INFO.metadata, participant.metadata)
58 assertEquals(INFO.name, participant.name) 58 assertEquals(INFO.name, participant.name)
59 assertEquals(Participant.Kind.fromProto(INFO.kind), participant.kind) 59 assertEquals(Participant.Kind.fromProto(INFO.kind), participant.kind)
  60 + assertEquals(INFO.attributesMap, participant.attributes)
  61 +
60 assertEquals(INFO, participant.participantInfo) 62 assertEquals(INFO, participant.participantInfo)
61 } 63 }
62 64
@@ -109,6 +111,29 @@ class ParticipantTest { @@ -109,6 +111,29 @@ class ParticipantTest {
109 } 111 }
110 112
111 @Test 113 @Test
  114 + fun setAttributesChangedEvent() = runTest {
  115 + participant.attributes = INFO.attributesMap
  116 +
  117 + val eventCollector = EventCollector(participant.events, coroutineRule.scope)
  118 + val oldAttributes = participant.attributes
  119 +
  120 + val newAttributes = mapOf("newAttribute" to "newValue")
  121 + participant.attributes = newAttributes
  122 +
  123 + val events = eventCollector.stopCollecting()
  124 +
  125 + assertEquals(1, events.size)
  126 + assertEquals(true, events[0] is ParticipantEvent.AttributesChanged)
  127 +
  128 + val event = events[0] as ParticipantEvent.AttributesChanged
  129 +
  130 + val expectedDiff = mapOf("attribute" to "", "newAttribute" to "newValue")
  131 + assertEquals(expectedDiff, event.changedAttributes)
  132 + assertEquals(oldAttributes, event.oldAttributes)
  133 + assertEquals(participant, event.participant)
  134 + }
  135 +
  136 + @Test
112 fun setIsSpeakingChangedEvent() = runTest { 137 fun setIsSpeakingChangedEvent() = runTest {
113 val eventCollector = EventCollector(participant.events, coroutineRule.scope) 138 val eventCollector = EventCollector(participant.events, coroutineRule.scope)
114 val newIsSpeaking = !participant.isSpeaking 139 val newIsSpeaking = !participant.isSpeaking
@@ -171,6 +196,7 @@ class ParticipantTest { @@ -171,6 +196,7 @@ class ParticipantTest {
171 .setMetadata("metadata") 196 .setMetadata("metadata")
172 .setName("name") 197 .setName("name")
173 .setKind(LivekitModels.ParticipantInfo.Kind.STANDARD) 198 .setKind(LivekitModels.ParticipantInfo.Kind.STANDARD)
  199 + .putAttributes("attribute", "value")
174 .build() 200 .build()
175 201
176 val TRACK_INFO = LivekitModels.TrackInfo.newBuilder() 202 val TRACK_INFO = LivekitModels.TrackInfo.newBuilder()
  1 +/*
  2 + * Copyright 2024 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.util
  18 +
  19 +import org.junit.Assert.assertEquals
  20 +import org.junit.Test
  21 +
  22 +class MapDiffUtilTest {
  23 +
  24 + @Test
  25 + fun newMapChangesValues() {
  26 + val oldMap = mapOf("a" to "1", "b" to "2", "c" to "3")
  27 + val newMap = mapOf("a" to "1", "b" to "0", "c" to "3")
  28 +
  29 + val diff = diffMapChange(newMap, oldMap, "").entries
  30 + assertEquals(1, diff.size)
  31 + val entry = diff.first()
  32 + assertEquals("b", entry.key)
  33 + assertEquals("0", entry.value)
  34 + }
  35 +
  36 + @Test
  37 + fun newMapAddsValues() {
  38 + val oldMap = mapOf("a" to "1", "b" to "2", "c" to "3")
  39 + val newMap = mapOf("a" to "1", "b" to "2", "c" to "3", "d" to "4")
  40 +
  41 + val diff = diffMapChange(newMap, oldMap, "").entries
  42 + assertEquals(1, diff.size)
  43 + val entry = diff.first()
  44 + assertEquals("d", entry.key)
  45 + assertEquals("4", entry.value)
  46 + }
  47 +
  48 + @Test
  49 + fun newMapDeletesValues() {
  50 + val oldMap = mapOf("a" to "1", "b" to "2", "c" to "3")
  51 + val newMap = mapOf("a" to "1", "b" to "2")
  52 +
  53 + val diff = diffMapChange(newMap, oldMap, "").entries
  54 + assertEquals(1, diff.size)
  55 + val entry = diff.first()
  56 + assertEquals("c", entry.key)
  57 + assertEquals("", entry.value)
  58 + }
  59 +}
1 -Subproject commit 90207b41e22999ef62acb8b1336d6cd5bbe369b3 1 +Subproject commit e099d367dd0bd8dac2df416279684c22693970e0