HydraIssues

Orientation handling must be consistent and verified across all hydrahead types
open improvement Project: experiencenet Reporter: cederik 13 May 2026 13:14

Description

Background

Portrait-mode experiences (e.g. mercator-talks) require three layers to all agree on the same resolution simultaneously:

  1. Stream encoding — the Moonlight client (or moonlight-web-stream) must request portrait resolution (1080x1920) from Sunshine
  2. Display resolution — Sunshine's dd_manual_resolution must be set to portrait
  3. App window — the UE experience must be launched with portrait-aware args (-ResX=1080 -ResY=1920 -windowed -ForceRes)

Plus the Virtual Display Driver (VDD) must have portrait resolutions in vdd_settings.xml (hydrabody provisions this at startup).

All three must match simultaneously or the stream renders incorrectly: app in wrong resolution, captured at wrong frame size, or stretched in the browser.

Two paths, same three layers

hydraheadflatscreen path

  • Layer 1 (stream encoding): moonlight_*.go directly passes --resolution 1080x1920 to the Moonlight client CLI args
  • Layers 2+3 (display + app): hydrabody owns both — reads orientation from its own experience cache (synced from hydraexperiencelibrary), sets dd_manual_resolution, and registers the Sunshine app with portrait launch args
  • Result: robust and direct because the flatscreen IS the Moonlight client — layer 1 is always in sync with layers 2+3

hydraheadwebstream (browser) path

  • Layer 1 (stream encoding): webstream passes orientation: portrait string to hydraneckwebrtc, which sets videoSizeCustom: {width: 1080, height: 1920} in moonlight-web-stream DefaultSettings
  • Layers 2+3 (display + app): hydrabody — same as flatscreen path, independently
  • Fragility: if hydraneckwebrtc is on an old version, layer 1 silently breaks while layers 2+3 still work. This happened in production: issue #81 showed the Brussels node on v1.10.81 silently stripped the orientation field, producing a portrait body streaming into a landscape WebRTC frame.

The problem

There is no cross-layer verification. Failures are silent:

  • hydraneckwebrtc on old version: layer 1 wrong, layers 2+3 correct — portrait body but landscape WebRTC frame
  • hydrabody with stale catalog: layers 2+3 wrong, layer 1 correct — correct stream resolution but app renders at wrong resolution
  • Brussels controller not in hydracluster (issue #81): silently different behaviour per district as versions drift

Proposed implementation

1. hydrabody — expose resolved orientation in the register/launch response

When hydraneckwebrtc calls hydrabody to register or launch an experience, hydrabody should include the orientation it actually applied in the response body. This gives hydraneckwebrtc the ability to verify its own layer 1 matches what hydrabody applied to layers 2+3.

2. hydraneckwebrtc — cross-layer orientation verification

In worker.go handleCreateSession, after calling RegisterSunshineApp (or receiving the hydrabody response), compare req.Orientation with the orientation hydrabody reports it applied. Log the result clearly:

[session abc] orientation: requested=portrait body_applied=portrait OK
[session abc] orientation: requested=portrait body_applied=landscape MISMATCH — check hydrabody catalog sync

Non-fatal: stream proceeds regardless. The log line makes silent mismatches visible.

3. hydraneckwebrtc — include orientation in session status response

Surface the orientation that was applied in GET /session/{id}/status so upstream callers (hydraheadwebstream) can read it back and use it to inform the browser user.

4. hydraheadwebstream — portrait hint in experience selection UI

The /api/v1/experiences response already includes orientation on every experience object. The experience dropdown in internal/web/templates/stream.html ignores it today. Add:

  • A portrait badge (e.g. a small vertical icon or label) next to portrait experiences in the dropdown
  • After a portrait experience is selected, show a contextual note below the dropdown: "Portrait experience — for the best view, hold your phone or tablet vertically."

Frontend-only change, no server-side changes needed.

5. hydraheadflatscreen — CLAUDE.md note

No code changes needed. Document in CLAUDE.md that hydraheadflatscreen controls layer 1 directly, and that any future path through hydraneckwebrtc must explicitly pass the orientation string — the same way hydraheadwebstream does.

Acceptance criteria

  • hydrabody register/launch API response includes orientation as actually applied
  • hydraneckwebrtc logs orientation agreement/mismatch between layer 1 and hydrabody layers 2+3
  • hydraneckwebrtc GET /session/{id}/status response includes orientation field
  • hydraheadwebstream shows portrait badge in experience dropdown
  • hydraheadwebstream shows rotate-device hint when portrait experience is selected
  • Verified end-to-end: mercator-talks (portrait) and a landscape experience on the same body, both heads

Affected repos

  • hydrabody — expose orientation in API response
  • hydraneckwebrtc — cross-layer orientation log + session status field
  • hydraheadwebstream — portrait hint in UI
  • hydraheadflatscreen — CLAUDE.md note only

Related

  • Issue #81 (Brussels node not in hydracluster) is a prerequisite for reliable orientation on the webstream Brussels path — version drift on that node is what triggered this investigation.