Skip to content

Commit 4385b9a

Browse files
1egomandavidliu
andauthored
Add support for RPC V2 (#946)
* feat: add rpc tester to sample-app * feat: advertise client protocol to other participants * feat: add initial rpc v2 implementation * feat: swap client protocol logic to work like regular sfu protocol * feat: add missing tests from rpc spec * fix: add missing changeset * fix: bump protocol version * feat: add bits to sdk which require bumped protocol version * feat: try to fix build in ci * fix: run spotless apply * fix: add unused protocol fields to whitelist * fix: run spotless apply * fix: commit detekt file * cleanup and merge fixes * Set clientProtocol on local participant when connecting * reset client protocol version for local participant when resetting * cleanup * update baseline * clean up protos * switch protobuf proguard to official recommended rule * changesets * switch to allowlist and cut down on the generated protos --------- Co-authored-by: davidliu <dl@livekit.io> Co-authored-by: davidliu <davidliu@deviange.net>
1 parent 3e1296d commit 4385b9a

33 files changed

Lines changed: 2665 additions & 423 deletions

File tree

.changeset/heavy-parrots-join.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+
Change proguard rule for protobufs to official recommended rule, allowing unused protobuf classes to be removed with minification

.changeset/wacky-turtles-thank.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"client-sdk-android": minor
3+
---
4+
5+
Add support for RPC V2

gradle/livekit-protobuf.gradle

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Shared protobuf setup for LiveKit client protos.
2+
//
3+
// Configure before applying:
4+
// ext.livekitProtoIncludes = ['livekit_models.proto', ...]
5+
// Optional (for modules that compile a subset but import others):
6+
// ext.livekitProtoImportSrc = true
7+
8+
if (!project.ext.has('livekitProtoIncludes')) {
9+
throw new GradleException("ext.livekitProtoIncludes must be set before applying livekit-protobuf.gradle")
10+
}
11+
12+
def stagedProtoSrcDir = layout.buildDirectory.dir("staged-proto-src")
13+
def stageProtoSources = tasks.register("stageProtoSources", Copy) {
14+
from(generated.protoSrc) {
15+
include project.ext.livekitProtoIncludes as String[]
16+
}
17+
into stagedProtoSrcDir
18+
}
19+
20+
android.sourceSets.main.proto {
21+
srcDir stagedProtoSrcDir
22+
}
23+
24+
android.sourceSets.main.java {
25+
srcDir "${protobuf.generatedFilesBaseDir}/main/javalite"
26+
}
27+
28+
configurations {
29+
descriptorProtoSource
30+
}
31+
32+
dependencies {
33+
descriptorProtoSource "com.google.protobuf:protobuf-java:${libs.versions.protobuf.get()}"
34+
}
35+
36+
def extractedImportProtosDir = layout.buildDirectory.dir("extracted-import-protos")
37+
def extractProtoImports = tasks.register("extractProtoImports", Copy) {
38+
into extractedImportProtosDir
39+
from { zipTree(configurations.descriptorProtoSource.singleFile) } {
40+
include "google/protobuf/descriptor.proto"
41+
}
42+
from(generated.protoSrc) {
43+
include "logger/**"
44+
}
45+
}
46+
47+
protobuf {
48+
protoc {
49+
// for apple m1, please add protoc_platform=osx-x86_64 in $HOME/.gradle/gradle.properties
50+
if (project.hasProperty('protoc_platform')) {
51+
artifact = "com.google.protobuf:protoc:${libs.versions.protobuf.get()}:${protoc_platform}"
52+
} else {
53+
artifact = "com.google.protobuf:protoc:${libs.versions.protobuf.get()}"
54+
}
55+
}
56+
57+
generateProtoTasks {
58+
all().each { task ->
59+
task.dependsOn stageProtoSources
60+
task.dependsOn extractProtoImports
61+
task.addIncludeDir files(extractedImportProtosDir)
62+
if (project.ext.has('livekitProtoImportSrc') && project.ext.livekitProtoImportSrc) {
63+
task.addIncludeDir files(generated.protoSrc)
64+
}
65+
task.builtins {
66+
java {
67+
option "lite"
68+
}
69+
}
70+
}
71+
}
72+
}

livekit-android-sdk/build.gradle

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,6 @@ android {
3030
}
3131
}
3232

33-
sourceSets {
34-
main {
35-
proto {
36-
srcDir generated.protoSrc
37-
exclude '*/*.proto' // only use top-level protos.
38-
}
39-
java {
40-
srcDir "${protobuf.generatedFilesBaseDir}/main/javalite"
41-
}
42-
}
43-
}
44-
4533
testOptions {
4634
unitTests {
4735
includeAndroidResources = true
@@ -74,26 +62,12 @@ android {
7462

7563
}
7664

77-
protobuf {
78-
protoc {
79-
// for apple m1, please add protoc_platform=osx-x86_64 in $HOME/.gradle/gradle.properties
80-
if (project.hasProperty('protoc_platform')) {
81-
artifact = "com.google.protobuf:protoc:${libs.versions.protobuf.get()}:${protoc_platform}"
82-
} else {
83-
artifact = "com.google.protobuf:protoc:${libs.versions.protobuf.get()}"
84-
}
85-
}
86-
87-
generateProtoTasks {
88-
all().each { task ->
89-
task.builtins {
90-
java {
91-
option "lite"
92-
}
93-
}
94-
}
95-
}
96-
}
65+
ext.livekitProtoIncludes = [
66+
'livekit_models.proto',
67+
'livekit_rtc.proto',
68+
'livekit_metrics.proto',
69+
]
70+
apply from: rootProject.file('gradle/livekit-protobuf.gradle')
9771

9872
jacoco {
9973
toolVersion = "0.8.14"

livekit-android-sdk/consumer-rules.pro

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,6 @@
4242

4343
# Protobuf
4444
#########################################
45-
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
45+
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
46+
<fields>;
47+
}

livekit-android-sdk/detekt-baseline-release.xml

Lines changed: 5 additions & 16 deletions
Large diffs are not rendered by default.

livekit-android-sdk/src/main/java/io/livekit/android/ConnectOptions.kt

Lines changed: 9 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-2026 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.
@@ -16,6 +16,7 @@
1616

1717
package io.livekit.android
1818

19+
import io.livekit.android.room.ClientProtocolVersion
1920
import io.livekit.android.room.ProtocolVersion
2021
import io.livekit.android.room.Room
2122
import livekit.org.webrtc.PeerConnection
@@ -53,6 +54,13 @@ data class ConnectOptions(
5354
* the protocol version to use with the server.
5455
*/
5556
val protocolVersion: ProtocolVersion = ProtocolVersion.v13,
57+
58+
/**
59+
* The client protocol version to advertise to other participants in the room
60+
* for peer-to-peer feature negotiation (RPC v2, etc.). Defaults to the latest
61+
* version supported by this SDK build.
62+
*/
63+
val clientProtocol: ClientProtocolVersion = ClientProtocolVersion.DATA_STREAM_RPC,
5664
) {
5765
internal var reconnect: Boolean = false
5866
internal var participantSid: String? = null

livekit-android-sdk/src/main/java/io/livekit/android/events/RoomEvent.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2025 LiveKit, Inc.
2+
* Copyright 2023-2026 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.
@@ -328,6 +328,7 @@ fun LivekitModels.DisconnectReason?.convert(): DisconnectReason {
328328
LivekitModels.DisconnectReason.SIP_TRUNK_FAILURE -> DisconnectReason.SIP_TRUNK_FAILURE
329329
LivekitModels.DisconnectReason.CONNECTION_TIMEOUT -> DisconnectReason.CONNECTION_TIMEOUT
330330
LivekitModels.DisconnectReason.MEDIA_FAILURE -> DisconnectReason.MEDIA_FAILURE
331+
LivekitModels.DisconnectReason.AGENT_ERROR,
331332
LivekitModels.DisconnectReason.UNKNOWN_REASON,
332333
LivekitModels.DisconnectReason.UNRECOGNIZED,
333334
null,

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ import io.livekit.android.room.participant.RpcHandler
6161
import io.livekit.android.room.participant.VideoTrackPublishDefaults
6262
import io.livekit.android.room.participant.publishTracksInfo
6363
import io.livekit.android.room.provisions.LKObjects
64+
import io.livekit.android.room.rpc.RPC_REQUEST_DATA_STREAM_TOPIC
65+
import io.livekit.android.room.rpc.RPC_RESPONSE_DATA_STREAM_TOPIC
66+
import io.livekit.android.room.rpc.RpcClientManager
6467
import io.livekit.android.room.rpc.RpcManager
68+
import io.livekit.android.room.rpc.RpcServerManager
6569
import io.livekit.android.room.track.LocalAudioTrackOptions
6670
import io.livekit.android.room.track.LocalTrackPublication
6771
import io.livekit.android.room.track.LocalVideoTrackOptions
@@ -146,6 +150,8 @@ constructor(
146150
private val connectionWarmer: ConnectionWarmer,
147151
private val audioRecordPrewarmer: AudioRecordPrewarmer,
148152
private val incomingDataStreamManager: IncomingDataStreamManager,
153+
private val rpcClientManager: RpcClientManager,
154+
private val rpcServerManager: RpcServerManager,
149155
private val remoteParticipantFactory: RemoteParticipant.Factory,
150156
) : RTCEngine.Listener, ParticipantListener, RpcManager, IncomingDataStreamManager by incomingDataStreamManager {
151157

@@ -155,6 +161,27 @@ constructor(
155161

156162
init {
157163
engine.listener = this
164+
165+
// Register SDK-internal text-stream handlers for the RPC v2 transport. These reserve
166+
// the topics `lk.rpc_request` and `lk.rpc_response` from user-level handler registration.
167+
incomingDataStreamManager.registerTextStreamHandler(RPC_REQUEST_DATA_STREAM_TOPIC) { receiver, fromIdentity ->
168+
coroutineScope.launch {
169+
rpcServerManager.handleIncomingDataStream(receiver, fromIdentity)
170+
}
171+
}
172+
incomingDataStreamManager.registerTextStreamHandler(RPC_RESPONSE_DATA_STREAM_TOPIC) { receiver, fromIdentity ->
173+
coroutineScope.launch {
174+
rpcClientManager.handleIncomingDataStreamResponse(receiver, fromIdentity)
175+
}
176+
}
177+
178+
// Wire each manager's clientProtocol lookup via the remote-participants store.
179+
val getRemoteClientProtocol: (Participant.Identity) -> Int = { id ->
180+
remoteParticipants[id]?.clientProtocol
181+
?: ClientProtocolVersion.DEFAULT.value
182+
}
183+
rpcClientManager.getRemoteClientProtocol = getRemoteClientProtocol
184+
rpcServerManager.getRemoteClientProtocol = getRemoteClientProtocol
158185
}
159186

160187
enum class State {
@@ -454,7 +481,7 @@ constructor(
454481
roomOptions = getCurrentRoomOptions()
455482

456483
// Setup local participant.
457-
localParticipant.reinitialize()
484+
localParticipant.reinitialize(options)
458485
setupLocalParticipantEventHandling()
459486

460487
if (roomOptions.e2eeOptions != null) {

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ constructor(
173173
// Clean up any pre-existing connection.
174174
close(reason = "Starting new connection", shouldClearQueuedRequests = false)
175175

176-
val wsUrlString = "${url.toWebsocketUrl()}/rtc${createConnectionParams(getClientInfo(), options, roomOptions)}"
176+
val wsUrlString = "${url.toWebsocketUrl()}/rtc${createConnectionParams(getClientInfo(options.clientProtocol), options, roomOptions)}"
177177
isReconnecting = options.reconnect
178178

179179
LKLog.i { "connecting to $wsUrlString" }
@@ -240,6 +240,7 @@ constructor(
240240
addParam(CONNECT_QUERY_OS, clientInfo.os)
241241
addParam(CONNECT_QUERY_OS_VERSION, clientInfo.osVersion)
242242
addParam(CONNECT_QUERY_NETWORK_TYPE, networkInfo.getNetworkType().protoName)
243+
addParam(CONNECT_QUERY_CLIENT_PROTOCOL, options.clientProtocol.value.toString())
243244

244245
return queryBuilder.toString()
245246
}
@@ -856,6 +857,18 @@ constructor(
856857
LivekitRtc.SignalResponse.MessageCase.SUBSCRIBED_AUDIO_CODEC_UPDATE -> {
857858
// TODO
858859
}
860+
861+
LivekitRtc.SignalResponse.MessageCase.PUBLISH_DATA_TRACK_RESPONSE -> {
862+
// TODO
863+
}
864+
865+
LivekitRtc.SignalResponse.MessageCase.UNPUBLISH_DATA_TRACK_RESPONSE -> {
866+
// TODO
867+
}
868+
869+
LivekitRtc.SignalResponse.MessageCase.DATA_TRACK_SUBSCRIBER_HANDLES -> {
870+
// TODO
871+
}
859872
}
860873
}
861874

@@ -959,6 +972,7 @@ constructor(
959972
const val CONNECT_QUERY_OS_VERSION = "os_version"
960973
const val CONNECT_QUERY_NETWORK_TYPE = "network"
961974
const val CONNECT_QUERY_PARTICIPANT_SID = "sid"
975+
const val CONNECT_QUERY_CLIENT_PROTOCOL = "client_protocol"
962976

963977
const val SD_TYPE_ANSWER = "answer"
964978
const val SD_TYPE_OFFER = "offer"
@@ -1012,6 +1026,27 @@ enum class ProtocolVersion(val value: Int) {
10121026
v13(13),
10131027
}
10141028

1029+
/**
1030+
* The protocol version this SDK advertises to **peers** (other participants) for
1031+
* client-to-client feature negotiation (RPC v2, etc.). Distinct from [ProtocolVersion],
1032+
* which tracks the signaling protocol between client and server.
1033+
*
1034+
* Sent to the server during the join handshake via the `client_protocol` connection
1035+
* query parameter and `ClientInfo.client_protocol`; the server then populates
1036+
* `ParticipantInfo.client_protocol` for other peers in the room to read.
1037+
*/
1038+
@Suppress("unused")
1039+
enum class ClientProtocolVersion(val value: Int) {
1040+
/** Initial client protocol. RPC v1 only (15 KB packet payload limit). */
1041+
DEFAULT(0),
1042+
1043+
/**
1044+
* RPC v2: request and success-response payloads are carried over text data streams
1045+
* instead of inline packets, lifting the 15 KB payload limit.
1046+
*/
1047+
DATA_STREAM_RPC(1),
1048+
}
1049+
10151050
class ServerInfo(
10161051
val edition: Edition,
10171052
val version: Semver?,

0 commit comments

Comments
 (0)