Android client
Toolchain
| Language | Kotlin 2.4.10 |
| UI | Jetpack Compose, Material 3 Expressive (material3 1.4.0), Compose BOM 2025.09.00 |
| Build | AGP 9.3.2, Gradle wrapper, minSdk 24, target/compileSdk 35 |
| NDK | 27.0.12077973, CMake 3.22.1 (RNNoise, ANDROID_STL=none) |
| ABIs | arm64-v8a, armeabi-v7a, x86_64 |
| WebRTC | io.github.webrtc-sdk:android:144.7559.14 |
| Networking | OkHttp 4.12 (WebSocket), Gson 2.11 |
| Version | 1.8.9 / build 89 - archivesName = Viora-v1.8.9-b89 |
Why this WebRTC build
io.github.webrtc-sdk is LiveKit’s build of libwebrtc M144. It has the full
H.265 RTP stack compiled in. The more common
io.getstream:stream-webrtc-android lacks it - sending H.265 there SIGSEGVs
in native code. The AAR also bundles the Chromium org.jni_zero runtime,
which matters for R8.
Package layout
com.example.videocall
├── MainActivity.kt # the only Activity - see below
├── data/
│ ├── AppSettings.kt # SharedPreferences-backed reactive settings
│ ├── CallHistory.kt # recent-calls list
│ └── AvatarArt.kt # monogram / preset / photo avatar rendering
├── service/
│ └── CallForegroundService.kt # camera|microphone|mediaProjection FGS + CallStyle notification
├── webrtc/
│ ├── WebRtcClient.kt # the media engine (see "WebRTC pipeline")
│ ├── SignalingClient.kt # OkHttp WebSocket wrapper
│ ├── SignalingModels.kt # wire types
│ ├── CallStats.kt # stats snapshot data class
│ ├── CallAudioRouter.kt # earpiece / speaker / BT / wired routing
│ ├── CameraZoomController.kt # pinch-zoom (Camera2 zoom ratio or crop)
│ ├── CameraCapabilities.kt # which resolutions the cameras can actually capture
│ ├── DeviceLoadSampler.kt # CPU %, battery temp, power draw
│ ├── RnnoiseProcessor.kt # JNI bridge to libomnicall_audio.so
│ ├── MicGainProcessor.kt # output gain on captured mic PCM
│ ├── ScreenAudioCapturer.kt # device-playback capture during screen share
│ ├── CompositeAudioProcessing.kt # chains the external audio processors
│ ├── AudioLevelMeter.kt # RMS → smoothed 0..1 loudness envelope
│ ├── PeerConnectionObserver.kt / SimpleSdpObserver.kt # no-op base impls
│ └── TextureViewRenderer.kt / VideoRenderer.kt # Compose ↔ SurfaceViewRenderer / TextureView
└── ui/
├── screens/ HomeScreen, CallScreen, SettingsScreen, CallLogScreen
├── components/ CallControls, CallStatsPanel, ChatBottomSheet, MicModeSheet,
│ AudioRouteSheet, ScreenShareResSheet, SpeakingWaveform,
│ Avatar, AvatarPickerSheet, AvatarCropperDialog, FloatingNavBar
├── theme/ Color, Theme, Type (+ the CallColorScheme call palette)
└── navigation/ ScreenMainActivity - the orchestrator
There is one Activity. It is launchMode="singleTask" so re-launching from the
launcher while the call is in Picture-in-Picture re-enters the existing instance
instead of stacking a second copy.
Its responsibilities:
| Area | What it owns |
|---|---|
| Navigation | A Screen enum (HOME, CALL, CALL_LOG, SETTINGS) driven by Compose state, plus a floating bottom nav bar |
| Permissions | Camera + mic request flow; builds WebRtcClient after grant so the Home preview lights up |
| Signaling dispatch | dispatchSignalingMessage() - the when (msg.type) that turns wire frames into WebRtcClient calls and state updates |
| Call state | remotePeerId, isOfferer, and the wall of remote* scalars (remoteVideoEnabledState, remoteScreenSharingState, remoteContentRotation, connectionStateValue, peerLeftState, …) |
| Lifecycle | onStart/onStop camera gate, onUserLeaveHint → PiP, onPictureInPictureModeChanged, onResume settings re-apply |
| PiP | buildPipParams(), the mic/camera/end RemoteActions, the broadcast receiver that handles PiP button taps |
| Foreground service | Starts/refreshes CallForegroundService with a CallStyle notification summarising the call |
| ICE recovery | iceRecoveryRunnable - re-offers when the connection goes DISCONNECTED/FAILED and we’re the offerer |
It’s a large file (~1,500 lines) because it is the join point between five
independent subsystems. The multi-party roadmap
factors the remote* scalars into a mutableStateListOf<Participant>.
AppSettings
A singleton object initialised in onCreate with AppSettings.init(context).
Every field is a Compose mutableStateOf / mutableFloatStateOf backed by
SharedPreferences (omnicall_settings.xml), so a settings change recomposes
any screen reading it and survives a restart.
| Setting | Type | Default |
|---|---|---|
displayName | String | device model or "You" |
avatarType / avatarPreset / avatarPhoto | enum / int / path | monogram |
themeMode | SYSTEM / LIGHT / DARK | SYSTEM |
defaultCameraFront | Boolean | true |
mirrorFrontCamera | Boolean | true |
videoQuality | VideoQuality (AUTO / Data Saver / HD / Full HD / 2K / 4K) | AUTO |
videoCodec | VideoCodec (AV1 / H265 / H264) | H264 |
micMode | MicMode (Standard / Voice Isolation / Wide Spectrum) | DEFAULT |
micGain | Float, 10 % steps | 1.0 (100 %) |
micOverboost | Boolean (raises gain ceiling to 500 %) | false |
screenShareResolution / screenShareFps | enum | HD / native |
screenShareAutoRotate | Boolean | false |
serverUrl | String (wss://…) | the hosted default |
See the settings reference for the exact value maps.
Compose UI shape
HomeScreen- camera self-preview, recent room chips, a read-only “You’ll join as {name}” card (the name is not editable here - it’s set in Settings), a room-code field with a shuffle button, and the Join button.CallScreen- the full call surface. Awhenon connection state renders waiting / connecting / connected / peer-left. The connected state is full-bleed remote video + a draggableSelfView+CallControls+ an optionalCallStatsPanel. A separate branch renders the PiP layout (remote video only).SettingsScreen- cards for Profile, Appearance, Camera, Resolution, Codec, Screen share, Microphone, Studio Microphone, Server, About.CallLogScreen- theCallHistorylist.
The call UI has its own theme-aware palette:
CallColorScheme with light/dark instances provided through a
CompositionLocalProvider, exposed as a @Composable CallColors accessor so
call sites don’t change.