Background
Portrait-mode experiences (e.g. mercator-talks) require three layers to all agree on the same resolution simultaneously:
- Stream encoding — the Moonlight client (or moonlight-web-stream) must request portrait resolution (1080x1920) from Sunshine
- Display resolution — Sunshine's
dd_manual_resolution must be set to portrait
- 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.