Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f865225
feat: add temporary maven local
1egoman Aug 5, 2026
975888b
chore(protocol): bump submodule to v1.50.4-4 for data streams v2 fields
1egoman Aug 5, 2026
6c4df62
build: consume livekit-uniffi and make it loadable from unit tests
1egoman Aug 5, 2026
506bd9e
feat(datastream): read peer client capabilities
1egoman Aug 5, 2026
666f773
feat(datastream): add the DataStreams FFI coordinator
1egoman Aug 5, 2026
b72830c
feat(datastream)!: back data streams with the Rust core
1egoman Aug 5, 2026
ada8932
feat(datastream): advertise data streams v2, and cover the FFI glue w…
1egoman Aug 5, 2026
d11b57e
style: apply spotless, and drain stragglers before RPC test tear-down
1egoman Aug 5, 2026
e64c274
feat(datastream): make the core's errors one to one with StreamException
1egoman Aug 6, 2026
808c713
revert: drop the unrelated RoomAgentDispatch.attributes addition
1egoman Aug 6, 2026
c9338e3
test(datastream): verify on a real device, and fix a 16 KB page-size …
1egoman Aug 6, 2026
80d6a31
build: fix the uniffi name collisions upstream, drop the patch script
1egoman Aug 6, 2026
fa009f0
fix(sample): stop LeakCanary crashing sample-app on launch
1egoman Aug 6, 2026
b991d5e
feat(sample): add a data streams panel to sample-app
1egoman Aug 6, 2026
b46b23c
fix(datastream): log every stream on an unhandled topic
1egoman Aug 7, 2026
ec3ade1
fix(datastream): hold an incoming stream to the encryption it opened …
1egoman Aug 7, 2026
5a2f33c
feat(datastream): adopt the new livekit-uniffi binding surface
1egoman Aug 18, 2026
70e4a75
refactor(datastream): drop the SDK-side encryption bookkeeping, now d…
1egoman Aug 18, 2026
784240a
feat(datastream): fail sends whose packets never reach the transport
1egoman Aug 18, 2026
c3a55da
fix(datastream): build a fresh incoming manager per session
1egoman Aug 18, 2026
dbe2cdc
test(datastream): expose the core's open-stream count for determinist…
1egoman Aug 18, 2026
bcc91bb
fix(datastream): stop rebuilding the incoming manager after close
1egoman Aug 18, 2026
27cfeff
fix(datastream): fail readers with a stream error when closed mid-read
1egoman Aug 18, 2026
2fe7ecb
fix: rename maxPayloadSize to maxPayloadByteLength to match other sdks
1egoman Aug 20, 2026
0b6e30d
remove test struts in prep for main merge
davidliu Sep 20, 2026
e5ec6ca
Mark the livekit-uniffi lib files as task inputs so that it's recheck…
davidliu Sep 20, 2026
00283b4
Hook up host library for ci tests
davidliu Sep 20, 2026
cd16fc6
Merge main into data-streams-v2
davidliu Sep 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,12 @@ jobs:
git fetch origin main --depth 1
./gradlew spotlessCheck

- name: Fetch livekit-uniffi host library
id: uniffi
run: echo "lib_dir=$(./ci/fetch_uniffi_cdylib.sh)" >> "$GITHUB_OUTPUT"

- name: Build and test with Gradle
run: ./gradlew assembleRelease livekit-android-test:testRelease
run: ./gradlew assembleRelease livekit-android-test:testRelease -PlivekitUniffiLibDir=${{ steps.uniffi.outputs.lib_dir }}

- name: Run Detekt
run: ./gradlew livekit-android-sdk:detektRelease
Expand Down
74 changes: 74 additions & 0 deletions ci/fetch_uniffi_cdylib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/bin/bash
set -e

# Downloads the prebuilt livekit-uniffi core for the host platform and prints the directory
# holding it, for `-PlivekitUniffiLibDir`.
#
# Unit tests in livekit-android-test call the Rust core through JNA, which needs a *host* build
# of it -- the livekit-uniffi AAR carries only Android .so files. rust-sdks attaches host cdylibs
# to each livekit-uniffi release, so CI can fetch one instead of installing a Rust toolchain and
# building the core from source.
#
# Local development doesn't need this: gradle/uniffi-native-lib.gradle finds a build in a sibling
# rust-sdks checkout on its own.

OUT_DIR=${1:-./build/livekit-uniffi-cdylib}

# Absolute from here on. Gradle resolves `-PlivekitUniffiLibDir` against the *subproject*
# (livekit-android-test), so a path relative to the repo root would silently miss.
mkdir -p "$OUT_DIR"
OUT_DIR=$(cd "$OUT_DIR" && pwd)

TAG=$(./ci/get_uniffi_tag.sh 2>/dev/null)

# Host triple, derived without rustc -- avoiding a Rust install is the point of this script.
case "$(uname -s)" in
Darwin) OS=apple-darwin ;;
Linux) OS=unknown-linux-gnu ;;
*) >&2 echo "error: unsupported OS $(uname -s)"; exit 1 ;;
esac
case "$(uname -m)" in
arm64|aarch64) ARCH=aarch64 ;;
x86_64|amd64) ARCH=x86_64 ;;
*) >&2 echo "error: unsupported architecture $(uname -m)"; exit 1 ;;
esac
TRIPLE="$ARCH-$OS"

ARCHIVE="build-$TRIPLE.zip"
BASE_URL="https://github.com/livekit/rust-sdks/releases/download/$TAG"
DEST="$OUT_DIR/$TRIPLE"

# Already fetched for this tag: the marker records which tag the contents came from, so a
# version bump re-downloads rather than silently testing against the previous core.
if [ -f "$DEST/.tag" ] && [ "$(cat "$DEST/.tag")" = "$TAG" ]; then
>&2 echo "livekit-uniffi $TAG for $TRIPLE already present"
echo "$DEST"
exit 0
fi

>&2 echo "fetching livekit-uniffi $TAG for $TRIPLE"

WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT

curl -fsSL --retry 3 -o "$WORK/$ARCHIVE" "$BASE_URL/$ARCHIVE"
curl -fsSL --retry 3 -o "$WORK/$ARCHIVE.sha256" "$BASE_URL/$ARCHIVE.sha256"

# The sidecar names the archive, so verify from its directory. Prefer sha256sum (coreutils) and
# fall back to shasum (always present on macOS). Both report "<file>: OK" on stdout, which would
# corrupt the directory path this script exists to print -- send it to stderr with the rest of
# the progress output.
if command -v sha256sum >/dev/null 2>&1; then
(cd "$WORK" && sha256sum -c "$ARCHIVE.sha256" >&2)
else
(cd "$WORK" && shasum -a 256 -c "$ARCHIVE.sha256" >&2)
fi

rm -rf "$DEST"
mkdir -p "$DEST"
unzip -o -q "$WORK/$ARCHIVE" -d "$DEST"
echo "$TAG" > "$DEST/.tag"

>&2 echo "extracted to $DEST"

echo "$DEST"
27 changes: 27 additions & 0 deletions ci/get_uniffi_tag.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash
set -e
set -x

# Prints the rust-sdks git tag matching the livekit-uniffi version this SDK builds against,
# e.g. `livekit-uniffi/v0.1.12`. Use it to check out the core that produced the published
# bindings -- notably when building the host library that unit tests load through JNA.

CATALOG=./gradle/libs.versions.toml

# Only the [versions] entry: `livekit-uniffi` also names a [libraries] entry, and a version
# catalog may declare a library as a plain "group:name:version" string.
UNIFFI_VERSION=$(awk '
/^\[/ { section = $0 }
section == "[versions]" && /^livekit-uniffi[[:space:]]*=/ {
if (match($0, /"[^"]*"/)) { print substr($0, RSTART + 1, RLENGTH - 2); exit }
}
' "$CATALOG")

if [ -z "$UNIFFI_VERSION" ]; then
>&2 echo "error: no livekit-uniffi version under [versions] in $CATALOG"
exit 1
fi

>&2 echo "livekit-uniffi version: $UNIFFI_VERSION"

echo "livekit-uniffi/v$UNIFFI_VERSION"
8 changes: 6 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ lifecycleProcess = "2.8.7"
agp = "8.7.2"
kotlin = "1.9.25"
livekit-uniffi = "0.1.12"
# Desktop JNA for the host unit-test classpath only: livekit-uniffi pulls in jna's *aar*, which
# ships Android .so dispatch libs, while Robolectric runs on the host JVM and needs the plain
# jar. Keep in step with the version livekit-uniffi resolves to; see gradle/uniffi-native-lib.gradle.
jna = "5.19.1"

[libraries]
livekit-uniffi = { module = "io.livekit:livekit-uniffi-android", version.ref = "livekit-uniffi" }

android-jain-sip-ri = { module = "javax.sip:android-jain-sip-ri", version.ref = "androidJainSipRi" }
androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidx-camera" }
Expand All @@ -62,6 +64,8 @@ androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process",
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanaryAndroid" }
livekit-uniffi = { module = "io.livekit:livekit-uniffi-android", version.ref = "livekit-uniffi" }
jna-desktop = { module = "net.java.dev.jna:jna", version.ref = "jna" }
okhttp-lib = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-coroutines = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobufJavalite" }
Expand Down
107 changes: 107 additions & 0 deletions gradle/uniffi-native-lib.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright 2026 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.
*/

// Points JNA at a *host* build of liblivekit_uniffi for local unit tests.
//
// Data streams are implemented in the Rust core, so any test that exercises them has to load
// the native library. Unit tests here run on the host JVM (Robolectric), but the
// livekit-uniffi AAR only ships Android .so files -- so the host build has to be supplied
// separately. This mirrors how client-sdk-swift's tests link the real xcframework rather than
// faking the core.
//
// Resolution order:
// 1. LIVEKIT_UNIFFI_LIB_DIR environment variable
// 2. livekitUniffiLibDir Gradle property
// 3. auto-discovery in a sibling rust-sdks checkout, which is the layout the LiveKit SDKs
// already assume for local uniffi development
//
// To produce the host library:
// cd ../rust-sdks/livekit-uniffi
// CARGO_PROFILE_RELEASE_STRIP=false cargo build --release --target <host triple>

import org.gradle.api.tasks.PathSensitivity

ext.resolveUniffiNativeLibDirs = {
def libNames = ["liblivekit_uniffi.dylib", "liblivekit_uniffi.so"]
def hasLib = { File dir -> dir.isDirectory() && libNames.any { new File(dir, it).isFile() } }

def explicit = System.getenv("LIVEKIT_UNIFFI_LIB_DIR") ?: project.findProperty("livekitUniffiLibDir")
if (explicit) {
def dir = file(explicit)
if (!hasLib(dir)) {
throw new GradleException(
"LIVEKIT_UNIFFI_LIB_DIR/livekitUniffiLibDir is set to '${dir}' but no " +
"liblivekit_uniffi.{dylib,so} was found there.",
)
}
return [dir]
}

// Sibling rust-sdks checkout. `target/<triple>/release` is where a --target build lands
// (the uniffi Makefile always passes --target); `target/release` covers a plain build.
def targetRoot = new File(rootDir, "../rust-sdks/target")
if (!targetRoot.isDirectory()) {
return []
}
def candidates = []
def plain = new File(targetRoot, "release")
if (hasLib(plain)) {
candidates << plain
}
targetRoot.listFiles({ File f -> f.isDirectory() } as FileFilter)?.sort { it.name }?.each { triple ->
def dir = new File(triple, "release")
if (hasLib(dir)) {
candidates << dir
}
}
return candidates
}

ext.configureUniffiForUnitTests = { Test task ->
// The generated bindings pick their cleaner by API level, and at 34+ use
// android.system.SystemCleaner. Robolectric's implementation of that delegates to
// jdk.internal.ref.CleanerFactory, which the module system hides by default -- so merely
// constructing an FFI object throws IllegalAccessError. Opening the package up lets the
// Android code path run as written, rather than forcing every test that touches the FFI down
// to an older Robolectric SDK.
task.jvmArgs += ["--add-exports", "java.base/jdk.internal.ref=ALL-UNNAMED"]

def dirs = resolveUniffiNativeLibDirs()
if (dirs.isEmpty()) {
task.doFirst {
logger.warn(
"[livekit] No host build of liblivekit_uniffi found. Tests that exercise data " +
"streams will fail to load the native library.\n" +
" Build it: cd ../rust-sdks/livekit-uniffi && " +
"CARGO_PROFILE_RELEASE_STRIP=false cargo build --release --target \$(rustc -vV | " +
"sed -n 's/^host: //p')\n" +
" Or point at one: -PlivekitUniffiLibDir=/path/to/dir",
)
}
return
}
// Add livekit-uniffi to the library search paths so that it can be found.
task.systemProperty("jna.library.path", dirs.collect { it.absolutePath }.join(File.pathSeparator))

// The core's *contents* are an input to every test that calls into it, but nothing above
// declares them: `jna.library.path` is just a path string, so rebuilding the library in place
// leaves the task's cache key unchanged and Gradle serves the previous run's results
// FROM-CACHE without executing anything. Registering the files themselves, hashed by content
// and not by location, makes a rebuilt core re-run the suite.
task.inputs.files(dirs.collect { dir -> fileTree(dir) { include "liblivekit_uniffi.*" } })
.withPropertyName("livekitUniffiNativeLib")
.withPathSensitivity(PathSensitivity.NONE)
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ data class ConnectOptions(
* for peer-to-peer feature negotiation (RPC v2, etc.). Defaults to the latest
* version supported by this SDK build.
*/
val clientProtocol: ClientProtocolVersion = ClientProtocolVersion.DATA_STREAM_RPC,
val clientProtocol: ClientProtocolVersion = ClientProtocolVersion.DATA_STREAM_V2,
Comment thread
1egoman marked this conversation as resolved.
) {
internal var reconnect: Boolean = false
internal var participantSid: String? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package io.livekit.android

import io.livekit.android.e2ee.E2EEOptions
import io.livekit.android.room.Room
import io.livekit.android.room.datastream.DataStreamOptions
import io.livekit.android.room.network.ReconnectPolicy
import io.livekit.android.room.participant.AudioTrackPublishDefaults
import io.livekit.android.room.participant.VideoTrackPublishDefaults
Expand Down Expand Up @@ -51,4 +52,9 @@ data class RoomOptions(
* @see [Room.reconnectPolicy]
*/
val reconnectPolicy: ReconnectPolicy? = null,

/**
* Room-wide data stream settings.
*/
val dataStreamOptions: DataStreamOptions = DataStreamOptions(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2026 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.room

import livekit.LivekitModels

/**
* An optional feature capability a client advertises at connect time.
*
* Capabilities are independent feature flags, in contrast to [ClientProtocolVersion], which is a
* single monotonic version number. A client may speak a given client protocol yet still lack an
* individual capability, so the two are negotiated separately.
*
* Advertised by this SDK during the join handshake, mirrored by the server onto every
* `ParticipantInfo`, and readable per-peer via [io.livekit.android.room.participant.Participant.capabilities].
*/
enum class ClientCapability(val value: Int) {
/**
* The client can accept RTP packet trailers passed through by the SFU instead of having
* them stripped.
*/
PACKET_TRAILER(1),

/**
* The client can decompress a `deflate-raw` compressed data stream payload.
*/
COMPRESSION_DEFLATE_RAW(2),
;

fun toProto(): LivekitModels.ClientInfo.Capability {
return when (this) {
PACKET_TRAILER -> LivekitModels.ClientInfo.Capability.CAP_PACKET_TRAILER
COMPRESSION_DEFLATE_RAW -> LivekitModels.ClientInfo.Capability.CAP_COMPRESSION_DEFLATE_RAW
}
}

companion object {
/**
* Converts from the protobuf enum, returning null for values this SDK build does not
* recognize.
*
* Unlike most `fromProto` helpers in this SDK this never throws: capabilities are an
* open, forward-extensible set, so a peer or server advertising a newer capability must
* be ignored rather than crash us.
*/
fun fromProto(capability: LivekitModels.ClientInfo.Capability): ClientCapability? {
return when (capability) {
LivekitModels.ClientInfo.Capability.CAP_PACKET_TRAILER -> PACKET_TRAILER
LivekitModels.ClientInfo.Capability.CAP_COMPRESSION_DEFLATE_RAW -> COMPRESSION_DEFLATE_RAW
LivekitModels.ClientInfo.Capability.CAP_UNUSED,
LivekitModels.ClientInfo.Capability.UNRECOGNIZED,
-> null
}
}
}
}

/**
* The capabilities this SDK advertises to the server and, through it, to peers.
*
* Declared once so that every place which announces capabilities agrees. Data streams v2
* compression is advertised unconditionally: deflate-raw is compressed and decompressed by the
* Rust data stream core, so support does not vary by device, API level, or room options.
*
* @suppress
*/
internal val ADVERTISED_CLIENT_CAPABILITIES = listOf(
ClientCapability.COMPRESSION_DEFLATE_RAW,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we also advertise the PACKET_TRAILER capability for our Android client?

@1egoman 1egoman Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK, android does not support stripping / processing packet trailers from video frames. If this capability isn't advertised, I believe the SFU will do the trailer stripping instead, but I may be misremembering, cc @chenosaurus for clarification here.

Once the android client is updated to support packet trailer processing (ie, stripping them out, and also extracting the metadata and providing apis to consume this metadata downstream) then this capability can be added to the advertisement here.

)
Loading
Loading