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).
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():
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
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()
HydraHead WebStream uses a more sophisticated selection process:
Before the user selects an experience, the browser probes all districts:
When the user starts an experience:
district, experience, and connectivity metricsFindBodyForDistrict() (hydraheadwebstream/internal/client/hydracluster.go) which queries HydraCluster's eligible endpointstatus == "online" and role "hydrabody"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.
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 |
| 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) |