继续操作前请注册或者登录。
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
  1 +---
  2 +"client-sdk-android": patch
  3 +---
  4 +
  5 +Add SCREEN_SHARE_AUDIO as a Track.Source.Type
  1 +---
  2 +"client-sdk-android": patch
  3 +---
  4 +
  5 +Surface canPublishSources, canUpdateMetadata, and canSubscribeMetrics on ParticipantPermission
@@ -362,6 +362,7 @@ constructor( @@ -362,6 +362,7 @@ constructor(
362 SD_TYPE_ANSWER -> SessionDescription.Type.ANSWER 362 SD_TYPE_ANSWER -> SessionDescription.Type.ANSWER
363 SD_TYPE_OFFER -> SessionDescription.Type.OFFER 363 SD_TYPE_OFFER -> SessionDescription.Type.OFFER
364 SD_TYPE_PRANSWER -> SessionDescription.Type.PRANSWER 364 SD_TYPE_PRANSWER -> SessionDescription.Type.PRANSWER
  365 + SD_TYPE_ROLLBACK -> SessionDescription.Type.ROLLBACK
365 else -> throw IllegalArgumentException("invalid RTC SdpType: ${sd.type}") 366 else -> throw IllegalArgumentException("invalid RTC SdpType: ${sd.type}")
366 } 367 }
367 return SessionDescription(rtcSdpType, sd.sdp) 368 return SessionDescription(rtcSdpType, sd.sdp)
@@ -882,6 +883,7 @@ constructor( @@ -882,6 +883,7 @@ constructor(
882 const val SD_TYPE_ANSWER = "answer" 883 const val SD_TYPE_ANSWER = "answer"
883 const val SD_TYPE_OFFER = "offer" 884 const val SD_TYPE_OFFER = "offer"
884 const val SD_TYPE_PRANSWER = "pranswer" 885 const val SD_TYPE_PRANSWER = "pranswer"
  886 + const val SD_TYPE_ROLLBACK = "rollback"
885 const val SDK_TYPE = "android" 887 const val SDK_TYPE = "android"
886 888
887 private val skipQueueTypes = listOf( 889 private val skipQueueTypes = listOf(
1 /* 1 /*
2 - * Copyright 2023-2024 LiveKit, Inc. 2 + * Copyright 2023-2025 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -591,6 +591,9 @@ data class ParticipantPermission( @@ -591,6 +591,9 @@ data class ParticipantPermission(
591 val canPublishData: Boolean, 591 val canPublishData: Boolean,
592 val hidden: Boolean, 592 val hidden: Boolean,
593 val recorder: Boolean, 593 val recorder: Boolean,
  594 + val canPublishSources: List<Track.Source>,
  595 + val canUpdateMetadata: Boolean,
  596 + val canSubscribeMetrics: Boolean,
594 ) { 597 ) {
595 companion object { 598 companion object {
596 fun fromProto(proto: LivekitModels.ParticipantPermission): ParticipantPermission { 599 fun fromProto(proto: LivekitModels.ParticipantPermission): ParticipantPermission {
@@ -600,6 +603,9 @@ data class ParticipantPermission( @@ -600,6 +603,9 @@ data class ParticipantPermission(
600 canPublishData = proto.canPublishData, 603 canPublishData = proto.canPublishData,
601 hidden = proto.hidden, 604 hidden = proto.hidden,
602 recorder = proto.recorder, 605 recorder = proto.recorder,
  606 + canPublishSources = proto.canPublishSourcesList.map { Track.Source.fromProto(it) },
  607 + canUpdateMetadata = proto.canUpdateMetadata,
  608 + canSubscribeMetrics = proto.canSubscribeMetrics,
603 ) 609 )
604 } 610 }
605 } 611 }
1 /* 1 /*
2 - * Copyright 2023-2024 LiveKit, Inc. 2 + * Copyright 2023-2025 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -98,25 +98,29 @@ abstract class Track( @@ -98,25 +98,29 @@ abstract class Track(
98 return when (tt) { 98 return when (tt) {
99 LivekitModels.TrackType.AUDIO -> AUDIO 99 LivekitModels.TrackType.AUDIO -> AUDIO
100 LivekitModels.TrackType.VIDEO -> VIDEO 100 LivekitModels.TrackType.VIDEO -> VIDEO
101 - else -> UNRECOGNIZED 101 + LivekitModels.TrackType.DATA, // TODO: does this need to be handled?
  102 + LivekitModels.TrackType.UNRECOGNIZED,
  103 + -> UNRECOGNIZED
102 } 104 }
103 } 105 }
104 } 106 }
105 } 107 }
106 108
107 enum class Source { 109 enum class Source {
  110 + UNKNOWN,
108 CAMERA, 111 CAMERA,
109 MICROPHONE, 112 MICROPHONE,
110 SCREEN_SHARE, 113 SCREEN_SHARE,
111 - UNKNOWN, 114 + SCREEN_SHARE_AUDIO,
112 ; 115 ;
113 116
114 fun toProto(): LivekitModels.TrackSource { 117 fun toProto(): LivekitModels.TrackSource {
115 return when (this) { 118 return when (this) {
  119 + UNKNOWN -> LivekitModels.TrackSource.UNKNOWN
116 CAMERA -> LivekitModels.TrackSource.CAMERA 120 CAMERA -> LivekitModels.TrackSource.CAMERA
117 MICROPHONE -> LivekitModels.TrackSource.MICROPHONE 121 MICROPHONE -> LivekitModels.TrackSource.MICROPHONE
118 SCREEN_SHARE -> LivekitModels.TrackSource.SCREEN_SHARE 122 SCREEN_SHARE -> LivekitModels.TrackSource.SCREEN_SHARE
119 - UNKNOWN -> LivekitModels.TrackSource.UNKNOWN 123 + SCREEN_SHARE_AUDIO -> LivekitModels.TrackSource.SCREEN_SHARE_AUDIO
120 } 124 }
121 } 125 }
122 126
@@ -126,7 +130,10 @@ abstract class Track( @@ -126,7 +130,10 @@ abstract class Track(
126 LivekitModels.TrackSource.CAMERA -> CAMERA 130 LivekitModels.TrackSource.CAMERA -> CAMERA
127 LivekitModels.TrackSource.MICROPHONE -> MICROPHONE 131 LivekitModels.TrackSource.MICROPHONE -> MICROPHONE
128 LivekitModels.TrackSource.SCREEN_SHARE -> SCREEN_SHARE 132 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
130 } 137 }
131 } 138 }
132 } 139 }
@@ -178,17 +178,20 @@ object TestData { @@ -178,17 +178,20 @@ object TestData {
178 val PERMISSION_CHANGE = with(LivekitRtc.SignalResponse.newBuilder()) { 178 val PERMISSION_CHANGE = with(LivekitRtc.SignalResponse.newBuilder()) {
179 update = with(LivekitRtc.ParticipantUpdate.newBuilder()) { 179 update = with(LivekitRtc.ParticipantUpdate.newBuilder()) {
180 addParticipants( 180 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 + },
192 ) 195 )
193 build() 196 build()
194 } 197 }
  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 +}
1 /* 1 /*
2 - * Copyright 2023-2024 LiveKit, Inc. 2 + * Copyright 2023-2025 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -18,7 +18,9 @@ package io.livekit.android.room.participant @@ -18,7 +18,9 @@ package io.livekit.android.room.participant
18 18
19 import io.livekit.android.events.ParticipantEvent 19 import io.livekit.android.events.ParticipantEvent
20 import io.livekit.android.events.RoomEvent 20 import io.livekit.android.events.RoomEvent
  21 +import io.livekit.android.room.track.Track
21 import io.livekit.android.test.MockE2ETest 22 import io.livekit.android.test.MockE2ETest
  23 +import io.livekit.android.test.assert.assertIsClass
22 import io.livekit.android.test.assert.assertIsClassList 24 import io.livekit.android.test.assert.assertIsClassList
23 import io.livekit.android.test.events.EventCollector 25 import io.livekit.android.test.events.EventCollector
24 import io.livekit.android.test.mock.TestData 26 import io.livekit.android.test.mock.TestData
@@ -58,11 +60,21 @@ class ParticipantMockE2ETest : MockE2ETest() { @@ -58,11 +60,21 @@ class ParticipantMockE2ETest : MockE2ETest() {
58 connect() 60 connect()
59 61
60 val eventCollector = EventCollector(room.events, coroutineRule.scope) 62 val eventCollector = EventCollector(room.events, coroutineRule.scope)
  63 + val participantEventCollector = EventCollector(room.localParticipant.events, coroutineRule.scope)
61 simulateMessageFromServer(TestData.PERMISSION_CHANGE) 64 simulateMessageFromServer(TestData.PERMISSION_CHANGE)
62 val events = eventCollector.stopCollecting() 65 val events = eventCollector.stopCollecting()
  66 + val participantEvents = participantEventCollector.stopCollecting()
63 67
64 assertEquals(1, events.size) 68 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)
66 } 78 }
67 79
68 @Test 80 @Test