use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use mineral_model::{Envelope, PlaybackMediaInfo, Song, SongId};
use mineral_playback::DirectMedia;
use mineral_protocol::{
AdvanceKind, CurrentSync, PlayCursor, PlayMode, PlaybackOrigin, PlayerSync, PlayerVersions,
QueueSync, SegmentVersion,
};
use tokio::sync::watch;
use crate::gapless::PrefetchState;
use crate::playback_instance::{PlaybackInstanceId, PlaybackSlot};
pub(crate) struct StatePublisher {
tx: watch::Sender<u64>,
next: AtomicU64,
}
impl StatePublisher {
pub(crate) fn new() -> (Arc<Self>, watch::Receiver<u64>) {
let (tx, rx) = watch::channel(0_u64);
(
Arc::new(Self {
tx,
next: AtomicU64::new(0),
}),
rx,
)
}
fn publish(&self) {
let value = self.next.fetch_add(1, Ordering::Relaxed).saturating_add(1);
let _ = self.tx.send(value);
}
}
pub(crate) struct QueueSnapshot {
pub(crate) queue: Vec<Song>,
pub(crate) original_queue: Option<Vec<Song>>,
pub(crate) cursor: PlayCursor,
}
pub(crate) struct State {
pub(crate) current_song: Option<Song>,
pub(crate) current_slot: Option<PlaybackSlot>,
pub(crate) media_info: Option<PlaybackMediaInfo>,
pub(crate) direct_media: Option<DirectMedia>,
pub(crate) play_origin: Option<PlaybackOrigin>,
pub(crate) queue_context: mineral_stats::QueueContext,
pub(crate) context_overrides: rustc_hash::FxHashMap<String, mineral_stats::QueueContext>,
pub(crate) queue: Vec<Song>,
pub(crate) cursor: PlayCursor,
pub(crate) original_queue: Option<Vec<Song>>,
pub(crate) queue_undo: Option<QueueSnapshot>,
pub(crate) play_mode: PlayMode,
pub(crate) advance: Option<(SongId, AdvanceKind)>,
pub(crate) current_lyrics: Option<mineral_model::Lyrics>,
pub(crate) current_lyrics_song_id: Option<SongId>,
pub(crate) prefetch: PrefetchState,
pub(crate) prefetch_vetoed: Vec<usize>,
pub(crate) queue_version: SegmentVersion,
pub(crate) current_version: SegmentVersion,
pub(crate) publisher: Option<Arc<StatePublisher>>,
}
impl State {
pub(crate) fn empty() -> Self {
Self {
current_song: None,
current_slot: None,
media_info: None,
direct_media: None,
play_origin: None,
advance: None,
queue_context: mineral_stats::QueueContext::Unknown,
context_overrides: rustc_hash::FxHashMap::default(),
queue: Vec::new(),
cursor: PlayCursor::default(),
queue_undo: None,
original_queue: None,
play_mode: PlayMode::default(),
current_lyrics: None,
current_lyrics_song_id: None,
prefetch: PrefetchState::default(),
prefetch_vetoed: Vec::new(),
queue_version: SegmentVersion::FIRST,
current_version: SegmentVersion::FIRST,
publisher: None,
}
}
pub(crate) fn invalidate_prefetch(&mut self) {
drop(self.prefetch.invalidate());
self.prefetch_vetoed.clear();
}
pub(crate) fn take_prefetch(&mut self) -> Option<crate::gapless::Queued> {
self.prefetch_vetoed.clear();
self.prefetch.invalidate()
}
pub(crate) fn revalidate_prefetch_after_edit(&mut self) -> bool {
self.prefetch_vetoed.clear();
let armed = self.prefetch.song_id().cloned();
let Some(armed) = armed else {
return false;
};
if crate::queue::next_in_queue(self).is_some_and(|next| next.id == armed) {
return false;
}
drop(self.prefetch.invalidate());
true
}
pub(crate) fn sync(&self, known: PlayerVersions) -> PlayerSync {
let queue = (known.queue != self.queue_version).then(|| QueueSync {
queue: self.queue.clone(),
original_queue: self.original_queue.clone(),
});
let current = (known.current != self.current_version).then(|| CurrentSync {
current_song: self.current_song.clone(),
direct_media: self.direct_media.clone(),
media_info: self.media_info.clone(),
current_lyrics: self.current_lyrics.clone(),
current_lyrics_song_id: self.current_lyrics_song_id.clone(),
current_envelope: self
.current_slot
.as_ref()
.filter(|slot| {
self.current_song
.as_ref()
.is_some_and(|song| song.id == slot.song_id)
})
.and_then(|slot| slot.envelope.clone()),
advance: self.advance.as_ref().map(|(_, kind)| *kind),
});
PlayerSync {
versions: PlayerVersions {
queue: self.queue_version,
current: self.current_version,
},
cursor: self.cursor,
play_mode: self.play_mode,
play_origin: self.play_origin,
queue,
current,
}
}
pub(crate) fn bump_queue(&mut self) {
self.queue_version = self.queue_version.next();
self.notify_subscribers();
}
pub(crate) fn bump_current(&mut self) {
self.current_version = self.current_version.next();
self.notify_subscribers();
}
pub(crate) fn notify_subscribers(&self) {
if let Some(publisher) = &self.publisher {
publisher.publish();
}
}
pub(crate) fn adopt_envelope(
&mut self,
instance_id: PlaybackInstanceId,
song_id: &SongId,
envelope: Envelope,
) -> bool {
if let Some(slot) = self.current_slot.as_mut()
&& slot.matches(instance_id, song_id)
&& !slot.cancellation.is_cancelled()
{
slot.envelope = Some(envelope);
self.bump_current();
return true;
}
if let Some(slot) = self.prefetch.slot_mut()
&& slot.matches(instance_id, song_id)
&& !slot.cancellation.is_cancelled()
{
slot.envelope = Some(envelope);
return true;
}
false
}
}
#[cfg(test)]
mod tests {
use color_eyre::eyre::eyre;
use mineral_protocol::{PlayCursor, PlayMode, PlayerVersions, SegmentVersion};
use mineral_test::song;
use pretty_assertions::assert_eq;
use super::State;
fn populated() -> State {
let mut st = State::empty();
st.queue = vec![song("a"), song("b")];
st.cursor = PlayCursor::InQueue(1);
st.current_song = Some(song("b"));
st.play_mode = PlayMode::RepeatAll;
st
}
#[test]
fn sync_zero_versions_gets_full_payload() -> color_eyre::Result<()> {
let st = populated();
let sync = st.sync(PlayerVersions::default());
assert_eq!(sync.versions.queue, SegmentVersion::FIRST);
assert_eq!(sync.versions.current, SegmentVersion::FIRST);
assert_eq!(sync.cursor, PlayCursor::InQueue(1));
assert_eq!(sync.play_mode, PlayMode::RepeatAll);
let q = sync.queue.ok_or_else(|| eyre!("queue 重段应存在"))?;
assert_eq!(q.queue.len(), 2);
assert!(q.original_queue.is_none());
let c = sync.current.ok_or_else(|| eyre!("current 重段应存在"))?;
assert_eq!(c.current_song, Some(song("b")));
Ok(())
}
#[test]
fn sync_matching_versions_light_only() {
let st = populated();
let sync = st.sync(PlayerVersions {
queue: SegmentVersion::FIRST,
current: SegmentVersion::FIRST,
});
assert!(sync.queue.is_none());
assert!(sync.current.is_none());
assert_eq!(sync.cursor, PlayCursor::InQueue(1));
assert_eq!(sync.play_mode, PlayMode::RepeatAll);
}
#[test]
fn sync_stale_queue_only_sends_queue_section() {
let mut st = populated();
st.bump_queue();
let sync = st.sync(PlayerVersions {
queue: SegmentVersion::FIRST,
current: SegmentVersion::FIRST,
});
assert_eq!(sync.versions.queue, SegmentVersion::new(2));
assert!(sync.queue.is_some());
assert!(sync.current.is_none());
}
#[test]
fn sync_stale_current_only_sends_current_section() {
let mut st = populated();
st.bump_current();
let sync = st.sync(PlayerVersions {
queue: SegmentVersion::FIRST,
current: SegmentVersion::FIRST,
});
assert_eq!(sync.versions.current, SegmentVersion::new(2));
assert!(sync.queue.is_none());
assert!(sync.current.is_some());
}
}