A Zoom-style video conferencing platform for Windows: WPF desktop client + ASP.NET Core backend with SignalR signaling and WebRTC (SIPSorcery) peer-to-peer media.
This is Phase 1 (MVP) of the larger platform roadmap: accounts, instant/scheduled meetings, join by ID + password, group audio/video, in-meeting chat (public & private), participant roster, and host controls.
| Project | Purpose |
|---|---|
src/ZPlus.Shared |
DTOs and hub method/event contracts shared by client and server |
src/ZPlus.Server |
ASP.NET Core Web API (JWT auth, meetings, admin) + SignalR MeetingHub (roster, chat, WebRTC signaling relay, host controls) + EF Core/SQLite persistence |
src/ZPlus.Client |
WPF (.NET 10) MVVM client with SIPSorcery WebRTC mesh media engine |
src/ZPlus.Admin |
WPF admin console: user management, roles/rights, server settings, active-meeting control |
src/ZPlus.ClientGui |
Cross-platform Avalonia meeting client (zplus-client) — the Linux GUI: meetings, roster, E2EE chat |
src/ZPlus.AdminGui |
Cross-platform Avalonia admin console (zplus-admin) — the Linux GUI, same features as the WPF admin |
Each application publishes to one self-contained binary (all DLLs and the .NET runtime embedded — nothing to install on the target machine).
Windows (net8.0 / net10.0-windows GUI apps):
dotnet publish src/ZPlus.Server -c Release -f net8.0 -o publish/server
dotnet publish src/ZPlus.Client -c Release -o publish/client
dotnet publish src/ZPlus.Admin -c Release -o publish/admin
Copy Z+ Server.exe, Z+ Client.exe, and Z+ Admin.exe wherever you need them.
Linux (net10.0, into publish/linux) — use the Makefile at the repository root:
scripts/install-dotnet.sh # one-time: install the .NET 10 SDK + native libraries
make # builds zplus-server, zplus-client and zplus-admin
make server # or build one target: server / client / admin
make clean
scripts/install-dotnet.sh sets up a Linux machine for Z+: it installs the .NET 10 SDK
(needed to build) plus the native libraries the self-contained binaries rely on at runtime
(ICU, OpenSSL, and X11/fontconfig for the GUI apps). It detects the distro (Debian/Ubuntu,
Fedora/RHEL, openSUSE, Arch, Alpine) and falls back to Microsoft's dotnet-install.sh on
others. To only prepare a machine to run the published binaries (no SDK), use
scripts/install-dotnet.sh --deps-only.
Requires GNU make and the .NET 10 SDK. The Linux binaries target net10.0 (Windows
builds stay on net8.0); the cross-platform projects multi-target net8.0;net10.0.
The RuntimeIdentifier is auto-detected from the build machine (x86_64 → linux-x64,
aarch64 → linux-arm64, armv7l/armv6l → linux-arm, with linux-musl-* on Alpine).
Cross-build by overriding it, e.g. make RID=linux-arm64; make DOTNET=dotnet.exe
runs the Makefile from WSL using the Windows .NET SDK.
publish/linux/ contains three self-contained x64 binaries — no .NET install required
(chmod +x them after copying):
zplus-server— the full Z+ server, identical feature set to Windows. Createszplus.dbandzplus.keynext to itself, binds0.0.0.0:5199. All environment overrides (ZPLUS_DB,ZPLUS_KEY,ZPLUS_ADMIN_EMAIL,ZPLUS_ADMIN_PASSWORD) work the same. Windows and Linux clients connect to it interchangeably.zplus-client— the Linux desktop meeting client (Avalonia, same dark Z+ look as Windows): sign in/register, start/join/schedule meetings with email invitations, live roster with host controls, end-to-end encrypted public/private chat. No audio/video yet — the media capture stack (Windows Media Foundation) is Windows-only; Linux participants join with roster + chat.zplus-admin— the Linux desktop admin console (Avalonia), same features as the Windows admin app: users/roles, all server settings including SMTP, active meetings with force-end.
The GUI apps share their view-models with the Windows WPF apps and also run on Windows.
The server also hosts a browser-based client — no install required. Open
http://<server>:5199/app (or https://…/app) and sign in. Invitation emails and the
/join/{code} landing page also offer a "Join in your browser" button that opens the web
client on /app#code=…&pw=…; it reads the meeting id (and password) from the URL fragment and
joins automatically after sign-in. It supports the full feature set — sign in (with MFA),
start / join / schedule meetings, live roster, end-to-end encrypted chat, reactions, polls,
file sharing, whiteboard, breakout rooms and the waiting room — plus in-browser audio and
video, and a personal email address book (add / edit / delete contacts, then one-click them
into a meeting's invite list). The web client's chat E2EE is byte-compatible with the native clients (ECDH P-256 →
AES-256-GCM via the browser's Web Crypto), so browser and desktop participants read each other's
encrypted messages.
In-browser audio/video. The web client captures the camera and microphone with
getUserMedia and connects over WebRTC in the same full-mesh topology as the Windows app: one
RTCPeerConnection per participant, the newcomer offering to everyone already in the room. The
offer/answer/ICE signaling is relayed over the SignalR hub using the same envelope format the
native SIPSorcery client emits, so browser and Windows participants interoperate (VP8 video).
Each tile shows live video or a name avatar, plus a connection indicator (connecting… / can't
connect) until media is flowing; Mute, Stop video, and Share screen toggle the local
tracks and update the roster. Screen sharing uses getDisplayMedia and swaps the shared screen
onto each peer's existing video sender via replaceTrack (no renegotiation); the presenter's tile
is badged 🖥 and shown whole (letterboxed) rather than cropped. A participant can present even
without a camera, and native/other viewers see the screen in place of the presenter's video.
In the browser this uses getDisplayMedia, so the OS/browser picker lets you choose a specific
display, a single window, or a tab. The Windows (WPF) client can also originate a screen
share and offers the same choice: a picker lists every display and every open window, and
the chosen source is captured (GDI CopyFromScreen for a display, PrintWindow for a single
window — which keeps working even when the window is covered) and fed through a software VP8
encoder (VideoEncoderEndPoint), switching each peer's outgoing video from the camera to the
screen on the same RTP track. The browser/other viewers see it exactly like a camera swap, and
presenter tiles are badged 🖥 everywhere. While a share is active the web client switches to a
presentation view — the shared screen takes the stage and the camera feeds shrink to a
thumbnail strip — and restores the equal grid automatically when sharing ends. When a presenter
stops sharing (or turns their camera
off), viewers show that person's avatar rather than a frozen last frame — clients broadcast
their real camera state, so tiles hide video for anyone whose camera is off and who isn't
presenting (the WPF sender also flushes a black frame on stop as a belt-and-suspenders). If no camera/mic is present the client joins receive-only (you still see and hear
others). getUserMedia needs a secure context — use https:// in production; localhost
also qualifies.
Active-speaker highlighting. Each client measures its own microphone level (RMS with a short debounce) and broadcasts a speaking flag over the hub, so every participant's tile shows a green glow while they're talking — consistent across all clients. The browser meters the mic via the Web Audio API; the WPF client decodes its encoded mic samples to measure level. Muted participants never light up.
Activity toasts. A transient popup (~5 seconds) announces meeting activity to everyone — a
new chat message (💬 Name: …), a shared file (📎 Name shared file), or whiteboard drawing
(debounced to once every 8 s). You never get a toast for your own actions. Implemented in all
clients — the browser, and the WPF / Avalonia / mobile apps (which share the view-model logic).
STUN/TURN (cross-network calls). Clients fetch their ICE server list from GET /api/ice.
The default is STUN-only (stun:stun.l.google.com:19302), which is enough when at least one peer
has an open/cone NAT. Participants on different networks behind symmetric NAT or restrictive
firewalls (common on cellular) will fail to connect with STUN alone and need a TURN relay.
Configure one without a client rebuild by setting the IceServersJson server-setting row (or the
ZPLUS_ICE_SERVERS environment variable) to a JSON array, e.g.:
[{"urls":"stun:stun.l.google.com:19302"},
{"urls":["turn:turn.example.com:3478","turns:turn.example.com:5349"],"username":"u","credential":"p"}]A common setup is coturn on the same host as the Z+ server —
reuse the server's TLS certificate for turns: on 5349 so media also traverses firewalls that only
allow TLS. The native Windows (WPF) client also fetches this list from /api/ice before creating
peer connections, so it inherits the same TURN config (it keeps stun:/turn: entries; turns:
and ?transport=tcp variants are dropped since SIPSorcery relays over UDP). The Linux and mobile
clients are roster + chat only, so they have no ICE configuration to apply.
Join before the host. A scheduled meeting can be joined before the host arrives — the first participant to join activates it (the waiting room, when enabled, still holds guests until the host admits them).
It is a small vanilla-JS single-page app embedded in the server executable (so the server
stays a single self-contained file) and served same-origin with the API and SignalR hub — no
CORS configuration is required. For http:// LAN use the browser will warn about an insecure
context but Web Crypto still works on localhost; use https:// in production (Web Crypto
requires a secure context on non-localhost origins).
src/ZPlus.Mobile is a .NET MAUI app targeting iOS and Android from one C# codebase.
It reuses the same shared pieces as the Linux client — ZPlus.Shared, MeetingHubClient,
ApiClient, and the Login/Home/Meeting view-models — so it has the same feature set:
sign in / register (with MFA), start/join/schedule meetings, live roster, E2EE chat,
reactions, polls, file sharing, whiteboard, breakout rooms, and the waiting room. Like the
Linux client it is roster + chat + features only — camera/mic capture (WebRTC) is not wired
up on mobile yet.
Requires the .NET MAUI workload (dotnet workload install maui). Build:
# Android APK (needs the Android SDK + a JDK; Visual Studio installs both)
dotnet build src/ZPlus.Mobile -c Release -f net10.0-android
# iOS — must be built on macOS with Xcode (Apple requirement); on Windows it
# compiles but cannot produce a signed .app/.ipa without a paired Mac.
dotnet build src/ZPlus.Mobile -c Release -f net10.0-ios
If the Android SDK/JDK aren't auto-detected, pass them explicitly, e.g.
-p:AndroidSdkDirectory="C:\Program Files (x86)\Android\android-sdk"
-p:JavaSdkDirectory="C:\Program Files\Android\openjdk\jdk-21.0.8".
http:// LAN servers are allowed (Android usesCleartextTraffic, iOS ATS override); for
production use an https:// server URL. On first launch, set the Server field to your Z+
server's public URL, then sign in.
-
Server — run
Z+ Server.exe(ordotnet run --project src/ZPlus.Server). On first run, next to the executable, it creates:zplus.db— SQLite database holding all users and all server configurationzplus.key— the 64-byte AES/HMAC master key protecting secrets inside the database
It binds to
http://0.0.0.0:5199by default so LAN clients can connect (Windows Firewall will prompt to allow inbound TCP 5199), and seeds the first super admin (admin@zplus.local/ChangeMe123!— change this immediately).Optional environment overrides:
ZPLUS_DB(db path),ZPLUS_KEY(key file path),ZPLUS_ADMIN_EMAIL/ZPLUS_ADMIN_PASSWORD(first-run seed account). -
Client — run
Z+ Client.exeon each participant's machine; point the Server field athttp://<server-ip>:5199, create an account, then start or join a meeting. -
Admin — run
Z+ Admin.exeand sign in with an Admin/SuperAdmin account:- Users — create accounts, edit display names, assign roles (
User,Admin,SuperAdmin), enable/disable accounts, reset passwords. Disabled accounts cannot sign in or join meetings. Only super admins can create/modify super admin accounts; nobody can demote or disable themselves. - Server settings — allow/deny self-registration, require passwords on all meetings, cap participants per meeting, change the server listen URL (restart required), and configure email invitations: SMTP host/port/from/credentials plus the Public URL used in invite links. All stored in the database and enforced live.
- Active meetings — see currently running meetings with participant counts and force-end any meeting (participants are notified instantly).
- Users — create accounts, edit display names, assign roles (
- End-to-end encryption. Audio/video flows peer-to-peer over DTLS-SRTP and never
touches the server. Chat is AES-256-GCM encrypted with a per-meeting key that
participants exchange directly via ECDH (P-256) — the server relays and stores only
ZE1$…ciphertext, so neither admins nor database access can read messages. - Passwords are hashed, then encrypted (AES + HMAC). Every password (user accounts and
meeting passwords) is first one-way hashed with PBKDF2 — so the original password can never
be recovered — and the hash is then wrapped in an AES-256-CBC + HMAC-SHA256
encrypt-then-MAC envelope (
ZP1$…values in the database). The HMAC rejects any tampered or foreign ciphertext in constant time. - Key separation. The AES/HMAC master key lives in
zplus.key, outside the database: a stolenzplus.dbexposes no usable credential material without the key file. Back up the key file securely; if it is lost, stored secrets cannot be decrypted (users would need password resets). - Configuration lives in SQLite. There is no
appsettings.json. Server behavior settings, the listen URL, and the JWT signing key (auto-generated, encrypted at rest) are all rows in theServerSettingstable, manageable through the admin app. - JWTs (12 h expiry) carry the user's role; SignalR connections authenticate via the
access_tokenquery parameter. Admin endpoints require the Admin/SuperAdmin role; disabled accounts are re-checked at meeting join, not just at login.
The server can terminate TLS natively — no reverse proxy required. In the admin app's Server settings:
- Set an HTTPS certificate (a server-side file path):
- PFX/PKCS#12 — set the certificate path to your
.pfxand the certificate password; leave the private-key path empty. - PEM (Let's Encrypt style) — set the certificate path to
fullchain.pem, the private key path toprivkey.pem, and the password only if the key is encrypted.
- PFX/PKCS#12 — set the certificate path to your
- Set the listen URL to
https://0.0.0.0:5199and the Public URL tohttps://<host>:5199. Save, then restart the server for the listen URL to apply.
Settings are validated on save — a missing file or wrong password is rejected before it
can break a restart. Clients, the admin app and SignalR all accept https:///wss://.
Automatic renewal reload. The certificate is re-read from disk automatically when its
files change (e.g. after certbot renew), so renewal needs no restart — the new cert
is picked up within ~5 minutes. Override the poll interval with ZPLUS_CERT_RELOAD_SECONDS.
Notes: use a CA-issued certificate in production (self-signed certs are correctly
rejected by the client). Ensure the account running the server can read the cert files
(Let's Encrypt's /etc/letsencrypt/live/** is root-only by default — grant access or copy
the pair somewhere readable). Enabling HTTPS also encrypts the admin/API traffic itself.
- Signaling: SignalR hub relays SDP offers/answers and ICE candidates between peers; the hub validates that both peers are in the same meeting.
- Media: full-mesh WebRTC — each client holds one
RTCPeerConnectionper remote participant, sharing a single microphone and camera source (VP8 video, one encode fan-out). The joining client always initiates offers, which avoids negotiation glare. - Host controls: mute all, ask-to-unmute, remove participant, transfer host, end for all. Host role auto-promotes to the longest-connected participant if the host drops.
- Persistence: users, meetings, attendance records, chat history, and configuration in
SQLite (swap the EF Core provider for SQL Server/PostgreSQL in production). Live roster
state is in-memory (
MeetingStateStore).
Beyond core audio/video/chat, meetings support:
- Waiting room — enable it when starting/scheduling; non-hosts wait until the host admits or denies them (host sees a pending list in the People tab).
- Reactions — one-click emoji that float over the meeting for everyone.
- Polls — the host starts a poll (question + 2–10 options); everyone votes and sees live tallies; the host can close it. Polls persist for the meeting.
- File sharing — share a file (up to 25 MB) into the meeting; everyone can download it.
- Whiteboard — a shared, collaborative drawing canvas; strokes replay for anyone who joins mid-session; anyone can clear it.
- Breakout rooms — the host creates rooms, auto-assigns participants, and opens/closes them; public chat is scoped to your room while breakouts are open.
The feature panels appear as tabs (People / Chat / Polls / Files / Board / Rooms) in the meeting window on both the Windows (WPF) and Linux (Avalonia) clients.
The admin console adds, alongside user/settings/active-meeting management:
- Dashboard — at-a-glance stats (users, admins, MFA-enrolled, active meetings/participants, meetings today/all-time, chat totals).
- Smart search — search users and meetings by name, email, topic, or meeting code.
- Audit log — an append-only record of security/admin actions (logins, user create/update, role/disable changes, password/MFA resets, settings saves, force-ends).
- MFA (TOTP) — require or reset multi-factor auth per user. Required users enroll an authenticator app (Google Authenticator, Authy, 1Password, …) at next sign-in; the shared secret is stored AES+HMAC-encrypted and never leaves the server in plaintext.
Implemented above: waiting room, reactions, file sharing, breakout rooms, whiteboard, polls, admin dashboard, MFA, audit logs, smart search. Still on the roadmap (these need external media/AI/identity infrastructure):
- Phase 2: screen sharing, recording
- Phase 3: captions/transcription
- Phase 4: SSO, compliance, large meetings (SFU media server)
- Phase 5: AI meeting summaries, action items, translation
- Native HTTPS/TLS is supported (see above) — use a CA-issued certificate in production.
- Add TURN servers for NAT traversal beyond STUN (
stun.l.google.comis used for dev). - Move from mesh to an SFU for meetings beyond ~4-6 participants.
- Switch
EnsureCreatedto EF Core migrations before real data matters.