forked from unom/punktfunk
GameStream sessions outlived their client: the only complete teardowns were the explicit ones (RTSP TEARDOWN, nvhttp /cancel, mgmt DELETE /session), and the only automatic detector was a media-UDP send error — which needs an ICMP port-unreachable, so a true vanish (Wi-Fi drop, sleep, power off, crash) left video+audio encoding into the void forever, and even a plain Moonlight quit (which sends neither TEARDOWN nor /cancel) leaked the session. The stale state then cascaded: a lingering launch 503-blocked a different client under mode_conflict=reject, and streaming=true made a reconnect's PLAY take its "stream already running" branch — no new threads, old threads still aimed at the dead endpoint, the reconnect got no media. ENet already detects all of this — the control peer's reliable-ping timeout (or clean disconnect) fires Event::Disconnect within ~5-30 s — but the handler only reset input state. Wire the real teardown into it: * AppState::end_session — THE compat-plane session teardown: stops both media threads (their flags), clears launch + negotiated stream config; idempotent. /cancel and mgmt stop_session now share it. * control.rs Disconnect → end_session. Gated on the TRACKED session peer, and Connect only tracks a peer from the /launch owner's IP (the same source-IP bind the RTSP/media plane uses), so an unauthenticated LAN peer connect+disconnect can't end a live session, and a fast reconnect's stale-peer timeout can't kill its successor. * Client-unreachable UDP send errors now end the whole session via an OnSessionLost callback (built at PLAY) instead of stopping only the plane that noticed — audio no longer keeps streaming after video detects the dead client, and vice versa. Linux check/clippy/tests green (53 gamestream tests incl. the new end_session regression test). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>