-
Notifications
You must be signed in to change notification settings - Fork 138
Get Rust SDK Android Example working #859
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
📝 WalkthroughWalkthroughThis PR integrates LiveKit audio capabilities into a mobile example project by updating Gradle and build configurations to Java 17/Android API 34, adding audio I/O management via a new MediaManager Kotlin class, implementing Rust-side audio capture/playback with room connection logic, and exposing JNI and FFI interfaces to bridge native and application layers. Changes
Sequence DiagramssequenceDiagram
participant Android as Android App
participant MediaMgr as MediaManager
participant Rust as Rust (lib.rs)
participant LiveKit as LiveKit Room
Android->>Rust: livekit_connect(url, token)
activate Rust
Rust->>LiveKit: Connect & Initialize Room
activate LiveKit
LiveKit-->>Rust: Room Connected
deactivate LiveKit
Rust->>Rust: Create AudioState (capture/playback buffers)
Rust-->>Android: Connection Established
deactivate Rust
Android->>MediaMgr: startMicrophone()
activate MediaMgr
MediaMgr->>MediaMgr: Create AudioRecord, spawn capture thread
MediaMgr-->>Android: Recording Started
deactivate MediaMgr
loop Audio Capture Loop
MediaMgr->>MediaMgr: Read from microphone
MediaMgr->>Rust: push_audio_capture(samples)
activate Rust
Rust->>Rust: Buffer and convert to AudioFrame
Rust->>LiveKit: Publish audio track
LiveKit-->>Rust: ACK
Rust-->>MediaMgr: Bytes processed
deactivate Rust
end
loop Audio Playback Loop
LiveKit->>Rust: Remote audio frames (subscription)
activate Rust
Rust->>Rust: Decode and buffer samples
deactivate Rust
MediaMgr->>Rust: pull_audio_playback(buffer)
activate Rust
Rust->>Rust: Drain playback buffer (or silence)
Rust-->>MediaMgr: Playback samples
deactivate Rust
MediaMgr->>MediaMgr: Write to speaker (AudioTrack)
end
Android->>MediaMgr: stopMicrophone()
Android->>Rust: livekit_disconnect()
activate Rust
Rust->>LiveKit: Disconnect Room
Rust-->>Android: Disconnected
deactivate Rust
sequenceDiagram
participant User as User
participant MainActivity as MainActivity
participant App as App
participant MediaMgr as MediaManager
participant Rust as Rust
User->>MainActivity: Enter URL & Token
User->>MainActivity: Tap Connect
MainActivity->>App: connect(url, token)
activate App
App->>Rust: connectNative(url, token)
activate Rust
Rust->>Rust: Initialize LiveKit audio state
Rust-->>App: Connection Success
deactivate Rust
App->>MainActivity: onConnected
deactivate App
MainActivity->>MainActivity: Update UI (show audio controls)
User->>MainActivity: Tap Start Audio
MainActivity->>MediaMgr: startAll()
activate MediaMgr
MediaMgr->>MediaMgr: Request RECORD_AUDIO permission
alt Permission Granted
MediaMgr->>MediaMgr: Start microphone & speaker threads
MediaMgr-->>MainActivity: Audio Active
else Permission Denied
MediaMgr-->>MainActivity: Permission Error
end
deactivate MediaMgr
MainActivity->>MainActivity: Update isAudioActive state
User->>MainActivity: Tap Stop Audio
MainActivity->>MediaMgr: stopAll()
MediaMgr->>MediaMgr: Stop capture & playback threads
MediaMgr-->>MainActivity: Audio Stopped
MainActivity->>MainActivity: Update UI
User->>MainActivity: Tap Disconnect
MainActivity->>App: disconnect()
activate App
App->>Rust: disconnectNative()
Rust->>Rust: Close LiveKit room
Rust-->>App: Disconnected
deactivate App
MainActivity->>MainActivity: Reset UI to disconnected state
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
examples/mobile/src/lib.rs (1)
401-406:⚠️ Potential issue | 🟠 MajorGuard FFI inputs to avoid UB/panics.
Line 401-405 assumes non-null UTF‑8; invalid pointers/UTF‑8 will panic or cause UB.
🛠️ Suggested fix
#[no_mangle] pub extern "C" fn livekit_connect(url: *const c_char, token: *const c_char) { - let (url, token) = unsafe { - let url = CStr::from_ptr(url).to_str().unwrap().to_owned(); - let token = CStr::from_ptr(token).to_str().unwrap().to_owned(); - (url, token) - }; + if url.is_null() || token.is_null() { + return; + } + let url = unsafe { CStr::from_ptr(url) }; + let token = unsafe { CStr::from_ptr(token) }; + let (url, token) = match (url.to_str(), token.to_str()) { + (Ok(u), Ok(t)) => (u.to_owned(), t.to_owned()), + _ => return, + }; super::livekit_connect(url, token); }
🤖 Fix all issues with AI agents
In `@examples/mobile/android/.idea/compiler.xml`:
- Line 4: The compiler.xml currently sets <bytecodeTargetLevel target="21" />
which mismatches build.gradle's Java 17 settings (sourceCompatibility,
targetCompatibility, jvmTarget); fix by making the two consistent—either change
the bytecodeTargetLevel value in compiler.xml to 17 to match build.gradle, or
update build.gradle's sourceCompatibility/targetCompatibility/jvmTarget to 21 if
Java 21 is intended—ensure you update the specific symbol <bytecodeTargetLevel
target="..."/> and the sourceCompatibility/targetCompatibility/jvmTarget entries
so IDE and Gradle targets align.
In `@examples/mobile/android/.idea/deploymentTargetSelector.xml`:
- Line 10: Remove the hardcoded physical device serial entry from the committed
config: delete or replace the <DeviceId ... identifier="serial=R3CW4047CGT" />
line (the DeviceId element containing the serial value "R3CW4047CGT") so no
personal device identifiers are tracked, and add
examples/mobile/android/.idea/deploymentTargetSelector.xml to .gitignore (or
remove the .idea file from repo) to prevent committing machine-specific
IDE/device configs going forward.
In
`@examples/mobile/android/app/src/main/java/io/livekit/rustexample/MainActivity.kt`:
- Around line 109-115: The code currently sets isConnected.value = true and
statusMessage.value = "Connected to room" inside a
Handler(mainLooper).postDelayed(..., 1000), which marks the app connected after
a fixed delay; remove that fixed-delay logic and instead update isConnected and
statusMessage from the actual connection-success callback/event coming from the
native layer (or the LiveKit connection listener) — subscribe to the
room/connection success callback provided by your native binding, call
isConnected.value = true, statusMessage.value = "Connected to room" and
Log.i(TAG, ...) only when that callback fires, and handle connection failure by
setting isConnected false and updating statusMessage accordingly (use the same
identifiers: isConnected, statusMessage, TAG, and replace the
Handler(mainLooper).postDelayed block).
- Around line 35-41: The companion object in MainActivity currently hardcodes a
sensitive JWT in DEFAULT_TOKEN; remove the real token and replace it with a
non-secret placeholder (e.g., "REPLACE_WITH_TOKEN") and update code that reads
DEFAULT_TOKEN to instead load the token at runtime (from secure storage,
BuildConfig field, Intent extras, environment/config, or user input).
Specifically, edit the companion object constant DEFAULT_TOKEN and ensure any
code paths using DEFAULT_TOKEN (in MainActivity) can accept a runtime-supplied
value so secrets are not committed to source control.
In
`@examples/mobile/android/app/src/main/java/io/livekit/rustexample/MediaManager.kt`:
- Around line 240-244: startAll() may leave one audio component running when the
other fails; make it atomic by rolling back any started component on failure.
Call startMicrophone() then startSpeaker(); if startSpeaker() fails but
startMicrophone() returned true, call stopMicrophone() before returning false
(and symmetrically if you call startSpeaker() first). Use startAll(),
startMicrophone(), startSpeaker(), stopMicrophone(), and stopSpeaker() to
implement this rollback and ensure the method returns true only when both starts
succeed.
In `@examples/mobile/android/gradle/wrapper/gradle-wrapper.properties`:
- Line 3: The example project is pinning a pre-release Gradle distribution
("distributionUrl" set to gradle-9.0-milestone-1) which can cause unstable
builds; update the distributionUrl value to use the latest stable Gradle release
(replace "gradle-9.0-milestone-1" with "gradle-9.3.1") so the wrapper points to
the stable bin distribution URL instead of the milestone build.
In `@examples/mobile/src/lib.rs`:
- Around line 112-135: The CI failed rustfmt; run cargo fmt to reformat the
affected lines around the LocalAudioTrack::create_audio_track call and the
room.local_participant().publish_track await chain (the block that constructs
TrackPublishOptions and calls publish_track on LocalTrack::Audio). Re-run
rustfmt/cargo fmt and commit the resulting formatting changes so the
create_audio_track, TrackPublishOptions { ..Default::default() }, and
publish_track(...).await lines conform to the project’s formatter.
- Around line 71-74: The log in function livekit_connect currently prints the
full token (log::info!("Connecting to {} with token {}", url, token)); change
this to avoid leaking credentials by redacting or masking the token before
logging — e.g., log only a safe substring or token length (mask middle
characters) or remove the token from the message entirely; update the log::info
call in livekit_connect to use the masked_token representation instead of the
raw token.
- Around line 466-474: The JNI string conversions in
Java_io_livekit_rustexample_App_connectNative currently call
env.get_string(...).unwrap(), which will panic on JNI errors and cross the FFI
boundary; change this to safely handle the Result by matching or using if let to
detect errors from env.get_string for both url and token, log or report the JNI
error (rather than panicking), and return early from
Java_io_livekit_rustexample_App_connectNative when conversion fails so no panic
crosses FFI; update references to the converted values only after successful
conversion.
🧹 Nitpick comments (2)
examples/mobile/android/.idea/AndroidProjectSystem.xml (1)
1-6: Consider whether.idea/files should be committed.IDE-specific configuration files under
.idea/can cause issues when team members use different IDE versions or configurations. Some files (like this one) are benign, but others in this PR contain local paths or machine-specific settings.Common practice is to either:
- Exclude most
.idea/files via.gitignore(exceptcodeStyles/if team shares formatting)- Or commit them all for IDE consistency across the team
examples/mobile/android/.idea/misc.xml (1)
1-10: Consider excluding.ideafiles from version control.IDE configuration files like
misc.xmlcontain local machine settings (JDK paths, project-specific IDE preferences) that may vary between developers. These files are typically added to.gitignoreto avoid unnecessary merge conflicts and to prevent enforcing specific IDE configurations on contributors.If the intent is to provide a ready-to-use example project, this is acceptable, but consider documenting the JDK 21 requirement in a README instead.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
examples/mobile/android/app/libs/libwebrtc.jaris excluded by!**/*.jarexamples/mobile/android/app/src/main/jniLibs/arm64-v8a/libmobile.sois excluded by!**/*.soexamples/mobile/android/gradle/wrapper/gradle-wrapper.jaris excluded by!**/*.jar
📒 Files selected for processing (21)
examples/mobile/Cargo.tomlexamples/mobile/android/.idea/AndroidProjectSystem.xmlexamples/mobile/android/.idea/codeStyles/Project.xmlexamples/mobile/android/.idea/codeStyles/codeStyleConfig.xmlexamples/mobile/android/.idea/compiler.xmlexamples/mobile/android/.idea/deploymentTargetSelector.xmlexamples/mobile/android/.idea/gradle.xmlexamples/mobile/android/.idea/migrations.xmlexamples/mobile/android/.idea/misc.xmlexamples/mobile/android/.idea/runConfigurations.xmlexamples/mobile/android/.idea/vcs.xmlexamples/mobile/android/app/build.gradleexamples/mobile/android/app/src/main/AndroidManifest.xmlexamples/mobile/android/app/src/main/java/io/livekit/rustexample/App.ktexamples/mobile/android/app/src/main/java/io/livekit/rustexample/MainActivity.ktexamples/mobile/android/app/src/main/java/io/livekit/rustexample/MediaManager.ktexamples/mobile/android/build.gradleexamples/mobile/android/gradle/wrapper/gradle-wrapper.propertiesexamples/mobile/android/gradlewexamples/mobile/android/gradlew.batexamples/mobile/src/lib.rs
🧰 Additional context used
🧬 Code graph analysis (1)
examples/mobile/android/app/src/main/java/io/livekit/rustexample/MainActivity.kt (1)
examples/mobile/android/app/src/main/java/io/livekit/rustexample/ui/theme/Theme.kt (1)
RustexampleTheme(40-70)
🪛 Blinter (1.0.102)
examples/mobile/android/gradlew.bat
[error] 88-88: Complex quote escaping error. Explanation: Complex quote patterns may not be handled correctly by the command interpreter. Recommendation: Use triple-quote pattern """text""" for quotes within quoted strings, or escape properly with variables. Context: Complex quote escaping detected
(E028)
🪛 GitHub Actions: Rust Formatting
examples/mobile/src/lib.rs
[error] 117-117: Rustfmt diff detected at line 117. Run 'cargo fmt' to apply formatting changes.
[error] 127-127: Rustfmt diff detected at line 127. Run 'cargo fmt' to apply formatting changes.
🪛 Gitleaks (8.30.0)
examples/mobile/android/app/src/main/java/io/livekit/rustexample/MainActivity.kt
[high] 40-40: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.
(jwt)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Test (x86_64-apple-darwin)
- GitHub Check: Test (x86_64-unknown-linux-gnu)
- GitHub Check: Test (x86_64-pc-windows-msvc)
🔇 Additional comments (16)
examples/mobile/android/gradle/wrapper/gradle-wrapper.properties (1)
4-5: LGTM!Adding
networkTimeoutandvalidateDistributionUrlare sensible defaults for more robust wrapper behavior.examples/mobile/android/.idea/migrations.xml (1)
1-10: LGTM!Standard Android Studio migration configuration using portable
$PROJECT_DIR$reference.examples/mobile/android/.idea/gradle.xml (1)
3-9: LGTM!The Gradle configuration updates use portable references (
#GRADLE_LOCAL_JAVA_HOME) and theCHOOSE_PER_TESTrunner provides flexibility for test execution.examples/mobile/android/.idea/vcs.xml (1)
5-6: LGTM!VCS mappings for submodule directories using portable
$PROJECT_DIR$references.examples/mobile/android/.idea/codeStyles/codeStyleConfig.xml (1)
1-5: LGTM!Enabling per-project code style settings ensures consistent formatting across team members.
examples/mobile/Cargo.toml (1)
12-13: LGTM!The
futuresandparking_lotdependencies are well-established crates appropriate for async audio I/O and efficient synchronization in the LiveKit integration.examples/mobile/android/.idea/runConfigurations.xml (1)
1-17: Standard IDE configuration.This suppresses unwanted run configuration suggestions. Same consideration applies as with other
.ideafiles regarding version control inclusion.examples/mobile/android/app/build.gradle (1)
8-8: LGTM!The build configuration updates are well-aligned:
- SDK 34 is the current stable target
- Java 17 compatibility with matching
jvmTarget- Kotlin compiler extension 1.5.8 is compatible with Kotlin 1.9.22
- Dependency versions are recent and consistent
Also applies to: 13-13, 29-34, 40-40, 51-55, 63-63
examples/mobile/android/build.gradle (1)
3-5: LGTM!Plugin version updates are consistent with the app-level build configuration and align with modern Android tooling.
examples/mobile/android/gradlew.bat (3)
16-17: LGTM on license header addition.Adding the SPDX license identifier is a good practice for license clarity.
46-53: Improved error handling.The changes from
==toequfor numeric comparisons (ERRORLEVEL) and redirecting errors to stderr (1>&2) follow Windows batch best practices.Also applies to: 62-66, 81-81
86-89: The original pattern on line 88 is correct; the suggested refactor would change behavior incorrectly.The pattern
if not ""=="%GRADLE_EXIT_CONSOLE%"is a standard, idiomatic Windows batch check for non-empty strings and is documented as a best practice. The suggested alternativeif defined GRADLE_EXIT_CONSOLEis not equivalent: it checks whether the variable is defined (exists), whereas the original checks whether it is non-empty. Usingif definedwould execute the exit regardless of whetherGRADLE_EXIT_CONSOLEis set to an empty value, changing the script's intended behavior. No changes needed on line 88.Likely an incorrect or invalid review comment.
examples/mobile/android/.idea/codeStyles/Project.xml (1)
1-123: LGTM: Project-level code style config is consistent.No issues spotted in this IDE-only configuration.
examples/mobile/android/gradlew (1)
1-251: LGTM: POSIX wrapper update looks standard.No issues found.
examples/mobile/android/app/src/main/AndroidManifest.xml (1)
5-7: LGTM: audio permissions added.Looks appropriate for microphone + audio output use.
examples/mobile/android/app/src/main/java/io/livekit/rustexample/App.kt (1)
1-90: LGTM: native availability guard and API surface look good.No issues found.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| <project version="4"> | ||
| <component name="CompilerConfiguration"> | ||
| <bytecodeTargetLevel target="17" /> | ||
| <bytecodeTargetLevel target="21" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
fd -t f 'build.gradle' examples/mobile/android --exec grep -n -E '(sourceCompatibility|targetCompatibility|jvmTarget|JavaVersion)' {} \;Repository: livekit/rust-sdks
Length of output: 196
Bytecode target level mismatches build.gradle Java version.
compiler.xml sets bytecodeTargetLevel to 21, but build.gradle configures Java 17 for sourceCompatibility, targetCompatibility, and jvmTarget. This inconsistency can cause the IDE to behave differently from command-line Gradle builds.
Align the bytecode target level in compiler.xml to match Java 17, or update build.gradle to use Java 21 if that's the intended target.
🤖 Prompt for AI Agents
In `@examples/mobile/android/.idea/compiler.xml` at line 4, The compiler.xml
currently sets <bytecodeTargetLevel target="21" /> which mismatches
build.gradle's Java 17 settings (sourceCompatibility, targetCompatibility,
jvmTarget); fix by making the two consistent—either change the
bytecodeTargetLevel value in compiler.xml to 17 to match build.gradle, or update
build.gradle's sourceCompatibility/targetCompatibility/jvmTarget to 21 if Java
21 is intended—ensure you update the specific symbol <bytecodeTargetLevel
target="..."/> and the sourceCompatibility/targetCompatibility/jvmTarget entries
so IDE and Gradle targets align.
| <DropdownSelection timestamp="2026-01-31T06:23:52.591285Z"> | ||
| <Target type="DEFAULT_BOOT"> | ||
| <handle> | ||
| <DeviceId pluginId="PhysicalDevice" identifier="serial=R3CW4047CGT" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoded device serial number should not be committed.
This file contains a device-specific serial number (R3CW4047CGT) that is unique to your physical device. Other contributors will not have this device, making this configuration ineffective and potentially confusing. This file should be added to .gitignore.
🤖 Prompt for AI Agents
In `@examples/mobile/android/.idea/deploymentTargetSelector.xml` at line 10,
Remove the hardcoded physical device serial entry from the committed config:
delete or replace the <DeviceId ... identifier="serial=R3CW4047CGT" /> line (the
DeviceId element containing the serial value "R3CW4047CGT") so no personal
device identifiers are tracked, and add
examples/mobile/android/.idea/deploymentTargetSelector.xml to .gitignore (or
remove the .idea file from repo) to prevent committing machine-specific
IDE/device configs going forward.
| companion object { | ||
| private const val TAG = "MainActivity" | ||
|
|
||
| // Default connection values - change these for your setup | ||
| private const val DEFAULT_URL = "ws://192.168.1.3:7880" | ||
| private const val DEFAULT_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzQyMDQ4MzQsImlkZW50aXR5IjoiZGMiLCJpc3MiOiJkZXZrZXkiLCJuYW1lIjoiZGMiLCJuYmYiOjE3NjgyMDQ4MzQsInN1YiI6ImRjIiwidmlkZW8iOnsicm9vbSI6ImNwcCIsInJvb21Kb2luIjp0cnVlfX0.e5dGHaTnEw6kToAJ_Z4SbhVuTKQ-3lUrMYhRxhE3Mu4" | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove hardcoded JWT from source.
Line 40 contains a real-looking token (also flagged by Gitleaks). Avoid committing secrets; use a placeholder or user input instead.
🛠️ Suggested fix
- private const val DEFAULT_TOKEN = "<redacted>"
+ private const val DEFAULT_TOKEN = "your-token-here"🧰 Tools
🪛 Gitleaks (8.30.0)
[high] 40-40: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.
(jwt)
🤖 Prompt for AI Agents
In
`@examples/mobile/android/app/src/main/java/io/livekit/rustexample/MainActivity.kt`
around lines 35 - 41, The companion object in MainActivity currently hardcodes a
sensitive JWT in DEFAULT_TOKEN; remove the real token and replace it with a
non-secret placeholder (e.g., "REPLACE_WITH_TOKEN") and update code that reads
DEFAULT_TOKEN to instead load the token at runtime (from secure storage,
BuildConfig field, Intent extras, environment/config, or user input).
Specifically, edit the companion object constant DEFAULT_TOKEN and ensure any
code paths using DEFAULT_TOKEN (in MainActivity) can accept a runtime-supplied
value so secrets are not committed to source control.
| // Update state after a short delay to allow connection to establish | ||
| // In a real app, you'd want callbacks from the native side | ||
| android.os.Handler(mainLooper).postDelayed({ | ||
| isConnected.value = true | ||
| statusMessage.value = "Connected to room" | ||
| Log.i(TAG, "Connected to room") | ||
| }, 1000) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don’t mark connected based on a fixed delay.
Line 109-115 sets connected after 1s regardless of actual result; update based on real connection status.
🛠️ Suggested fix
- android.os.Handler(mainLooper).postDelayed({
- isConnected.value = true
- statusMessage.value = "Connected to room"
- Log.i(TAG, "Connected to room")
- }, 1000)
+ android.os.Handler(mainLooper).postDelayed({
+ val connected = currentApp.isConnected()
+ isConnected.value = connected
+ statusMessage.value = if (connected) "Connected to room" else "Connection failed"
+ Log.i(TAG, "Connection result: $connected")
+ }, 1000)🤖 Prompt for AI Agents
In
`@examples/mobile/android/app/src/main/java/io/livekit/rustexample/MainActivity.kt`
around lines 109 - 115, The code currently sets isConnected.value = true and
statusMessage.value = "Connected to room" inside a
Handler(mainLooper).postDelayed(..., 1000), which marks the app connected after
a fixed delay; remove that fixed-delay logic and instead update isConnected and
statusMessage from the actual connection-success callback/event coming from the
native layer (or the LiveKit connection listener) — subscribe to the
room/connection success callback provided by your native binding, call
isConnected.value = true, statusMessage.value = "Connected to room" and
Log.i(TAG, ...) only when that callback fires, and handle connection failure by
setting isConnected false and updating statusMessage accordingly (use the same
identifiers: isConnected, statusMessage, TAG, and replace the
Handler(mainLooper).postDelayed block).
| fun startAll(): Boolean { | ||
| val micStarted = startMicrophone() | ||
| val speakerStarted = startSpeaker() | ||
| return micStarted && speakerStarted | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rollback partial audio startup on failure.
startAll() can return false while leaving mic or speaker running (Line 240-244). That’s a privacy/UX risk.
🛠️ Suggested fix
fun startAll(): Boolean {
val micStarted = startMicrophone()
val speakerStarted = startSpeaker()
+ if (!micStarted || !speakerStarted) {
+ if (micStarted) stopMicrophone()
+ if (speakerStarted) stopSpeaker()
+ }
return micStarted && speakerStarted
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fun startAll(): Boolean { | |
| val micStarted = startMicrophone() | |
| val speakerStarted = startSpeaker() | |
| return micStarted && speakerStarted | |
| } | |
| fun startAll(): Boolean { | |
| val micStarted = startMicrophone() | |
| val speakerStarted = startSpeaker() | |
| if (!micStarted || !speakerStarted) { | |
| if (micStarted) stopMicrophone() | |
| if (speakerStarted) stopSpeaker() | |
| } | |
| return micStarted && speakerStarted | |
| } |
🤖 Prompt for AI Agents
In
`@examples/mobile/android/app/src/main/java/io/livekit/rustexample/MediaManager.kt`
around lines 240 - 244, startAll() may leave one audio component running when
the other fails; make it atomic by rolling back any started component on
failure. Call startMicrophone() then startSpeaker(); if startSpeaker() fails but
startMicrophone() returned true, call stopMicrophone() before returning false
(and symmetrically if you call startSpeaker() first). Use startAll(),
startMicrophone(), startSpeaker(), stopMicrophone(), and stopSpeaker() to
implement this rollback and ensure the method returns true only when both starts
succeed.
| distributionBase=GRADLE_USER_HOME | ||
| distributionPath=wrapper/dists | ||
| distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip | ||
| distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-milestone-1-bin.zip |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
latest stable Gradle version 2025 2026
💡 Result:
As of February 1, 2026, the latest stable Gradle release is 9.3.1, released January 29, 2026. [1]
For 2025 context: Gradle 9.1.0 was released September 19, 2025. [2]
Sources: Gradle release notes pages. [1] [2]
Avoid using Gradle pre-release/milestone versions in example projects.
gradle-9.0-milestone-1 is a pre-release version that may contain bugs, incomplete features, or breaking changes before the stable release. This can cause unexpected build failures for users following the example.
Use the latest stable Gradle version instead (currently 9.3.1).
🤖 Prompt for AI Agents
In `@examples/mobile/android/gradle/wrapper/gradle-wrapper.properties` at line 3,
The example project is pinning a pre-release Gradle distribution
("distributionUrl" set to gradle-9.0-milestone-1) which can cause unstable
builds; update the distributionUrl value to use the latest stable Gradle release
(replace "gradle-9.0-milestone-1" with "gradle-9.3.1") so the wrapper points to
the stable bin distribution URL instead of the milestone build.
| /// Connect to a LiveKit room and set up audio handling | ||
| pub fn livekit_connect(url: String, token: String) { | ||
| log::info!("Connecting to {} with token {}", url, token); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redact access tokens from logs.
Line 73 logs the full token, which can leak credentials via logs.
🛠️ Suggested fix
- log::info!("Connecting to {} with token {}", url, token);
+ log::info!("Connecting to {}", url);
+ log::debug!("LiveKit token length: {}", token.len());🤖 Prompt for AI Agents
In `@examples/mobile/src/lib.rs` around lines 71 - 74, The log in function
livekit_connect currently prints the full token (log::info!("Connecting to {}
with token {}", url, token)); change this to avoid leaking credentials by
redacting or masking the token before logging — e.g., log only a safe substring
or token length (mask middle characters) or remove the token from the message
entirely; update the log::info call in livekit_connect to use the masked_token
representation instead of the raw token.
| // Store room reference | ||
| { | ||
| let mut state = audio_state.lock(); | ||
| state.room = Some(room.clone()); | ||
| state.is_connected = true; | ||
| } | ||
|
|
||
| // Create and publish local audio track | ||
| let track = LocalAudioTrack::create_audio_track( | ||
| "microphone", | ||
| RtcAudioSource::Native(audio_source), | ||
| ); | ||
|
|
||
| if let Err(e) = room | ||
| .local_participant() | ||
| .publish_track( | ||
| LocalTrack::Audio(track), | ||
| TrackPublishOptions { | ||
| source: TrackSource::Microphone, | ||
| ..Default::default() | ||
| }, | ||
| ) | ||
| .await | ||
| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Run rustfmt to satisfy CI.
Pipeline shows rustfmt diffs at Line 117 and Line 127; please run cargo fmt.
🧰 Tools
🪛 GitHub Actions: Rust Formatting
[error] 117-117: Rustfmt diff detected at line 117. Run 'cargo fmt' to apply formatting changes.
[error] 127-127: Rustfmt diff detected at line 127. Run 'cargo fmt' to apply formatting changes.
🤖 Prompt for AI Agents
In `@examples/mobile/src/lib.rs` around lines 112 - 135, The CI failed rustfmt;
run cargo fmt to reformat the affected lines around the
LocalAudioTrack::create_audio_track call and the
room.local_participant().publish_track await chain (the block that constructs
TrackPublishOptions and calls publish_track on LocalTrack::Audio). Re-run
rustfmt/cargo fmt and commit the resulting formatting changes so the
create_audio_track, TrackPublishOptions { ..Default::default() }, and
publish_track(...).await lines conform to the project’s formatter.
| pub extern "C" fn Java_io_livekit_rustexample_App_connectNative( | ||
| mut env: JNIEnv, | ||
| _: JClass, | ||
| url: JString, | ||
| token: JString, | ||
| ) { | ||
| let url: String = env.get_string(&url).unwrap().into(); | ||
| let token: String = env.get_string(&token).unwrap().into(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, check if the file exists and read the relevant section
if [ -f "examples/mobile/src/lib.rs" ]; then
echo "=== File exists, reading lines 466-480 ==="
sed -n '466,480p' examples/mobile/src/lib.rs
else
echo "File not found, listing examples directory structure"
find examples -type f -name "*.rs" | head -20
fiRepository: livekit/rust-sdks
Length of output: 533
🏁 Script executed:
# Search for other JNI get_string usage patterns in the file
rg "get_string" examples/mobile/src/lib.rs -A 2 -B 2Repository: livekit/rust-sdks
Length of output: 269
🏁 Script executed:
# Check if there's error handling pattern for JNI errors in the file
rg "unwrap|expect|match.*Err|log::error" examples/mobile/src/lib.rs | head -20Repository: livekit/rust-sdks
Length of output: 933
Avoid panics on invalid JNI strings.
Lines 472-473 unwrap get_string(...) results; JNI errors will panic across the FFI boundary causing undefined behavior.
🛠️ Suggested fix
- let url: String = env.get_string(&url).unwrap().into();
- let token: String = env.get_string(&token).unwrap().into();
+ let url: String = match env.get_string(&url) {
+ Ok(s) => s.into(),
+ Err(e) => {
+ log::error!("Failed to read url JString: {}", e);
+ return;
+ }
+ };
+ let token: String = match env.get_string(&token) {
+ Ok(s) => s.into(),
+ Err(e) => {
+ log::error!("Failed to read token JString: {}", e);
+ return;
+ }
+ };📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| pub extern "C" fn Java_io_livekit_rustexample_App_connectNative( | |
| mut env: JNIEnv, | |
| _: JClass, | |
| url: JString, | |
| token: JString, | |
| ) { | |
| let url: String = env.get_string(&url).unwrap().into(); | |
| let token: String = env.get_string(&token).unwrap().into(); | |
| pub extern "C" fn Java_io_livekit_rustexample_App_connectNative( | |
| mut env: JNIEnv, | |
| _: JClass, | |
| url: JString, | |
| token: JString, | |
| ) { | |
| let url: String = match env.get_string(&url) { | |
| Ok(s) => s.into(), | |
| Err(e) => { | |
| log::error!("Failed to read url JString: {}", e); | |
| return; | |
| } | |
| }; | |
| let token: String = match env.get_string(&token) { | |
| Ok(s) => s.into(), | |
| Err(e) => { | |
| log::error!("Failed to read token JString: {}", e); | |
| return; | |
| } | |
| }; |
🤖 Prompt for AI Agents
In `@examples/mobile/src/lib.rs` around lines 466 - 474, The JNI string
conversions in Java_io_livekit_rustexample_App_connectNative currently call
env.get_string(...).unwrap(), which will panic on JNI errors and cross the FFI
boundary; change this to safely handle the Result by matching or using if let to
detect errors from env.get_string for both url and token, log or report the JNI
error (rather than panicking), and return early from
Java_io_livekit_rustexample_App_connectNative when conversion fails so no panic
crosses FFI; update references to the converted values only after successful
conversion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need this binary here? if so it should be git LFSed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, we don't need this libmobile.so. Let me clean them up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this jar be checked in? interesting it's only 22kb
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it only contains java files
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it needs to be checked in.
|
would be worth discussing whether we should use libwebrtc native mediadevices vs rewriting them in our example |
@cloudwebrtc mentioned that before, I wonder how the native mediadevice modules can be integrated into Rust SDK, how large the binary will add, and will it cause conflict problems for users who don't want them. I will sync with Duan to understand the details better. And I think it won't be done in this PR. |
Summary by CodeRabbit
New Features
Chores
✏️ Tip: You can customize this high-level summary in your review settings.