AppRTC is a WebRTC reference application and signaling server in the webrtc-rs ecosystem. The Rust implementation
supports the AppRTC-compatible P2P V1 flow and the token-authenticated V2 P2P/SFU flow. The room-selection page defaults
to V1 and provides an unchecked V2 P2P/SFU checkbox. V2 uses numeric u64 room/client IDs, namespaced HTTP routes,
signaling-issued admission tokens, explicit WebSocket registration acknowledgement, signal epochs, symmetric WebSocket
offer/answer/trickle-ICE relay, reconnect grace, and survivor promotion.
The first two V2 members use a direct P2P connection. When a third member joins, signaling assigns a ready SFU worker, waits for all three worker-side joins, commits a new signal epoch, and tells the existing browsers to create fresh SFU peer connections while their P2P connection remains active. The third browser joins directly in SFU mode. SFU→P2P downgrade is not implemented yet.
The Rust implementation replaces the previous unified Go Collider. The legacy implementation is retained only on the
repository's go branch.
Architecture
The workspace has four Rust crates:
| Crate | Responsibility |
|---|---|
apprtc |
Standalone appweb, signaling, and sfu binaries plus their runtime adapters: CLI parsing, TLS listeners, logging, graceful shutdown, browser WebSocket I/O, private gRPC, UDP media I/O, and the Sans-I/O SFU driver. |
appweb |
AppRTC HTTP room API, configuration parameters, Jinja templates, static web assets, and a reusable gRPC client for the signaling authority. |
signaling |
Authoritative V1 and V2 P2P/SFU room, client, worker, lifecycle, transition, browser-protocol, token/epoch, replay, and reconnect state — a pure Sans-I/O crate with no sockets, threads, clock, or entropy source of its own. |
signaling-proto |
Generated Protobuf and tonic contract shared by AppWeb, signaling, and SFU workers. |
AppWeb, signaling, and SFU are separate processes and may run on different machines. AppWeb serves HTTP(S) and uses
concurrent unary gRPC calls over one reusable HTTP/2 channel to submit V1 and V2 admission, removal, occupancy, V1
injection, and status operations to signaling. Browser WebSocket traffic connects directly to signaling and never passes
through AppWeb. Each SFU process owns one reconnecting bidirectional OpenSfuSession gRPC stream to signaling; browser
media travels directly to the SFU over ICE/DTLS/SRTP and never passes through AppWeb or signaling.
The signaling state is composed from Sans-I/O protocols:
Collider
└── RoomTable
└── Room
└── Client
Every layer implements the sansio::Protocol trait, so the whole signaling state machine is deterministic and testable
in memory, without sockets or a wall clock.
The apprtc library keeps each runtime responsibility in a dedicated module:
apprtc/src/
├── grpc_server.rs private signaling gRPC service adapter
├── sfu_server.rs signaling stream, UDP media shards, and Sans-I/O SFU adapter
├── signaling_server.rs command channel and single-owner Collider event loop
└── ws_server.rs public browser TCP/TLS, HTTP upgrade, and WebSocket sessions
apprtc/src/ws_server.rs accepts browser /ws connections and converts WebSocket lifecycle
events and text frames into driver commands. apprtc/src/grpc_server.rs adapts private
gRPC requests to the same command channel. apprtc/src/signaling_server.rs owns the
single Collider event loop, serializes every browser and authority operation, fires protocol timeouts, and routes
outputs back to the WebSocket or gRPC caller. Tasks sleep on async I/O, deadlines, or shutdown without polling.
Successful V1 registration is intentionally silent, and a disconnected registered client remains eligible to reconnect for 10 seconds before its membership is removed.
Current P2P behavior
V1 preserves the legacy AppRTC contract:
- Room and client IDs are opaque non-empty strings.
- A room contains at most two clients; a third join returns
FULL. - The first client is the initiator and the second is the callee.
- Removing a client promotes the survivor to initiator.
- Offers and trickle ICE candidates sent before the peer joins are queued.
- The second
/joinresponse returns queued messages inparams.messages. - The stock AppRTC asymmetric signaling flow is preserved: the initiator sends early signaling through
/message, while the callee normally sends through WebSocket. - A WebSocket disconnect starts a 10-second reconnect grace period instead of removing the client immediately.
- Root-path and
/_internalPOST/DELETE fallback routes are both supported. - V1 identifiers are not restricted to numeric values.
P2P V2 adds:
- An unchecked Use signaling V2 room-selection checkbox; V1 remains the default.
/v2/r/{roomid},/v2/join/{roomid},/v2/leave/{roomid}/{clientid}, and/v2/paramsroutes.- Canonical decimal
u64room and client IDs. - A signaling-issued admission token bound to the room/client pair.
- Explicit
{control:"registered"}acknowledgement before signaling starts. - An
epochon every browsersendframe; stale or malformed epochs are dropped. - Symmetric WebSocket relay for offers, answers, and trickle-ICE candidates; V2 does not use
/messageor the V1 WebSocket POST fallback. - Authenticated leave using
Authorization: Bearer <admission_token>andp2p-promotefor the surviving participant.
SFU-capable V2 adds:
- Capacity-aware selection of a ready SFU worker when the third member joins.
- An ordered
JoinMemberbarrier for all room members before signaling commitsUpgradingtoSFUand increments the signal epoch. - A fresh browser SFU peer connection while the existing P2P connection remains active; the old P2P connection closes only after SFU ICE connects.
- Grid-based remote participants: the peer video/audio fills the window as a responsive grid of per-publisher tiles, the only UI change from P2P — the self-view and call controls keep their P2P positions. Each tile groups a peer's forwarded video and audio, and a peer's tile is removed from the grid when it leaves the room (reconciled from the negotiated transceivers after each SFU re-offer).
- SFU publish/subscribe SDP and full trickle-ICE exchange through the same browser V2 WebSocket envelope.
- Reliable worker events, command result correlation and deduplication, health/capacity reporting, same-instance reconnect synchronization, and command replay.
- Ordered worker-side joins and leaves for members admitted to or removed from an existing SFU room.
SFU→P2P downgrade is intentionally deferred.
Build and test
Use a Rust toolchain with Edition 2024 support.
The integration tests are black-box clients of real standalone AppWeb and signaling TLS servers:
# 1. Start signaling.
&
# 2. Start one SFU worker.
&
# 3. Start AppWeb.
&
# 4. Run the integration tests.
# 5. Stop all three services.
||
CI performs the same sequence with a release build in .github/workflows/tests.yml and uploads the service logs when
the job finishes. Its black-box V2 test verifies the real AppWeb→signaling→SFU third-member join barrier and browser
upgrade controls.
Run over HTTP and WebSocket
Run signaling, one SFU worker, and AppWeb separately from the repository root:
AppWeb prints:
AppWeb listening on http://127.0.0.1:8080/
Open http://127.0.0.1:8080 in a browser.
--host-ip controls the bind address for each process. In the signaling process it applies to both the browser
WebSocket listener and the private gRPC listener; --port and --grpc-port select their respective ports. AppWeb's
--public-url controls the browser-facing HTTP origin, --ws-url controls the browser-facing WebSocket URL and must
include /ws, and --grpc-url independently selects the private signaling gRPC origin.
Run over HTTPS and secure WebSocket
Add --tls to serve real HTTPS and WSS from the same listener:
Without certificate options, AppRTC uses the bundled development certificate at
apprtc/cert/cert.pem. Its subject alternative names include localhost, 127.0.0.1, and
::1, but it is self-signed. Trust that certificate in the browser or operating-system trust store before opening the
page; otherwise HTTPS and WSS clients will reject it with CertificateUnknown or an equivalent certificate-authority
error.
For a deployment, supply a certificate issued by a trusted authority. Both options must be supplied together:
Command-line options
Run cargo run -p apprtc --bin appweb -- --help, cargo run -p apprtc --bin signaling -- --help, or
cargo run -p apprtc --bin sfu -- --help for the authoritative lists.
| Option | Default | Description |
|---|---|---|
--host-ip <HOST-IP> |
127.0.0.1 |
Local TCP or UDP bind address (all binaries). |
--public-url <URL> |
listener address/scheme | Browser-facing HTTP(S) origin (appweb) or WS(S) origin (signaling). |
-p, --port <PORT> |
8080/8081 |
AppWeb HTTP(S), signaling WS(S), or SFU redirect (--redirect-url) port. |
--web-root <PATH> |
appweb |
Static asset directory (appweb). |
--tls |
off | Serve HTTPS/WSS instead of HTTP/WS (all binaries, incl. the SFU redirect). |
--certificate <PATH> |
bundled certificate | PEM certificate chain used with --tls. |
--private-key <PATH> |
bundled key | PEM private key used with --tls. |
--ws-url <URL> |
none | Public browser signaling WebSocket URL ending in /ws (appweb). |
--grpc-url <URL> |
http://127.0.0.1:50051 |
Private signaling gRPC origin (appweb and sfu). |
--insecure-tls |
off | Disable gRPC verification for local self-signed TLS (appweb, sfu). |
--grpc-port <PORT> |
50051 |
Private gRPC listener port (signaling). |
--ice-server-url <URLS> |
empty | ICE server URLs (appweb). |
--ice-server-base-url <URL> |
AppWeb origin | External ICE credential service origin (appweb). |
--ice-server-api-key <KEY> |
empty | API key for the ICE credential service (appweb). |
--header-message <TEXT> |
empty | Banner displayed by the web application (appweb). |
--bypass-join-confirmation |
off | Skip the browser ready-to-join prompt (appweb). |
--media-public-ip <IP> |
--host-ip |
ICE candidate address advertised by sfu; set only when it differs from the bind address (NAT). |
--redirect-url <URL> |
empty | When set, sfu runs a server on --host-ip:--port that redirects every request here. |
--media-port-min <PORT> |
3478 |
First UDP media port owned by sfu. |
--media-port-max <PORT> |
3495 |
Last UDP media port owned by sfu. |
--max-rooms <COUNT> |
1000 |
SFU room capacity advertised to signaling. |
--max-clients <COUNT> |
10000 |
SFU client capacity advertised to signaling. |
--instance-id <ID> |
generated value | Optional SFU process-incarnation ID; normally omit it. |
-d, --debug |
off | Enable application logging (all binaries). |
-l, --level <LEVEL> |
info |
Log filter (all binaries). |
-o, --output-log-file <PATH> |
stdout | Write formatted logs to a file (all binaries). |
Example ICE configuration:
HTTP API
| Method and path | Behavior |
|---|---|
GET / |
Render the room-selection page. |
GET /r/{roomid} |
Render the call page or the full-room page when occupancy is two. |
POST /join/{roomid} |
Generate an eight-digit client ID, admit it, and return legacy AppRTC room parameters. |
POST /message/{roomid}/{clientid} |
Queue or relay the raw signaling message and return { "result": "SUCCESS" }. |
POST /leave/{roomid}/{clientid} |
Remove the client and return the legacy empty success response. |
GET /params |
Return room-independent AppRTC parameters. |
GET or POST /v1alpha/iceconfig |
Return the configured ICE server list. |
GET /status |
Return uptime and WebSocket/HTTP counters. |
POST /{roomid}/{clientid} |
V1 wss_post_url fallback: inject a raw signaling message. |
DELETE /{roomid}/{clientid} |
V1 wss_post_url fallback: remove the client. |
POST or DELETE /_internal/{roomid}/{clientid} |
Compatibility alias for the fallback bridge. |
Static files under appweb/js, appweb/css, appweb/images, and appweb/html are served by the same process.
Join response
A successful join returns the legacy shape consumed by appweb/js/call.js:
The actual params object also contains peer-connection constraints, ICE configuration, media constraints, room links,
loopback settings, and UI configuration.
V1 WebSocket protocol
Browsers connect to /ws and send a registration frame first:
A successful V1 registration has no acknowledgement. The registered client sends an opaque signaling payload with:
The peer receives the payload without the signaling authority parsing or modifying the inner JSON:
Protocol errors are sent once before the socket is closed:
The inner msg string may contain an SDP offer, SDP answer, trickle ICE candidate, end-of-candidates marker, or bye
object. V1 signaling treats it as an opaque UTF-8 string.
Status endpoint
GET /status preserves the Collider-compatible response:
License
This project is dual-licensed under the MIT and Apache-2.0 licenses.
Contributing
Contributors and pull requests are welcome.