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() {
class NameChanged(participant: Participant, val name: String?) : ParticipantEvent(participant)
/**
* When a participant's attributes are changed, fired for all participants
*/
class AttributesChanged(
participant: Participant,
/**
* The attributes that have changed and their new associated values.
*/
val changedAttributes: Map<String, String>,
/**
* The old attributes prior to change.
*/
val oldAttributes: Map<String, String>,
) : ParticipantEvent(participant)
/**
* Fired when the current participant's isSpeaking property changes. (including LocalParticipant)
*/
class SpeakingChanged(participant: Participant, val isSpeaking: Boolean) : ParticipantEvent(participant)
... ...
... ... @@ -511,10 +511,11 @@ constructor(
sendRequest(request)
}
fun sendUpdateLocalMetadata(metadata: String?, name: String?) {
fun sendUpdateLocalMetadata(metadata: String?, name: String?, attributes: Map<String, String>? = emptyMap()) {
val update = LivekitRtc.UpdateParticipantMetadata.newBuilder()
.setMetadata(metadata ?: "")
.setName(name ?: "")
.putAllAttributes(attributes)
val request = LivekitRtc.SignalRequest.newBuilder()
.setUpdateMetadata(update)
... ... @@ -765,6 +766,10 @@ constructor(
// TODO
}
LivekitRtc.SignalResponse.MessageCase.ERROR_RESPONSE -> {
// TODO
}
LivekitRtc.SignalResponse.MessageCase.MESSAGE_NOT_SET,
null,
-> {
... ...
... ... @@ -27,6 +27,7 @@ import io.livekit.android.room.track.RemoteTrackPublication
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.TrackPublication
import io.livekit.android.util.FlowObservable
import io.livekit.android.util.diffMapChange
import io.livekit.android.util.flow
import io.livekit.android.util.flowDelegate
import kotlinx.coroutines.CoroutineDispatcher
... ... @@ -82,6 +83,8 @@ open class Participant(
private set
/**
* The participant's identity on the server. [name] should be preferred for UI usecases.
*
* Changes can be observed by using [io.livekit.android.util.flow]
*/
@FlowObservable
... ... @@ -99,6 +102,9 @@ open class Participant(
/**
* Changes can be observed by using [io.livekit.android.util.flow]
*
* A [ParticipantEvent.SpeakingChanged] event is emitted from [events] whenever
* this changes.
*/
@FlowObservable
@get:FlowObservable
... ... @@ -113,6 +119,14 @@ open class Participant(
}
@VisibleForTesting set
/**
* The participant's name. To be used for user-facing purposes (i.e. when displayed in the UI).
*
* Changes can be observed by using [io.livekit.android.util.flow]
*
* A [ParticipantEvent.NameChanged] event is emitted from [events] whenever
* this changes.
*/
@FlowObservable
@get:FlowObservable
var name by flowDelegate<String?>(null) { newValue, oldValue ->
... ... @@ -123,7 +137,12 @@ open class Participant(
@VisibleForTesting set
/**
* The metadata for this participant.
*
* Changes can be observed by using [io.livekit.android.util.flow]
*
* A [ParticipantEvent.MetadataChanged] event is emitted from [events] whenever
* this changes.
*/
@FlowObservable
@get:FlowObservable
... ... @@ -136,7 +155,33 @@ open class Participant(
@VisibleForTesting set
/**
* The attributes set on this participant.
*
* Changes can be observed by using [io.livekit.android.util.flow]
*
* A [ParticipantEvent.AttributesChanged] event is emitted from [events] whenever
* this changes.
*/
@FlowObservable
@get:FlowObservable
var attributes: Map<String, String> by flowDelegate(emptyMap()) { newAttributes, oldAttributes ->
if (newAttributes != oldAttributes) {
val diff = diffMapChange(newAttributes, oldAttributes, "")
if (diff.isNotEmpty()) {
eventBus.postEvent(ParticipantEvent.AttributesChanged(this, diff, oldAttributes), scope)
}
}
}
@VisibleForTesting set
/**
* The permissions for this participant.
*
* Changes can be observed by using [io.livekit.android.util.flow]
*
* A [ParticipantEvent.ParticipantPermissionsChanged] event is emitted from [events] whenever
* this changes.
*/
@FlowObservable
@get:FlowObservable
... ... @@ -331,6 +376,7 @@ open class Participant(
if (info.hasPermission()) {
permissions = ParticipantPermission.fromProto(info.permission)
}
attributes = info.attributesMap
}
override fun equals(other: Any?): Boolean {
... ...
/*
* Copyright 2024 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.livekit.android.util
fun <K, V> diffMapChange(newMap: Map<K, V>, oldMap: Map<K, V>, defaultValue: V): MutableMap<K, V> {
val allKeys = newMap.keys + oldMap.keys
val diff = mutableMapOf<K, V>()
for (key in allKeys) {
if (newMap[key] != oldMap[key]) {
diff[key] = newMap[key] ?: defaultValue
}
}
return diff
}
... ...
... ... @@ -57,6 +57,8 @@ class ParticipantTest {
assertEquals(INFO.metadata, participant.metadata)
assertEquals(INFO.name, participant.name)
assertEquals(Participant.Kind.fromProto(INFO.kind), participant.kind)
assertEquals(INFO.attributesMap, participant.attributes)
assertEquals(INFO, participant.participantInfo)
}
... ... @@ -109,6 +111,29 @@ class ParticipantTest {
}
@Test
fun setAttributesChangedEvent() = runTest {
participant.attributes = INFO.attributesMap
val eventCollector = EventCollector(participant.events, coroutineRule.scope)
val oldAttributes = participant.attributes
val newAttributes = mapOf("newAttribute" to "newValue")
participant.attributes = newAttributes
val events = eventCollector.stopCollecting()
assertEquals(1, events.size)
assertEquals(true, events[0] is ParticipantEvent.AttributesChanged)
val event = events[0] as ParticipantEvent.AttributesChanged
val expectedDiff = mapOf("attribute" to "", "newAttribute" to "newValue")
assertEquals(expectedDiff, event.changedAttributes)
assertEquals(oldAttributes, event.oldAttributes)
assertEquals(participant, event.participant)
}
@Test
fun setIsSpeakingChangedEvent() = runTest {
val eventCollector = EventCollector(participant.events, coroutineRule.scope)
val newIsSpeaking = !participant.isSpeaking
... ... @@ -171,6 +196,7 @@ class ParticipantTest {
.setMetadata("metadata")
.setName("name")
.setKind(LivekitModels.ParticipantInfo.Kind.STANDARD)
.putAttributes("attribute", "value")
.build()
val TRACK_INFO = LivekitModels.TrackInfo.newBuilder()
... ...
/*
* Copyright 2024 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.livekit.android.util
import org.junit.Assert.assertEquals
import org.junit.Test
class MapDiffUtilTest {
@Test
fun newMapChangesValues() {
val oldMap = mapOf("a" to "1", "b" to "2", "c" to "3")
val newMap = mapOf("a" to "1", "b" to "0", "c" to "3")
val diff = diffMapChange(newMap, oldMap, "").entries
assertEquals(1, diff.size)
val entry = diff.first()
assertEquals("b", entry.key)
assertEquals("0", entry.value)
}
@Test
fun newMapAddsValues() {
val oldMap = mapOf("a" to "1", "b" to "2", "c" to "3")
val newMap = mapOf("a" to "1", "b" to "2", "c" to "3", "d" to "4")
val diff = diffMapChange(newMap, oldMap, "").entries
assertEquals(1, diff.size)
val entry = diff.first()
assertEquals("d", entry.key)
assertEquals("4", entry.value)
}
@Test
fun newMapDeletesValues() {
val oldMap = mapOf("a" to "1", "b" to "2", "c" to "3")
val newMap = mapOf("a" to "1", "b" to "2")
val diff = diffMapChange(newMap, oldMap, "").entries
assertEquals(1, diff.size)
val entry = diff.first()
assertEquals("c", entry.key)
assertEquals("", entry.value)
}
}
... ...
Subproject commit 90207b41e22999ef62acb8b1336d6cd5bbe369b3
Subproject commit e099d367dd0bd8dac2df416279684c22693970e0
... ...