Keep queue edits from resetting the playback clock, preserve Play next and Play later batches, and report observed renderer state and owned output telemetry. Guard duplicate-track gapless transitions by queue occurrence.
cx
Keep queue reads from starving controls without weakening real queue-write serialization. Restore official proto3 zero defaults and next/previous loop, shuffle and autoplay semantics.
Project controls from the active peer state and target exact upcoming queue occurrences without optimistic cursor changes or local playback fallback. Cover takeover, seek, mute, wire decoding and manual skip regressions in the CI gate.
cx
Shuffle and Repeat now go to the CLOUD whenever connected (renderer OR controller),
gating on transport_connected like Tauri's v2_toggle_shuffle / v2_set_repeat_mode,
instead of the old peer-active gate that fell through to a LOCAL reshuffle when QBZ
was the active renderer — that did nothing visible AND risked a divergent local
order. QBZ sends only the seed + pivot; the cloud owns the order; the toggle reads
the cloud-authoritative queue.shuffle_mode. The local shuffle path is now reachable
only when NOT connected.
- shuffle/repeat button reflects the cloud queue flag (queue.shuffle_mode for the
controller branch, core queue state via refresh_transport_modes for the renderer
branch), not the per-renderer field the cloud never populates for a peer — fixes
the iOS-reflects / WebPlayer-doesn't asymmetry.
- pause/play to a peer renderer: send a BARE SetPlayerState (playing_state only).
iOS reports current_queue_item_id=null while playing, so resolving the qid landed
on queue_item_id 0 — the protocol's "unknown" placeholder — and iOS could not
anchor the pause, auto-resuming. A bare transport toggle is spec-sanctioned: the
renderer pauses/resumes its own current item in place.
- skip_serializing_if on the SetPlayerState request structs so a None field is
OMITTED, not serialized as null; and the mapper now treats a null current_queue_item
as absent. Without this the bare command failed to encode and was never sent.
- log the outbound SetPlayerState payload for controller diagnostics.
cc
Add the RndrSrvrDeviceAudioQualityChanged(27) renderer report end to end:
the report-type variant + message string in qconnect-protocol renderer.rs,
the RendererDeviceAudioQualityChangedMessage proto struct plus the genuinely
free tag-27 field on QConnectMessage (26=file AQ, 28=max AQ), and the mapper
arm. Wire the adapter emitter report_device_audio_quality_if_changed (deduped
via a new last_reported_device_audio_quality sync-state field) next to the
file-AQ report, sourcing the real output stream config. Fix the deferred-join
StateUpdated report to resolve the real current track duration (via CoreBridge)
and current/next queue_item_ids instead of hardcoding nulls.
cc
Decodes PlaybackErrorMessage off the InboundPayloadBytes batch and emits a
PlaybackError app event; the Tauri event sink forwards it to qconnect:event
via its existing catch-all emit.
cc
Models the renderer per-track failure (TRACK_NOT_STREAMABLE/NETWORK_ERROR/...).
Envelope-vs-batch placement verified against transport bytes: the decoder
receives the batch QConnectMessages (CloudPayload.payload tag 7), and the
official Message record carries playback_error at tag 3, so the field is
attached to QConnectMessage inside messages[], NOT to the batch wrapper.
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
Qobuz changed their protocol to sometimes send QueueVersionRef
without major/minor populated. Our decoder treated these as
required fields, causing all cloud_type=6 messages to fail with
"missing required numeric field 'queue_version.major'".
Default to 0 when absent — matches the dev panel behavior.
cc
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.
ErrorMessage.code was defined as int32 but server sends string codes
(e.g. 'ERROR_QUEUE_INSERT_TRACKS'). Changed to string type and added
logging for queue error responses.
Added context_uuid (random UUID v4) to queue insert and add commands -
the server likely requires this to associate tracks with a context.
track_ids in QueueLoadTracks, QueueInsertTracks, QueueAddTracks, and
AutoplayLoadTracks was int32 packed=false but the server expects
fixed32 (packed by default). This silently broke all outbound queue
manipulation commands.
Server sends track_id as fixed32 (wire type 5) and shuffle_seed as
fixed32, but our structs declared them as int32 (wire type 0/varint),
causing decode failures on QueueState, TracksLoaded, and
RendererSetState messages.
Root cause: inner_payload from CloudPayload.payload IS a QConnectBatch
(QConnectMessages) with repeated QConnectMessage entries in field 3.
The QCloudInnerEnvelope struct incorrectly treated field 3 as opaque
bytes, keeping only the last repeated entry and losing all messages.
Additionally, QConnectMessages.messages_time was declared as uint64
(wire type 0/varint) but the server sends fixed64 (wire type 1),
causing a wire type mismatch that would fail decoding.
Changes:
- Remove QCloudInnerEnvelope struct entirely
- Pass inner_payload directly to decode_queue_server_events/decode_renderer_server_commands
- Fix messages_time from uint64 to fixed64 to match server wire format
Verified against qonductor proto definitions (QConnectBatch) used in
working POC v1 and v2 implementations.
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
- Add DeviceCapabilitiesMessage to protobuf (tag 7 on DeviceInfo) with
volume_remote_control=ALLOWED, min/max audio quality
- Extend JoinSessionMessage with reason, initial_state, is_active fields
matching Android renderer proto (tags 3-5)
- Bootstrap sends renderer JoinSession with is_active=true and initial
RendererState (STOPPED) so other devices see QBZ immediately
- Send initial StateUpdated, VolumeChanged, MaxAudioQualityChanged
reports after join for full renderer presence
- Default subscribe channels to 0x01,0x02,0x03 instead of empty
- Request queue state from server after bootstrap
cc