mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 22:45:41 +00:00
feat(playback): stream buffering UI, M4A moov-at-end streaming, hot-cache spill (#737)
* feat(playback): stream buffering UI, ranged M4A tail prefetch, demuxer fix Defer seekbar/progress until HTTP stream is armed for both legacy and RangedHttpSource; show buffering overlay on cover art. Add MP4 tail prefetch and Symphonia isomp4 bounded-mdat/moov-at-EOF probing so moov-at-end M4A can start without reading the full mdat. * feat(hot-cache): spill large ranged streams to disk for promote When a ranged HTTP download completes above the 64 MiB RAM promote cap, write the existing buffer once to app-data stream-spill/ and register it for hot-cache promote (rename) and replay via fetch_data. Analysis seeds from the spill file up to the local-file cap (512 MiB). * fix(ui): stream buffering — grayscale cover and static clock icon Desaturate player and queue cover art while isPlaybackBuffering; keep a non-animated clock overlay for visibility without the spinning animation. * fix(playback): review follow-up — tests, i18n, spill cleanup, changelog Clippy and test layout fixes; stream spill orphan cleanup on startup; buffering flag guard in progress handler; bufferingStream in all player locales; CHANGELOG and contributor credits for stream/M4A work. * docs: attribute stream buffering and M4A streaming to PR #737 * test(audio): avoid create_engine in stream spill unit test CI runners have no audio output device; test spill take/consume via the Mutex slot only, matching install_stream_completed_spill tests.
This commit is contained in:
@@ -15,8 +15,8 @@ use super::engine::{audio_http_client, AudioEngine};
|
||||
use super::helpers::*;
|
||||
use super::ipc::{maybe_emit_normalization_state, NormalizationStatePayload};
|
||||
use super::play_input::{
|
||||
build_source_from_play_input, select_play_input, swap_in_new_sink, url_format_hint,
|
||||
PlayInputContext, SinkSwapInputs,
|
||||
build_source_from_play_input, select_play_input, spawn_legacy_stream_start_when_armed,
|
||||
swap_in_new_sink, url_format_hint, PlayInputContext, SinkSwapInputs,
|
||||
};
|
||||
use super::preview::preview_clear_for_new_main_playback;
|
||||
use super::progress_task::spawn_progress_task;
|
||||
@@ -97,6 +97,8 @@ pub async fn audio_play(
|
||||
// Bump generation first so the old progress task stops before we peel
|
||||
// chained_info (avoids a race where it sees current_done + empty chain).
|
||||
let gen = state.generation.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
// Ranged/legacy HTTP paths reset this to false in `select_play_input`.
|
||||
state.stream_playback_armed.store(true, Ordering::SeqCst);
|
||||
|
||||
// Manual skip onto the gapless-pre-chained track: reuse raw bytes (no HTTP;
|
||||
// preload cache was already consumed when the chain was built). Otherwise
|
||||
@@ -325,7 +327,8 @@ pub async fn audio_play(
|
||||
// without an underrun on the very first period.
|
||||
// Standard mode: no pre-fill needed — default 44.1/48 kHz quantum is small.
|
||||
let needs_prefill = hi_res_enabled && output_rate > 48_000;
|
||||
if needs_prefill {
|
||||
let defer_playback_start = !state.stream_playback_armed.load(Ordering::Relaxed);
|
||||
if needs_prefill || defer_playback_start {
|
||||
sink.pause();
|
||||
}
|
||||
|
||||
@@ -372,7 +375,9 @@ pub async fn audio_play(
|
||||
if state.generation.load(Ordering::SeqCst) != gen {
|
||||
return Ok(()); // skipped during pre-fill — abort silently
|
||||
}
|
||||
sink.play();
|
||||
if !defer_playback_start {
|
||||
sink.play();
|
||||
}
|
||||
}
|
||||
|
||||
swap_in_new_sink(&state, SinkSwapInputs {
|
||||
@@ -386,7 +391,24 @@ pub async fn audio_play(
|
||||
actual_fade_secs,
|
||||
});
|
||||
|
||||
app.emit("audio:playing", duration_secs).ok();
|
||||
if defer_playback_start {
|
||||
{
|
||||
let mut cur = state.current.lock().unwrap();
|
||||
cur.play_started = None;
|
||||
cur.paused_at = Some(0.0);
|
||||
}
|
||||
spawn_legacy_stream_start_when_armed(
|
||||
gen,
|
||||
state.generation.clone(),
|
||||
state.stream_playback_armed.clone(),
|
||||
state.samples_played.clone(),
|
||||
state.current.clone(),
|
||||
app.clone(),
|
||||
duration_secs,
|
||||
);
|
||||
} else {
|
||||
app.emit("audio:playing", duration_secs).ok();
|
||||
}
|
||||
|
||||
// ── Progress + ended detection ────────────────────────────────────────────
|
||||
spawn_progress_task(
|
||||
@@ -403,6 +425,7 @@ pub async fn audio_play(
|
||||
state.current_channels.clone(),
|
||||
state.gapless_switch_at.clone(),
|
||||
state.current_playback_url.clone(),
|
||||
state.stream_playback_armed.clone(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user