Skip to Content
ArchitectureAndroid client

Android client

Toolchain

LanguageKotlin 2.4.10
UIJetpack Compose, Material 3 Expressive (material3 1.4.0), Compose BOM 2025.09.00
BuildAGP 9.3.2, Gradle wrapper, minSdk 24, target/compileSdk 35
NDK27.0.12077973, CMake 3.22.1 (RNNoise, ANDROID_STL=none)
ABIsarm64-v8a, armeabi-v7a, x86_64
WebRTCio.github.webrtc-sdk:android:144.7559.14
NetworkingOkHttp 4.12 (WebSocket), Gson 2.11
Version1.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/ Screen

MainActivity - 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:

AreaWhat it owns
NavigationA Screen enum (HOME, CALL, CALL_LOG, SETTINGS) driven by Compose state, plus a floating bottom nav bar
PermissionsCamera + mic request flow; builds WebRtcClient after grant so the Home preview lights up
Signaling dispatchdispatchSignalingMessage() - the when (msg.type) that turns wire frames into WebRtcClient calls and state updates
Call stateremotePeerId, isOfferer, and the wall of remote* scalars (remoteVideoEnabledState, remoteScreenSharingState, remoteContentRotation, connectionStateValue, peerLeftState, …)
LifecycleonStart/onStop camera gate, onUserLeaveHint → PiP, onPictureInPictureModeChanged, onResume settings re-apply
PiPbuildPipParams(), the mic/camera/end RemoteActions, the broadcast receiver that handles PiP button taps
Foreground serviceStarts/refreshes CallForegroundService with a CallStyle notification summarising the call
ICE recoveryiceRecoveryRunnable - 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.

SettingTypeDefault
displayNameStringdevice model or "You"
avatarType / avatarPreset / avatarPhotoenum / int / pathmonogram
themeModeSYSTEM / LIGHT / DARKSYSTEM
defaultCameraFrontBooleantrue
mirrorFrontCameraBooleantrue
videoQualityVideoQuality (AUTO / Data Saver / HD / Full HD / 2K / 4K)AUTO
videoCodecVideoCodec (AV1 / H265 / H264)H264
micModeMicMode (Standard / Voice Isolation / Wide Spectrum)DEFAULT
micGainFloat, 10 % steps1.0 (100 %)
micOverboostBoolean (raises gain ceiling to 500 %)false
screenShareResolution / screenShareFpsenumHD / native
screenShareAutoRotateBooleanfalse
serverUrlString (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. A when on connection state renders waiting / connecting / connected / peer-left. The connected state is full-bleed remote video + a draggable SelfView + CallControls + an optional CallStatsPanel. 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 - the CallHistory list.

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.