feat: add connection lifecycle events and auto-resume for Sendspin players#4719
Draft
foobarth wants to merge 4 commits into
Draft
feat: add connection lifecycle events and auto-resume for Sendspin players#4719foobarth wants to merge 4 commits into
foobarth wants to merge 4 commits into
Conversation
…dspin players Add event handlers for ClientConnectedEvent, ClientDisconnectedEvent, and ClientReconnectedEvent in the Sendspin provider: - _handle_client_connected: refresh player info on every transport connect - _handle_client_disconnected: immediately capture _was_playing state on disconnect (CONNECTION_LOST), mark player unavailable for UI - _handle_client_reconnected: triggers auto-resume via existing _auto_resume_after_reconnect() in player.py Player enhancements: - _was_playing flag tracks playback state across WS disconnects - disconnect_behaviour set to UNGROUP (keep player alive on drop) - Auto-resume via resume() when client reconnects after network drop - Guard _on_group_stopped to not cancel session during reconnect window - GoodbyeReason.CONNECTION_LOST distinguishes network errors from intentional disconnects Tests: - Unit tests for _was_playing lifecycle, guard logic, auto-resume flow - Integration flow tests for short and long reconnect scenarios
foobarth
force-pushed
the
pr/connection-resilience
branch
2 times, most recently
from
July 10, 2026 21:03
1ab745a to
4609f0b
Compare
foobarth
marked this pull request as ready for review
July 10, 2026 21:04
foobarth
force-pushed
the
pr/connection-resilience
branch
from
July 11, 2026 07:14
4609f0b to
1ab745a
Compare
Author
|
Lint / test issues are expected due to the required companion PR for Sendspin/aiosendspin#288 |
Contributor
|
Keeping this as draft until the tests can pass. |
OzGav
marked this pull request as draft
July 12, 2026 11:24
ClientReconnectedEvent was removed from aiosendspin. The auto-resume mechanism already uses _was_playing as the guard, so switching to ClientConnectedEvent is transparent — the first connect has _was_playing =False (no resume), every reconnect has the correct state. - Remove ClientReconnectedEvent import and handler case - Remove _handle_client_reconnected method (its logic is already covered by _handle_client_connected + _refresh_client_info) - Update test comment
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.
What does this implement/fix?
Problem
When an iPhone (or other mobile Sendspin client) loses its network
connection (tunnel dropout, airplane mode, elevator, WiFi handoff),
the MA server detects the WebSocket disconnect and stops playback
immediately. When the client reconnects within the cleanup window
(300s, up from upstream's 30s), the player is already in "stopped"
state, the user has to manually press play.
For unintended drops shorter than 5 minutes there is no reason to force a
manual resume; the server should recognize the reconnect and
automatically continue playback, if it was playing before the dropout
and the disconnect reason was clearly an unintended network issue.
In the area where i live, 30s is sometimes just not enough and having
to grab the phone while driving, opening the app and restarting playback
is both dangerous and unexpected from a "proper" music player these days.
Solution
Two layers of changes. This PR (MA server) and a companion PR on
aiosendspin (Sendspin/aiosendspin#…):
Sendspin/aiosendspin companion PR#288
ClientConnectedEvent,ClientDisconnectedEventfire on everyconnection establish / teardown (not just first-add)
ClientReconnectedEventfires when a known client reconnectswith unchanged hello payload (gap in upstream:
ClientUpdatedEventonly fires on hello change, so 99% of reconnects were silent)
GoodbyeReason.CONNECTION_LOSTdistinguishes unexpected networkdrops from intentional shutdowns
CLIENT_CLEANUP_DELAY30s → 300s gives mobile clients a longerwindow to reconnect before being evicted from the registry
This PR (MA server)
New event handlers in Sendspin provider:
_handle_client_disconnectedimmediately captures_was_playingstate and marks player as
unavailablefor the UI, instead ofwaiting 300s for
ClientRemovedEvent_handle_client_connectedrefreshes player info on everytransport connect (existing player or fresh reconnect)
_handle_client_reconnectedtriggers auto-resume via theexisting
_auto_resume_after_reconnect()in player.pyPlayer enhancements:
_was_playingflag survives across WS disconnects; setTrueonplay_media(), cleared only after successfulresume()disconnect_behaviourset toUNGROUP(keep player alive duringdrop, avoid cancelling the session)
_on_group_stoppedto not cancel the playback sessionduring the reconnect window (race condition with aiosendspin's
automatic
ControllerStopEventafter reconnect)GoodbyeReason.CONNECTION_LOSTis handled as a warm disconnect(roles preserved, cleanup scheduled with delay)
Behaviour
Testing
Live-tested on K8s cluster with custom CI image. Scenarios:
Companion PR
Sendspin/aiosendspin PR#288 must be merged first (or simultaneously)
since the MA code depends on the new events and
GoodbyeReason.Related issue (if applicable):
Types of changes
bugfixnew-featureenhancementnew-providerbreaking-changerefactordocumentationmaintenancecidependenciesChecklist
pre-commit run --all-filespasses.pytestpasses, and tests have been added/updated undertests/where applicable.music-assistant/modelsis linked.music-assistant/frontendis linked.