mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
fix(isomp4): fix M4A moov-at-end probe failures and streaming fallback (#757)
* fix(stream): defer M4A probe until moov tail or fast-start prefix Ranged moov-at-end M4A started Symphonia format probe as soon as ~384 KiB linear data arrived, before the parallel tail prefetch filled the moov atom — probe hit end of stream and skipped the track. Wait for tail_ready or detect fast-start moov in the prefix; do not arm playback from linear bytes alone when tail prefetch is active. * fix(isomp4): skip EOF-spanning mdat after moov-at-end is parsed Second-pass header scan with moov already loaded still tried to read through mdat→EOF on RangedHttpSource holes, causing format probe end of stream. Re-check play generation after moov wait. * fix(isomp4): fix AtomIterator overread after seek in patched demuxer `AtomIterator::new_root(reader, len)` treats `len` as bytes available from the current `reader.pos()`, not the absolute file length. After `mss.seek(resume_at)` we were passing the absolute `total_len`, so the iterator thought there were `total_len` bytes left and tried to read past EOF on the next iteration, returning "end of stream". Fix: pass `total_len.map(|tl| tl.saturating_sub(resume_at))` (remaining bytes from the new position) in both branches: - `moov.is_none()` (moov-at-end layout, seek to moov offset) - `moov.is_some()` (fast-start layout, skip bounded mdat body) This caused Symphonia to fail probing moov-at-end M4A files read from local disk (hot-cache) and from in-memory buffers — every decode attempt returned "end of stream", analysis fell back to `byte_envelope_no_ebu` (no EBU R128 loudness), and rodio produced distorted audio. Also in this commit: - `resolve_playback_format_hint()` helper to resolve hint from URL, stream suffix, Content-Disposition, or byte sniff - ISO-BMFF diagnostic helpers (`isobmff_buffer_looks_complete`, `log_isobmff_buffer_diagnostic`, `mp4_suspect_zero_holes`) - Probe-fallback path for ranged-stream failures now uses these helpers to decide whether to refetch or wait for the in-flight download * chore(audio): remove redundant hint recomputation and add missing blank line `bytes_hint_for_wait` in the ranged-stream fallback path was an exact duplicate of `effective_hint` already in scope — reuse the existing binding. Also add missing blank line after `wait_for_ranged_mp4_probe_ready` in mod.rs. * chore(release): CHANGELOG and credits for PR #757 Add Fixed entry for M4A moov-at-end probe fix and credits line in settingsCredits.ts under existing cucadmuh contributions. * fix(audio): extract BuildSourceArgs to fix clippy::too_many_arguments `build_playback_source_with_probe_fallback` had 12 parameters, exceeding the clippy limit of 7. Group url/gen/hints/fade/hi-res/duration into `BuildSourceArgs` so the function signature stays at 4 arguments.
This commit is contained in:
@@ -17,6 +17,10 @@ mod ranged_http;
|
||||
mod reader;
|
||||
mod track_stream;
|
||||
|
||||
pub(crate) use mp4::{
|
||||
container_hint_is_mp4, isobmff_buffer_looks_complete, log_isobmff_buffer_diagnostic,
|
||||
mp4_needs_tail_prefetch, mp4_suspect_zero_holes,
|
||||
};
|
||||
pub(crate) use local_file::LocalFileSource;
|
||||
pub(crate) use radio::{RadioLiveState, RadioSharedFlags, radio_download_task};
|
||||
pub(crate) use ranged_http::{RangedHttpSource, ranged_download_task};
|
||||
@@ -65,5 +69,56 @@ pub(crate) fn maybe_arm_stream_playback(downloaded: u64, playback_armed: &std::s
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Held until `RangedHttpSource` has moov metadata for Symphonia probe (tail prefetch
|
||||
/// or fast-start moov in the linear prefix).
|
||||
pub(crate) struct RangedMp4ProbeGate {
|
||||
pub(crate) tail_ready: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
pub(crate) buf: std::sync::Arc<std::sync::Mutex<Vec<u8>>>,
|
||||
pub(crate) downloaded_to: std::sync::Arc<std::sync::atomic::AtomicUsize>,
|
||||
pub(crate) gen_arc: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||
pub(crate) gen: u64,
|
||||
pub(crate) format_hint: Option<String>,
|
||||
}
|
||||
|
||||
/// Block until moov is reachable: tail prefetch completed or moov already in the
|
||||
/// downloaded prefix (fast-start). Avoids Symphonia probing moov-at-end M4A before
|
||||
/// the tail range is filled (format probe failed: end of stream).
|
||||
pub(crate) async fn wait_for_ranged_mp4_probe_ready(gate: &RangedMp4ProbeGate) -> Result<(), String> {
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const PREFIX_SCAN_MIN: usize = 64 * 1024;
|
||||
let deadline = Instant::now() + Duration::from_secs(TRACK_READ_TIMEOUT_SECS);
|
||||
|
||||
loop {
|
||||
if gate.gen_arc.load(Ordering::SeqCst) != gate.gen {
|
||||
return Err("ranged-stream: superseded before moov metadata ready".into());
|
||||
}
|
||||
if gate.tail_ready.load(Ordering::Relaxed) {
|
||||
crate::app_deprintln!("[stream] ranged: moov metadata ready (tail prefetch)");
|
||||
return Ok(());
|
||||
}
|
||||
let dl = gate.downloaded_to.load(Ordering::Relaxed);
|
||||
if dl >= PREFIX_SCAN_MIN {
|
||||
let guard = gate.buf.lock().unwrap();
|
||||
let n = dl.min(guard.len());
|
||||
if !mp4::mp4_needs_tail_prefetch(&guard[..n], gate.format_hint.as_deref()) {
|
||||
crate::app_deprintln!(
|
||||
"[stream] ranged: moov metadata ready (fast-start, {} KiB prefix)",
|
||||
n / 1024
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
return Err(
|
||||
"ranged-stream: timed out waiting for moov metadata (tail prefetch)".into(),
|
||||
);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sleep interval when ring buffer is empty (prevents CPU spin).
|
||||
pub(crate) const RADIO_YIELD_MS: u64 = 2;
|
||||
|
||||
Reference in New Issue
Block a user