fix(qconnect): restore local repeat renderer flow

cx
This commit is contained in:
vicrodh committed 2026-03-14 13:09:23 -06:00
1 parent f47f691063
commit 8a468ed725
4 files changed
+125 -19

No files matched your search

+82 -12
View File
@@ -2463,6 +2463,10 @@ async fn apply_renderer_command_to_corebridge(
.. ..
} => { } => {
let resolved_playing_state = renderer_state.playing_state.or(*playing_state); let resolved_playing_state = renderer_state.playing_state.or(*playing_state);
let target_position_secs = renderer_state
.current_position_ms
.or(*current_position_ms)
.map(|position_ms| position_ms / 1000);
let mut projection_renderer_state = renderer_state.clone(); let mut projection_renderer_state = renderer_state.clone();
if projection_renderer_state.current_track.is_none() { if projection_renderer_state.current_track.is_none() {
projection_renderer_state.current_track = current_track.clone(); projection_renderer_state.current_track = current_track.clone();
@@ -2471,6 +2475,7 @@ async fn apply_renderer_command_to_corebridge(
projection_renderer_state.next_track = next_track.clone(); projection_renderer_state.next_track = next_track.clone();
} }
let resolved_current_track = projection_renderer_state.current_track.as_ref(); let resolved_current_track = projection_renderer_state.current_track.as_ref();
let mut reloaded_for_restart = false;
if let Some(current_track) = resolved_current_track { if let Some(current_track) = resolved_current_track {
let queue_state = { let queue_state = {
let state = sync_state.lock().await; let state = sync_state.lock().await;
@@ -2500,13 +2505,33 @@ async fn apply_renderer_command_to_corebridge(
resolved_playing_state, resolved_playing_state,
Some(PLAYING_STATE_PLAYING | PLAYING_STATE_PAUSED) Some(PLAYING_STATE_PLAYING | PLAYING_STATE_PAUSED)
) { ) {
if let Err(err) = if let Some(target_position_secs) = target_position_secs {
ensure_remote_track_loaded(bridge, current_track.track_id).await let playback_state = bridge.get_playback_state();
{ if should_force_remote_track_restart(
log::warn!( &playback_state,
"[QConnect] Failed to load remote track {}: {err}", current_track.track_id,
current_track.track_id target_position_secs,
); ) {
log::info!(
"[QConnect] Restarting remote track {} from the beginning (current={}s target={}s)",
current_track.track_id,
playback_state.position,
target_position_secs
);
load_remote_track_into_player(bridge, current_track.track_id).await?;
reloaded_for_restart = true;
}
}
if !reloaded_for_restart {
if let Err(err) =
ensure_remote_track_loaded(bridge, current_track.track_id).await
{
log::warn!(
"[QConnect] Failed to load remote track {}: {err}",
current_track.track_id
);
}
} }
} }
} }
@@ -2532,6 +2557,9 @@ async fn apply_renderer_command_to_corebridge(
if let Some(position_ms) = renderer_state.current_position_ms.or(*current_position_ms) { if let Some(position_ms) = renderer_state.current_position_ms.or(*current_position_ms) {
let current_pos_secs = bridge.get_playback_state().position; let current_pos_secs = bridge.get_playback_state().position;
let target_secs = position_ms / 1000; let target_secs = position_ms / 1000;
if reloaded_for_restart && target_secs <= 1 {
return Ok(());
}
// Only seek if the position differs by more than 2 seconds to // Only seek if the position differs by more than 2 seconds to
// avoid audio hiccups from redundant seeks (e.g. when the server // avoid audio hiccups from redundant seeks (e.g. when the server
// echoes back our own position in a SET_STATE after queue_load). // echoes back our own position in a SET_STATE after queue_load).
@@ -2969,12 +2997,18 @@ fn should_reload_remote_track(
playback_state.track_id != track_id || !has_loaded_audio playback_state.track_id != track_id || !has_loaded_audio
} }
async fn ensure_remote_track_loaded(bridge: &CoreBridge, track_id: u64) -> Result<(), String> { fn should_force_remote_track_restart(
let playback_state = bridge.get_playback_state(); playback_state: &qbz_player::PlaybackState,
if !should_reload_remote_track(&playback_state, bridge.has_loaded_audio(), track_id) { track_id: u64,
return Ok(()); target_position_secs: u64,
} ) -> bool {
playback_state.track_id == track_id
&& track_id != 0
&& target_position_secs <= 1
&& playback_state.position > target_position_secs.saturating_add(2)
}
async fn load_remote_track_into_player(bridge: &CoreBridge, track_id: u64) -> Result<(), String> {
let stream_url = bridge let stream_url = bridge
.get_stream_url(track_id, Quality::UltraHiRes) .get_stream_url(track_id, Quality::UltraHiRes)
.await .await
@@ -3003,6 +3037,15 @@ async fn ensure_remote_track_loaded(bridge: &CoreBridge, track_id: u64) -> Resul
} }
} }
async fn ensure_remote_track_loaded(bridge: &CoreBridge, track_id: u64) -> Result<(), String> {
let playback_state = bridge.get_playback_state();
if !should_reload_remote_track(&playback_state, bridge.has_loaded_audio(), track_id) {
return Ok(());
}
load_remote_track_into_player(bridge, track_id).await
}
struct QconnectRemoteStreamInfo { struct QconnectRemoteStreamInfo {
content_length: u64, content_length: u64,
sample_rate: u32, sample_rate: u32,
@@ -4729,6 +4772,33 @@ mod tests {
assert_eq!(super::qconnect_repeat_mode_from_loop_mode(99), None); assert_eq!(super::qconnect_repeat_mode_from_loop_mode(99), None);
} }
#[test]
fn forces_remote_restart_for_same_track_reset_to_zero() {
let playback_state = qbz_player::PlaybackState {
is_playing: false,
position: 248,
duration: 251,
track_id: 72930174,
volume: 1.0,
};
assert!(super::should_force_remote_track_restart(
&playback_state,
72930174,
0,
));
assert!(!super::should_force_remote_track_restart(
&playback_state,
72930174,
120,
));
assert!(!super::should_force_remote_track_restart(
&playback_state,
72930175,
0,
));
}
#[test] #[test]
fn resolves_current_and_next_queue_item_ids_from_queue_order() { fn resolves_current_and_next_queue_item_ids_from_queue_order() {
let queue: QConnectQueueState = serde_json::from_value(json!({ let queue: QConnectQueueState = serde_json::from_value(json!({
+23
View File
@@ -6,6 +6,7 @@ import {
isQconnectPeerRendererActive, isQconnectPeerRendererActive,
isQconnectRemoteModeActive, isQconnectRemoteModeActive,
resolveQconnectPlayNextAuthoritativeTrackId, resolveQconnectPlayNextAuthoritativeTrackId,
shouldQconnectSuppressLocalPlaybackAutomation,
type QconnectConnectionStatus type QconnectConnectionStatus
} from './qconnectRuntime'; } from './qconnectRuntime';
import type { QconnectQueueSnapshot, QconnectRendererSnapshot } from './qconnectRemoteQueue'; import type { QconnectQueueSnapshot, QconnectRendererSnapshot } from './qconnectRemoteQueue';
@@ -182,6 +183,28 @@ describe('QConnect runtime state helpers', () => {
).toBe(true); ).toBe(true);
}); });
it('suppresses local playback automation only when a peer renderer is active', () => {
expect(
shouldQconnectSuppressLocalPlaybackAutomation(true, {
session_uuid: 'session-1',
active_renderer_id: 4,
local_renderer_id: 11,
renderers: []
})
).toBe(true);
expect(
shouldQconnectSuppressLocalPlaybackAutomation(true, {
session_uuid: 'session-1',
active_renderer_id: 11,
local_renderer_id: 11,
renderers: []
})
).toBe(false);
expect(shouldQconnectSuppressLocalPlaybackAutomation(false, null)).toBe(false);
});
it('does not trust the local current track for play next when a peer renderer is active', () => { it('does not trust the local current track for play next when a peer renderer is active', () => {
expect( expect(
resolveQconnectPlayNextAuthoritativeTrackId({ resolveQconnectPlayNextAuthoritativeTrackId({
+7
View File
@@ -194,6 +194,13 @@ export function isQconnectPeerRendererActive(
return activeRendererId !== localRendererId; return activeRendererId !== localRendererId;
} }
export function shouldQconnectSuppressLocalPlaybackAutomation(
connected: boolean,
sessionSnapshot: QconnectSessionSnapshot | null | undefined
): boolean {
return connected && isQconnectPeerRendererActive(sessionSnapshot);
}
export function resolveQconnectPlayNextAuthoritativeTrackId(params: { export function resolveQconnectPlayNextAuthoritativeTrackId(params: {
sessionSnapshot: QconnectSessionSnapshot | null | undefined; sessionSnapshot: QconnectSessionSnapshot | null | undefined;
localCurrentTrackId: number | null | undefined; localCurrentTrackId: number | null | undefined;
+13 -7
View File
@@ -325,6 +325,7 @@
isQconnectRemoteModeActive as computeQconnectRemoteModeActive, isQconnectRemoteModeActive as computeQconnectRemoteModeActive,
logQconnectPlaybackReport as appendQconnectPlaybackReport, logQconnectPlaybackReport as appendQconnectPlaybackReport,
qconnectAdmissionReasonKey, qconnectAdmissionReasonKey,
shouldQconnectSuppressLocalPlaybackAutomation,
toggleQconnectConnection toggleQconnectConnection
} from '$lib/services/qconnectRuntime'; } from '$lib/services/qconnectRuntime';
import type { import type {
@@ -914,6 +915,12 @@
const qconnectPeerRendererActive = $derived( const qconnectPeerRendererActive = $derived(
isQconnectPeerRendererActive(qobuzConnectSessionSnapshot) isQconnectPeerRendererActive(qobuzConnectSessionSnapshot)
); );
const qconnectSuppressLocalPlaybackAutomation = $derived(
shouldQconnectSuppressLocalPlaybackAutomation(
isQobuzConnectConnected,
qobuzConnectSessionSnapshot
)
);
const effectiveIsPlaying = $derived( const effectiveIsPlaying = $derived(
qconnectPeerRendererActive qconnectPeerRendererActive
? qobuzConnectRendererSnapshot?.playing_state === 2 ? qobuzConnectRendererSnapshot?.playing_state === 2
@@ -1022,14 +1029,13 @@
function applyQobuzConnectStatus(status: QconnectConnectionStatus): void { function applyQobuzConnectStatus(status: QconnectConnectionStatus): void {
qobuzConnectStatus = status; qobuzConnectStatus = status;
const nextConnected = Boolean(status.transport_connected); const nextConnected = Boolean(status.transport_connected);
if (nextConnected !== isQobuzConnectConnected) {
isQobuzConnectConnected = nextConnected;
setRemoteControlMode(nextConnected);
return;
}
isQobuzConnectConnected = nextConnected; isQobuzConnectConnected = nextConnected;
} }
$effect(() => {
setRemoteControlMode(qconnectSuppressLocalPlaybackAutomation);
});
function mapBackendQueueTrackToPlayingTrack(track: BackendQueueTrack): PlayingTrack { function mapBackendQueueTrackToPlayingTrack(track: BackendQueueTrack): PlayingTrack {
const rawRate = track.sample_rate ?? undefined; const rawRate = track.sample_rate ?? undefined;
const normalizedRate = rawRate == null const normalizedRate = rawRate == null
@@ -4146,7 +4152,7 @@
setOnTrackEnded(async () => { setOnTrackEnded(async () => {
// When QConnect controls playback, the server/controller manages track advancement. // When QConnect controls playback, the server/controller manages track advancement.
// The frontend must NOT auto-advance or it will fight the remote controller. // The frontend must NOT auto-advance or it will fight the remote controller.
if (isQobuzConnectConnected) { if (qconnectSuppressLocalPlaybackAutomation) {
console.log('[Player] Auto-advance suppressed: QConnect is controlling playback'); console.log('[Player] Auto-advance suppressed: QConnect is controlling playback');
return; return;
} }
@@ -4188,7 +4194,7 @@
// Set up resume-from-stop callback: re-play the queue's current track // Set up resume-from-stop callback: re-play the queue's current track
setOnResumeFromStop(async () => { setOnResumeFromStop(async () => {
if (isQobuzConnectConnected) return; if (qconnectSuppressLocalPlaybackAutomation) return;
const queueState = await getBackendQueueState(); const queueState = await getBackendQueueState();
if (!queueState) return; if (!queueState) return;
const tryQueueIndices = async (indices: number[]): Promise<BackendQueueTrack | null> => { const tryQueueIndices = async (indices: number[]): Promise<BackendQueueTrack | null> => {