A simple, privacy-focused 1:1 video calling application built with WebRTC. No accounts, no tracking, just instant video calls.
SocialTypeScript · KotlinGitHub @agatxOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Simple cross-platform video calling using WebRTC
Translation pending
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
A simple, privacy-focused video calling application built with WebRTC. No accounts, no tracking, just instant calls with up to 4 participants.Read the full README ↗A simple, privacy-focused video calling application built with WebRTC. No accounts, no tracking, just instant calls with up to 4 participants.
getDisplayMedia (not shown on mobile browsers)selfie (default) -> world -> composite (world feed with circular selfie overlay), automatically skips composite when unsupported, and shows a flashlight toggle in world/composite when flash hardware is available; flashlight preference is remembered during the call and reapplied when returning to supported modesworld or composite, pinch gesture zooms the camera capture itself so both local preview and the remote participant see the zoomed detailHD Video (experimental) switch for higher camera/composite quality; default mode keeps legacy 640x480 camera constraints for stabilityclient-ios/ mirrors Android parity flow: saved rooms + recents ordering, structured deep-link parsing, invite push toggle, encrypted push snapshots, waiting-room invite action, adaptive multi-party layout with local PIP, diagnostics screen, mode-based camera cycle with composite fallback, world/composite pinch zoom, ReplayKit screen share toggle, and in-call realtime stats/debug panelSerenadaSession.captureSnapshot(source) returns the current frame at full intrinsic resolution as a JPEG/Blob; the call UI can show an opt-in shutter button anchored to the short edge of the large preview (bottom in portrait, right in landscape)Copy the environment template:
cp .env.example .env
Build the frontend:
cd client
npm install
npm run build
Start the development stack:
docker compose up -d --build
Open http://localhost in your browser
If you prefer to run the components manually:
cd client
npm install
npm run dev
cd server
go run .
Requires Go 1.24+ and a .env file in the root directory.
The native Android app lives in client-android/.
client-android/ in Android Studio.local7827 (client-android/serenada-core/libs/libwebrtc-7827-universal.aar).bash tools/build_libwebrtc_android_7827.sh.client-android/serenada-core/libs/libwebrtc-7827-universal.aar.sha256 (Gradle now verifies it before build).By default the app targets https://serenada.app, and the server host can be changed in Settings.
The Android app language can also be set in Settings: Auto (default), English, Русский, Español, Français. Auto follows the device language and falls back to English.
To enable native Android push receive, provide Firebase Gradle properties when building the app (firebaseAppId, firebaseApiKey, firebaseProjectId, firebaseSenderId).
The native iOS app lives in client-ios/.
xcodegen (if not already installed).cd client-ios
xcodegen generate
SerenadaiOS.xcodeproj in Xcode.SerenadaiOS on iOS 16+ simulator/device.Package.swift) — no vendored XCFramework step is needed.client-ios/LocalSigning.xcconfig. See client-ios/README.md.iOS universal links are enabled for serenada.app, ru.serenada.app, and serenada-app.ru via associated domains plus /.well-known/apple-app-site-association.
Note: iOS Simulator can run signaling and call flow, but local camera preview reliability varies by host setup; use a physical iPhone to validate local camera capture.
See DEPLOY.md for detailed self-hosting instructions.
Quick setup script (downloads, installs dependencies, and provisions the stack):
curl -fsSL https://serenada.app/tools/setup.sh -o setup-serenada.sh
bash setup-serenada.sh
The server includes an in-repo load conduit for signaling capacity validation.
Quick run (local Docker stack):
./server/loadtest/run-local.sh
What it does:
ENABLE_INTERNAL_STATS=1INTERNAL_STATS_TOKEN automatically (override via env if needed)/api/room-id and /api/internal/statsgo run ./cmd/loadconduit from server/server/loadtest/reports/Common overrides:
START_CLIENTS=20 STEP_CLIENTS=20 MAX_CLIENTS=200 STEADY_SECONDS=600 ./server/loadtest/run-local.sh
Stabilization and join-tolerance controls:
PRE_RAMP_STABILIZE_SECONDS=10 MAX_JOIN_ERROR_RATE=0.005 ./server/loadtest/run-local.sh
To avoid local/NAT throttling while testing, set a bypass allowlist:
RATE_LIMIT_BYPASS_IPS=127.0.0.1,::1 ./server/loadtest/run-local.sh
Direct conduit usage:
cd server
go run ./cmd/loadconduit --base-url http://localhost --report-json ./loadtest/reports/manual.json
Detailed request/timing sequence:
bash tools/integration-test/run.sh # Run signaling integration tests (requires Go 1.24+)
Verify SDK versions and shared protocol constants match across all platforms:
node scripts/check-version-parity.mjs
node scripts/check-signaling-protocol-constants.mjs
┌─────────────────┐ ┌─────────────────┐
│ Browser A │◄───────►│ Browser B │
│ (React SPA) │ WebRTC │ (React SPA) │
└────────┬────────┘ └────────┬────────┘
│ │
│ WS/SSE (signaling) │
▼ ▼
┌─────────────────────────────────────────────┐
│ Go Signaling Server │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ STUN/TURN Server (Coturn) │
└─────────────────────────────────────────────┘
All three native SDKs follow a headless core + optional UI pattern:
SerenadaCore — Entry point. createRoom() returns the room URL and ID (async on all platforms). Call join() afterward to start the call and get a SerenadaSession.SerenadaSession — The active call session. Exposes two observable snapshots:
state (CallState) — App-facing call state: phase, local/remote participants, connection status, errors.diagnostics (CallDiagnostics) — Low-level transport info: ICE/peer/signaling states, realtime stats (including active inbound/outbound audio and video codecs), camera/flash state, feature degradations.SerenadaCallFlow — Pre-built UI component (SwiftUI / Jetpack Compose / React) that consumes a session and renders the full call flow. Optional — you can build your own UI from state and diagnostics.Android enforces main-thread access on all public SDK entrypoints with fail-fast preconditions.
All SDKs now support two signaling modes through the same SerenadaConfig, and they validate the choice at construction time:
serverHost.signalingProvider.Provide exactly one of serverHost or signalingProvider.
In custom-provider mode:
join() still creates a normal SerenadaSession.peerId; built-in cid mapping stays internal.hostPeerId and roomStateUpdated remain optional.runAll() still works, and TURN probing uses provider-supplied ICE servers.serverHost: createRoom(), native createRoomId(), RoomWatcher, validateServerHost(), and runConnectivityChecks().On web, provider-delivered peer messages are exposed through session.onPeerMessage(...). Raw Serenada transport envelopes are no longer part of the public SDK surface.
Screen share is moving from the single camera-video path to its own independent content stream, so a capable peer can send camera and screen share at the same time. The SDK surface and media path are gated behind a config flag that is off by default in the headless SDK and enabled explicitly by the bundled Serenada host apps. Third-party host apps should enable it once their media integration and UI can render the content stream. With the flag off, behavior is exactly today's: screen share reuses the camera video path and replaces the camera track while sharing.
enableIndependentContentVideo (SerenadaConfig, default false). It advertises capabilities.independentContentVideo=true at join and enables the independent content media path for capable peers. Legacy peers fall back to the single-video screen-share path for that connection.videoMediaEnabled (SerenadaConfig, default true) is now also signaled at join as mediaPolicy.videoMediaEnabled. It is immutable for the session and controls whether any video (camera or content) is negotiated. Strict audio-only (videoMediaEnabled=false) negotiates no video at all.Participant and LocalParticipant):
cameraEnabled — camera video specifically. With the flag on this is the precise camera signal; with the flag off it mirrors videoEnabled.videoEnabled — retained legacy "video active" field; mirrors cameraEnabled when the flag is on.content — { active, type, revision }, the screen-share presentation state (absent when not sharing). type is currently "screenShare"; revision orders quick stop/start toggles.session.getRemoteContentStream(cid), session.getRemoteCameraStream(cid), session.getLocalContentStream().session.attachRemoteContentRenderer(renderer, cid) / detachRemoteContentRenderer(renderer, cid), session.attachLocalContentRenderer(renderer) / detachLocalContentRenderer(renderer).session.attachRemoteContentRenderer(_:forParticipant:) / detachRemoteContentRenderer(_:forParticipant:), session.attachLocalContentRenderer(_:) / detachLocalContentRenderer(_:).When enabled, capability and policy are negotiated per peer: a capable peer receives camera and screen share on independent streams; a legacy peer falls back to the single-video model for that connection only (screen share preempts camera on that peer). See the Protocol Specification for the join capability/mediaPolicy fields and content_state wire format.
On iOS and Android, the SDKs support a pluggable audio session model via SerenadaAudioCoordinator. This lets the host app control the process-global audio session, routing policies, and Bluetooth state machines (useful when embedding Serenada alongside another audio engine like a push-to-talk system), while retaining the SDK's internal call routing and proximity monitor behaviors.
If audioCoordinator is omitted, the SDK uses its internal default coordinator. The concrete default coordinator is not public API; custom integrations should implement SerenadaAudioCoordinator and inject that implementation through SerenadaConfig.
To use a custom coordinator, provide it and an AudioIntent in SerenadaConfig:
let config = SerenadaConfig(
serverHost: "serenada.app",
audioCoordinator: MyCustomAudioCoordinator(),
audioIntent: AudioIntent(
supportsVideo: true,
muteOwnMicDuringExternalAudio: true, // Mute when interrupted (e.g. by PTT)
duckOwnPlaybackDuringExternalAudio: true // Duck remote playback during interruption
)
)
val config = SerenadaConfig(
serverHost = "serenada.app",
audioCoordinator = MyCustomAudioCoordinator(),
audioIntent = AudioIntent(
supportsVideo = true,
muteOwnMicDuringExternalAudio = true,
duckOwnPlaybackDuringExternalAudio = true
)
)
The coordinator implementation is responsible for:
On Android, coordinator handoff is serialized process-wide across Serenada sessions, including
handoffs between default and custom coordinators. The SDK does not activate the next coordinator
until the previous coordinator's deactivateCallSession() returns. Custom implementations must
therefore suspend until focus, mode, route, Bluetooth, and callback cleanup is complete instead of
launching cleanup in the background; blocking system work should run off the main thread.
The session exposes coordinator state for custom UI:
availableAudioDevices: routes published by the active coordinator.currentAudioDevice: selected or active output route.isMicMuted: effective microphone mute state.isMicMutedByExternalAudio: whether external audio is currently forcing a mute.selectAudioDevice(...): requests routing to a coordinator-published route.setMicMuted(...): sets the user-requested microphone mute state.The session maintains a composed microphone mute state calculated as:
isMicMuted = userMuted || externalAudioMuted || !routeInputAvailable
When the coordinator publishes an audioSessionInterrupted event (e.g., due to incoming/outgoing PTT or a phone call), the SDK:
muteOwnMicDuringExternalAudio is enabled) to prevent audio leaks.duckOwnPlaybackDuringExternalAudio is enabled).audioSessionResumed.SerenadaSession.captureSnapshot API and call-UI shutter buttonserver/loadtest/run-local.sh – Local signaling load sweep runnerserver/loadtest/LOAD_SIMULATION_SEQUENCE.md – Detailed load-conduit HTTP/WS call sequence and timing| Component | Technology |
|---|---|
| Frontend | React 19, TypeScript, Vite |
| Backend | Go 1.24+ |
| Media | WebRTC, Coturn |
| Proxy | Nginx |
| Containers | Docker, Docker Compose |
This project is licensed under the BSD 3-Clause License. See LICENSE for details.