use std::path::Path;
use mineral_model::{Song, SongId};
use mineral_playback::{DirectMedia, OpenedMedia};
use mineral_protocol::PlaybackOrigin;
use super::Queued;
use crate::playback_instance::PlaybackSlot;
use crate::player::PlayerCore;
use crate::queue::next_in_queue;
pub(crate) fn record_prefetch(
player: &PlayerCore,
song: SongId,
source: mineral_stats::PrefetchSource,
resolution: mineral_stats::PrefetchResolution,
) {
player.inner.stats.event(mineral_stats::StatsEvent::System(
mineral_stats::SystemEvent::Prefetch {
song,
source,
resolution,
},
));
}
pub(crate) fn prefetch_source(local_path: Option<&Path>) -> mineral_stats::PrefetchSource {
if local_path.is_some() {
mineral_stats::PrefetchSource::Local
} else {
mineral_stats::PrefetchSource::Remote
}
}
fn prefetch_window_open(
engine_duration_ms: Option<u64>,
metadata_duration_ms: Option<u64>,
position_ms: u64,
window_ms: u64,
) -> bool {
let Some(duration_ms) = engine_duration_ms.or(metadata_duration_ms) else {
return false;
};
duration_ms.saturating_sub(position_ms) <= window_ms
}
pub(crate) fn check_prefetch(player: &PlayerCore) {
let snap = player.audio_snapshot();
let metadata_duration_ms =
player.with_state(|st| st.current_song.as_ref().and_then(|s| s.duration_ms));
if !prefetch_window_open(
snap.duration_ms,
metadata_duration_ms,
snap.position_ms,
player.gapless_prefetch_ms(),
) {
return;
}
let next = player.with_state(|st| {
st.current_song.as_ref()?;
if st.prefetch.is_armed() {
return None;
}
let next = next_in_queue(st);
if let Some(n) = next.as_ref()
&& st
.prefetch
.song_id()
.is_some_and(|song_id| *song_id == n.id)
{
return None;
}
next
});
let Some(next) = next else {
return;
};
let slot = PlaybackSlot::new(next.id.clone());
player.with_state(|st| {
st.prefetch.replace_opening(slot.clone());
});
crate::playback::start_prefetch(player, next, slot);
}
pub(crate) fn arm_opened(
player: &PlayerCore,
song: Song,
slot: &PlaybackSlot,
opened: OpenedMedia,
direct: Option<DirectMedia>,
origin: PlaybackOrigin,
) -> bool {
let info = opened.info().clone();
let source = prefetch_source(
direct
.as_ref()
.and_then(|media| media.locator().local_path()),
);
let armed = player.with_state(|st| {
let Some(active) = st.prefetch.take_opening(slot.instance_id, &song.id) else {
return false;
};
player.audio().append_next(opened);
st.prefetch.arm(
active,
Queued {
song: song.clone(),
media_info: info.clone(),
direct_media: direct,
origin,
},
);
true
});
if !armed {
return false;
}
record_prefetch(
player,
song.id,
source,
mineral_stats::PrefetchResolution::Armed,
);
true
}
#[cfg(test)]
mod tests {
use super::prefetch_window_open;
#[test]
fn prefetch_window_prefers_engine_falls_back_to_metadata() {
assert!(
prefetch_window_open(Some(200_000), None, 195_000, 10_000),
"实测时长,窗口内应开"
);
assert!(
!prefetch_window_open(Some(200_000), None, 100_000, 10_000),
"实测时长,窗口外不开"
);
assert!(
prefetch_window_open(None, Some(200_000), 195_000, 10_000),
"实测缺失应回落元数据"
);
assert!(
!prefetch_window_open(Some(300_000), Some(200_000), 195_000, 10_000),
"两口径都有值时实测优先(顶换流元数据描述的是原源音频)"
);
assert!(
!prefetch_window_open(None, None, 195_000, 10_000),
"时长全未知不应开窗"
);
}
}