HydraNeck

Getting Started Overview

Head Body Selection

Heads are responsible for choosing the best Body to connect to. The selection logic differs between native Heads (HydraHead Flatscreen) and browser-based Heads (HydraHead WebStream).

Native Head: Body Discovery

The Head runs in self-service kiosk mode. When the user selects an experience, the Head discovers an eligible Body in the district by calling discoverBody():

  1. Queries the HydraHead API for eligible Bodies in the district
  2. Receives a list of Bodies with both LAN IP and WireGuard IP
  3. For each Body, probes the LAN IP on TCP port 47990 (Sunshine API) with a 1-second timeout
  4. If a Body is reachable on LAN: uses the LAN IP (direct, lowest latency)
  5. If no Body is reachable on LAN: falls back to the first Body's WireGuard IP
flowchart TD
    START["discoverBody(district)"]
    FETCH["Query HydraHead API\nfor eligible Bodies"]
    LOOP{"Next Body\nin list?"}
    PROBE{"TCP probe\nLAN IP :47990\n1 second timeout"}
    LAN["Use LAN IP\ndirect, lowest latency"]
    WG_FALLBACK["Use first Body's\nWireGuard IP"]
    NONE["No reachable Bodies"]
    RESULT["Selected Body + resolved IP\nfor Moonlight connection"]

    START --> FETCH --> LOOP
    LOOP -- YES --> PROBE
    LOOP -- "NO (exhausted)" --> WG_FALLBACK
    PROBE -- Success --> LAN
    PROBE -- Failure --> LOOP
    LAN --> RESULT
    WG_FALLBACK --> RESULT

Why LAN-first?

Self-Service Flow

The Head polls the HydraHead API every 30 seconds for its configuration (district, venue). The kiosk app fetches available experiences from the Experience Library and shows them to the user. When the user selects an experience, the Head discovers a Body, pairs with Sunshine, and launches the Moonlight stream. When the stream ends, the kiosk returns to the experience selection screen.

Key code: hydrahead/pkg/client/discovery.go -- discoverBody()

Browser Head: District-Aware Selection

HydraHead WebStream uses a more sophisticated selection process:

Step 1: Connectivity Pre-Assessment

Before the user selects an experience, the browser probes all districts:

Step 2: Body Selection

When the user starts an experience:

  1. Browser sends a WebSocket message with district, experience, and connectivity metrics
  2. WebStream calls FindBodyForDistrict() (hydraheadwebstream/internal/client/hydracluster.go) which queries HydraCluster's eligible endpoint
  3. Eligible endpoint returns Bodies pre-sorted by:
    • Idle status (idle Bodies first)
    • GPU VRAM (highest VRAM first)
  4. Filters for status == "online" and role "hydrabody"
  5. Excludes Bodies already in use (via exclude list)
  6. Returns the first match with its WireGuard IP (browser streams always route through the mesh)

Step 3: Session Creation

WebStream sends the selected Body's WireGuard IP to HydraNeck WebRTC:

{
  "body_ip": "10.10.100.42",
  "sunshine_user": "sunshine",
  "sunshine_pass": "...",
  "experience": "museum-tour",
  "district": "bxl1",
  "connection_profile": {
    "round_trip_time_milliseconds": 45,
    "network_type": "wifi",
    "downlink_megabits": 50,
    "device_type": "desktop"
  }
}

The controller routes the session to the least-loaded worker, which spawns moonlight-web-stream connected to the Body via WireGuard.

Step 4: Quality Selection

Based on the connection profile, HydraNeck WebRTC selects the quality tier:

Condition Tier Resolution FPS Bitrate
Desktop, round-trip < 50ms Excellent 1080p 60 15 Mbps
Desktop, round-trip 50-100ms Good 1080p 60 10 Mbps
Mobile, round-trip < 75ms Mobile Good 720p 60 7 Mbps
Any, round-trip > 200ms Poor 720p 30 5 Mbps

Comparison

Feature Native Head Browser Head
Body selection User selects experience, Head discovers Body User selects district, system picks Body
IP selection LAN probe per Body, WireGuard fallback Always WireGuard IP
Local path Yes (direct LAN) No (always through WebRTC relay)
Quality control Fixed (1080p60, 150 Mbps) Adaptive (based on connection profile)
Streaming protocol Moonlight (native) WebRTC (browser)
Interaction On-demand (user selects experience) On-demand (user action)