Skip to main content

Live Demo Architecture

The "try a demo with an avatar for free" feature.

note

Simplified model (2026-06-22): Pixel Streaming already exists and runs independently — one domain per avatar. All we need is a thin reservation layer (queue + 15-minute limit + gate), with no custom agent on the Unreal Engine side.

Status: design / not yet implemented.

Agreed decisions

TopicChoice
AccessThrough sign-in / sign-up (identity = account).
Slots3 domains = 3 avatars, each an independent PS stack, one user at a time.
HostingOwn bare-metal Linux + RTX 5090 (~3 GB per instance).
15-minute controlThe frontend removes the iframe, and the token expires (no agent).
No sharingmaxPlayerCount=1 on signalling (a DevOps config item).

The Pixel Streaming setup, as reported by the team: each domain is an Epic Signalling Web Server (start.sh ... --streamer_port 8888 --player_port 8080 --sfu_port 8890) plus a UE application (-PixelStreamingURL=ws://127.0.0.1:8888). The player is served by signalling itself on player_port. Domains: demo-adam.amadeq.com, demo-anna.amadeq.com, demo-<third>.amadeq.com, over HTTPS through nginx.

Components

Our backend — reservation/queue service

A module inside the existing Express + ws application.

  • Login gate (the existing httpOnly session).
  • A registry of the domains (3, from config): { id/avatar, url, status: free|busy, reservedFor, endsAt }.
  • A queue per domain (per avatar), or one shared queue — a product decision.
  • Assignment of a free domain, a 15-minute reservation, and issuance of an access token valid for the session and bound to {userId, domain, sessionId}.
  • A WebSocket to the frontend (queue, assignment, countdown, heartbeat). It never touches Pixel Streaming.
  • An endpoint for nginx: GET /internal/demo/validate?token&domain200/403.

Frontend

  • The Live Demo button is shown only to signed-in users; otherwise it routes to /signin?next=/demo.
  • Opens a WebSocket to the backend and shows the queue screen (position / ETA).
  • On assigned, it embeds the player in an iframehttps://{domain}/?token=...&HoveringMouse=true&FakeMouseWithTouches=true — with a 15-minute countdown overlay.
  • At the 15-minute mark, on "leave", or on close, it removes the iframe (which tears down WebRTC and frees the slot) and sends leave over the WebSocket.
  • Sends a heartbeat over the WebSocket, so a closed tab frees the slot early.

DevOps / Pixel Streaming side

Configuration only — no Unreal Engine code.

  • Each domain sits behind nginx + TLS, with auth_request pointing at our /internal/demo/validate (a missing or expired token gives 403). This blocks direct access and reconnection after the limit.
  • Signalling runs with maxPlayerCount=1, so two users cannot land on the same stream.

How the requirements are met

  • No two users on one instancemaxPlayerCount=1 on the PS side, plus we never assign a busy domain.
  • 15 minutes — our server timer is the source of truth for the reservation, the frontend removes the iframe (tearing down the stream), and the token expires so there is no reconnect. No separate process is needed to kick an established stream, because the player lives inside our page.
  • Login, queue, fairness — our backend.
  • Direct URL access — the nginx token gate.

Lifecycle

QUEUED ──(domain free)──► ASSIGNED (token + url, endsAt) ──► ACTIVE (iframe streaming, counting down)

(15 min / leave / tab closed) │

frontend removes iframe → WS leave → slot free → cooldown

Residual case: if the tab is closed without a leave, the slot is held until the 15-minute timer expires. The heartbeat shortens that window.

Observability and capacity planning

The backend sees every event — queueing, assignment, start/end, waiting — so it is the source of metrics.

  • Metrics: domains free/busy and utilisation %; queue length, peak, and waiting time (p50/p95); session starts and ends, duration, share reaching the full 15 minutes, abandons; session throughput per hour.
  • Exposure: GET /admin/demo/stats (live snapshot) → Prometheus /metrics → Grafana → Postgres demo_session for reports.
  • Signals to add capacity: p95 waiting time consistently above 10–15 minutes; rising abandons; ~100 % utilisation at peak with a non-empty queue; arrivals per hour approaching capacity.
  • Capacity (Little's Law): N ≥ λ·T, where λ is arrivals per minute at peak and T is duration (≤ 15 min). For 3 slots that is roughly 3 × 60/15 ≈ 12 sessions per hour. The pool is fixed, so adding a domain or GPU is a manual decision driven by those signals.

Data

In-memory (single-process Express, so no races): domains, queue(s), sessions, tokens, cooldowns, client WebSockets, and a one-second timer. Redis becomes necessary once there is more than one backend process, or when state must survive a restart.

Postgres demo_session: id, userId, domain, queuedAt, startedAt, endedAt, endReason, durationSec.

Open questions

  1. DevOps: is an nginx token gate (auth_request) possible on each domain?
  2. DevOps: is signalling maxPlayerCount=1 (or an equivalent) supported?
  3. DevOps: are the 3 HTTPS domains confirmed?
  4. Product: does the user choose an avatar (3 independent queues), or take any free one (a shared queue)?

Phases

  • Phase 0 (DevOps): 3 HTTPS domains, an nginx token gate, and maxPlayerCount=1.
  • Phase 1 (us): the reservation/queue service (in-memory), the login gate, the 15-minute timer, the token, and /internal/demo/validate; on the frontend, the queue, the iframe player, and the countdown. See Phase 1.
  • Phase 2: history in Postgres, cooldown, heartbeat-driven release, metrics/Grafana, and polish (avatar choice, ETA).