Commits
Move playlist, playlist cursor, playlist-enabled flag, and the URL-keyed
media cache out of RoomActor and into PlaybackState. The actor now
dispatches every mutation through state.transition() and persists the
returned effects. Three new state-machine methods own the data:
- handle_playlist_entry_added/removed, handle_set_playlist_enabled
- next_playlist_entry (returns next entry, advances cursor)
- handle_download_resolved/failed (updates media cache + propagates)
Kills the duplicate ad-hoc fields (download_results, playlist,
playlist_index) and the next_playlist_track helper, leaving RoomActor
with download_tasks as its only ephemeral I/O state.
Frontend: RoomState gains playlist_enabled; Queue adds an Autoplay
toggle that round-trips over the existing WebSocket.
74 tests pass (was 56). 0 clippy warnings. 0 dead-code warnings.
Frontend now receives video data as MoQ objects on the single control
WS connection instead of a separate raw-fMP4 WebSocket. This gives the
client access to the MoQ group_id, which is used as the sole
synchronisation primitive between video data and state updates.
Server changes:
- ActiveTrackInfo and TrackState carry group_id
- Player::start() returns the assigned group_id
- execute_effects stores group_id on the active track
- Subscribe handler for video track sends cached init + forwards live
MoQ objects via the control WS object channel
- Removed handle_video_session and video WS route
Frontend changes:
- Subscribe to video track alongside chat and state
- handleVideoObject() uses group_id to discard stale old-track data
and gate on init reception
- updateNowPlaying() sets awaitingInit only if processedGroupId <
newGroupId (no re-buffer if init already arrived)
- No more separate video WS connection, no pendingTrackChange flag
- onopen resets sync state (currentGroupId, processedGroupId, awaitingInit)
149 tests pass, 0 warnings.
Video starts muted per browser autoplay policy. Slider and icon
now start at muted (0 / speaker-muted icon). Click icon to unmute
and restore volume to 1. Dragging slider adjusts volume and
auto-mutes at 0.
Extracts a pure, deterministic PlaybackState machine (src/state.rs) with
the impure-pure-impure sandwich pattern:
1. Gather — RoomActor fetches DB data and wall clock
2. Transition — state.transition(event, now) returns Vec<Effect>
3. Commit — RoomActor executes effects (DB writes, pipeline, broadcast)
Player is thinned: no longer tracks active_track or is_playing(). The
state machine owns queue/history in-memory, so snapshot publishing is
SQLite-free.
- MediaSource trait + SourceRegistry for pluggable URL resolution
- YtdlpSource extracted from media.rs, DirectSource added
- Player struct encapsulates pipeline lifecycle (start/abort/ended)
- media.rs is now a pure ffmpeg transcoder (transcode_to_fmp4)
- extractor field renamed to SourceKind enum across domain + DB
- TrackPublishers moved from types.rs to transport.rs
- Fix: init segment now published to broadcast so skip advances video
- Frontend: debug logging gated, connection indicator, loading states,
better empty states, button disabled states
Move playlist, playlist cursor, playlist-enabled flag, and the URL-keyed
media cache out of RoomActor and into PlaybackState. The actor now
dispatches every mutation through state.transition() and persists the
returned effects. Three new state-machine methods own the data:
- handle_playlist_entry_added/removed, handle_set_playlist_enabled
- next_playlist_entry (returns next entry, advances cursor)
- handle_download_resolved/failed (updates media cache + propagates)
Kills the duplicate ad-hoc fields (download_results, playlist,
playlist_index) and the next_playlist_track helper, leaving RoomActor
with download_tasks as its only ephemeral I/O state.
Frontend: RoomState gains playlist_enabled; Queue adds an Autoplay
toggle that round-trips over the existing WebSocket.
74 tests pass (was 56). 0 clippy warnings. 0 dead-code warnings.
Frontend now receives video data as MoQ objects on the single control
WS connection instead of a separate raw-fMP4 WebSocket. This gives the
client access to the MoQ group_id, which is used as the sole
synchronisation primitive between video data and state updates.
Server changes:
- ActiveTrackInfo and TrackState carry group_id
- Player::start() returns the assigned group_id
- execute_effects stores group_id on the active track
- Subscribe handler for video track sends cached init + forwards live
MoQ objects via the control WS object channel
- Removed handle_video_session and video WS route
Frontend changes:
- Subscribe to video track alongside chat and state
- handleVideoObject() uses group_id to discard stale old-track data
and gate on init reception
- updateNowPlaying() sets awaitingInit only if processedGroupId <
newGroupId (no re-buffer if init already arrived)
- No more separate video WS connection, no pendingTrackChange flag
- onopen resets sync state (currentGroupId, processedGroupId, awaitingInit)
149 tests pass, 0 warnings.
Extracts a pure, deterministic PlaybackState machine (src/state.rs) with
the impure-pure-impure sandwich pattern:
1. Gather — RoomActor fetches DB data and wall clock
2. Transition — state.transition(event, now) returns Vec<Effect>
3. Commit — RoomActor executes effects (DB writes, pipeline, broadcast)
Player is thinned: no longer tracks active_track or is_playing(). The
state machine owns queue/history in-memory, so snapshot publishing is
SQLite-free.
- MediaSource trait + SourceRegistry for pluggable URL resolution
- YtdlpSource extracted from media.rs, DirectSource added
- Player struct encapsulates pipeline lifecycle (start/abort/ended)
- media.rs is now a pure ffmpeg transcoder (transcode_to_fmp4)
- extractor field renamed to SourceKind enum across domain + DB
- TrackPublishers moved from types.rs to transport.rs
- Fix: init segment now published to broadcast so skip advances video
- Frontend: debug logging gated, connection indicator, loading states,
better empty states, button disabled states