Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 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
bef88c6
Suppress the two detekt findings the FFI boundary forces
davidliu Sep 20, 2026
8af515f
Guard against loadLibrary failures
davidliu Sep 21, 2026
484aea4
test fixes
davidliu Sep 21, 2026
1b3294a
guard against destroyed incoming manager race condition
davidliu Sep 21, 2026
26f0355
release readers for streams on unhandled topics
davidliu Sep 21, 2026
f256a1e
release reader and writer handles instead of leaving them to the cleaner
davidliu Sep 21, 2026
d93f31c
ci fixes
davidliu Sep 21, 2026
864ffb9
map every FFI failure onto StreamException
davidliu Sep 21, 2026
9d39c83
cleanup unneeded manifest declaration
davidliu Sep 21, 2026
966efcc
restore the RPC test helper docs to their own functions
davidliu Sep 21, 2026
e0e7fa2
document the threading contract on the stream handler typealiases
davidliu Sep 24, 2026
1863314
update to livekit-uniffi 0.2.0
davidliu Sep 24, 2026
ed42325
Regenerate the detekt release baseline
davidliu Oct 6, 2026
1a747f0
clean up duplicate uniffi dependency declaration
davidliu Oct 6, 2026
c98bd6c
Validate the uniffi lib dir that was selected, not every input
davidliu Oct 6, 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
8 changes: 7 additions & 1 deletion .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,14 @@ jobs:
git fetch origin main --depth 1
./gradlew spotlessCheck

- name: Fetch livekit-uniffi host library
id: uniffi
run: |
lib_dir=$(./ci/fetch_uniffi_cdylib.sh)
echo "lib_dir=$lib_dir" >> "$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)

# 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"
26 changes: 26 additions & 0 deletions ci/get_uniffi_tag.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/bin/bash
set -e

# 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"
10 changes: 7 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ noise = "2.0.0"
lifecycleProcess = "2.8.7"
agp = "8.7.2"
kotlin = "1.9.25"
livekit-uniffi = "0.1.12"
livekit-uniffi = "0.2.0"
# 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
126 changes: 126 additions & 0 deletions gradle/uniffi-native-lib.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* 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 rawEnv = System.getenv("LIVEKIT_UNIFFI_LIB_DIR")
def rawProp = project.findProperty("livekitUniffiLibDir")?.toString()

// Blank means whatever was supposed to produce the path failed quietly -- a CI step whose
// command substitution swallowed an error, say. Trimmed rather than left to Groovy's
// truthiness, which counts a whitespace-only string as set and would hand `file()` a junk
// path.
def explicit = rawEnv?.trim() ?: rawProp?.trim()

// Blank is unset for the purpose of precedence above, so a valid lower-priority source still
// wins. It is not unset here: falling through to auto-discovery when a path was asked for and
// nothing came back would run the FFI tests against whatever core happens to be lying around,
// or none at all.
if (!explicit && (rawEnv != null || rawProp != null)) {
throw new GradleException(
"LIVEKIT_UNIFFI_LIB_DIR/livekitUniffiLibDir is set but empty; expected a " +
"directory holding a host build of liblivekit_uniffi.",
)
}

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)
}
Loading
Loading