Comparison seam joins the existing resync path. Local hash algorithm is a
documented BLOCKING open question; stub returns None so detection stays inert
(no spurious resyncs) until the Qobuz algorithm is verified.
cc
Decoder now reads field #100 (previously decoded-then-dropped) on QueueState
and carries it through to QConnectQueueState.last_server_queue_hash via the
QueueStateReplaced reducer path (full-state replace already clones it).
cc
105 carries only bare queue_item_ids (no track_id/context), unreconstructable
by the reducer; resync pulls the authoritative QueueState so autoplay-grown
tracks become visible. Matches the ShuffleModeSet/Reordered/Removed pattern.
cc
Fixes#387: seeks issued from the official Qobuz mobile app while qbz
acts as the active renderer were silently dropped — audio kept playing
from the original position while the controller's progress bar locked.
Two independent gates were swallowing the command between the
WebSocket and the audio thread; either alone is enough to break the
flow, so both move together.
1. Upstream filter (crates/qconnect-app/src/app.rs)
The is_echo guard treated every SET_STATE with playing_state=None
AND current_track=None as a pure server echo and skipped emitting
RendererCommandApplied. iOS sends a real seek as exactly that
shape (only current_position_ms set, the rest unchanged), so the
command never reached the bridge handler.
Add current_position_ms.is_none() to the guard. Pure echoes still
match (server echoes carry only next_track); position-only seeks
pass through.
2. Downstream gate (src-tauri/src/qconnect/corebridge.rs)
The seek branch was conditioned on peer_renderer_active — i.e. it
only acted when qbz was the controller observing a peer renderer,
never when qbz was the renderer itself. Removed; the existing
is_echo_reset and abs_diff > 2 checks already filter the redundant-
seek case the gate was originally added to defend against (147bcbd7).
Drop the now-unused is_peer_renderer_active import.
Verified manually: seeks from iPhone (renderer_id=3) targeting qbz
(renderer_id=2) at ~1/4 and ~5/6 of a 150s track now translate to
local audio jumps to the correct positions.
cc
Pure enum + decision function for QConnect auto-connect-on-startup.
Persistence and CLI parsing stay in the Tauri adapter; this crate
remains UI-agnostic.
cc
The MSG_TYPE_ERROR (cloud_type=9) arm previously logged only the payload
byte count, so when Qobuz cloud rejected a session (zombie session,
conflicting device, expired JWT, ...) we had no signal beyond an opaque
"bytes=13" TransportError. That hid the actual cause of the 2s flicker
loop seen after the app sat minimized for 1-2 hours (issue #358).
The qws.proto ErrorMessage (messageTypeId=9) carries:
uint32 msg_id = 1
uint64 msg_date = 2
uint32 code = 3
string descr = 4
Decode it via a local QwsErrorMessage prost struct, log a warn line with
the decoded code+descr, and emit a new TransportEvent::CloudError so
qconnect-app and qconnect_service can react. qconnect_service forwards
it as a "qconnect:cloud_error" diagnostic (level=warning) and logs it.
qconnect-app treats it as a no-op forwarder for now, matching the
existing pattern for other transport-only events. Decode failures fall
back to the previous TransportError path under a distinct stage name so
they are still visible.
Adds a tokio test that builds a real MSG_TYPE_ERROR frame and asserts
the transport emits CloudError (not the legacy TransportError).
Refs #358
cc
The native WS transport reset its reconnect counters as soon as the WS
upgrade succeeded. Qobuz cloud accepts the WS connection well before the
JOIN_SESSION exchange and rejects unhealthy sessions with MSG_TYPE_ERROR
right after, so the counter reset every cycle and the loop hammered the
endpoint at base backoff (~2s) forever (issue #358).
Move the counter reset to the moment the cloud emits a
MESSAGE_TYPE_SRVR_CTRL_SESSION_STATE event (the actual proof of a healthy
session-level join), and add a configurable maximum-attempts cap that
makes the transport stop and emit MaxReconnectAttemptsExceeded instead
of looping indefinitely.
- Add SessionEstablished and MaxReconnectAttemptsExceeded transport events.
- Add reconnect_max_attempts to WsTransportConfig (default Some(10)).
- handle_incoming_binary now returns whether SESSION_STATE was decoded.
- Reconnect helper returns a ReconnectOutcome enum (Continue/Shutdown/
Exhausted) instead of a bare bool.
- qconnect_service.rs logs the new events and emits a diagnostic when
the cap is exceeded so the higher layer can react (#358-fix-3 will
surface it in the UI).
- Unit tests cover the cap behavior and the unbounded fallback.
Refs #358
cc
SrvrCtrlQueueTracksReordered and SrvrCtrlQueueTracksRemoved were not
triggering AskForQueueState, so the local queue never reflected
reorder/remove actions performed via QConnect. Same pattern as the
existing shuffle resync.
cc
The server echoes every state report as a SET_STATE with only next_track
(playing_state=null, current_track=null). These echoes were triggering
align_corebridge_queue_cursor which uses the accumulated renderer state
current_track (from a different context, e.g. iOS). When the track
wasn't found in the local queue, it replaced the entire queue with a
single track, destroying the user's loaded queue.
Now echo SET_STATE commands are detected early and only update the
internal renderer state (for next_track tracking). CoreBridge actions
(align cursor, load track, play/pause) and state reports are skipped
entirely for these echoes.
The server echoes every RNDR_SRVR_STATE_UPDATED as a SET_STATE command
with only next_track updated (playing_state=null, current_track=null).
send_renderer_reports was replying to these echo SET_STATE commands
with another state report, creating an infinite loop that generated
47k log lines (8.2MB) in seconds and completely blocked playback.
Fix: only send state reports for substantive SET_STATE commands that
carry playing_state or current_track changes. Echo commands (next_track
only) are silently consumed.
Also demote diagnostic logging from info to debug to reduce terminal
noise during normal operation.
The server rejects renderer state reports with "Current track not found
in queue nor autoplay" after QBZ-initiated queue loads, because it assigns
non-standard queue_item_ids (first track = track_id, rest = 1..N).
Omit current_queue_item_id and next_queue_item_id from all outbound
renderer reports. The server already knows the current track from
SET_STATE commands and adds the correct queue_item_id when forwarding
to controllers.
Also add info-level diagnostic logging to both report paths (periodic
frontend reports and command-response reports) and the protobuf encoder
for verifying wire content.
Root cause: server pause/resume commands send None for position and
tracks, which the renderer state blindly overwrote. Then renderer
reports sent null position to the server, making controllers show the
track as finished.
Three-part fix:
1. renderer.rs: only overwrite position/tracks when server provides
a non-None value (retain last known values through pause/resume)
2. app.rs + qconnect_service.rs: update renderer state position from
frontend periodic reports so the internal state always has the
real playback position
3. +page.svelte: increase position report frequency from 5s to 2s
for smoother seekbar updates on controllers
Add missing SRVR_CTRL session management message types (81-87, 97-101)
that were completely absent from the protobuf definitions. Without these,
prost silently discarded all session topology messages from the server.
- Add 12 new proto structs for session/renderer/volume/quality events
- Add decoder branches and map functions for all new message types
- Add SessionManagementEvent variant to bypass queue reducer
- Add v2_qconnect_report_playback_state and v2_qconnect_report_volume
- Add comprehensive logging for inbound message diagnostics