apprtc 0.1.0-alpha.2

AppRTC P2P/SFU Signaling Server in Rust
apprtc-0.1.0-alpha.2 is not a library.
Visit the last successful build: apprtc-0.0.2

AppRTC is a WebRTC reference application and signaling server in the webrtc-rs ecosystem. The current Rust implementation provides the complete AppRTC-compatible P2P V1 room and signaling flow: HTTP join/message/leave APIs, initiator election, two-member rooms, queued signaling messages, tokenless browser WebSocket registration, reconnect grace, fallback POST/DELETE signaling, HTML templates, static web assets, ICE configuration, and HTTP/HTTPS plus WS/WSS serving.

The current executable supports P2P V1. The repository also contains the Sans-I/O SFU implementation and the architecture for P2P/SFU call modes, but V2 mode transitions and SFU worker integration are not yet enabled in the apprtc binary.

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 three Rust crates:

Crate Responsibility
apprtc Executable composition, CLI parsing, logging, HTTP or TLS listener, and graceful shutdown.
appweb AppRTC HTTP room API, configuration parameters, Jinja templates, static web assets, and the in-process client for the signaling authority.
signaling Authoritative room/client state, V1 browser protocol, message queueing and relay, reconnect deadlines, Sans-I/O protocol layers, and the Axum WebSocket driver.

The initial deployment is an all-in-one process. appweb does not maintain a second room table: its HTTP handlers submit admit, remove, occupancy, inject, and status operations to the single serialized Collider owner task. Browser WebSocket traffic terminates directly in signaling and never passes through the web handlers.

The signaling state is composed from Sans-I/O protocols:

Collider
└── RoomTable
    └── Room
        └── Client

Tokio tasks own sockets and timers, while Collider owns deterministic signaling state. 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 V1 behavior

  • 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 /join response returns queued messages in params.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 /_internal POST/DELETE fallback routes are both supported.
  • V1 identifiers are not restricted to numeric values. Numeric u64 validation belongs to the future V2 protocol.

Build and test

Use a Rust toolchain with Edition 2024 support.

git submodule update --init --recursive
cargo build --workspace
cargo test --workspace --lib --bins
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all -- --check

The integration tests are black-box clients of a real AppRTC TLS server. Run them locally in three steps from the repository root:

# 1. Start AppRTC on loopback in the background.
cargo run -p apprtc -- --host 127.0.0.1 --port 8080 --web-root appweb --tls --debug --level info &

# 2. Run the integration tests against that server.
cargo test -p apprtc --test '*' -- --nocapture

# 3. Stop the background server.
kill $(pgrep -f "target/debug/apprtc") || true

CI performs the same sequence with a release build in .github/workflows/tests.yml and uploads the server log when the job finishes.

Run over HTTP and WebSocket

Run this command from the repository root:

cargo run -p apprtc -- \
  --host 127.0.0.1 \
  --port 8080 \
  --web-root appweb \
  --debug \
  --level info

The server prints:

AppRTC listening on http://127.0.0.1:8080

Open http://127.0.0.1:8080 in a browser.

--host is used both as the local listening host and in generated HTTP/WebSocket URLs. Use an address that browsers can reach; the default is 127.0.0.1. The generated URLs include --port.

Run over HTTPS and secure WebSocket

Add --tls to serve real HTTPS and WSS from the same listener:

cargo run -p apprtc -- \
  --host 127.0.0.1 \
  --port 8080 \
  --web-root appweb \
  --tls \
  --debug \
  --level info

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:

cargo run -p apprtc -- \
  --host apprtc.example.com \
  --port 443 \
  --web-root appweb \
  --tls \
  --certificate /path/to/fullchain.pem \
  --private-key /path/to/privkey.pem

Command-line options

Run cargo run -p apprtc -- --help for the authoritative list.

Option Default Description
--host <HOST> 127.0.0.1 Host used by the listener and generated HTTP/WebSocket URLs.
-p, --port <PORT> 8080 HTTP/HTTPS and WS/WSS listening port.
--web-root <PATH> appweb Directory containing the HTML, JavaScript, CSS, and image assets.
--tls off Serve HTTPS and WSS instead of HTTP and WS.
--certificate <PATH> bundled development certificate PEM certificate chain used with --tls.
--private-key <PATH> bundled development key PEM private key used with --tls.
--ice-server-url <URLS> empty ICE server URL; repeat the option or provide comma-separated URLs.
--ice-server-base-url <URL> same AppRTC origin External ICE credential service origin.
--ice-server-api-key <KEY> empty API key appended to the ICE credential service URL.
--header-message <TEXT> empty Banner displayed by the web application.
--bypass-join-confirmation off Skip the browser's ready-to-join prompt.
-d, --debug off Enable application logging.
-l, --level <LEVEL> info Log filter: error, warn, info, debug, or trace.
-o, --output-log-file <PATH> stdout Truncate and write formatted logs to a file.

Example ICE configuration:

cargo run -p apprtc -- \
  --ice-server-url stun:stun.l.google.com:19302 \
  --ice-server-url turn:turn.example.com:3478

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:

{
  "result": "SUCCESS",
  "params": {
    "room_id": "example-room",
    "client_id": "12345678",
    "is_initiator": "true",
    "wss_url": "ws://127.0.0.1:8080/ws",
    "wss_post_url": "http://127.0.0.1:8080"
  }
}

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:

{
  "cmd": "register",
  "roomid": "example-room",
  "clientid": "12345678"
}

A successful V1 registration has no acknowledgement. The registered client sends an opaque signaling payload with:

{
  "cmd": "send",
  "msg": "{\"type\":\"candidate\",\"label\":0,\"id\":\"0\",\"candidate\":\"candidate:...\"}"
}

The peer receives the payload without the signaling authority parsing or modifying the inner JSON:

{
  "msg": "{\"type\":\"candidate\",\"label\":0,\"id\":\"0\",\"candidate\":\"candidate:...\"}",
  "error": ""
}

Protocol errors are sent once before the socket is closed:

{
  "msg": "",
  "error": "Client not registered"
}

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:

{
  "upsec": 12.5,
  "openws": 2,
  "totalws": 5,
  "wserrors": 0,
  "httperrors": 0
}

Repository layout

apprtc/       Rust executable crate and development TLS certificate
appweb/       Rust web/API crate plus AppRTC browser assets
signaling/    Rust Sans-I/O signaling authority and WebSocket driver
sfu/          Rust Sans-I/O selective-forwarding media server
docs/design/  Signaling architecture and protocol design

License

This project is dual-licensed under the MIT and Apache-2.0 licenses.

Contributing

Contributors and pull requests are welcome.