Committed by
GitHub
Add support for participant attributes (#468)
* Update protocol submodule * Add support for participant attributes * spotless
正在显示
7 个修改的文件
包含
183 行增加
和
2 行删除
| @@ -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 | +} |
-
请 注册 或 登录 后发表评论