Skip to content

Commit b712807

Browse files
authored
Surface canPublishSources, canUpdateMetadata, and canSubscribeMetrics on ParticipantPermission (#610)
Add tests to verify protobuf and sdk fields match Add SCREEN_SHARE_AUDIO as a Track.Source.Type
1 parent 75cbcaa commit b712807

File tree

8 files changed

+168
-19
lines changed

8 files changed

+168
-19
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"client-sdk-android": patch
3+
---
4+
5+
Add SCREEN_SHARE_AUDIO as a Track.Source.Type

.changeset/hot-poems-yawn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"client-sdk-android": patch
3+
---
4+
5+
Surface canPublishSources, canUpdateMetadata, and canSubscribeMetrics on ParticipantPermission

livekit-android-sdk/src/main/java/io/livekit/android/room/SignalClient.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ constructor(
362362
SD_TYPE_ANSWER -> SessionDescription.Type.ANSWER
363363
SD_TYPE_OFFER -> SessionDescription.Type.OFFER
364364
SD_TYPE_PRANSWER -> SessionDescription.Type.PRANSWER
365+
SD_TYPE_ROLLBACK -> SessionDescription.Type.ROLLBACK
365366
else -> throw IllegalArgumentException("invalid RTC SdpType: ${sd.type}")
366367
}
367368
return SessionDescription(rtcSdpType, sd.sdp)
@@ -882,6 +883,7 @@ constructor(
882883
const val SD_TYPE_ANSWER = "answer"
883884
const val SD_TYPE_OFFER = "offer"
884885
const val SD_TYPE_PRANSWER = "pranswer"
886+
const val SD_TYPE_ROLLBACK = "rollback"
885887
const val SDK_TYPE = "android"
886888

887889
private val skipQueueTypes = listOf(

livekit-android-sdk/src/main/java/io/livekit/android/room/participant/Participant.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 LiveKit, Inc.
2+
* Copyright 2023-2025 LiveKit, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -591,6 +591,9 @@ data class ParticipantPermission(
591591
val canPublishData: Boolean,
592592
val hidden: Boolean,
593593
val recorder: Boolean,
594+
val canPublishSources: List<Track.Source>,
595+
val canUpdateMetadata: Boolean,
596+
val canSubscribeMetrics: Boolean,
594597
) {
595598
companion object {
596599
fun fromProto(proto: LivekitModels.ParticipantPermission): ParticipantPermission {
@@ -600,6 +603,9 @@ data class ParticipantPermission(
600603
canPublishData = proto.canPublishData,
601604
hidden = proto.hidden,
602605
recorder = proto.recorder,
606+
canPublishSources = proto.canPublishSourcesList.map { Track.Source.fromProto(it) },
607+
canUpdateMetadata = proto.canUpdateMetadata,
608+
canSubscribeMetrics = proto.canSubscribeMetrics,
603609
)
604610
}
605611
}

livekit-android-sdk/src/main/java/io/livekit/android/room/track/Track.kt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 LiveKit, Inc.
2+
* Copyright 2023-2025 LiveKit, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -98,25 +98,29 @@ abstract class Track(
9898
return when (tt) {
9999
LivekitModels.TrackType.AUDIO -> AUDIO
100100
LivekitModels.TrackType.VIDEO -> VIDEO
101-
else -> UNRECOGNIZED
101+
LivekitModels.TrackType.DATA, // TODO: does this need to be handled?
102+
LivekitModels.TrackType.UNRECOGNIZED,
103+
-> UNRECOGNIZED
102104
}
103105
}
104106
}
105107
}
106108

107109
enum class Source {
110+
UNKNOWN,
108111
CAMERA,
109112
MICROPHONE,
110113
SCREEN_SHARE,
111-
UNKNOWN,
114+
SCREEN_SHARE_AUDIO,
112115
;
113116

114117
fun toProto(): LivekitModels.TrackSource {
115118
return when (this) {
119+
UNKNOWN -> LivekitModels.TrackSource.UNKNOWN
116120
CAMERA -> LivekitModels.TrackSource.CAMERA
117121
MICROPHONE -> LivekitModels.TrackSource.MICROPHONE
118122
SCREEN_SHARE -> LivekitModels.TrackSource.SCREEN_SHARE
119-
UNKNOWN -> LivekitModels.TrackSource.UNKNOWN
123+
SCREEN_SHARE_AUDIO -> LivekitModels.TrackSource.SCREEN_SHARE_AUDIO
120124
}
121125
}
122126

@@ -126,7 +130,10 @@ abstract class Track(
126130
LivekitModels.TrackSource.CAMERA -> CAMERA
127131
LivekitModels.TrackSource.MICROPHONE -> MICROPHONE
128132
LivekitModels.TrackSource.SCREEN_SHARE -> SCREEN_SHARE
129-
else -> UNKNOWN
133+
LivekitModels.TrackSource.SCREEN_SHARE_AUDIO -> SCREEN_SHARE_AUDIO
134+
LivekitModels.TrackSource.UNKNOWN,
135+
LivekitModels.TrackSource.UNRECOGNIZED,
136+
-> UNKNOWN
130137
}
131138
}
132139
}

livekit-android-test/src/main/java/io/livekit/android/test/mock/TestData.kt

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -178,17 +178,20 @@ object TestData {
178178
val PERMISSION_CHANGE = with(LivekitRtc.SignalResponse.newBuilder()) {
179179
update = with(LivekitRtc.ParticipantUpdate.newBuilder()) {
180180
addParticipants(
181-
LOCAL_PARTICIPANT.toBuilder()
182-
.setPermission(
183-
LivekitModels.ParticipantPermission.newBuilder()
184-
.setCanPublish(false)
185-
.setCanSubscribe(false)
186-
.setCanPublishData(false)
187-
.setHidden(false)
188-
.setRecorder(false)
189-
.build(),
190-
)
191-
.build(),
181+
with(LOCAL_PARTICIPANT.toBuilder()) {
182+
permission = with(LivekitModels.ParticipantPermission.newBuilder()) {
183+
canPublish = true
184+
canSubscribe = false
185+
canPublishData = false
186+
addCanPublishSources(LivekitModels.TrackSource.CAMERA)
187+
canUpdateMetadata = false
188+
canSubscribeMetrics = false
189+
hidden = false
190+
recorder = false
191+
build()
192+
}
193+
build()
194+
},
192195
)
193196
build()
194197
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2025 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.proto
18+
19+
import io.livekit.android.room.RegionSettings
20+
import io.livekit.android.room.participant.ParticipantPermission
21+
import io.livekit.android.rpc.RpcError
22+
import livekit.LivekitModels
23+
import livekit.LivekitRtc
24+
import livekit.org.webrtc.SessionDescription
25+
import org.junit.Assert.assertTrue
26+
import org.junit.Test
27+
import org.junit.runner.RunWith
28+
import org.junit.runners.Parameterized
29+
30+
@RunWith(Parameterized::class)
31+
class ProtoConverterTest(
32+
val protoClass: Class<*>,
33+
val sdkClass: Class<*>,
34+
/** fields to ignore */
35+
val whitelist: List<String>,
36+
/** field mapping proto field to sdk field */
37+
val mapping: Map<String, String>,
38+
@Suppress("unused") val testName: String,
39+
) {
40+
41+
data class ProtoConverterTestCase(
42+
val protoClass: Class<*>,
43+
val sdkClass: Class<*>,
44+
/** fields to ignore */
45+
val whitelist: List<String> = emptyList(),
46+
/** field mapping proto field to sdk field */
47+
val mapping: Map<String, String> = emptyMap(),
48+
) {
49+
fun toTestData(): Array<Any> {
50+
return arrayOf(protoClass, sdkClass, whitelist, mapping, protoClass.simpleName)
51+
}
52+
}
53+
54+
companion object {
55+
val testCases = listOf(
56+
ProtoConverterTestCase(
57+
LivekitModels.ParticipantPermission::class.java,
58+
ParticipantPermission::class.java,
59+
whitelist = listOf("agent"),
60+
),
61+
ProtoConverterTestCase(
62+
LivekitRtc.RegionSettings::class.java,
63+
RegionSettings::class.java,
64+
),
65+
ProtoConverterTestCase(
66+
LivekitModels.RpcError::class.java,
67+
RpcError::class.java,
68+
),
69+
ProtoConverterTestCase(
70+
LivekitRtc.SessionDescription::class.java,
71+
SessionDescription::class.java,
72+
mapping = mapOf("sdp" to "description"),
73+
),
74+
)
75+
76+
@JvmStatic
77+
@Parameterized.Parameters(name = "Input: {4}")
78+
fun params(): List<Array<Any>> {
79+
return testCases.map { it.toTestData() }
80+
}
81+
}
82+
83+
@Test
84+
fun participantPermission() {
85+
val protoFields = protoClass.declaredFields
86+
.asSequence()
87+
.map { it.name }
88+
.filter { it.isNotBlank() }
89+
.filter { it[0].isLowerCase() }
90+
.map { it.slice(0 until it.indexOf('_')) }
91+
.filter { it.isNotBlank() }
92+
.filterNot { whitelist.contains(it) }
93+
.map { mapping[it] ?: it }
94+
.toSet()
95+
val fields = sdkClass.declaredFields
96+
.map { it.name }
97+
.toSet()
98+
99+
println("Local fields")
100+
fields.forEach { println(it) }
101+
println()
102+
println("Proto fields")
103+
protoFields.forEach { println(it) }
104+
105+
for (protoField in protoFields) {
106+
assertTrue("$protoField not found!", fields.contains(protoField))
107+
}
108+
}
109+
}

livekit-android-test/src/test/java/io/livekit/android/room/participant/ParticipantMockE2ETest.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 LiveKit, Inc.
2+
* Copyright 2023-2025 LiveKit, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,7 +18,9 @@ package io.livekit.android.room.participant
1818

1919
import io.livekit.android.events.ParticipantEvent
2020
import io.livekit.android.events.RoomEvent
21+
import io.livekit.android.room.track.Track
2122
import io.livekit.android.test.MockE2ETest
23+
import io.livekit.android.test.assert.assertIsClass
2224
import io.livekit.android.test.assert.assertIsClassList
2325
import io.livekit.android.test.events.EventCollector
2426
import io.livekit.android.test.mock.TestData
@@ -58,11 +60,21 @@ class ParticipantMockE2ETest : MockE2ETest() {
5860
connect()
5961

6062
val eventCollector = EventCollector(room.events, coroutineRule.scope)
63+
val participantEventCollector = EventCollector(room.localParticipant.events, coroutineRule.scope)
6164
simulateMessageFromServer(TestData.PERMISSION_CHANGE)
6265
val events = eventCollector.stopCollecting()
66+
val participantEvents = participantEventCollector.stopCollecting()
6367

6468
assertEquals(1, events.size)
65-
assertEquals(true, events[0] is RoomEvent.ParticipantPermissionsChanged)
69+
assertIsClass(RoomEvent.ParticipantPermissionsChanged::class.java, events[0])
70+
71+
assertEquals(1, participantEvents.size)
72+
assertIsClass(ParticipantEvent.ParticipantPermissionsChanged::class.java, participantEvents[0])
73+
74+
val newPermissions = (participantEvents[0] as ParticipantEvent.ParticipantPermissionsChanged).newPermissions!!
75+
val permissionData = TestData.PERMISSION_CHANGE.update.participantsList[0].permission
76+
assertEquals(permissionData.canPublish, newPermissions.canPublish)
77+
assertEquals(permissionData.canPublishSourcesList.map { Track.Source.fromProto(it) }, newPermissions.canPublishSources)
6678
}
6779

6880
@Test

0 commit comments

Comments
 (0)