Repository navigation
Decide whether a visualizer that turns available again gets a join trigger #26
Copy link
Copy link
Closed
Labels
Description
Problem
PlayerStream returns a JOIN trigger when its client turns available again; VisualizerStream returns none, although its eligibility depends on availability. No comment says whether the asymmetry is intended. Their on_activation_written() bodies are also identical.
Evidence
Checked against the code at e69f3b46a801:
- PlayerStream::on_activation_written (player_stream.cpp:47-64) and VisualizerStream::on_activation_written (visualizer_stream.cpp:52-70) are textually near-identical: the same added/role_written_/state_since_activation_=false/JOIN logic, and the same drop of sent_, ledger_, cursor_ and announce_.
- PlayerStream::on_client_state returns JOIN when available turns true (first report, or after being unavailable) (player_stream.cpp:66-86). VisualizerStream::on_client_state just assigns available_ and has no state_received_ or was_available tracking (visualizer_stream.cpp:72-84).
- VisualizerStream::eligible() (visualizer_stream.h:130) does depend on available_, so a visualizer returning to available gets no JOIN trigger. Session::on_client_state (session.cpp:392-394) then queues the marker with kind nullopt. VisualizerStream::bind_cursor defaults pending_kind_ to GROUP, so startup_lead_us gives only join_min_us rather than the group lead (visualizer_stream.cpp:365-380). Neither the code nor docs/internals.md explains this asymmetry (grep found nothing).
- Session::on_activation_taken (session.cpp:801-806) calls artwork_ and source_role_ with role_active(string), and player_ and visualizer_ with roles.contains(StreamRole).
Checked and not part of this issue
- 'Line for line' is true only for on_activation_written, and not exactly there: the visualizer version also resets offset_ (visualizer_stream.cpp:64) and the player version does not (player_stream.cpp:56-62).
- prepare, announce_due, commit_announce, bind_cursor and cursor_ring are not line for line. VisualizerStream::prepare handles in-place configuration announcements, usable_wanted and on_stream_changed. PlayerStream::prepare handles follow_wanted and switch_ formats. Visualizer cursor_ring and ring_of are keyed by VisualizerConfig, and bind_cursor resets offset_ and uses its own startup_lead_us (visualizer_stream.cpp:365-380). Player bind_cursor uses the PlayerLead and InputKind (player_stream.cpp:482-488).
- set_support also differs: the player constructs Ledger(cap, true) and sets output_delay, the visualizer constructs Ledger(cap, false).
- ArtworkStream's 'returned' is not a drifted copy of the player logic. It clears left_ only when the client was previously received and unavailable (artwork_stream.cpp:63-67). That is the artwork role's own stream-lifecycle semantics (it has left_ and no cursor or trigger kind), so it is not comparable.
- Two role-identification schemes is a deliberate split, not drift. sched::StreamRole/RoleSet (sched/types.h:81-107) covers only the roles whose streams the cursors and markers carry (PLAYER, VISUALIZER) and is what the queues use (queues_.head_roles, roles_ahead, roles_written_). Artwork and source have separate stream objects with no cursor markers and use the string-based role_active (session.cpp:515, 801-802).
Direction
Decide (owner) whether the visualizer should trigger a join, then fix it or comment the asymmetry. Optionally share the identical activation code.
Done when
- The behavior is decided and either tested or commented at the asymmetric site.
Reactions are currently unavailable
Activity
kahrendt commentedon Oct 16, 2026
Claimed 4fa604ac; branch issue-26-visualizer-available-join
kahrendt commentedon Oct 16, 2026
Rescoped
The question this issue asks is answered: yes, a visualizer that turns available again joins, and it already does. The evidence above missed the path that gives it the kind.
Why the premise was wrong
Checked against the code at
655a5db:PlayerStream::on_client_statereturns JOIN whenavailableturns true whether or not the player role is active (player_stream.cpp:74-76), so it tracks availability for every client.Session::on_client_statetakesplayer ? player : visualizer(session.cpp:408-410), andset_pending_kind()sets the kind on both streams (session.cpp:631-637). A visualizer-only client's marker carries JOIN, andVisualizerStream::startup_lead_usgives it the group lead.stream/startonavailable: true(messaging.md "Server -> Client:stream/start"), whatever the role; the player and source wording in "Client -> Server:client/state" only adds the clock-sync precondition for those two. aiosendspin's visualizer joins on turning available too (server/roles/visualizer/v1.py,on_availability_changed).What is left
PlayerStream.ServerCorereads it for every client assession().player().available()(server_core.cpp:713), andPlayerStream::on_client_state's doc says the turn "made the player eligible" (player_stream.h:68-69). That placement is what made the asymmetry look like a bug.ServerVisualizer's availability test (test_server_visualizer.cpp:560-567) only checks that a client back in a stopped solo group gets nothing.on_activation_written()bodies are near-identical.Direction
Move the availability tracking (
available_,state_received_) and the turn-to-available JOIN fromPlayerStreamintoSession, haveServerCoreread availability from the session, and keep each stream'seligible()fed from it. Share the activation code if it falls out naturally.Done when
PlayerStream, and no comment ties them to the player role.client/statereportsavailable: falsesit in a playing group, then reporttrueand get a visualizerstream/startat the group lead.kahrendt commentedon Oct 16, 2026
Released 4fa604ac; resumed at the owner's request
kahrendt commentedon Oct 16, 2026
Claimed 1c47c52c; branch issue-26-visualizer-available-join
kahrendt commentedon Oct 16, 2026
PR: #98
Decide a client's availability and its join in the session (#98)