mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +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:
+50
-26
@@ -141,39 +141,63 @@ pub async fn promote_stream_cache_to_hot_cache(
|
||||
return Ok(Some(HotCacheDownloadResult { path: path_str, size }));
|
||||
}
|
||||
|
||||
let bytes = match audio::take_stream_completed_for_url(&state, &url) {
|
||||
Some(b) => b,
|
||||
None => {
|
||||
crate::app_deprintln!(
|
||||
"[hot-cache] promote skip track_id={} reason=no_completed_stream_for_url",
|
||||
track_id
|
||||
);
|
||||
return Ok(None);
|
||||
if let Some(bytes) = audio::take_stream_completed_for_url(&state, &url) {
|
||||
let part_path = file_path.with_extension(format!("{suffix}.part"));
|
||||
if let Err(e) = tokio::fs::write(&part_path, &bytes).await {
|
||||
let _ = tokio::fs::remove_file(&part_path).await;
|
||||
return Err(e.to_string());
|
||||
}
|
||||
};
|
||||
tokio::fs::rename(&part_path, &file_path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let part_path = file_path.with_extension(format!("{suffix}.part"));
|
||||
if let Err(e) = tokio::fs::write(&part_path, &bytes).await {
|
||||
let _ = tokio::fs::remove_file(&part_path).await;
|
||||
return Err(e.to_string());
|
||||
let _ = enqueue_analysis_seed(&app, &track_id, &bytes).await;
|
||||
|
||||
let size = tokio::fs::metadata(&file_path)
|
||||
.await
|
||||
.map(|m| m.len())
|
||||
.unwrap_or(0);
|
||||
crate::app_deprintln!(
|
||||
"[hot-cache] promote from_stream_ram track_id={} server_id={} bytes={}",
|
||||
track_id,
|
||||
server_id,
|
||||
size
|
||||
);
|
||||
return Ok(Some(HotCacheDownloadResult { path: path_str, size }));
|
||||
}
|
||||
tokio::fs::rename(&part_path, &file_path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = enqueue_analysis_seed(&app, &track_id, &bytes).await;
|
||||
if let Some(spill_path) = audio::take_stream_completed_spill_for_url(&state, &url) {
|
||||
if let Err(e) = tokio::fs::rename(&spill_path, &file_path).await {
|
||||
if let Err(copy_err) = tokio::fs::copy(&spill_path, &file_path).await {
|
||||
let _ = tokio::fs::remove_file(&spill_path).await;
|
||||
return Err(format!("promote spill rename: {e}; copy: {copy_err}"));
|
||||
}
|
||||
let _ = tokio::fs::remove_file(&spill_path).await;
|
||||
}
|
||||
let app_seed = app.clone();
|
||||
let tid = track_id.clone();
|
||||
let fp = file_path.clone();
|
||||
tokio::spawn(async move {
|
||||
enqueue_analysis_seed_from_file(&app_seed, &tid, &fp).await;
|
||||
});
|
||||
let size = tokio::fs::metadata(&file_path)
|
||||
.await
|
||||
.map(|m| m.len())
|
||||
.unwrap_or(0);
|
||||
crate::app_deprintln!(
|
||||
"[hot-cache] promote from_stream_spill track_id={} server_id={} bytes={}",
|
||||
track_id,
|
||||
server_id,
|
||||
size
|
||||
);
|
||||
return Ok(Some(HotCacheDownloadResult { path: path_str, size }));
|
||||
}
|
||||
|
||||
let size = tokio::fs::metadata(&file_path)
|
||||
.await
|
||||
.map(|m| m.len())
|
||||
.unwrap_or(0);
|
||||
crate::app_deprintln!(
|
||||
"[hot-cache] promote from_stream track_id={} server_id={} bytes={}",
|
||||
track_id,
|
||||
server_id,
|
||||
size
|
||||
"[hot-cache] promote skip track_id={} reason=no_completed_stream_for_url",
|
||||
track_id
|
||||
);
|
||||
Ok(Some(HotCacheDownloadResult { path: path_str, size }))
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
Reference in New Issue
Block a user