Native Android (Kotlin) client for Serenada WebRTC calls. This app mirrors the core call flow of the web client, prefers WebSocket signaling with automatic SSE fallback, and supports the same adaptive 1:1-to-group room behavior as web.
- WebRTC audio/video calls with adaptive mesh multi-party rooms (up to 4 participants)
- New-capable clients create group-capable rooms by default; legacy-first rooms stay capped at 2 participants
- Calls stay in the familiar 1:1 presentation until participant
#3joins, then switch to the adaptive remote-stage + local-PIP layout - WebSocket signaling with automatic SSE fallback (protocol v1)
- In-call camera source cycle:
selfie(default) ->world->composite(world view with circular mirrored selfie overlay), with automatic composite skip on unsupported devices or composite start failure - In-call pinch zoom for
world/compositewhen local feed is the large view (not PIP), applied at camera capture level so remote participants receive the zoomed stream too - In-call flashlight toggle shown in the top-right corner only for
world/compositecamera modes when the device reports flash support; flashlight turns off automatically when leaving those modes or ending the call, while the user’s flashlight preference is remembered during the same call and reapplied after returning toworld/composite - In-call diagnostics panel in the top-left corner, toggled with a double-tap in that corner zone (same gesture as web), showing the same working-copy web diagnostics sections/metrics (
Connection,Latency,Audio Quality,Video Quality) including active WebRTC transport, RTT/path, packet loss, jitter/playout delay, bitrate, FPS, freezes, and retransmit - In-call performance locks (partial CPU wake lock + Wi-Fi low-latency lock) to reduce call-time scheduling/network jitter while the call is active
- Call-scoped audio session management (
MODE_IN_COMMUNICATION+ audio focus request / restore on hangup), with route priorityBluetooth headset -> proximity earpiece -> speakerduring active calls - Proximity sensor integration for call ergonomics: when the phone is against the ear, audio switches to earpiece and local camera video is paused until the phone is moved away (Bluetooth headset route takes precedence)
- WebRTC audio path configured with
JavaAudioDeviceModule(VOICE_COMMUNICATION, hardware AEC/NS, low-latency path) - Recent calls on home (max 3, deduped) with live room occupancy status, saved-room name subtitle when matched, and icon-based quick actions
- Saved rooms with custom names, quick join, share/rename/remove actions, and configurable position above/below recent calls
- Settings flow to create shareable saved-room links; opening such a link in the app adds the room with the creator-defined name and uses per-room host overrides for non-default hosts
- Deep links for
https://serenada.app/call/* - Foreground service to keep active calls running in the background
- Settings screen to change server host, with host validation on save
- Join attempts include a timeout guard so the app does not stay in
Joining room...indefinitely on signaling/connectivity failures - Android system back support for internal navigation (toolbar back button, hardware back, and edge-swipe gesture behave the same across Settings/Diagnostics/Join-by-code/Error screens)
- Encrypted join snapshot upload (
snapshotIdonjoin) so server push notifications can include a thumbnail when Android is the joiner - Native push receive via Firebase Cloud Messaging, including encrypted snapshot decryption and
BigPicturenotifications in background/terminated app states
- Android Studio (Giraffe+ recommended)
- JDK 17
- Android SDK 34
- minSdk 26 for the sample app (
app/); theserenada-coreandserenada-call-uilibraries support minSdk 24
app/— Android app modulekeystore/— Release keystore + properties (ignored by git)
- Open
client-android/in Android Studio. - Sync Gradle.
- Run on a device or emulator.
Debug APK:
cd client-android
./gradlew :app:assembleDebugForce SSE-only signaling in debug build (test mode):
cd client-android
./gradlew :app:assembleDebug -PforceSseSignaling=trueLocal WebRTC AAR location:
cd client-android
ls serenada-core/libs/libwebrtc-7559_173-universal.aarRebuild the local WebRTC AAR on a Linux VPS:
cd /path/to/connected
bash tools/build_libwebrtc_android_7559.shThe script outputs:
/opt/webrtc-build/artifacts/libwebrtc-7559_173-universal-curlroots.aar
By default it builds armeabi-v7a, arm64-v8a, x86, and x86_64 into a universal AAR.
It also recompresses the final AAR so the stored artifact is substantially smaller than the raw WebRTC output.
After replacing serenada-core/libs/libwebrtc-7559_173-universal.aar, update the pinned SHA-256 file used by Gradle verification:
cd client-android
shasum -a 256 serenada-core/libs/libwebrtc-7559_173-universal.aar | awk '{print $1}' > serenada-core/libs/libwebrtc-7559_173-universal.aar.sha256assembleDebug/assembleRelease will fail if the checksum does not match.
Publish the Android SDK artifacts to mavenLocal:
cd client-android
./gradlew publishSdkToMavenLocalThis publishes:
app.serenada:libwebrtc-7559_173-universal:0.6.6app.serenada:core:0.6.6app.serenada:call-ui:0.6.6
Release APK (signed):
cd client-android
./gradlew :app:assembleReleaseRelease output:
app/build/outputs/apk/release/app-release.apk
Run unit tests:
cd client-android
./gradlew :app:testDebugUnitTestNative push receive requires these Gradle properties at build time:
firebaseAppIdfirebaseApiKeyfirebaseProjectIdfirebaseSenderId
Example:
cd client-android
./gradlew :app:assembleDebug \
-PfirebaseAppId=1:1234567890:android:abc123 \
-PfirebaseApiKey=AIza... \
-PfirebaseProjectId=your-project-id \
-PfirebaseSenderId=1234567890Enable USB debugging on the device and connect it. Then run:
Debug:
adb install -r app/build/outputs/apk/debug/serenada-debug.apkRelease:
adb install -r app/build/outputs/apk/release/serenada.apkRelease signing reads keystore/keystore.properties if present. This file is ignored by git.
Expected properties:
storeFile=../keystore/serenada-release.keystore
storePassword=YOUR_PASSWORD
keyAlias=serenada-release
keyPassword=YOUR_PASSWORD
Create the keystore (choose your own password and alias):
cd client-android
keytool -genkeypair -v \
-keystore keystore/serenada-release.keystore \
-alias serenada-release \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-storepass YOUR_PASSWORD \
-keypass YOUR_PASSWORD \
-dname "CN=Serenada, OU=Serenada, O=Serenada, L=, ST=, C=US"Then create keystore/keystore.properties:
cd client-android
cat > keystore/keystore.properties <<'EOF'
storeFile=../keystore/serenada-release.keystore
storePassword=YOUR_PASSWORD
keyAlias=serenada-release
keyPassword=YOUR_PASSWORD
EOFGet the SHA-256 fingerprint (needed for App Links):
keytool -list -v -keystore keystore/serenada-release.keystore -storepass YOUR_PASSWORD | \
rg -m1 "SHA-256|SHA256"The app handles:
https://serenada.app/call/*https://serenada.app/call/*?name=<room-name>(adds a named saved room instead of joining immediately)
Deep-link host query behavior:
- Trusted hosts (
serenada.app,serenada-app.ru) are allowed to update the global server host setting. - Other hosts are treated as one-off: calls use them only for that action, and saved-room links store them as per-room host overrides without mutating global settings.
For App Links verification, the web server must serve:
client/public/.well-known/assetlinks.json
This file must include the release SHA-256 fingerprint for the signing certificate.
Edit client/public/.well-known/assetlinks.json and add your release SHA-256:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "app.serenada.android",
"sha256_cert_fingerprints": [
"RELEASE_SHA256_HERE"
]
}
}
]Then deploy the web app so the file is available at:
https://serenada.app/.well-known/assetlinks.json
Quick checks:
adb shell pm get-app-links app.serenada.android
adb shell am start -a android.intent.action.VIEW -d "https://serenada.app/call/ROOM_ID"
adb shell am start -a android.intent.action.VIEW -d "https://serenada.app/call/ROOM_ID?host=serenada.app&name=Family"Server host is configurable in the in-app Settings screen (Join screen → Settings).
On Save, the app validates https://<host>/api/room-id and only persists hosts that respond with the expected Serenada room ID payload.
Call defaults also include HD Video (experimental); when disabled (default), camera capture uses legacy 640x480, and when enabled the app applies higher per-mode camera/composite targets.
Saved rooms settings include:
- A switch to show saved rooms above or below recent calls on the home screen
The app version is shown at the bottom of the Settings screen for quick support/debug reference.
Named-room creation and sharing is available from the home screen (
Saved roomssection →+ Create). It creates a room ID and shareable link that adds this room on recipient devices and preserves per-room host overrides for non-default hosts.Device Checkin Settings opens a native diagnostics screen with: - Runtime permission checks (
CAMERA,RECORD_AUDIO,POST_NOTIFICATIONSon Android 13+) - Audio/video capability inspection (camera inventory, composite prerequisites, audio processing feature availability)
- Connectivity checks (
/api/room-id, WebSocket/ws, SSE/sseGET+POST,/api/diagnostic-token,/api/turn-credentials) - ICE tests for full STUN/TURN and TURNS-only modes
- Title-bar share action that copies the full diagnostic report to clipboard and opens Android share sheet
During active call flows, WebRTC runtime stats are emitted to logcat every ~2s as:
CallManager: [WebRTCStats] ...- Debug builds also enable native WebRTC verbose logging (tag
org.webrtc/libjingle) for ICE/TURN investigation.
- Composite mode depends on device support for concurrent front+back camera capture; unsupported devices fall back to non-composite camera sources