Skip to Content
ArchitectureOverview

Architecture overview

Viora has two deployable pieces and one protocol between them.

The two components

1. Signaling server (server/)

A ~240-line Rust program (axum 0.8 + tokio) that does room membership and message relay and nothing else.

  • No database. State is RwLock<HashMap<String, HashMap<String, Peer>>> - room code → peer id → { name, tx }. A Peer’s tx is an mpsc::UnboundedSender into that peer’s dedicated writer task.
  • No persistence. When the last peer leaves a room, the room entry is removed.
  • One binary. cargo build --release with lto = true, codegen-units = 1, strip = true.
  • One route. any("/") - it inspects the Upgrade header and either performs the WebSocket upgrade or returns 404.

See Signaling server.

2. Android client (android/)

A single-activity Jetpack Compose app (minSdk 24, targetSdk 35, Kotlin 2.4, Compose BOM 2025.09, Material 3 Expressive). The WebRTC engine is io.github.webrtc-sdk:android:144.7559.14 - LiveKit’s build of libwebrtc M144, chosen because it ships a complete H.265 RTP stack.

Key modules:

PackageResponsibility
com.example.videocallMainActivity - the single Activity, signaling dispatch, call state, lifecycle, PiP
com.example.videocall.webrtcWebRtcClient and its helpers - the entire media engine
com.example.videocall.uiCompose screens and components
com.example.videocall.dataAppSettings, CallHistory, AvatarArt - SharedPreferences-backed
com.example.videocall.serviceCallForegroundService - keeps the call alive when backgrounded
cpp/rnnoise_jni.c + vendored RNNoise, built to libomnicall_audio.so

See Android client and The WebRTC pipeline.

The protocol

JSON text frames over one WebSocket. The client always connects to the server root (wss://host/); the same URL serves the upgrade.

DirectionTypes
Client → serverjoin, offer, answer, candidate, chat, screen-share, video, leave
Server → clientroom-joined, user-joined, offer, answer, candidate, chat, screen-share, video, user-left

Relay messages carry an optional targetPeerId; if present the server unicasts, otherwise it broadcasts to every other peer in the room. The server stamps a senderPeerId on everything it relays. Full schema in the signaling protocol reference.

What flows where

DataPath
Room join, presenceClient ↔ server WebSocket
SDP offer / answerClient → server → client (unicast via targetPeerId)
ICE candidatesClient → server → client
Audio & video RTPClient ↔ client, direct, DTLS-SRTP
Text chatClient → server → client (relayed, never stored)
Camera / mic on-off, avatar, screen-share stateClient → server → client, as video / screen-share frames

Design constraints that shaped the code

  • Exactly two peers. MainActivity tracks a single remotePeerId and a wall of scalar remote* state. The multi-party roadmap replaces this with a participant list.
  • The media stack is expensive to build. WebRtcClient builds the PeerConnectionFactory, AudioDeviceModule, and capturers once and reuses them; a mid-call mic-mode change only swaps the software layer and defers the hardware rebuild to the next call.
  • Android lifecycle is hostile to a camera. Backgrounding, PiP close, screen lock, and other apps all yank the camera. The capture recovery logic is built around that.
  • The debug panel must cost nothing when closed. Stats polling only runs while the panel is visible and the Activity is STARTED.