feat(streaming): add Sunshine-based streaming for split-screen sessions#22
Merged
Conversation
87279d7 to
634f755
Compare
…tance streaming Add standalone spike scripts that validate Sunshine can run as a managed subprocess with per-display capture. Covers multi-instance concurrent execution, virtual display creation, config generation, and virtual input device detection patterns.
…ine config generator Add per-instance output mode (physical/streaming) to InstanceConfig with streaming-specific fields. Extend D-Bus helper with virtual display and PipeWire null-sink creation. Add SunshineConfig utility for per-instance config generation with port spacing and codec support.
Add StreamManager for Sunshine subprocess lifecycle with crash recovery and configurable startup timeout. Extend GamescopeInstance with virtual output mode for streaming targets. Wire SessionRunner for mixed physical and streaming sessions with Sunshine device attribution. Extend VirtualDeviceWatcher to recognize Sunshine virtual input patterns.
Add output mode selector and streaming options (resolution, FPS, bitrate, codec) to SessionSetupPage with i18nc strings and streaming profile indicator. Add integration tests for mixed session configuration and StreamManager link fix in test CMake targets.
Spike validation scripts served their purpose proving the Sunshine subprocess approach. Remove before merge — production code is in src/core/StreamManager and src/core/SunshineConfig.
P1 fixes: - Fix command injection in DestroyNullSink (validate sinkName) - Remove global D-Bus disconnect breaking multi-stream monitoring - Guard startStream against entry overwrite during auto-restart - Write Sunshine credentials.json with proper SHA256(salt+password) format - Clean up streaming resources on natural gamescope exit - Fix log_file -> log_path config key P2 fixes: - Persist virtual display/null-sink state across helper restarts - Add port range validation (clamp to [MIN_PORT, MAX_PORT]) - Fix startup timeout dead code (Waiting -> Streaming on grace period) - Fix stopInstance cleanup order (stop gamescope before destroying display) - Use per-instance borderless instead of global setting - Fix ComboBox currentIndex bindings breaking on user interaction - Add cleanupTestCase to test_sunshine_config
- Fix Sunshine credential hash: password+salt with reversed digest (P1) - Stop orphaned systemd units on CreateVirtualOutput failure paths - Wire PULSE_SINK + Sunshine sink config for per-instance audio routing - Remove dead port-conflict retry; add crash-during-startup port bump - Fix bitrateSlider reactivity with Connections pattern - Restore borderless default to true (regression from per-instance migration) - Fix stop()/cleanupInstances() streaming teardown paths - Track restart timers to prevent stale lambda race - Move padding from invalid RowLayout to Kirigami.Heading - Populate virtualDisplaySocket member at launch
…tance field The streaming integration repointed SessionRunner's borderless flag to a per-instance InstanceConfig.borderless field, orphaning the global 'Borderless windows' toggle in SettingsPage (it wrote settingsManager.borderlessWindows, which SessionRunner no longer read). Restore the global setting as the single source of truth and remove the per-instance field (no UI, no remaining consumer). GamescopeInstance still consumes config["borderless"] unchanged.
…mitation Strip CR/LF from output_name/encoder/sink before writing sunshine.conf so a malformed value cannot inject extra config lines. Also note at the permissions site that the config dir is world-traversable by design (cross-UID read for the streaming user) and that tightening it needs a helper-owned config dir + runtime validation against real Sunshine -- deferred.
…trols Add objectName + Accessible to the output-mode, resolution, frame-rate, bitrate and codec controls so they are reachable by the selenium-webdriver-at-spi suite (same id-first hybrid strategy as PR #26). Add select_combo_option / wait_for_absence helpers and a new test_streaming.py covering the conditional reveal of the streaming fields (hidden in physical mode, shown when 'Moonlight Stream' is selected, hidden again when switched back).
Add CreateVirtualOutput/DestroyVirtualOutput/CreateNullSink/DestroyNullSink to the mock helper (signatures mirror helper/CouchPlayHelper.h) so the streaming orchestration can be driven end-to-end; each records to the mock launch log. Add test_streaming_session_calls_helper (requires_helper tier) that switches an instance to 'Moonlight Stream', starts the session, and asserts via the launch log that CreateVirtualOutput + CreateNullSink reach the helper -- proving the streaming path through SessionRunner.setupStreamingInstance works, independent of the (xfail) window-positioning side-effect.
… selectability Helper-tier test now also asserts a sunshine LaunchInstance (gameCommand contains 'sunshine' + '/sunshine.conf') fires -- the end-to-end streaming launch proof, independent of the physical-session window-class xfail. Smoke tier: add resolution + frame-rate selectability tests exercising the streaming combo onActivated -> InstanceConfig write-back bindings.
…tests Qt6 ComboBox popup items are not exposed with accessible names (ListModel text isn't promoted), so selection-by-NAME cannot work. Switch select_combo_option to popup type-ahead (focus the combo, type the option text, Enter) -- verified green in the #27 rootless container harness. Lower streaming controls (frame rate/codec/bitrate) fall below the fold in the headless container's small viewport; those tests now skip cleanly there and run in a full-size session. The streaming->helper e2e test is xfail until the harness pre-assigns a user to the streaming instance (logic is C++ unit-covered by test_streaming_session).
… green test_streaming_session_calls_helper was xfail because the streaming instance had no username, so SessionRunner::setupStreamingInstance aborted on its empty-username guard before any helper call. Assign player2 via comboUser before Start Session; the test now passes in the #27 container harness, verifying CreateVirtualOutput + CreateNullSink + the sunshine LaunchInstance (gameCommand referencing sunshine.conf) reach the helper end-to-end through the real app UI.
…ce test Install Sunshine into appiumtests/Dockerfile from its upstream GitHub release RPM (Sunshine-<ver>-1.fc43.x86_64) -- repo.lizardbyte.dev is unreachable from some sandboxes, but GitHub releases are not. Pinned for reproducibility. Add a tiny sunshine_config_generator binary (links SunshineConfig) that emits a REAL SunshineConfig config set, and test_sunshine_integration.py which feeds it to the real sunshine binary and asserts Sunshine accepts every key (logs config: 'key' = value) and stays up. This is the test the unit suite cannot be: it catches drift between SunshineConfig and what Sunshine actually parses -- the silent-failure class that would make pairing/streaming fail. Verified in the #27 rootless container: Sunshine runs headless (software render + nested-kwin Wayland capture), connects to wayland-0, binds 47989, and accepts the SunshineConfig format. Full streaming suite: 6 passed, 3 skipped (lower controls off-screen in the headless viewport), 0 failed.
P1 (streaming was broken in any real/non-mock deployment): - StreamManager: pass the STREAMING user's uid (getpwnam) as compositorUid, not getuid() -- the virtual output lives in /run/user/<streamingUserUid>, so getuid() pointed Sunshine at the GUI host compositor (wrong/privacy capture). - StreamManager: track the attemptRestart sync-failure timer in m_restartTimers so stopStream can cancel it (was an anonymous singleShot that could resurrect and corrupt a freshly-restarted instance). P2 code defects: - helper: re-resolve the null-sink module index by name before unload (PipeWire reuses indexes across session restarts; a saved index could unload the wrong module). Shared unloadNullSinkModule used by DestroyNullSink + destructor. - helper: CreateVirtualOutput polls for a genuinely NEW Wayland socket and errors if none appears (was falling back to a pre-existing socket -> Sunshine bound the wrong compositor). - StreamManager: port-bump on startup crash now skips ports used by sibling streams (bumping by exactly PORT_SPACING landed on the next instance's slot). - SunshineConfig: check write() return in writeCredentialsFile (silent corruption on the auth path; inconsistent with sibling writers). - SessionRunner: uninhibit the screen saver on the streaming-setup failure path (inhibit ran before the loop; the failure return leaked the cookie). Test gaps (the mock suites missed the P1s): - test_streammanager: cover the auto-restart state machine (startup-crash port bump, no-bump after streaming, autoRestart=false immediate removal, and stopStream cancelling the tracked restart timer). - test_sunshine_config: injection test for sanitizeConfValue; honest framing on testCustomCredentials (self-consistency guard, not Sunshine-correctness). - test_sunshine_integration: assert unique config VALUES, not just key names (Sunshine's defaults contain the same keys). Verified in the #27 container: couchplay + couchplay-helper + tests compile; test_streammanager 25/25, test_sunshine_config 19/19, sunshine integration pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Sunshine-based streaming for mixed physical + streaming split-screen sessions. Closes #21.
What it does
/tmp/couchplay-sunshine-<n>/physical/streaming) + streaming fields (resolution, FPS, bitrate, codec, port)CreateVirtualOutput) and PipeWire null-sink (CreateNullSink) creation, both Polkit-gated, with state save/reconcileSessionSetupPageStatus
Rebased onto
develop(#24/#25/#26/#27). Reviewed by 8 parallel reviewer agents; their P1/P2 findings + test gaps are addressed (see below). The security-critical gating is clean — no command injection / auth bypass (username always validated before interpolation; sinkName regex-checked;execvenot shell).Review fixes (P1 — streaming was broken in any real/non-mock deployment)
getpwnam) ascompositorUid, notgetuid()— the virtual output lives in/run/user/<streamingUserUid>, sogetuid()pointed Sunshine at the GUI host compositor (wrong/privacy capture). The mock suites missed it because the mock never spawns Sunshine.attemptRestartsync-failure timer is now tracked inm_restartTimerssostopStreamcan cancel it (was an anonymoussingleShotthat could resurrect/corrupt a freshly-restarted instance).Review fixes (P2)
unloadNullSinkModule.CreateVirtualOutputpolls for a genuinely new Wayland socket and errors if none appears (was falling back to a pre-existing socket → Sunshine bound the wrong compositor).PORT_SPACINGlanded on the next instance's slot).write()return inwriteCredentialsFile(silent auth-path corruption).Earlier fixes
output_name/encoder/sinkagainst CR/LF config injection; documented the/tmpcredentials world-readability limitation.Tests — closed the gaps that hid the P1s
test_streammanager: auto-restart state machine (startup-crash port bump, no-bump after streaming, autoRestart=false, stopStream cancels the tracked restart timer).test_sunshine_config: injection test forsanitizeConfValue; honest framing on the hash test (self-consistency guard, not Sunshine-correctness).test_sunshine_integration: asserts unique config values, not just key names (Sunshine's defaults contain the same keys).SunshineConfigoutput and confirms it accepts every key + runs headless (software render + nested-kwin, binds :47989).Test results
Known follow-ups
/tmp/couchplay-sunshine-<n>/credentials.jsonworld-readable (cross-UID read; Sunshine writes log/state there); secure fix = helper-owned config dir. Documented atSunshineConfig::ensureConfigDirectory./dev/dri, so Sunshine has no hardware encoder; stays manual/beta (would need a GPU-passthrough container + Moonlight client).