Skip to main content

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 mode 0600 regardless, since its ExecStart names 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 systemctl cannot 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-name must be default. The device consults /data/misc/streaming/certs/default/root_ca.pem and nothing else. Any other name installs successfully and is then silently ignored.
  • --cert and --key are 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-window seconds, 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_rejected and the server logs the skew. Have the glasses sync their clock first by passing --ntp-sync to aria_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 0 accepts 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-servers widens what the auth token is worth. The list, including any TURN credential in it, is sent to both peers in the paired envelope, 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)

Source distribution for projectaria-webrtc-signaling-server 0.1.2
File Size Uploaded
projectaria_webrtc_signaling_server-0.1.2.tar.gz 101.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for projectaria-webrtc-signaling-server 0.1.2
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page