Merge branch 'pr/593' into integration/pr-review-smoke-2026-07-13

This commit is contained in:
vicrodh committed 2026-07-13 14:12:39 -06:00
commit c2e23dfa95
2 files changed
+75 -5

No files matched your search

+73 -3
View File
@@ -3909,6 +3909,10 @@ impl Player {
self.state.begin_play() self.state.begin_play()
} }
/// A passing check is a snapshot, not a lock: a newer intent can still
/// begin between this check and the subsequent AudioCommand send. That
/// residual window is accepted; closing it would require the generation
/// to travel inside the audio command itself.
fn is_current_play(&self, gen: u64) -> bool { fn is_current_play(&self, gen: u64) -> bool {
self.state.is_current_play(gen) self.state.is_current_play(gen)
} }
@@ -4036,7 +4040,8 @@ impl Player {
); );
// Create the streaming buffer and start playback immediately. // Create the streaming buffer and start playback immediately.
let buffer_writer = self.play_streaming_dynamic( // Non-bumping variant: this intent already holds `gen`.
let buffer_writer = self.apply_play_streaming_dynamic(
track_id, track_id,
sample_rate, sample_rate,
channels, channels,
@@ -4640,6 +4645,7 @@ impl Player {
/// track. DST-compressed DFF and >2ch files are rejected with a /// track. DST-compressed DFF and >2ch files are rejected with a
/// readable error before anything starts. /// readable error before anything starts.
pub fn play_dsd_file(&self, path: std::path::PathBuf, track_id: u64) -> Result<(), String> { pub fn play_dsd_file(&self, path: std::path::PathBuf, track_id: u64) -> Result<(), String> {
let _gen = self.begin_play();
let demux = qbz_dsd::open_dsd(&path).map_err(|e| e.to_string())?; let demux = qbz_dsd::open_dsd(&path).map_err(|e| e.to_string())?;
let dsd_rate = demux.info().dsd_rate; let dsd_rate = demux.info().dsd_rate;
@@ -4728,8 +4734,15 @@ impl Player {
); );
self.state.set_stream_quality(rate, 24); self.state.set_stream_quality(rate, 24);
let writer = let writer = self.apply_play_streaming(
self.play_streaming(track_id, rate, channels, content_length, 3, duration_secs, 0)?; track_id,
rate,
channels,
content_length,
3,
duration_secs,
0,
)?;
std::thread::spawn(move || { std::thread::spawn(move || {
if writer if writer
@@ -4816,6 +4829,32 @@ impl Player {
buffer_seconds: u8, buffer_seconds: u8,
duration_secs: u64, duration_secs: u64,
start_position_secs: u64, start_position_secs: u64,
) -> Result<BufferWriter, String> {
let _gen = self.begin_play();
self.apply_play_streaming(
track_id,
sample_rate,
channels,
content_length,
buffer_seconds,
duration_secs,
start_position_secs,
)
}
/// `play_streaming` without bumping generation (used by play paths that
/// already did their own `begin_play` + supersede checks, like
/// `play_dsd_file`; a mid-intent bump here would invalidate a strictly
/// newer play that started in the meantime).
fn apply_play_streaming(
&self,
track_id: u64,
sample_rate: u32,
channels: u16,
content_length: u64,
buffer_seconds: u8,
duration_secs: u64,
start_position_secs: u64,
) -> Result<BufferWriter, String> { ) -> Result<BufferWriter, String> {
log::info!( log::info!(
"Player: Starting streaming playback for track {} ({}Hz, {}ch, {} bytes total, {}s, start={}s)", "Player: Starting streaming playback for track {} ({}Hz, {}ch, {} bytes total, {}s, start={}s)",
@@ -4865,6 +4904,34 @@ impl Player {
speed_mbps: f64, speed_mbps: f64,
duration_secs: u64, duration_secs: u64,
start_position_secs: u64, start_position_secs: u64,
) -> Result<BufferWriter, String> {
let _gen = self.begin_play();
self.apply_play_streaming_dynamic(
track_id,
sample_rate,
channels,
bit_depth,
content_length,
speed_mbps,
duration_secs,
start_position_secs,
)
}
/// `play_streaming_dynamic` without bumping generation (used by
/// `play_track`'s CMAF path, which already holds a generation token; a
/// mid-intent bump here would invalidate a strictly newer play that
/// started in the meantime).
fn apply_play_streaming_dynamic(
&self,
track_id: u64,
sample_rate: u32,
channels: u16,
bit_depth: u32,
content_length: u64,
speed_mbps: f64,
duration_secs: u64,
start_position_secs: u64,
) -> Result<BufferWriter, String> { ) -> Result<BufferWriter, String> {
log::info!( log::info!(
"Player: Starting dynamic streaming for track {} ({}Hz, {}ch, {}-bit, {:.2} MB, {:.1} MB/s, {}s, start={}s)", "Player: Starting dynamic streaming for track {} ({}Hz, {}ch, {}-bit, {:.2} MB, {:.1} MB/s, {}s, start={}s)",
@@ -4961,6 +5028,9 @@ impl Player {
/// Stop playback /// Stop playback
pub fn stop(&self) -> Result<(), String> { pub fn stop(&self) -> Result<(), String> {
// Supersede any in-flight play_track so a slow CMAF/legacy fetch cannot
// restart audio after the user stopped.
let _ = self.begin_play();
self.tx self.tx
.send(AudioCommand::Stop) .send(AudioCommand::Stop)
.map_err(|e| format!("Failed to send stop command: {}", e)) .map_err(|e| format!("Failed to send stop command: {}", e))
+2 -2
View File
@@ -614,7 +614,7 @@ async fn play_audible(runtime: &Runtime, weak: &slint::Weak<AppWindow>, track_id
Err(e) => { Err(e) => {
log::error!("[qbz-slint] playback: play_track {track_id} failed: {e}"); log::error!("[qbz-slint] playback: play_track {track_id} failed: {e}");
// Superseded fetch: the user already started another play while this // Superseded fetch: the user already started another play while this
// one was resolving — that newer play owns the cursor; do NOT skip. // one was resolving. That newer play owns the cursor; do NOT skip.
let still_current = PENDING_PLAY_ID.load(std::sync::atomic::Ordering::Relaxed) let still_current = PENDING_PLAY_ID.load(std::sync::atomic::Ordering::Relaxed)
== track_id; == track_id;
// The fetch failed: no audio will advance, so the poll loop would never // The fetch failed: no audio will advance, so the poll loop would never
@@ -623,7 +623,7 @@ async fn play_audible(runtime: &Runtime, weak: &slint::Weak<AppWindow>, track_id
// Tauri-parity regression fix: an unavailable track used to be // Tauri-parity regression fix: an unavailable track used to be
// auto-skipped by the frontend (playbackService `autoSkipToNext`, // auto-skipped by the frontend (playbackService `autoSkipToNext`,
// bounded, issue #467). Without this the queue cursor parks on the // bounded, issue #467). Without this the queue cursor parks on the
// dead track and playback stops. Terminal errors only — transient // dead track and playback stops. Terminal errors only: transient
// failures were already retried by the client and should not skip. // failures were already retried by the client and should not skip.
if still_current && is_terminal_unavailable(&e) { if still_current && is_terminal_unavailable(&e) {
auto_skip_unavailable(runtime, weak, track_id).await; auto_skip_unavailable(runtime, weak, track_id).await;