A multi-platform, WhatsApp-like real-time communication app built with Kotlin, JavaFX, and WebRTC.
| Feature | Desktop (JavaFX) | Web (browser) | Mobile (Gluon) |
|---|---|---|---|
| Voice call | ✅ | ✅ | ✅ |
| Video call | ✅ | ✅ | ✅ |
| Screen / desktop share | ✅ Windows/macOS/Linux | ✅ | — |
| Send images | ✅ | ✅ | — |
| View incoming images | ✅ | ✅ | ✅ |
| Group calls (mesh) | ✅ | ✅ | ✅ |
whisper/
├── shared/ Shared Kotlin DTOs & SignalMessage protocol
├── signaling-server/ Ktor WebSocket signaling + static web assets
├── desktop-client/ JavaFX + webrtc-java desktop app
├── web-client/ TypeScript browser app (RTCPeerConnection)
├── mobile-client/ Gluon Mobile JavaFX app (Android / iOS)
├── infra/ docker-compose, coturn config, Dockerfile
└── gradle.properties Shared dependency versions
┌──────────────────┐ WS signaling ┌──────────────────────────┐
│ Desktop Client │◄────────────────►│ │
│ (JavaFX) │ │ Ktor Signaling Server │
└────────┬─────────┘ │ /ws (WebSocket) │
│ WebRTC │ /app (static web files)│
│ P2P mesh │ /health │
┌────────▼─────────┐ │ │
│ Web Client │◄────────────────►│ │
│ (browser) │ └──────────┬───────────────┘
└────────┬─────────┘ │
│ WebRTC │ ICE config
│ ▼
┌────────▼─────────┐ ┌──────────────────────────┐
│ Mobile Client │◄─────────────────│ coturn STUN/TURN │
│ (Gluon) │ WebRTC │ :3478 │
└──────────────────┘ └──────────────────────────┘
Media (audio, video, screen) flows peer-to-peer over WebRTC. The Ktor server only handles signaling (SDP/ICE exchange), presence, room state, and image relay.
| Tool | Version |
|---|---|
| JDK | 17+ |
| Gradle (wrapper included) | 8.7 |
| Node.js (web client build) | 20+ |
| Docker + Docker Compose | any recent |
| GraalVM (optional, Gluon native) | 22+ |
# Copy and configure environment
cp infra/.env.example infra/.env
# Edit infra/.env if needed (TURN_HOST, secrets, etc.)
cd infra
docker compose upThe signaling server will be available at http://localhost:8080.
cd web-client
npm install
npm run build # compiles TS → JS and copies to signaling-server resourcesThen open http://localhost:8080/app/index.html in a browser.
./gradlew :desktop-client:runSet the server URL via environment variable (default: ws://localhost:8080/ws):
WHISPER_SERVER=ws://your-server:8080/ws ./gradlew :desktop-client:run./gradlew :mobile-client:runRequires GraalVM 22+ and Android SDK:
./gradlew :mobile-client:gluonfxBuild -Ptarget=android
./gradlew :mobile-client:gluonfxInstall -Ptarget=android
./gradlew :mobile-client:gluonfxRun -Ptarget=androidSet the server URL for Android (10.0.2.2 routes to host loopback in the emulator):
WHISPER_SERVER=ws://10.0.2.2:8080/ws ./gradlew :mobile-client:gluonfxRun| Variable | Default | Description |
|---|---|---|
PORT |
8080 |
Signaling server HTTP port |
TURN_HOST |
localhost |
TURN server hostname (clients receive this) |
TURN_USER |
whisper |
TURN username |
TURN_PASS |
whisper_secret |
TURN password |
WHISPER_SERVER |
ws://localhost:8080/ws |
WebSocket URL used by desktop + mobile |
WHISPER_SERVER_HTTP |
http://10.0.2.2:8080 |
HTTP base URL used by the mobile WebView |
Kotlin JVM module. Contains:
SignalMessage– sealed class with@Serializablesubtypes covering all signaling eventsUser,CallRoom,IceServerConfig– core domain modelswhisperJson– sharedkotlinx.serializationJSON codec
Ktor server (Netty). Responsibilities:
- WebSocket endpoint
/ws– SDP offer/answer relay, ICE forwarding, room state, image relay Registry– in-memory connected user + room store (replace with Redis for multi-instance)SignalingHandler– pure message dispatch logic- Serves compiled web client from
resources/web
JavaFX app. Key classes:
DesktopSignalingClient– Ktor WS client with automatic reconnectDesktopCallManager– mesh call lifecycle,PeerConnectionFactory, device captureDesktopPeerConnection– wrapsRTCPeerConnectionfromwebrtc-javaWebRTCVideoView– JavaFXCanvasthat renders I420 video framesMainController– FXML controller wiring UI ↔ call manager- Screen sharing gated by
isScreenShareSupported(Windows / macOS / Linux only)
TypeScript app compiled with tsc. Key modules:
SignalingClient– WebSocket client with auto-reconnectPeerConnection– thin RTCPeerConnection wrapperCallManager– mesh call manager (one PC per remote peer)app.ts– UI event wiring
Built assets are copied to signaling-server/src/main/resources/web by npm run build.
Gluon Mobile JavaFX app. Three views:
LoginView– user ID + display name entryLobbyView– room join/leave, participants, incoming image galleryCallView– embeds the web client viaWebViewfor the WebRTC call session (see note below)
WebRTC on Gluon: Native org.webrtc bindings do not integrate with Gluon's JavaFX runtime. The call session is implemented by loading the Whisper web client inside a
javafx.scene.web.WebView. This gives full WebRTC support (voice + video) on Android API 21+ and desktop. Screen share is intentionally excluded on mobile.
docker-compose.yml– coturn + Ktor serverDockerfile.server– multi-stage Docker build (Node web → Ktor JAR → slim JRE)turnserver.conf– coturn STUN/TURN configuration.env.example– environment template
All participants in a room connect directly to each other (full mesh). Concretely:
- When a participant joins a room, the server sends
room_stateto all members. - Each client opens a
PeerConnectionto every other member it doesn't already have one for. - The alphabetically-first
userIdcreates the WebRTC offer; the other side answers.
Scaling note: Mesh is efficient for up to ~4–6 participants. For larger rooms, migrate to an SFU (e.g. LiveKit, Janus, mediasoup) and update the signaling server to route media through it.
- Run only the signaling server (no Docker):
./gradlew :signaling-server:run - Run tests:
./gradlew test - Format code:
./gradlew ktlintFormat(add ktlint plugin to root build if desired) - The web client uses plain TypeScript with no bundler – import maps or a simple HTTP server work for local dev:
npx serve web-client/public