mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25: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:
@@ -414,6 +414,11 @@ impl<B: ReadBytes> AtomIterator<B> {
|
||||
&mut self.reader
|
||||
}
|
||||
|
||||
/// Exclusive end offset of the current atom in the stream.
|
||||
pub fn current_atom_end(&self) -> u64 {
|
||||
self.next_atom_pos
|
||||
}
|
||||
|
||||
pub fn next(&mut self) -> Result<Option<AtomHeader>> {
|
||||
// Ignore any remaining data in the current atom that was not read.
|
||||
let cur_pos = self.reader.pos();
|
||||
|
||||
@@ -26,6 +26,29 @@ use crate::stream::*;
|
||||
|
||||
use log::{debug, info, trace, warn};
|
||||
|
||||
/// When `mdat` claims the rest of the file (common moov-at-end M4A), locate `moov` in the
|
||||
/// tail instead of seeking to EOF (which yields end-of-stream during probe).
|
||||
fn find_moov_atom_offset_in_tail(
|
||||
mss: &mut MediaSourceStream,
|
||||
total_len: u64,
|
||||
) -> Result<Option<u64>> {
|
||||
const MAX_SCAN: u64 = 8 * 1024 * 1024;
|
||||
if total_len < 16 {
|
||||
return Ok(None);
|
||||
}
|
||||
let scan_len = MAX_SCAN.min(total_len) as usize;
|
||||
let scan_start = total_len - scan_len as u64;
|
||||
mss.seek(SeekFrom::Start(scan_start))?;
|
||||
let mut buf = vec![0u8; scan_len];
|
||||
mss.read_buf_exact(&mut buf).map_err(Error::from)?;
|
||||
for i in 0..buf.len().saturating_sub(8) {
|
||||
if &buf[i + 4..i + 8] == b"moov" {
|
||||
return Ok(Some(scan_start + i as u64));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub struct TrackState {
|
||||
codec_params: CodecParameters,
|
||||
/// The track number.
|
||||
@@ -397,6 +420,29 @@ impl FormatReader for IsoMp4Reader {
|
||||
// The remainder of the stream will be read incrementally.
|
||||
break;
|
||||
}
|
||||
let end = iter.current_atom_end();
|
||||
let file_len = total_len.unwrap_or(end);
|
||||
if moov.is_none() {
|
||||
let mut mss = iter.into_inner();
|
||||
let resume_at = if end < file_len.saturating_sub(8) {
|
||||
// Bounded mdat — `moov` is the next top-level sibling.
|
||||
end
|
||||
} else if let Some(tl) = total_len {
|
||||
match find_moov_atom_offset_in_tail(&mut mss, tl)? {
|
||||
Some(off) => off,
|
||||
None => return unsupported_error("isomp4: missing moov atom"),
|
||||
}
|
||||
} else {
|
||||
end
|
||||
};
|
||||
mss.seek(SeekFrom::Start(resume_at))?;
|
||||
iter = AtomIterator::new_root(mss, total_len);
|
||||
} else if end < file_len.saturating_sub(8) {
|
||||
// Fast-start: skip a bounded mdat without linear read.
|
||||
let mut mss = iter.into_inner();
|
||||
mss.seek(SeekFrom::Start(end))?;
|
||||
iter = AtomIterator::new_root(mss, total_len);
|
||||
}
|
||||
}
|
||||
AtomType::Meta => {
|
||||
// Read the metadata atom and append it to the log.
|
||||
|
||||
Reference in New Issue
Block a user