Skip to content

Commit 3a5e32c

Browse files
1egomandavidliu
andauthored
Increase data packet size check to 64KB in raw data packets (#948)
* feat: remove data packet size check * fix: address spotless format * fix: add changeset * Separate target size and put back max data size at 64kb * test * Update changeset * spotless --------- Co-authored-by: davidliu <davidliu@deviange.net>
1 parent f098324 commit 3a5e32c

6 files changed

Lines changed: 63 additions & 17 deletions

File tree

.changeset/tricky-carpets-fall.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+
Increased max data packet size for `LocalParticipant.publishData` to 65535 bytes (64KB - 1)

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,10 @@ internal constructor(
771771

772772
val packetBytes = dataPacket.toByteArray()
773773

774+
if (packetBytes.size > MAX_DATA_PACKET_SIZE) {
775+
return Result.failure(IllegalArgumentException("packet size (${packetBytes.size}) exceeds the max size (${MAX_DATA_PACKET_SIZE})"))
776+
}
777+
774778
if (isReliable && this.connectionState == ConnectionState.RECONNECTING) {
775779
reliableMessageBuffer.queue(DataPacketItem(ByteBuffer.wrap(packetBytes), dataPacket.sequence))
776780
reliableDataSequence++
@@ -1049,7 +1053,17 @@ internal constructor(
10491053
*/
10501054
@VisibleForTesting
10511055
const val LOSSY_DATA_CHANNEL_LABEL = "_lossy"
1052-
internal const val MAX_DATA_PACKET_SIZE = 15 * 1024 // 15 KB
1056+
internal const val TARGET_DATA_PACKET_SIZE = 15 * 1024 // 15 KB
1057+
1058+
/**
1059+
* Corresponds to the max-message-size in SDP. Attempting to send packets
1060+
* over this size will cause the data channel to close, so this must be enforced
1061+
* within the SDK. Note that [DataChannel.send] will still report true
1062+
* even if a huge packet is sent, so it's not a usable signal.
1063+
*
1064+
* TODO: get max-message-size from SDP or equivalent from libwebrtc.
1065+
*/
1066+
internal const val MAX_DATA_PACKET_SIZE = 64 * 1024 - 1 // 64 KB
10531067
private const val MAX_RECONNECT_RETRIES = 30
10541068
private const val MAX_RECONNECT_TIMEOUT = 60 * 1000
10551069
private const val MAX_ICE_CONNECT_TIMEOUT_MS = 20000

livekit-android-sdk/src/main/java/io/livekit/android/room/datastream/outgoing/OutgoingDataStreamManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ constructor(
289289
if (!isOpen) {
290290
return Result.failure(StreamException.TerminatedException("Stream is closed!"))
291291
}
292-
val chunks = chunker.invoke(data, RTCEngine.MAX_DATA_PACKET_SIZE)
292+
val chunks = chunker.invoke(data, RTCEngine.TARGET_DATA_PACKET_SIZE)
293293

294294
for (chunk in chunks) {
295295
val result = sendChunk(streamId, chunk)

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

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -964,7 +964,7 @@ internal constructor(
964964

965965
/**
966966
* Publish a new data payload to the room. Data will be forwarded to each participant in the room.
967-
* Each payload must not exceed 15k in size
967+
* Each payload must not exceed 65535 bytes (64KB - 1) in size.
968968
*
969969
* @param data payload to send
970970
* @param reliability for delivery guarantee, use RELIABLE. for fastest delivery without guarantee, use LOSSY
@@ -981,10 +981,6 @@ internal constructor(
981981
topic: String? = null,
982982
identities: List<Identity>? = null,
983983
): Result<Unit> {
984-
if (data.size > RTCEngine.MAX_DATA_PACKET_SIZE) {
985-
return Result.failure(IllegalArgumentException("cannot publish data larger than " + RTCEngine.MAX_DATA_PACKET_SIZE))
986-
}
987-
988984
val kind = when (reliability) {
989985
DataPublishReliability.RELIABLE -> DataPacket.Kind.RELIABLE
990986
DataPublishReliability.LOSSY -> DataPacket.Kind.LOSSY
@@ -1108,7 +1104,7 @@ internal constructor(
11081104
// one second to complete, even after accounting for round-trip latency.
11091105
val minEffectiveTimeout = 1.seconds
11101106

1111-
if (payload.byteLength() > RTCEngine.MAX_DATA_PACKET_SIZE) {
1107+
if (payload.byteLength() > RpcError.MAX_V1_PAYLOAD_BYTES) {
11121108
throw RpcError.BuiltinRpcError.REQUEST_PAYLOAD_TOO_LARGE.create()
11131109
}
11141110

@@ -1209,8 +1205,8 @@ internal constructor(
12091205
payload: String,
12101206
responseTimeout: Duration = 10.seconds,
12111207
): Result<Unit> {
1212-
if (payload.byteLength() > RTCEngine.MAX_DATA_PACKET_SIZE) {
1213-
throw IllegalArgumentException("cannot publish data larger than " + RTCEngine.MAX_DATA_PACKET_SIZE)
1208+
if (payload.byteLength() > RpcError.MAX_V1_PAYLOAD_BYTES) {
1209+
return Result.failure(RpcError.BuiltinRpcError.REQUEST_PAYLOAD_TOO_LARGE.create())
12141210
}
12151211

12161212
val dataPacket = with(DataPacket.newBuilder()) {
@@ -1237,8 +1233,8 @@ internal constructor(
12371233
payload: String?,
12381234
error: RpcError?,
12391235
): Result<Unit> {
1240-
if (payload.byteLength() > RTCEngine.MAX_DATA_PACKET_SIZE) {
1241-
throw IllegalArgumentException("cannot publish data larger than " + RTCEngine.MAX_DATA_PACKET_SIZE)
1236+
if (payload.byteLength() > RpcError.MAX_V1_PAYLOAD_BYTES) {
1237+
return Result.failure(RpcError.BuiltinRpcError.RESPONSE_PAYLOAD_TOO_LARGE.create())
12421238
}
12431239

12441240
val dataPacket = with(DataPacket.newBuilder()) {
@@ -1359,7 +1355,7 @@ internal constructor(
13591355
),
13601356
)
13611357

1362-
if (response.byteLength() > RTCEngine.MAX_DATA_PACKET_SIZE) {
1358+
if (response.byteLength() > RpcError.MAX_V1_PAYLOAD_BYTES) {
13631359
responseError = RpcError.BuiltinRpcError.RESPONSE_PAYLOAD_TOO_LARGE.create()
13641360
LKLog.w { "RPC Response payload too large for $method" }
13651361
} else {
@@ -1980,7 +1976,8 @@ private fun isBackupCodec(codecName: String) = backupCodecs.contains(codecName)
19801976

19811977
/**
19821978
* A handler that processes an RPC request and returns a string
1983-
* that will be sent back to the requester.
1979+
* that will be sent back to the requester. The payload must
1980+
* be less than 15KB in size.
19841981
*
19851982
* Throwing an [RpcError] will send the error back to the requester.
19861983
*

livekit-android-sdk/src/main/java/io/livekit/android/rpc/RpcError.kt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2025 LiveKit, Inc.
2+
* Copyright 2025-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,7 +16,7 @@
1616

1717
package io.livekit.android.rpc
1818

19-
import io.livekit.android.room.RTCEngine
19+
import io.livekit.android.rpc.RpcError.Companion.MAX_V1_PAYLOAD_BYTES
2020
import io.livekit.android.util.truncateBytes
2121
import livekit.LivekitModels
2222

@@ -57,11 +57,19 @@ data class RpcError(
5757
CONNECTION_TIMEOUT(1501, "Connection timeout"),
5858
RESPONSE_TIMEOUT(1502, "Response timeout"),
5959
RECIPIENT_DISCONNECTED(1503, "Recipient disconnected"),
60+
61+
/**
62+
* @see [MAX_V1_PAYLOAD_BYTES]
63+
*/
6064
RESPONSE_PAYLOAD_TOO_LARGE(1504, "Response payload too large"),
6165
SEND_FAILED(1505, "Failed to send"),
6266

6367
UNSUPPORTED_METHOD(1400, "Method not supported at destination"),
6468
RECIPIENT_NOT_FOUND(1401, "Recipient not found"),
69+
70+
/**
71+
* @see [MAX_V1_PAYLOAD_BYTES]
72+
*/
6573
REQUEST_PAYLOAD_TOO_LARGE(1402, "Request payload too large"),
6674
UNSUPPORTED_SERVER(1403, "RPC not supported by server"),
6775
UNSUPPORTED_VERSION(1404, "Unsupported RPC version"),
@@ -75,11 +83,16 @@ data class RpcError(
7583
companion object {
7684
const val MAX_MESSAGE_BYTES = 256
7785

86+
/**
87+
* The maximum payload size for v1 RPC requests and responses in bytes.
88+
*/
89+
const val MAX_V1_PAYLOAD_BYTES = 15 * 1024 // 15KB
90+
7891
fun fromProto(proto: LivekitModels.RpcError): RpcError {
7992
return RpcError(
8093
code = proto.code,
8194
message = (proto.message ?: "").truncateBytes(MAX_MESSAGE_BYTES),
82-
data = proto.data.truncateBytes(RTCEngine.MAX_DATA_PACKET_SIZE),
95+
data = proto.data.truncateBytes(MAX_V1_PAYLOAD_BYTES),
8396
)
8497
}
8598
}

livekit-android-test/src/test/java/io/livekit/android/room/RTCEngineMockE2ETest.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,23 @@ class RTCEngineMockE2ETest : MockE2ETest() {
416416
assertEquals(TestData.JOIN.join.participant.sid, sid)
417417
}
418418

419+
@Test
420+
fun publishDataRejectsLargePacket() = runTest {
421+
connect()
422+
val pubDataChannel = getPublisherPeerConnection()
423+
.dataChannels[RTCEngine.RELIABLE_DATA_CHANNEL_LABEL] as MockDataChannel
424+
425+
val oversizedPayload = ByteArray(65 * 1024) // See RTCEngine.MAX_DATA_PACKET_SIZE
426+
val result = room.localParticipant.publishData(oversizedPayload)
427+
428+
assertTrue(result.isFailure)
429+
assertTrue(
430+
"Expected IllegalArgumentException, got ${result.exceptionOrNull()}",
431+
result.exceptionOrNull() is IllegalArgumentException,
432+
)
433+
assertEquals(0, pubDataChannel.sentBuffers.size)
434+
}
435+
419436
@Test
420437
fun resendReliableMessagesReplaysFullPayloadAcrossMultipleResumes() = runTest {
421438
connect()

0 commit comments

Comments
 (0)