fix(qconnect): honor peer-controller seeks when qbz is the active renderer

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
This commit is contained in:
vicrodh committed 2026-05-06 14:21:27 -06:00
1 parent 86e22bf886
commit 6afbc94171
2 files changed
+30 -14

No files matched your search

+13 -9
View File
@@ -15,7 +15,6 @@ use crate::core_bridge::CoreBridge;
use super::queue_resolution::{
dedupe_track_ids, resolve_core_shuffle_order, resolve_remote_start_index,
};
use super::session::is_peer_renderer_active;
use super::track_loading::ensure_remote_track_loaded;
use super::{
model_track_to_core_queue_track, normalize_volume_to_fraction,
@@ -84,10 +83,6 @@ pub(super) async fn apply_renderer_command_to_corebridge(
projection_renderer_state.next_track = next_track.clone();
}
let resolved_current_track = projection_renderer_state.current_track.as_ref();
let peer_renderer_active = {
let state = sync_state.lock().await;
is_peer_renderer_active(&state.session)
};
if let Some(projected_track) = resolved_current_track {
let queue_state = {
let state = sync_state.lock().await;
@@ -197,10 +192,19 @@ pub(super) async fn apply_renderer_command_to_corebridge(
.unwrap_or(false)
&& target_secs <= 1
&& current_pos_secs > 2;
if peer_renderer_active
&& !is_echo_reset
&& current_pos_secs.abs_diff(target_secs) > 2
{
// Issue #387: honor seeks regardless of which device is the
// active renderer. The previous gate (`peer_renderer_active`)
// skipped seeks entirely when local was the active renderer,
// breaking the case where a peer controller (e.g. official
// Qobuz mobile app) sends a real seek to qbz acting as the
// renderer — the audio thread never moved while the cloud
// state advanced, so the controller's progress bar locked.
// The is_echo_reset + abs_diff > 2 gates already filter the
// cloud-echo case the peer_renderer_active check was added
// to defend against in commit 147bcbd7. If hiccups return,
// revert this change and reintroduce a more targeted echo
// detector (UUID-based) instead of the all-or-nothing gate.
if !is_echo_reset && current_pos_secs.abs_diff(target_secs) > 2 {
log::info!(
"[QConnect] SetState seek: current={}s target={}s",
current_pos_secs,