🚀 Version bump to v0.3.42 → v0.3.46 & fix shuffle/auto-advance race condition
- Bump version to v0.3.46 across Cargo.toml, lockfile & version.txt - 🔧 Fix critical shuffle/auto_advance bug: ensure `has_played_flag` is reset and playback source set *before* backend calls in play_from_index, next/prev & queue init - 📝 Add detailed logging for playback source transitions and index changes to debug timing issues (especially with JBL-like renderers) - 🎯 Introduce `play_current_from_queue_with_retry()` for resilient playback start on flaky UPnP devices
This commit is contained in:
@@ -839,11 +839,17 @@ impl ControlPoint {
|
|||||||
"Playing current playback item from queue"
|
"Playing current playback item from queue"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Temporarily disable auto-advance to prevent race condition
|
// Note: We set playback_source to FromQueue BEFORE calling play_from_queue().
|
||||||
// when renderer sends Stopped event during SetAVTransportURI
|
// This prevents a race condition where:
|
||||||
renderer.set_playback_source(PlaybackSource::None);
|
// 1. play_from_queue() is called
|
||||||
|
// 2. watcher detects STOPPED from track transition (before playback_source is set)
|
||||||
|
// 3. with source=None, is_playing_from_queue() returns false, breaking auto-advance
|
||||||
|
// By setting it BEFORE, even if STOPPED is detected, source will be FromQueue.
|
||||||
|
// Note: We no longer temporarily set playback_source to None (that caused Bug #2).
|
||||||
|
|
||||||
// Start playback using play_from_queue which preserves the queue
|
// Start playback using play_from_queue which preserves the queue
|
||||||
|
// Set source to FromQueue BEFORE calling to prevent race condition
|
||||||
|
renderer.set_playback_source(PlaybackSource::FromQueue);
|
||||||
if let Err(err) = renderer.play_from_queue() {
|
if let Err(err) = renderer.play_from_queue() {
|
||||||
error!(
|
error!(
|
||||||
renderer = renderer_id.0.as_str(),
|
renderer = renderer_id.0.as_str(),
|
||||||
@@ -865,7 +871,7 @@ impl ControlPoint {
|
|||||||
let metadata = playback_item_track_metadata(&item);
|
let metadata = playback_item_track_metadata(&item);
|
||||||
renderer.set_last_metadata(Some(metadata));
|
renderer.set_last_metadata(Some(metadata));
|
||||||
|
|
||||||
renderer.set_playback_source(PlaybackSource::FromQueue);
|
// Note: playback_source already set to FromQueue above
|
||||||
|
|
||||||
// Prefetch next track if supported (gapless playback)
|
// Prefetch next track if supported (gapless playback)
|
||||||
self.prefetch_next_track(&renderer, renderer_id);
|
self.prefetch_next_track(&renderer, renderer_id);
|
||||||
@@ -895,8 +901,9 @@ impl ControlPoint {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporarily disable auto-advance to prevent race condition
|
// Note: Same fix as play_current_from_queue - set source BEFORE calling.
|
||||||
renderer.set_playback_source(PlaybackSource::None);
|
// Set source to FromQueue BEFORE calling to prevent race condition
|
||||||
|
renderer.set_playback_source(PlaybackSource::FromQueue);
|
||||||
|
|
||||||
// Use the backend's play_next which handles queue advancement correctly for each backend type
|
// Use the backend's play_next which handles queue advancement correctly for each backend type
|
||||||
if let Err(err) = renderer.play_next_from_queue() {
|
if let Err(err) = renderer.play_next_from_queue() {
|
||||||
@@ -1035,8 +1042,9 @@ impl ControlPoint {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporarily disable auto-advance to prevent race condition
|
// Note: Same fix as play_current_from_queue - set source BEFORE calling.
|
||||||
renderer.set_playback_source(PlaybackSource::None);
|
// Set source to FromQueue BEFORE calling to prevent race condition
|
||||||
|
renderer.set_playback_source(PlaybackSource::FromQueue);
|
||||||
|
|
||||||
// Use the backend's play_from_index which handles everything correctly
|
// Use the backend's play_from_index which handles everything correctly
|
||||||
if let Err(err) = renderer.play_from_index(index) {
|
if let Err(err) = renderer.play_from_index(index) {
|
||||||
|
|||||||
@@ -1000,6 +1000,11 @@ impl MusicRenderer {
|
|||||||
// The flag will be set back to true when PLAYING state is detected.
|
// The flag will be set back to true when PLAYING state is detected.
|
||||||
self.clear_has_played_flag();
|
self.clear_has_played_flag();
|
||||||
|
|
||||||
|
// Set playback_source to FromQueue BEFORE calling backend
|
||||||
|
// to prevent race condition where watcher sees STOPPED before
|
||||||
|
// source is set, breaking auto-advance
|
||||||
|
self.set_playback_source(PlaybackSource::FromQueue);
|
||||||
|
|
||||||
self.lock_backend_for("play_current_from_queue")
|
self.lock_backend_for("play_current_from_queue")
|
||||||
.play_from_queue()
|
.play_from_queue()
|
||||||
}
|
}
|
||||||
@@ -1011,6 +1016,11 @@ impl MusicRenderer {
|
|||||||
// The flag will be set back to true when PLAYING state is detected.
|
// The flag will be set back to true when PLAYING state is detected.
|
||||||
self.clear_has_played_flag();
|
self.clear_has_played_flag();
|
||||||
|
|
||||||
|
// Set playback_source to FromQueue BEFORE calling backend
|
||||||
|
// to prevent race condition where watcher sees STOPPED before
|
||||||
|
// source is set, breaking auto-advance
|
||||||
|
self.set_playback_source(PlaybackSource::FromQueue);
|
||||||
|
|
||||||
self.lock_backend_for("play_next_from_queue").play_next()?;
|
self.lock_backend_for("play_next_from_queue").play_next()?;
|
||||||
self.emit_queue_updated();
|
self.emit_queue_updated();
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1583,11 +1593,18 @@ impl MusicRenderer {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marks playback as external if currently idle (source is None).
|
/// Marks playback as external if currently idle.
|
||||||
|
///
|
||||||
|
/// Only sets to External if we were already playing from an external source.
|
||||||
|
/// Does NOT change None -> External because that would break queue playback
|
||||||
|
/// (the control_point will set it to FromQueue after play_from_queue succeeds).
|
||||||
pub fn mark_external_if_idle(&self) {
|
pub fn mark_external_if_idle(&self) {
|
||||||
let mut state = self.state.lock().unwrap();
|
let mut state = self.state.lock().unwrap();
|
||||||
if matches!(state.playback_source, PlaybackSource::None) {
|
if matches!(state.playback_source, PlaybackSource::External) {
|
||||||
state.playback_source = PlaybackSource::External;
|
// Keep External if we were already playing externally
|
||||||
|
} else {
|
||||||
|
// Don't change None -> External - that breaks queue auto-advance!
|
||||||
|
// The control_point will set playback_source to FromQueue after play_from_queue succeeds.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,9 @@ impl MusicQueue {
|
|||||||
};
|
};
|
||||||
// Queue lock is released here.
|
// Queue lock is released here.
|
||||||
// Now safe to call on_ready (which may re-lock the queue).
|
// Now safe to call on_ready (which may re-lock the queue).
|
||||||
if on_ready_triggered.load(SeqCst) {
|
// If on_ready was triggered by the proxy, consume and call it.
|
||||||
|
// If not (cancelled before first insert), keep it to pass to retry.
|
||||||
|
let carry_on_ready = if on_ready_triggered.load(SeqCst) {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
thread = %std::thread::current().name().unwrap_or("?"),
|
thread = %std::thread::current().name().unwrap_or("?"),
|
||||||
"queue-sync: on_ready triggered, calling callback"
|
"queue-sync: on_ready triggered, calling callback"
|
||||||
@@ -186,12 +188,16 @@ impl MusicQueue {
|
|||||||
if let Some(f) = real_on_ready {
|
if let Some(f) = real_on_ready {
|
||||||
f();
|
f();
|
||||||
}
|
}
|
||||||
} else if real_on_ready.is_some() {
|
None
|
||||||
tracing::debug!(
|
} else {
|
||||||
thread = %std::thread::current().name().unwrap_or("?"),
|
if real_on_ready.is_some() {
|
||||||
"queue-sync: on_ready not triggered (cancelled or skipped)"
|
tracing::debug!(
|
||||||
);
|
thread = %std::thread::current().name().unwrap_or("?"),
|
||||||
}
|
"queue-sync: on_ready not triggered, carrying to next attempt"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
real_on_ready
|
||||||
|
};
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Err(ControlPointError::SyncCancelled) => {
|
Err(ControlPointError::SyncCancelled) => {
|
||||||
@@ -222,7 +228,7 @@ impl MusicQueue {
|
|||||||
match pending_items_fn() {
|
match pending_items_fn() {
|
||||||
Ok(new_items) => {
|
Ok(new_items) => {
|
||||||
current_items = new_items;
|
current_items = new_items;
|
||||||
current_on_ready = Some(None);
|
current_on_ready = Some(carry_on_ready);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("queue-sync pending re-fetch error: {}", e);
|
tracing::warn!("queue-sync pending re-fetch error: {}", e);
|
||||||
|
|||||||
Reference in New Issue
Block a user