davidliu
Committed by GitHub

Surface canPublishSources, canUpdateMetadata, and canSubscribeMetrics on Partici…

…pantPermission (#610)

Add tests to verify protobuf and sdk fields match
Add SCREEN_SHARE_AUDIO as a Track.Source.Type
---
"client-sdk-android": patch
---
Add SCREEN_SHARE_AUDIO as a Track.Source.Type
... ...
---
"client-sdk-android": patch
---
Surface canPublishSources, canUpdateMetadata, and canSubscribeMetrics on ParticipantPermission
... ...
... ... @@ -362,6 +362,7 @@ constructor(
SD_TYPE_ANSWER -> SessionDescription.Type.ANSWER
SD_TYPE_OFFER -> SessionDescription.Type.OFFER
SD_TYPE_PRANSWER -> SessionDescription.Type.PRANSWER
SD_TYPE_ROLLBACK -> SessionDescription.Type.ROLLBACK
else -> throw IllegalArgumentException("invalid RTC SdpType: ${sd.type}")
}
return SessionDescription(rtcSdpType, sd.sdp)
... ... @@ -882,6 +883,7 @@ constructor(
const val SD_TYPE_ANSWER = "answer"
const val SD_TYPE_OFFER = "offer"
const val SD_TYPE_PRANSWER = "pranswer"
const val SD_TYPE_ROLLBACK = "rollback"
const val SDK_TYPE = "android"
private val skipQueueTypes = listOf(
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -591,6 +591,9 @@ data class ParticipantPermission(
val canPublishData: Boolean,
val hidden: Boolean,
val recorder: Boolean,
val canPublishSources: List<Track.Source>,
val canUpdateMetadata: Boolean,
val canSubscribeMetrics: Boolean,
) {
companion object {
fun fromProto(proto: LivekitModels.ParticipantPermission): ParticipantPermission {
... ... @@ -600,6 +603,9 @@ data class ParticipantPermission(
canPublishData = proto.canPublishData,
hidden = proto.hidden,
recorder = proto.recorder,
canPublishSources = proto.canPublishSourcesList.map { Track.Source.fromProto(it) },
canUpdateMetadata = proto.canUpdateMetadata,
canSubscribeMetrics = proto.canSubscribeMetrics,
)
}
}
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -98,25 +98,29 @@ abstract class Track(
return when (tt) {
LivekitModels.TrackType.AUDIO -> AUDIO
LivekitModels.TrackType.VIDEO -> VIDEO
else -> UNRECOGNIZED
LivekitModels.TrackType.DATA, // TODO: does this need to be handled?
LivekitModels.TrackType.UNRECOGNIZED,
-> UNRECOGNIZED
}
}
}
}
enum class Source {
UNKNOWN,
CAMERA,
MICROPHONE,
SCREEN_SHARE,
UNKNOWN,
SCREEN_SHARE_AUDIO,
;
fun toProto(): LivekitModels.TrackSource {
return when (this) {
UNKNOWN -> LivekitModels.TrackSource.UNKNOWN
CAMERA -> LivekitModels.TrackSource.CAMERA
MICROPHONE -> LivekitModels.TrackSource.MICROPHONE
SCREEN_SHARE -> LivekitModels.TrackSource.SCREEN_SHARE
UNKNOWN -> LivekitModels.TrackSource.UNKNOWN
SCREEN_SHARE_AUDIO -> LivekitModels.TrackSource.SCREEN_SHARE_AUDIO
}
}
... ... @@ -126,7 +130,10 @@ abstract class Track(
LivekitModels.TrackSource.CAMERA -> CAMERA
LivekitModels.TrackSource.MICROPHONE -> MICROPHONE
LivekitModels.TrackSource.SCREEN_SHARE -> SCREEN_SHARE
else -> UNKNOWN
LivekitModels.TrackSource.SCREEN_SHARE_AUDIO -> SCREEN_SHARE_AUDIO
LivekitModels.TrackSource.UNKNOWN,
LivekitModels.TrackSource.UNRECOGNIZED,
-> UNKNOWN
}
}
}
... ...
... ... @@ -178,17 +178,20 @@ object TestData {
val PERMISSION_CHANGE = with(LivekitRtc.SignalResponse.newBuilder()) {
update = with(LivekitRtc.ParticipantUpdate.newBuilder()) {
addParticipants(
LOCAL_PARTICIPANT.toBuilder()
.setPermission(
LivekitModels.ParticipantPermission.newBuilder()
.setCanPublish(false)
.setCanSubscribe(false)
.setCanPublishData(false)
.setHidden(false)
.setRecorder(false)
.build(),
)
.build(),
with(LOCAL_PARTICIPANT.toBuilder()) {
permission = with(LivekitModels.ParticipantPermission.newBuilder()) {
canPublish = true
canSubscribe = false
canPublishData = false
addCanPublishSources(LivekitModels.TrackSource.CAMERA)
canUpdateMetadata = false
canSubscribeMetrics = false
hidden = false
recorder = false
build()
}
build()
},
)
build()
}
... ...
/*
* Copyright 2025 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.proto
import io.livekit.android.room.RegionSettings
import io.livekit.android.room.participant.ParticipantPermission
import io.livekit.android.rpc.RpcError
import livekit.LivekitModels
import livekit.LivekitRtc
import livekit.org.webrtc.SessionDescription
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
class ProtoConverterTest(
val protoClass: Class<*>,
val sdkClass: Class<*>,
/** fields to ignore */
val whitelist: List<String>,
/** field mapping proto field to sdk field */
val mapping: Map<String, String>,
@Suppress("unused") val testName: String,
) {
data class ProtoConverterTestCase(
val protoClass: Class<*>,
val sdkClass: Class<*>,
/** fields to ignore */
val whitelist: List<String> = emptyList(),
/** field mapping proto field to sdk field */
val mapping: Map<String, String> = emptyMap(),
) {
fun toTestData(): Array<Any> {
return arrayOf(protoClass, sdkClass, whitelist, mapping, protoClass.simpleName)
}
}
companion object {
val testCases = listOf(
ProtoConverterTestCase(
LivekitModels.ParticipantPermission::class.java,
ParticipantPermission::class.java,
whitelist = listOf("agent"),
),
ProtoConverterTestCase(
LivekitRtc.RegionSettings::class.java,
RegionSettings::class.java,
),
ProtoConverterTestCase(
LivekitModels.RpcError::class.java,
RpcError::class.java,
),
ProtoConverterTestCase(
LivekitRtc.SessionDescription::class.java,
SessionDescription::class.java,
mapping = mapOf("sdp" to "description"),
),
)
@JvmStatic
@Parameterized.Parameters(name = "Input: {4}")
fun params(): List<Array<Any>> {
return testCases.map { it.toTestData() }
}
}
@Test
fun participantPermission() {
val protoFields = protoClass.declaredFields
.asSequence()
.map { it.name }
.filter { it.isNotBlank() }
.filter { it[0].isLowerCase() }
.map { it.slice(0 until it.indexOf('_')) }
.filter { it.isNotBlank() }
.filterNot { whitelist.contains(it) }
.map { mapping[it] ?: it }
.toSet()
val fields = sdkClass.declaredFields
.map { it.name }
.toSet()
println("Local fields")
fields.forEach { println(it) }
println()
println("Proto fields")
protoFields.forEach { println(it) }
for (protoField in protoFields) {
assertTrue("$protoField not found!", fields.contains(protoField))
}
}
}
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -18,7 +18,9 @@ package io.livekit.android.room.participant
import io.livekit.android.events.ParticipantEvent
import io.livekit.android.events.RoomEvent
import io.livekit.android.room.track.Track
import io.livekit.android.test.MockE2ETest
import io.livekit.android.test.assert.assertIsClass
import io.livekit.android.test.assert.assertIsClassList
import io.livekit.android.test.events.EventCollector
import io.livekit.android.test.mock.TestData
... ... @@ -58,11 +60,21 @@ class ParticipantMockE2ETest : MockE2ETest() {
connect()
val eventCollector = EventCollector(room.events, coroutineRule.scope)
val participantEventCollector = EventCollector(room.localParticipant.events, coroutineRule.scope)
simulateMessageFromServer(TestData.PERMISSION_CHANGE)
val events = eventCollector.stopCollecting()
val participantEvents = participantEventCollector.stopCollecting()
assertEquals(1, events.size)
assertEquals(true, events[0] is RoomEvent.ParticipantPermissionsChanged)
assertIsClass(RoomEvent.ParticipantPermissionsChanged::class.java, events[0])
assertEquals(1, participantEvents.size)
assertIsClass(ParticipantEvent.ParticipantPermissionsChanged::class.java, participantEvents[0])
val newPermissions = (participantEvents[0] as ParticipantEvent.ParticipantPermissionsChanged).newPermissions!!
val permissionData = TestData.PERMISSION_CHANGE.update.participantsList[0].permission
assertEquals(permissionData.canPublish, newPermissions.canPublish)
assertEquals(permissionData.canPublishSourcesList.map { Track.Source.fromProto(it) }, newPermissions.canPublishSources)
}
@Test
... ...