projectaria-webrtc-signaling-server
WebRTC signaling server for Project Aria Gen2 streaming.
A stateless rendezvous point: two peers connect to it, it matches them up in a room, authenticates them, and passes SDP offers/answers and ICE candidates between them until they have negotiated a connection. Run it on any host both peers can reach.
Audio and video never pass through this server. That is a property of the signaling server specifically, not of the deployment as a whole: once the peers are introduced, media flows directly between them where the network allows it, and through a TURN server when it does not. If you run TURN — which most cross-NAT deployments need — then your media does flow through that relay, and it should be sized and placed accordingly. TURN is a separate process from this one, even when both run on the same host.
You need this only for cross-network streaming. If the glasses and the viewer are on
the same network, the viewer's --listen mode connects them directly and no server is
involved.
Signaling is not the same job as STUN/TURN, and you generally need both across NATs: this server introduces the peers to each other, STUN lets them discover their own public addresses, and TURN carries the media when the two cannot reach each other directly. This package is only the first of those; you install and run STUN/TURN separately.
Install
pip install projectaria-webrtc-signaling-server
The server is pure Python standard library — no third-party dependencies on any platform. Runs on Linux, macOS and Windows, Python 3.9+.
The optional HTTP/2 listener (for peers behind proxies or firewalls that only allow HTTPS) needs one extra:
pip install "projectaria-webrtc-signaling-server[http2]"
On a Linux server, give it its own virtualenv. Debian 12, Ubuntu 23.04, Fedora 38 and
their successors mark the system Python as externally managed (PEP 668), so a system-wide
pip install there fails with error: externally-managed-environment:
sudo apt-get install -y python3-venv # or your distribution's equivalent
sudo python3 -m venv /opt/aria-signaling
sudo /opt/aria-signaling/bin/pip install projectaria-webrtc-signaling-server
# In each shell you run the server from. Every example below then works as written.
source /opt/aria-signaling/bin/activate
Activation does not survive sudo, which replaces PATH with its own secure_path on
most distributions, so the sudo commands below name the full path instead. That is also
what you want for --install-service: the unit's ExecStart is built from the copy of
the server you ran the installation with, so invoking the venv's copy is what pins the
service to that interpreter.
pip install --user is not an alternative to the virtualenv — it puts the console script
in the invoking user's ~/.local/bin, which a service running as another account cannot
reach.
Two ways to run it
1. Run it directly — signaling only. Nothing else is installed, and it stops when you stop it. Use this when you already have STUN/TURN, or when the peers can reach each other directly and only need introducing:
# Generate a shared secret once, and give the same value to both peers. The `install`
# line creates the file empty, mode 0600 and owned by you, before anything is written
# to it: that is what lets the unprivileged redirect below write into root-owned
# /etc/aria, and it means the secret is never briefly readable by other accounts.
sudo mkdir -p /etc/aria
sudo install -m 600 -o "$USER" /dev/null /etc/aria/auth.secret
openssl rand -hex 32 > /etc/aria/auth.secret
projectaria_webrtc_signaling_server --port 8443 --auth-token-file /etc/aria/auth.secret
The token may also come from the ARIA_SIGNALING_AUTH_TOKEN environment variable. It is
deliberately not accepted as a command-line argument — argv leaks into ps output
and /proc/<pid>/cmdline.
Run projectaria_webrtc_signaling_server --help for the full flag list.
2. Install it as a systemd service with --install-service — survives reboots. This
is the recommended path for a VPS. Pass the flags you want the service to run with, plus
--install-service, and the server writes a unit instead of starting:
sudo /opt/aria-signaling/bin/projectaria_webrtc_signaling_server \
--install-service \
--port 8443 \
--auth-token-file /etc/aria/auth.secret
That writes /etc/systemd/system/aria-signaling.service, enables it and starts it. The
unit's ExecStart is rebuilt from the very flags you passed, so the running service and
the command you typed cannot drift apart, and a flag added to a later release is carried
across without editing anything by hand.
| Flag | Effect |
|---|---|
--install-service |
write and activate the unit instead of running the server |
--service-name <name> |
unit name, default aria-signaling |
--service-user <name> |
account the service runs as. Defaults to the user behind sudo, then root |
Details worth knowing:
- The token must come from a file. Installation is refused when the token comes from
ARIA_SIGNALING_AUTH_TOKEN, because that would inline the secret into a unit file. The unit is written mode0600regardless, since itsExecStartnames the token path. - Relative paths are absolutized. systemd runs units from
/, so a path that worked in your shell would otherwise resolve somewhere else. - It is Linux-only and exits with a clear message elsewhere. The server itself runs on macOS and Windows; wrap it in launchd or a Windows service if you need it at boot.
- Failed activation rolls back. If
systemctlcannot enable or start the unit, the unit file is removed rather than left half-installed.
Check it and read its logs the usual way:
systemctl status aria-signaling
journalctl -u aria-signaling -f
Deploying STUN/TURN as well. This package installs only the signaling server. Across
NATs you will usually also want coturn, which is a separate daemon with its own
lifecycle: install and configure it with your own package manager and service manager,
then point the peers at it with --stun / --turn, or configure it once on the server
with --ice-servers.
Connecting the peers
Point both ends at the server. Any shared --room id pairs them; add a room password so
that knowing the room id is not by itself enough to join.
Both peers also need STUN/TURN in most real deployments — anything crossing NATs — so they are shown here rather than as an afterthought. If coturn runs on the same host, these are that host and the TURN credentials you configured it with.
# Glasses (over USB). --turn takes one comma-separated url,username,credential.
aria_gen2 streaming webrtc start \
--signaling-url tcp://<server-host>:8443 \
--room my-room --room-password "$ROOM_PASSWORD" --auth-token "$AUTH_TOKEN" \
--stun stun:<server-host>:3478 \
--turn turn:<server-host>:3478,aria,"$TURN_PASSWORD" \
--interface wifi_sta
# Receiver computer. Note --turn-username / --turn-password are separate flags here.
aria_streaming_viewer --transport webrtc \
--signaling-host <server-host> --signaling-port 8443 \
--room my-room --room-password "$ROOM_PASSWORD" --auth-token "$AUTH_TOKEN" \
--stun stun:<server-host>:3478 \
--turn turn:<server-host>:3478 \
--turn-username aria --turn-password "$TURN_PASSWORD"
You can drop the STUN/TURN flags only when the two peers can already reach each other directly — same LAN, or both on public addresses.
If the glasses fail to pair and the server logs auth_rejected with a timestamp skew,
their clock has drifted outside the freshness window: add --ntp-sync to the device
command so it syncs before the session starts.
As an alternative to passing them per peer, configure them once on the server with
--ice-servers <file> and it will push them to both peers in the paired envelope. The
two sources are additive, not exclusive: server-pushed entries are placed first so the
operator controls the primary set, and any peer-supplied --stun / --turn entries
follow as fallback.
Note what that trades away. Any TURN username and credential in the file is handed to every peer that authenticates and pairs, and it crosses the plain TCP signaling channel to get there. Configured per peer, the auth token and the TURN credential are independent secrets; served from here, the auth token alone grants relay access as well. Convenient when the same operator holds both, worth avoiding when peers should not be able to use your TURN server for their own traffic. The file is sent verbatim — the server does not mint short-lived TURN REST credentials, so rotation means editing the TURN server config.
Signaling over TLS
Everything above puts signaling on the plain TCP listener, in the clear. The server can
bind a second listener that speaks the same protocol over HTTP/2 inside TLS, which a
peer reaches with an https:// signaling URL. Both listeners share one relay, so peers
pair with each other whichever one they arrive on and you can move one end at a time.
Peers on the TLS listener authenticate with a bearer token presented inside the session rather than the HMAC handshake, so it takes a secret of its own.
# 1. The TLS listener needs the one optional dependency. Into a root-owned virtualenv,
# that is `sudo /opt/aria-signaling/bin/pip install "...[http2]"`.
pip install "projectaria-webrtc-signaling-server[http2]"
# 2. The host peers will dial. Use your DNS name if you have one; otherwise ask the
# internet what this host looks like from outside, which is not necessarily what
# `hostname -I` says on a cloud VM behind NAT. Note the value down — the receiver
# needs the same one.
export SERVER_HOST=$(curl -s https://api.ipify.org)
echo "$SERVER_HOST"
# 3. A self-signed certificate for it, generated here so the private key never travels.
# The SAN is not strictly required — peers carry your anchor and skip the hostname
# check — but `openssl` and `curl` do check it when you verify by hand, so it is worth
# the line. Its type has to match what SERVER_HOST holds: openssl rejects a name that
# arrives as `IP:`.
export SAN="IP:$SERVER_HOST" # for a DNS name: export SAN="DNS:$SERVER_HOST"
openssl req -x509 -newkey rsa:4096 -nodes -days 3650 \
-subj "/CN=aria-signaling" -addext "subjectAltName=$SAN" \
-keyout server.key -out server.pem
sudo install -m 600 -o "$USER" server.key /etc/aria/server.key
sudo install -m 644 -o "$USER" server.pem /etc/aria/server.pem
# 4. A bearer token for this listener, separate from the TCP one. Same pre-create
# idiom as auth.secret above. Both peers need this value.
sudo install -m 600 -o "$USER" /dev/null /etc/aria/http2.secret
openssl rand -hex 32 > /etc/aria/http2.secret
export HTTP2_TOKEN="$(cat /etc/aria/http2.secret)"
# 5. Run both listeners from one process. To write the unit with these flags instead of
# starting, add --install-service — and invoke it as
# `sudo /opt/aria-signaling/bin/projectaria_webrtc_signaling_server`, since sudo does
# not find the bare name and ExecStart is pinned to whichever copy you ran.
projectaria_webrtc_signaling_server \
--port 8443 --auth-token-file /etc/aria/auth.secret \
--http2-port 9443 \
--http2-tls-cert /etc/aria/server.pem \
--http2-tls-key /etc/aria/server.key \
--http2-auth-token-file /etc/aria/http2.secret
Open 9443 in both firewalls, the host's and your provider's.
If you added the TLS flags to an existing service with --install-service, restart it
explicitly: the installer rewrites the unit and then starts it, which is a no-op on an
already-running service, so the old process stays up on the old ports.
sudo ss -lntp | grep -E ':8443|:9443' shows which are actually bound.
Point the receiver at it
For a self-signed or private-CA deployment there is nothing to install into a system store — copy the certificate across and name it as the trust anchor. Only the certificate travels; the key stays on the server.
# On the receiver computer.
export SERVER_HOST=<the value step 2 printed>
export HTTP2_TOKEN=<the contents of /etc/aria/http2.secret>
export ROOM_PASSWORD=<the room password you chose>
export TURN_PASSWORD=<the credential you configured coturn with>
scp "$SERVER_HOST":/etc/aria/server.pem ~/aria-signaling-ca.pem
# Prove the chain and the route before involving a peer. `-CAfile` is not optional on a
# self-signed deployment: without it openssl checks the system store and reports
# `Verify return code: 18` for a perfectly good server.
openssl s_client -connect "$SERVER_HOST:9443" -alpn h2 -CAfile ~/aria-signaling-ca.pem \
</dev/null 2>&1 | grep -E "Verify return code|ALPN protocol"
Expect Verify return code: 0 (ok) and ALPN protocol: h2. ALPN protocol: none means
peers will fail with an opaque connect error, and is the one to act on. Then start the
receiver:
aria_streaming_viewer --transport webrtc \
--signaling-url "https://$SERVER_HOST:9443" \
--ca-root ~/aria-signaling-ca.pem \
--room my-room --room-password "$ROOM_PASSWORD" \
--auth-token "$HTTP2_TOKEN" \
--stun "stun:$SERVER_HOST:3478" \
--turn "turn:$SERVER_HOST:3478" \
--turn-username aria --turn-password "$TURN_PASSWORD"
--ca-root names the anchor to verify the server against, so pass the path you just
copied; it is not the default and the connection fails without it. A peer carrying your
anchor skips the hostname check — the anchor is the identity, because you issued
exactly one certificate with it — which is what lets the deployment stay on a bare IP
with no DNS name. Keep such a CA single-purpose: anything else it ever signs is accepted
for your signaling server too.
Install the anchor on the glasses
The device takes its anchor from a certificate set installed on it, not from a flag. The set also carries a publisher certificate and key, which signaling does not use but the installer requires, so generate a throwaway pair for them:
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-keyout node.key -out node.pem -subj "/CN=aria-node-dummy"
aria_gen2 streaming install-certs user-defined-certs \
--ca-root ~/aria-signaling-ca.pem \
--cert node.pem \
--key node.key \
--cert-name default
Two things worth knowing before you debug the wrong thing:
--cert-namemust bedefault. The device consults/data/misc/streaming/certs/default/root_ca.pemand nothing else. Any other name installs successfully and is then silently ignored.--certand--keyare required even though only the root matters here. One throwaway pair, reused, is the least confusing way to satisfy that.
With adb root, adb shell ls -lZ /data/misc/streaming/certs/default/ should list
root_ca.pem, cert.pem and key.pem as 0600 system:system, labelled
u:object_r:streaming_cert_file:s0. The trust path itself is worth proving from the
device before streaming anything:
adb shell 'curl -sS -o /dev/null -w "code=%{http_code} verify=%{ssl_verify_result}\n" \
--cacert /data/misc/streaming/certs/default/root_ca.pem \
https://'"$SERVER_HOST"':9443/v2/signaling'
verify=0 is the chain validating; code=405 is the endpoint refusing a GET, which
means you reached it through TLS. Then start the device against the TLS listener:
aria_gen2 streaming webrtc start \
--signaling-url "https://$SERVER_HOST:9443" \
--room my-room --room-password "$ROOM_PASSWORD" --auth-token "$HTTP2_TOKEN" \
--interface wifi_sta
Without an installed anchor the device refuses to start an https:// session rather than
falling back to a certificate it cannot authenticate, and names the path it looked in.
Installing the anchor is therefore a required step for TLS signaling, not an optional
hardening one.
Checking a deployment
The smoke-test subcommand connects two synthetic peers, authenticates them, pairs them
and passes an envelope between them — the same handshake the glasses perform, with no
hardware involved:
# If you just backgrounded the server, wait for the listener first.
until nc -z signaling.example.com 8443 2>/dev/null; do sleep 0.2; done
projectaria_webrtc_signaling_server smoke-test \
--host signaling.example.com --port 8443 \
--auth-token-file ./auth.secret --room-password "$ROOM_PASSWORD"
Run this before involving the glasses. It separates "server, firewall or token is wrong" from "glasses or TURN is wrong", which are hard to tell apart from the viewer. A pass means signaling is healthy; it says nothing about whether media flows, since that depends on STUN/TURN and no media passes through this server.
Ports and firewall
| Port | Protocol | Purpose | Needed when |
|---|---|---|---|
| 8443 | TCP | Signaling | always |
| 9443 | TCP | Signaling over TLS | only with --http2-port |
| 3478 | TCP + UDP | STUN/TURN control | only with a STUN/TURN server |
| 49152–49200 | UDP | TURN media | only with a STUN/TURN server |
Running signaling without STUN/TURN needs only the signaling port.
Open these in the host firewall (ufw, firewalld or equivalent). Take care
enabling an inactive ufw on a remote host: add the allow OpenSSH rule before enabling
it, or you will lock yourself out of the VPS.
Your hosting provider almost certainly has a second firewall in front of the host — a cloud security group, a hosting-panel firewall, an upstream appliance. The host firewall above does not touch it, and it cannot be configured from inside the VM. Open the same ports there, using whatever console or CLI your provider gives you.
If you started the server directly rather than using the script, nothing was opened for
you. Verify reachability from your receiver computer with nc -vz <host> 8443 before
glasses.
Security model
-
Put signaling behind TLS on any untrusted network. The plain TCP listener sends everything the peers exchange to find each other in the clear: the room id and room password, the auth handshake, session ids, and the SDP — including the ICE candidates, which carry both peers' addresses.
Media is encrypted either way, and that is the easiest thing here to be wrong about. The SDP carries the DTLS fingerprints with which the peers authenticate each other's media keys, so an attacker who can modify the signaling stream substitutes their own, and the media negotiates against them instead — encrypted, to the wrong party. One who can only read it still comes away with the room password to join on, a captured handshake to replay while it is fresh, and both peers' addresses.
Both listeners share one relay and peers pair across them, so you can turn TLS on for one end at a time and decide separately whether to keep the plain listener open.
-
Auth on the plain TCP listener is HMAC-SHA256 over a client-supplied timestamp, keyed by the shared token. The timestamp is also range-checked against the server clock —
--auth-freshness-windowseconds, 60 by default — so a captured handshake stops replaying once it ages out.This assumes the device's clock is roughly right. If it is not, the peer is rejected with
auth_rejectedand the server logs the skew. Have the glasses sync their clock first by passing--ntp-synctoaria_gen2 streaming webrtc start; the device runs an SNTP sync before the session begins. Note that this makes NTP reachability a hard precondition for starting the session.For a fleet that genuinely cannot sync,
--auth-freshness-window 0accepts any timestamp and the server warns at startup. That leaves a captured handshake replayable indefinitely, so set a room password, restrict who can reach the signaling port at all, and prefer signaling over TLS — peers there authenticate with a bearer token and send no replayable handshake. -
The TLS listener authenticates separately, with that bearer token presented inside the TLS session (
--http2-auth-token-file) instead of the HMAC handshake. Each listener needs its own token, and the server exits rather than binding one it cannot authenticate. -
Room passwords are established by the first peer to register in a room; every later peer, including one resuming a dropped session, must present the same value or is rejected with
error{code:"room_password_mismatch"}. Rooms where no peer sets a password are unaffected. -
This server never sees media. Only SDP and ICE candidates pass through it. Media goes directly between the peers, or through your TURN server when a direct path cannot be negotiated — so TURN, not this process, is what carries media on a NAT-ed deployment.
-
--ice-serverswidens what the auth token is worth. The list, including any TURN credential in it, is sent to both peers in thepairedenvelope, so a leaked auth token yields relay access too. Rotate the two together, or configure STUN/TURN per peer instead. -
The plain TCP listener has no TLS of its own, and fronting it with a terminating proxy does not change that: peers dial that substrate in the clear and have no way to negotiate TLS with the proxy. Encrypting signaling means the HTTP/2 listener, either terminating TLS itself or behind an edge proxy with
--http2-trust-proxy-tls.
Protocol
The wire protocol (v2, JSON-lines) is specified in full in the module docstring of
projectaria_webrtc_signaling_server/signaling_server.py — envelope shapes, error codes,
resume semantics, and the forward-compatibility rule that lets clients add new envelope
types without a server roll.
Peers must agree with the server on these values. Changing one of them on the server alone breaks streaming:
| Value | Setting |
|---|---|
| Wire version | 2 |
| Max envelope line | 64 KiB |
| Handshake timeout | 5 s |
| Resume grace window | 5 s |
| HTTP/2 signaling path | /v2/signaling |
| Default port | 8443 |
| Max role / room / session-id / room-password length | 32 / 128 / 128 / 128 |
| Auth digest | HMAC-SHA256 hex over the decimal-ns timestamp string |
Running without installing
signaling_server.py runs directly as a single file
(python3 projectaria_webrtc_signaling_server/signaling_server.py --help) if you would
rather not install anything. It imports only the standard library.
License
Apache 2.0. See LICENSE.
Release files for projectaria-webrtc-signaling-server 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| projectaria_webrtc_signaling_server-0.1.2.tar.gz | 101.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| projectaria_webrtc_signaling_server-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 164.9 kB
Release files / projectaria_webrtc_signaling_server-0.1.2.tar.gz
| Download URL | projectaria_webrtc_signaling_server-0.1.2.tar.gz |
|---|---|
| Size | 101.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8dc0959299f71ec46bac50a7ba13ae396155e445683c76d4066ca86645a38efe
|
|
BLAKE2b-256 checksum How to use checksums |
eab350a703164e67c18b5b35073c1a603625a70d156aebd19d28ee9bb324a57d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Release files / projectaria_webrtc_signaling_server-0.1.2-py3-none-any.whl
| Download URL | projectaria_webrtc_signaling_server-0.1.2-py3-none-any.whl |
|---|---|
| Size | 63.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5ae7a1c402882fb8dad13f4a07b480f54818ac192ea113feac4e2902a390d4ae
|
|
BLAKE2b-256 checksum How to use checksums |
fdee8318c7707445dfe7b4183d2ae54857f21ff85673810ab6c5f8d76b84d715
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|