Portrait streaming — orientation contract implementation and remaining robustness improvements
This tracks the full portrait streaming orientation work for cosmic-pretzel-98 (and all multi-orientation bodies). The root cause was a visible artifact: a landscape kiosk overlay showing behind a portrait Unreal Engine app when a portrait stream started.
Root cause breakdown:
kioskoverlay did not respond to WM_DISPLAYCHANGE — overlay stayed landscape when VDD switched to portraithandleStreamStarted did not receive orientation from the prep-cmd; it had to resolve it from cache with no explicit contracthandleStreamEnded was not implemented — the Undo hook silently 404'd, leaving VDD at the last stream's resolutiondd_manual_resolution and Sunshine beginning capturecmd/kioskoverlay/main.go
WM_DISPLAYCHANGE handler: on resolution change, reads new screen dimensions via GetSystemMetrics and resizes the overlay window to fill the screen.pkg/provider/experiences.go
Resolution string field to liveExperience and experienceInstallInfo for explicit WxH override (e.g. 1668x2224 for iPad kiosk).streamResolution(info experienceInstallInfo) (vddRes, ueArgs string) as the single source of truth for all three layers of the orientation contract.pkg/provider/sunshine_provider.go
desiredApp struct: {cmd string, orientation string}.buildOrientationHooks — replaces sunshine.CallbackHooks() from go-sunshine. Bakes orientation into the JSON body at app registration time so handleStreamStarted receives it explicitly.SyncApps to accept map[string]desiredApp.pkg/provider/sunshine.go
streamResolution for UE launch args.pkg/provider/httpserver.go
Orientation field to streamEventRequest.handleStreamStarted: uses orientation from prep-cmd request body; falls back to experience cache; logs mismatch; calls streamResolution for VDD resolution; sleeps 500 ms after setSunshineConfig; returns {"orientation": "..."} for head verification.handleStreamEnded: resets dd_manual_resolution to 1920x1080. The Undo hook was silently 404'ing before this.docs/testbooks/portrait-mode.md
orientation field.docs/testbooks/kiosk-mode.md
| Layer | Controls | Owner |
|---|---|---|
| 1 — Stream encoding | Resolution Moonlight client requests from Sunshine | Head (hydraheadflatscreen / hydraneckwebrtc) |
| 2 — Display resolution | dd_manual_resolution in sunshine.conf, applied on stream-started |
httpserver.go:handleStreamStarted |
| 3 — App window | UE launch args registered in Sunshine app cmd | sunshine.go:tickSunshineApps |
The prep-cmd fires before Sunshine begins capture, so layer 2 is set before the first video frame. The 500 ms sleep inside the handler allows the VDD to stabilize. Layer 3 is set at app registration time and matches layer 2 because both derive from the same experience state cache.
setSunshineConfigsetSunshineConfig in pkg/provider/streamprovider.go does read/merge/write on sunshine.conf with no lock. In practice Sunshine is single-session so this race cannot happen, but the fix is 3 lines. Files: pkg/provider/provider.go, pkg/provider/streamprovider.go.
vdd_settings.xmlwriteVDDSettings in vdd_windows.go has five hardcoded resolutions and is called only on first VDD install. Any experience with a custom Resolution field (e.g. 1668x2224 for iPad kiosk) silently fails — VDD does not support that resolution, stream renders at wrong aspect ratio with no error.
Fix: add ensureVDDResolutions() called from tickVirtualDisplay each tick when VDD is already installed. Reads experience state, extracts custom Resolution values, hashes the list, rewrites vdd_settings.xml if changed, logs a one-time warning that a VDD reinstall/reboot is needed for the change to take effect.
dd_manual_resolution after prep-cmd completes). Polling inside the handler cannot detect the switch.handleStreamStarted: Same timing problem.OnStreamStarted. detectStreamStatus in stream_windows.go is a log-reader for idle/streaming state only, bypassed entirely when SunshineStreamProvider is active.