1use crate::background::{acquire_background_work, BackgroundWorkLease};
34use crate::host::LifecycleEvent;
35use crate::host::LifecycleState;
36use crate::registry::ServiceRegistry;
37use cranpose_core::{rememberEventStream, EventStream, State};
38use parking_lot::Mutex;
39use std::path::{Path, PathBuf};
40use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
41use std::sync::Arc;
42use std::time::Duration;
43
44pub const DUCKED_GAIN: f32 = 0.2;
49
50#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct MediaArtwork {
53 pub bytes: Arc<[u8]>,
55 pub mime: String,
58}
59
60impl MediaArtwork {
61 pub fn new(bytes: impl Into<Arc<[u8]>>, mime: impl Into<String>) -> MediaArtwork {
63 MediaArtwork {
64 bytes: bytes.into(),
65 mime: mime.into(),
66 }
67 }
68}
69
70#[derive(Clone, Debug, Default, PartialEq, Eq)]
73pub struct MediaMetadata {
74 pub title: String,
75 pub artist: String,
76 pub album: String,
77 pub duration: Option<Duration>,
81 pub artwork: Option<MediaArtwork>,
82}
83
84impl MediaMetadata {
85 pub fn titled(title: impl Into<String>) -> MediaMetadata {
87 MediaMetadata {
88 title: title.into(),
89 ..MediaMetadata::default()
90 }
91 }
92
93 pub fn artist(mut self, artist: impl Into<String>) -> MediaMetadata {
95 self.artist = artist.into();
96 self
97 }
98
99 pub fn album(mut self, album: impl Into<String>) -> MediaMetadata {
101 self.album = album.into();
102 self
103 }
104
105 pub fn duration(mut self, duration: Duration) -> MediaMetadata {
107 self.duration = Some(duration);
108 self
109 }
110
111 pub fn artwork(mut self, artwork: MediaArtwork) -> MediaMetadata {
113 self.artwork = Some(artwork);
114 self
115 }
116
117 pub fn is_empty(&self) -> bool {
119 self.title.is_empty() && self.artist.is_empty() && self.album.is_empty()
120 }
121}
122
123#[derive(Clone, Debug, Default, PartialEq, Eq)]
131pub struct MediaItem {
132 pub uri: String,
133 pub metadata: MediaMetadata,
134}
135
136impl MediaItem {
137 pub fn new(uri: impl Into<String>) -> MediaItem {
139 MediaItem {
140 uri: uri.into(),
141 metadata: MediaMetadata::default(),
142 }
143 }
144
145 pub fn with_metadata(mut self, metadata: MediaMetadata) -> MediaItem {
147 self.metadata = metadata;
148 self
149 }
150
151 pub fn display_title(&self) -> &str {
154 if !self.metadata.title.is_empty() {
155 return &self.metadata.title;
156 }
157 let path = self.uri.split(['?', '#']).next().unwrap_or(&self.uri);
158 match path.rsplit(['/', '\\']).next() {
159 Some(name) if !name.is_empty() => name,
160 _ => &self.uri,
161 }
162 }
163}
164
165#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
170pub struct MediaCapabilities {
171 pub seeking: bool,
173 pub speed: bool,
175 pub looping: bool,
177 pub analysis: bool,
179 pub session: bool,
182 pub equalizer: bool,
186 pub probing: bool,
190}
191
192impl MediaCapabilities {
193 pub const TRANSPORT: MediaCapabilities = MediaCapabilities {
196 seeking: true,
197 speed: false,
198 looping: true,
199 analysis: false,
200 session: false,
201 equalizer: false,
202 probing: false,
203 };
204}
205
206#[derive(Clone, Copy, Debug, PartialEq)]
208pub struct EqualizerBand {
209 pub center_hz: f32,
211 pub min_gain_db: f32,
213 pub max_gain_db: f32,
215}
216
217impl EqualizerBand {
218 pub fn new(center_hz: f32, range_db: f32) -> EqualizerBand {
220 let range = range_db.abs();
221 EqualizerBand {
222 center_hz,
223 min_gain_db: -range,
224 max_gain_db: range,
225 }
226 }
227
228 pub fn clamp_gain(&self, gain_db: f32) -> f32 {
230 gain_db.clamp(self.min_gain_db, self.max_gain_db)
231 }
232}
233
234pub const OCTAVE_BAND_CENTERS_HZ: [f32; 10] = [
241 31.0, 62.0, 125.0, 250.0, 500.0, 1_000.0, 2_000.0, 4_000.0, 8_000.0, 16_000.0,
242];
243
244pub fn octave_equalizer_bands(range_db: f32) -> Vec<EqualizerBand> {
246 OCTAVE_BAND_CENTERS_HZ
247 .iter()
248 .map(|center| EqualizerBand::new(*center, range_db))
249 .collect()
250}
251
252#[derive(Clone, Debug, Default, PartialEq)]
259pub struct EqualizerSettings {
260 pub enabled: bool,
263 pub preamp_db: f32,
265 pub gains_db: Vec<f32>,
267}
268
269impl EqualizerSettings {
270 pub fn flat(bands: usize) -> EqualizerSettings {
272 EqualizerSettings {
273 enabled: true,
274 preamp_db: 0.0,
275 gains_db: vec![0.0; bands],
276 }
277 }
278
279 pub fn clamped_to(&self, bands: &[EqualizerBand]) -> EqualizerSettings {
282 EqualizerSettings {
283 enabled: self.enabled,
284 preamp_db: self.preamp_db,
285 gains_db: bands
286 .iter()
287 .enumerate()
288 .map(|(index, band)| {
289 band.clamp_gain(self.gains_db.get(index).copied().unwrap_or(0.0))
290 })
291 .collect(),
292 }
293 }
294}
295
296#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
297pub enum MediaError {
298 #[error("media playback is not supported here")]
300 Unsupported,
301 #[error("cannot play {0}")]
304 UnsupportedSource(String),
305 #[error("no media item is loaded")]
307 NothingLoaded,
308 #[error("this item cannot be seeked")]
311 NotSeekable,
312 #[error("{0}")]
314 Failed(String),
315}
316
317#[derive(Clone, Debug, Default, PartialEq, Eq)]
319pub enum PlaybackState {
320 #[default]
322 Idle,
323 Loading,
327 Playing,
329 Paused,
331 Ended,
334 Failed(MediaError),
336}
337
338impl PlaybackState {
339 pub fn is_playing(&self) -> bool {
341 matches!(self, PlaybackState::Playing)
342 }
343
344 pub fn is_active(&self) -> bool {
346 matches!(
347 self,
348 PlaybackState::Loading | PlaybackState::Playing | PlaybackState::Paused
349 )
350 }
351
352 pub fn failure(&self) -> Option<&MediaError> {
354 match self {
355 PlaybackState::Failed(error) => Some(error),
356 _ => None,
357 }
358 }
359}
360
361#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
363pub struct PlaybackProgress {
364 pub position: Duration,
366 pub duration: Option<Duration>,
368 pub buffered: Duration,
371}
372
373impl PlaybackProgress {
374 pub fn new(position: Duration, duration: Duration) -> PlaybackProgress {
376 PlaybackProgress {
377 position: position.min(duration),
378 duration: Some(duration),
379 buffered: duration,
380 }
381 }
382
383 pub fn fraction(&self) -> Option<f32> {
385 let duration = self.duration?;
386 if duration.is_zero() {
387 return Some(0.0);
388 }
389 Some((self.position.as_secs_f32() / duration.as_secs_f32()).clamp(0.0, 1.0))
390 }
391
392 pub fn buffered_fraction(&self) -> Option<f32> {
394 let duration = self.duration?;
395 if duration.is_zero() {
396 return Some(0.0);
397 }
398 Some((self.buffered.as_secs_f32() / duration.as_secs_f32()).clamp(0.0, 1.0))
399 }
400}
401
402#[derive(Clone, Copy, Debug, PartialEq, Eq)]
405pub enum MediaCommand {
406 Play,
407 Pause,
408 TogglePlayPause,
410 Stop,
411 Next,
413 Previous,
415 SeekTo(Duration),
416}
417
418impl MediaCommand {
419 pub fn is_transport(self) -> bool {
425 !matches!(self, MediaCommand::Next | MediaCommand::Previous)
426 }
427}
428
429#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
431pub enum AudioFocus {
432 #[default]
434 Gained,
435 Ducked,
438 LostTransient,
441 Lost,
444}
445
446#[derive(Clone, Debug, PartialEq)]
448pub struct MediaSamples {
449 pub sample_rate: u32,
451 pub channels: u16,
453 pub samples: Arc<[f32]>,
455 pub sequence: u64,
458}
459
460impl MediaSamples {
461 pub fn new(
465 sample_rate: u32,
466 channels: u16,
467 sequence: u64,
468 samples: impl Into<Arc<[f32]>>,
469 ) -> Option<MediaSamples> {
470 let samples = samples.into();
471 if sample_rate == 0 || channels == 0 || samples.len() % channels as usize != 0 {
472 return None;
473 }
474 Some(MediaSamples {
475 sample_rate,
476 channels,
477 samples,
478 sequence,
479 })
480 }
481
482 pub fn frames(&self) -> usize {
484 self.samples.len() / self.channels.max(1) as usize
485 }
486
487 pub fn span(&self) -> Duration {
489 if self.sample_rate == 0 {
490 return Duration::ZERO;
491 }
492 Duration::from_secs_f64(self.frames() as f64 / self.sample_rate as f64)
493 }
494}
495
496pub trait MediaPlayer: Send + Sync {
509 fn capabilities(&self) -> MediaCapabilities;
512
513 fn prepare(&self, item: &MediaItem) -> Result<(), MediaError>;
519
520 fn play(&self) -> Result<(), MediaError>;
522
523 fn pause(&self);
525
526 fn stop(&self);
528
529 fn seek_to(&self, _position: Duration) -> Result<(), MediaError> {
531 Err(MediaError::NotSeekable)
532 }
533
534 fn set_volume(&self, volume: f32);
537
538 fn set_speed(&self, _speed: f32) -> bool {
541 false
542 }
543
544 fn set_looping(&self, _looping: bool) {}
546
547 fn set_analysis_enabled(&self, _enabled: bool) -> bool {
551 false
552 }
553
554 fn set_session_metadata(&self, _metadata: &MediaMetadata) {}
558
559 fn equalizer_bands(&self) -> Vec<EqualizerBand> {
567 Vec::new()
568 }
569
570 fn set_equalizer(&self, _settings: &EqualizerSettings) {}
572
573 fn probe_duration(&self, _item: &MediaItem) -> Option<Duration> {
581 None
582 }
583}
584
585pub type MediaPlayerRef = Arc<dyn MediaPlayer>;
587
588static PLATFORM_MEDIA: ServiceRegistry<dyn MediaPlayer> = ServiceRegistry::new();
589
590pub fn set_platform_media_player(player: MediaPlayerRef) {
592 PLATFORM_MEDIA.set(player);
593}
594
595pub fn clear_platform_media_player() {
597 if let Some(player) = PLATFORM_MEDIA.get() {
598 player.stop();
599 }
600 PLATFORM_MEDIA.clear();
601 STATE_OBSERVERS.clear();
602 PROGRESS_OBSERVERS.clear();
603 COMMAND_OBSERVERS.clear();
604 FOCUS_OBSERVERS.clear();
605 SAMPLE_OBSERVERS.clear();
606 *STATE.lock() = PlaybackState::Idle;
607 *PROGRESS.lock() = PlaybackProgress::default();
608 *CURRENT_ITEM.lock() = None;
609 *LATEST_SAMPLES.lock() = None;
610 *FOCUS.lock() = AudioFocus::Gained;
611 *VOLUME.lock() = 1.0;
612 PAUSED_BY_FOCUS.store(false, Ordering::Release);
613 DROPPED_SAMPLES.store(0, Ordering::Release);
614 release_background_lease();
615}
616
617pub fn media_player() -> Option<MediaPlayerRef> {
619 PLATFORM_MEDIA.get()
620}
621
622pub fn media_playback_supported() -> bool {
624 PLATFORM_MEDIA.get().is_some()
625}
626
627pub fn media_capabilities() -> MediaCapabilities {
630 media_player()
631 .map(|player| player.capabilities())
632 .unwrap_or_default()
633}
634
635static STATE: Mutex<PlaybackState> = Mutex::new(PlaybackState::Idle);
638static PROGRESS: Mutex<PlaybackProgress> = Mutex::new(PlaybackProgress {
639 position: Duration::ZERO,
640 duration: None,
641 buffered: Duration::ZERO,
642});
643static CURRENT_ITEM: Mutex<Option<MediaItem>> = Mutex::new(None);
644static LATEST_SAMPLES: Mutex<Option<MediaSamples>> = Mutex::new(None);
645static FOCUS: Mutex<AudioFocus> = Mutex::new(AudioFocus::Gained);
646static VOLUME: Mutex<f32> = Mutex::new(1.0);
648static EQUALIZER: Mutex<EqualizerSettings> = Mutex::new(EqualizerSettings {
652 enabled: false,
653 preamp_db: 0.0,
654 gains_db: Vec::new(),
655});
656static PAUSED_BY_FOCUS: AtomicBool = AtomicBool::new(false);
657
658static DROPPED_SAMPLES: AtomicU64 = AtomicU64::new(0);
662
663pub fn playback_state() -> PlaybackState {
665 STATE.lock().clone()
666}
667
668pub fn playback_progress() -> PlaybackProgress {
673 *PROGRESS.lock()
674}
675
676pub fn current_media_item() -> Option<MediaItem> {
678 CURRENT_ITEM.lock().clone()
679}
680
681pub fn latest_media_samples() -> Option<MediaSamples> {
684 LATEST_SAMPLES.lock().clone()
685}
686
687pub fn dropped_media_samples() -> u64 {
689 DROPPED_SAMPLES.load(Ordering::Acquire)
690}
691
692pub fn audio_focus() -> AudioFocus {
694 *FOCUS.lock()
695}
696
697pub fn media_volume() -> f32 {
699 *VOLUME.lock()
700}
701
702struct ObserverList<T: ?Sized> {
707 entries: Mutex<Vec<(u64, Arc<T>)>>,
708}
709
710impl<T: ?Sized> ObserverList<T> {
711 const fn new() -> ObserverList<T> {
712 ObserverList {
713 entries: Mutex::new(Vec::new()),
714 }
715 }
716
717 fn add(&self, observer: Arc<T>) -> u64 {
718 let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
719 self.entries.lock().push((id, observer));
720 id
721 }
722
723 fn remove(&self, id: u64) {
724 self.entries.lock().retain(|(entry, _)| *entry != id);
725 }
726
727 fn snapshot(&self) -> Vec<Arc<T>> {
728 self.entries
729 .lock()
730 .iter()
731 .map(|(_, observer)| Arc::clone(observer))
732 .collect()
733 }
734
735 fn clear(&self) {
736 self.entries.lock().clear();
737 }
738}
739
740static NEXT_OBSERVER: AtomicU64 = AtomicU64::new(1);
741
742type StateObserverFn = dyn Fn(PlaybackState) + Send + Sync;
743type ProgressObserverFn = dyn Fn(PlaybackProgress) + Send + Sync;
744type CommandObserverFn = dyn Fn(MediaCommand) + Send + Sync;
745type FocusObserverFn = dyn Fn(AudioFocus) + Send + Sync;
746type SampleObserverFn = dyn Fn(MediaSamples) + Send + Sync;
747
748static STATE_OBSERVERS: ObserverList<StateObserverFn> = ObserverList::new();
749static PROGRESS_OBSERVERS: ObserverList<ProgressObserverFn> = ObserverList::new();
750static COMMAND_OBSERVERS: ObserverList<CommandObserverFn> = ObserverList::new();
751static FOCUS_OBSERVERS: ObserverList<FocusObserverFn> = ObserverList::new();
752static SAMPLE_OBSERVERS: ObserverList<SampleObserverFn> = ObserverList::new();
753
754pub struct MediaObserver {
756 id: u64,
757 remove: fn(u64),
758}
759
760impl Drop for MediaObserver {
761 fn drop(&mut self) {
762 (self.remove)(self.id);
763 }
764}
765
766pub fn observe_playback_state(
770 observer: impl Fn(PlaybackState) + Send + Sync + 'static,
771) -> MediaObserver {
772 let observer: Arc<StateObserverFn> = Arc::new(observer);
773 let id = STATE_OBSERVERS.add(Arc::clone(&observer));
774 observer(playback_state());
775 MediaObserver {
776 id,
777 remove: |id| STATE_OBSERVERS.remove(id),
778 }
779}
780
781pub fn observe_playback_progress(
784 observer: impl Fn(PlaybackProgress) + Send + Sync + 'static,
785) -> MediaObserver {
786 let observer: Arc<ProgressObserverFn> = Arc::new(observer);
787 let id = PROGRESS_OBSERVERS.add(Arc::clone(&observer));
788 observer(playback_progress());
789 MediaObserver {
790 id,
791 remove: |id| PROGRESS_OBSERVERS.remove(id),
792 }
793}
794
795pub fn observe_media_commands(
797 observer: impl Fn(MediaCommand) + Send + Sync + 'static,
798) -> MediaObserver {
799 let id = COMMAND_OBSERVERS.add(Arc::new(observer));
800 MediaObserver {
801 id,
802 remove: |id| COMMAND_OBSERVERS.remove(id),
803 }
804}
805
806pub fn observe_audio_focus(observer: impl Fn(AudioFocus) + Send + Sync + 'static) -> MediaObserver {
809 let observer: Arc<FocusObserverFn> = Arc::new(observer);
810 let id = FOCUS_OBSERVERS.add(Arc::clone(&observer));
811 observer(audio_focus());
812 MediaObserver {
813 id,
814 remove: |id| FOCUS_OBSERVERS.remove(id),
815 }
816}
817
818pub fn observe_media_samples(
820 observer: impl Fn(MediaSamples) + Send + Sync + 'static,
821) -> MediaObserver {
822 let id = SAMPLE_OBSERVERS.add(Arc::new(observer));
823 MediaObserver {
824 id,
825 remove: |id| SAMPLE_OBSERVERS.remove(id),
826 }
827}
828
829pub fn publish_playback_state(state: PlaybackState) {
837 {
838 let mut current = STATE.lock();
839 if *current == state {
840 return;
841 }
842 *current = state.clone();
843 }
844 if state.is_playing() {
845 acquire_background_lease();
846 } else {
847 release_background_lease();
848 }
849 if !state.is_active() {
850 *PROGRESS.lock() = PlaybackProgress::default();
851 *LATEST_SAMPLES.lock() = None;
852 }
853 if matches!(state, PlaybackState::Idle) {
854 *CURRENT_ITEM.lock() = None;
855 DROPPED_SAMPLES.store(0, Ordering::Release);
856 }
857 for observer in STATE_OBSERVERS.snapshot() {
858 observer(state.clone());
859 }
860}
861
862pub fn publish_playback_progress(progress: PlaybackProgress) {
865 let progress = clamp_progress(progress);
866 {
867 let mut current = PROGRESS.lock();
868 if *current == progress {
869 return;
870 }
871 *current = progress;
872 }
873 for observer in PROGRESS_OBSERVERS.snapshot() {
874 observer(progress);
875 }
876}
877
878fn clamp_progress(mut progress: PlaybackProgress) -> PlaybackProgress {
879 if let Some(duration) = progress.duration {
880 progress.position = progress.position.min(duration);
881 progress.buffered = progress.buffered.min(duration);
882 }
883 progress
884}
885
886pub fn publish_media_command(command: MediaCommand) {
893 match command {
894 MediaCommand::Play => {
895 let _ = play_media();
896 }
897 MediaCommand::Pause => pause_media(),
898 MediaCommand::TogglePlayPause => toggle_media(),
899 MediaCommand::Stop => stop_media(),
900 MediaCommand::SeekTo(position) => {
901 let _ = seek_media(position);
902 }
903 MediaCommand::Next | MediaCommand::Previous => {}
904 }
905 for observer in COMMAND_OBSERVERS.snapshot() {
906 observer(command);
907 }
908}
909
910pub fn publish_audio_focus(focus: AudioFocus) {
924 {
925 let mut current = FOCUS.lock();
926 if *current == focus {
927 return;
928 }
929 *current = focus;
930 }
931 apply_volume();
932 match focus {
933 AudioFocus::Gained => {
934 if PAUSED_BY_FOCUS.swap(false, Ordering::AcqRel) {
935 let _ = play_media();
936 }
937 }
938 AudioFocus::Ducked => {}
939 AudioFocus::LostTransient => {
940 if playback_state().is_playing() {
941 PAUSED_BY_FOCUS.store(true, Ordering::Release);
942 pause_media();
943 }
944 }
945 AudioFocus::Lost => {
946 PAUSED_BY_FOCUS.store(false, Ordering::Release);
947 stop_media();
948 }
949 }
950 for observer in FOCUS_OBSERVERS.snapshot() {
951 observer(focus);
952 }
953}
954
955pub fn publish_media_samples(samples: MediaSamples) {
962 *LATEST_SAMPLES.lock() = Some(samples.clone());
963 let observers = SAMPLE_OBSERVERS.snapshot();
964 if observers.is_empty() {
965 return;
966 }
967 for observer in observers {
968 observer(samples.clone());
969 }
970}
971
972pub fn record_dropped_media_samples() {
974 DROPPED_SAMPLES.fetch_add(1, Ordering::AcqRel);
975}
976
977pub fn open_media(item: MediaItem) -> Result<(), MediaError> {
987 let Some(player) = media_player() else {
988 publish_playback_state(PlaybackState::Failed(MediaError::Unsupported));
989 return Err(MediaError::Unsupported);
990 };
991 PAUSED_BY_FOCUS.store(false, Ordering::Release);
992 DROPPED_SAMPLES.store(0, Ordering::Release);
993 *CURRENT_ITEM.lock() = Some(item.clone());
994 publish_playback_progress(PlaybackProgress {
995 position: Duration::ZERO,
996 duration: item.metadata.duration,
997 buffered: Duration::ZERO,
998 });
999 publish_playback_state(PlaybackState::Loading);
1000 if player.capabilities().session {
1001 player.set_session_metadata(&item.metadata);
1002 }
1003 player.prepare(&item).inspect_err(|error| {
1004 publish_playback_state(PlaybackState::Failed(error.clone()));
1005 })
1006}
1007
1008pub fn play_media() -> Result<(), MediaError> {
1010 let Some(player) = media_player() else {
1011 return Err(MediaError::Unsupported);
1012 };
1013 if CURRENT_ITEM.lock().is_none() {
1014 return Err(MediaError::NothingLoaded);
1015 }
1016 player.play().inspect_err(|error| {
1017 publish_playback_state(PlaybackState::Failed(error.clone()));
1018 })
1019}
1020
1021pub fn pause_media() {
1023 if let Some(player) = media_player() {
1024 player.pause();
1025 }
1026}
1027
1028pub fn stop_media() {
1030 PAUSED_BY_FOCUS.store(false, Ordering::Release);
1031 if let Some(player) = media_player() {
1032 player.stop();
1033 }
1034 publish_playback_state(PlaybackState::Idle);
1035}
1036
1037pub fn toggle_media() {
1040 if playback_state().is_playing() {
1041 pause_media();
1042 } else {
1043 let _ = play_media();
1044 }
1045}
1046
1047pub fn seek_media(position: Duration) -> Result<(), MediaError> {
1053 let Some(player) = media_player() else {
1054 return Err(MediaError::Unsupported);
1055 };
1056 if CURRENT_ITEM.lock().is_none() {
1057 return Err(MediaError::NothingLoaded);
1058 }
1059 if !player.capabilities().seeking {
1060 return Err(MediaError::NotSeekable);
1061 }
1062 let position = match playback_progress().duration {
1063 Some(duration) => position.min(duration),
1064 None => position,
1065 };
1066 player.seek_to(position)
1067}
1068
1069pub fn seek_media_fraction(fraction: f32) -> Result<(), MediaError> {
1074 let Some(duration) = playback_progress().duration else {
1075 return Err(MediaError::NotSeekable);
1076 };
1077 let fraction = fraction.clamp(0.0, 1.0) as f64;
1078 seek_media(Duration::from_secs_f64(duration.as_secs_f64() * fraction))
1079}
1080
1081pub fn set_media_volume(volume: f32) {
1087 *VOLUME.lock() = volume.clamp(0.0, 1.0);
1088 apply_volume();
1089}
1090
1091fn apply_volume() {
1092 let Some(player) = media_player() else {
1093 return;
1094 };
1095 let gain = match audio_focus() {
1096 AudioFocus::Ducked => DUCKED_GAIN,
1097 _ => 1.0,
1098 };
1099 player.set_volume(media_volume() * gain);
1100}
1101
1102pub fn set_media_speed(speed: f32) -> bool {
1105 match media_player() {
1106 Some(player) if player.capabilities().speed => player.set_speed(speed),
1107 _ => false,
1108 }
1109}
1110
1111pub fn set_media_looping(looping: bool) {
1113 if let Some(player) = media_player() {
1114 player.set_looping(looping);
1115 }
1116}
1117
1118pub fn set_media_analysis_enabled(enabled: bool) -> bool {
1124 match media_player() {
1125 Some(player) if player.capabilities().analysis => {
1126 if !enabled {
1127 *LATEST_SAMPLES.lock() = None;
1128 }
1129 player.set_analysis_enabled(enabled)
1130 }
1131 _ => false,
1132 }
1133}
1134
1135pub fn probe_media_duration(item: &MediaItem) -> Option<Duration> {
1140 media_player()?.probe_duration(item)
1141}
1142
1143pub fn media_equalizer_bands() -> Vec<EqualizerBand> {
1148 match media_player() {
1149 Some(player) if player.capabilities().equalizer => player.equalizer_bands(),
1150 _ => Vec::new(),
1151 }
1152}
1153
1154pub fn media_equalizer() -> EqualizerSettings {
1156 EQUALIZER.lock().clone()
1157}
1158
1159pub fn set_media_equalizer(settings: EqualizerSettings) -> bool {
1166 *EQUALIZER.lock() = settings.clone();
1167 let Some(player) = media_player() else {
1168 return false;
1169 };
1170 if !player.capabilities().equalizer {
1171 return false;
1172 }
1173 player.set_equalizer(&settings.clamped_to(&player.equalizer_bands()));
1174 true
1175}
1176
1177pub fn set_media_metadata(metadata: MediaMetadata) {
1181 {
1182 let mut item = CURRENT_ITEM.lock();
1183 let Some(item) = item.as_mut() else {
1184 return;
1185 };
1186 item.metadata = metadata.clone();
1187 }
1188 if let Some(player) = media_player() {
1189 if player.capabilities().session {
1190 player.set_session_metadata(&metadata);
1191 }
1192 }
1193}
1194
1195static BACKGROUND_LEASE: Mutex<Option<BackgroundWorkLease>> = Mutex::new(None);
1198
1199fn acquire_background_lease() {
1200 let mut lease = BACKGROUND_LEASE.lock();
1201 if lease.is_none() {
1202 *lease = Some(acquire_background_work());
1203 }
1204}
1205
1206fn release_background_lease() {
1207 BACKGROUND_LEASE.lock().take();
1208}
1209
1210#[cfg(test)]
1216fn holds_background_work() -> bool {
1217 BACKGROUND_LEASE.lock().is_some()
1218}
1219
1220pub(crate) fn on_lifecycle(event: LifecycleEvent) {
1227 if event.to == LifecycleState::Destroyed {
1228 stop_media();
1229 }
1230}
1231
1232pub fn uri_for_path(path: &Path) -> String {
1241 let text = path.to_string_lossy();
1242 let mut uri = String::with_capacity(text.len() + 8);
1243 uri.push_str("file://");
1244 if !text.starts_with('/') {
1245 uri.push('/');
1248 }
1249 for byte in text.bytes() {
1250 match byte {
1251 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' | b'/' => {
1252 uri.push(byte as char);
1253 }
1254 b'\\' => uri.push('/'),
1255 _ => uri.push_str(&format!("%{byte:02X}")),
1256 }
1257 }
1258 uri
1259}
1260
1261pub fn path_from_uri(uri: &str) -> Option<PathBuf> {
1267 let rest = match uri.split_once("://") {
1268 Some(("file", rest)) => rest,
1269 Some(_) => return None,
1270 None => return non_empty_path(uri),
1271 };
1272 let path = rest.strip_prefix('/')?;
1275 let decoded = percent_decode(path)?;
1276 if decoded.starts_with('/') || decoded.is_empty() {
1277 return non_empty_path(&decoded);
1278 }
1279 if decoded.as_bytes().get(1) == Some(&b':') {
1282 non_empty_path(&decoded)
1283 } else {
1284 non_empty_path(&format!("/{decoded}"))
1285 }
1286}
1287
1288fn non_empty_path(text: &str) -> Option<PathBuf> {
1289 if text.is_empty() {
1290 return None;
1291 }
1292 Some(PathBuf::from(text))
1293}
1294
1295fn percent_decode(text: &str) -> Option<String> {
1296 if !text.contains('%') {
1297 return Some(text.to_string());
1298 }
1299 let bytes = text.as_bytes();
1300 let mut out = Vec::with_capacity(bytes.len());
1301 let mut index = 0;
1302 while index < bytes.len() {
1303 if bytes[index] == b'%' {
1304 let hex = text.get(index + 1..index + 3)?;
1305 out.push(u8::from_str_radix(hex, 16).ok()?);
1306 index += 3;
1307 } else {
1308 out.push(bytes[index]);
1309 index += 1;
1310 }
1311 }
1312 String::from_utf8(out).ok()
1313}
1314
1315#[allow(non_snake_case)]
1320pub fn rememberPlaybackState() -> State<PlaybackState> {
1321 let updates = rememberEventStream((), |sender| {
1322 observe_playback_state(move |state| sender.send(state))
1323 });
1324 cranpose_core::collectAsState(updates, (), playback_state())
1325}
1326
1327#[allow(non_snake_case)]
1334pub fn rememberPlaybackProgress() -> State<PlaybackProgress> {
1335 let updates = rememberEventStream((), |sender| {
1336 observe_playback_progress(move |progress| sender.send(progress))
1337 });
1338 cranpose_core::collectAsState(updates, (), playback_progress())
1339}
1340
1341#[allow(non_snake_case)]
1344pub fn rememberAudioFocus() -> State<AudioFocus> {
1345 let updates = rememberEventStream((), |sender| {
1346 observe_audio_focus(move |focus| sender.send(focus))
1347 });
1348 cranpose_core::collectAsState(updates, (), audio_focus())
1349}
1350
1351#[allow(non_snake_case)]
1358pub fn rememberMediaCommands() -> EventStream<MediaCommand> {
1359 rememberEventStream((), |sender| {
1360 observe_media_commands(move |command| sender.send(command))
1361 })
1362}
1363
1364#[allow(non_snake_case)]
1369pub fn rememberMediaSamples() -> EventStream<MediaSamples> {
1370 rememberEventStream((), |sender| {
1371 observe_media_samples(move |samples| sender.send(samples))
1372 })
1373}
1374
1375#[cfg(test)]
1376mod tests {
1377 use super::*;
1378 use crate::registry::test_service_guard;
1379
1380 struct FakePlayer {
1383 capabilities: MediaCapabilities,
1384 calls: Mutex<Vec<String>>,
1385 volume: Mutex<f32>,
1386 prepare_fails: bool,
1387 }
1388
1389 impl FakePlayer {
1390 fn new() -> Arc<FakePlayer> {
1391 Arc::new(FakePlayer {
1392 capabilities: MediaCapabilities {
1393 seeking: true,
1394 speed: true,
1395 looping: true,
1396 analysis: true,
1397 session: true,
1398 equalizer: true,
1399 probing: true,
1400 },
1401 calls: Mutex::new(Vec::new()),
1402 volume: Mutex::new(1.0),
1403 prepare_fails: false,
1404 })
1405 }
1406
1407 fn with(capabilities: MediaCapabilities) -> Arc<FakePlayer> {
1408 Arc::new(FakePlayer {
1409 capabilities,
1410 calls: Mutex::new(Vec::new()),
1411 volume: Mutex::new(1.0),
1412 prepare_fails: false,
1413 })
1414 }
1415
1416 fn failing() -> Arc<FakePlayer> {
1417 Arc::new(FakePlayer {
1418 capabilities: MediaCapabilities::TRANSPORT,
1419 calls: Mutex::new(Vec::new()),
1420 volume: Mutex::new(1.0),
1421 prepare_fails: true,
1422 })
1423 }
1424
1425 fn note(&self, call: impl Into<String>) {
1426 self.calls.lock().push(call.into());
1427 }
1428
1429 fn calls(&self) -> Vec<String> {
1430 self.calls.lock().clone()
1431 }
1432 }
1433
1434 impl MediaPlayer for FakePlayer {
1435 fn capabilities(&self) -> MediaCapabilities {
1436 self.capabilities
1437 }
1438
1439 fn prepare(&self, item: &MediaItem) -> Result<(), MediaError> {
1440 self.note(format!("prepare {}", item.uri));
1441 if self.prepare_fails {
1442 return Err(MediaError::UnsupportedSource(item.uri.clone()));
1443 }
1444 publish_playback_state(PlaybackState::Paused);
1445 Ok(())
1446 }
1447
1448 fn play(&self) -> Result<(), MediaError> {
1449 self.note("play");
1450 publish_playback_state(PlaybackState::Playing);
1451 Ok(())
1452 }
1453
1454 fn pause(&self) {
1455 self.note("pause");
1456 publish_playback_state(PlaybackState::Paused);
1457 }
1458
1459 fn stop(&self) {
1460 self.note("stop");
1461 }
1462
1463 fn seek_to(&self, position: Duration) -> Result<(), MediaError> {
1464 self.note(format!("seek {}", position.as_millis()));
1465 Ok(())
1466 }
1467
1468 fn set_volume(&self, volume: f32) {
1469 *self.volume.lock() = volume;
1470 }
1471
1472 fn set_speed(&self, speed: f32) -> bool {
1473 self.note(format!("speed {speed}"));
1474 true
1475 }
1476
1477 fn set_looping(&self, looping: bool) {
1478 self.note(format!("looping {looping}"));
1479 }
1480
1481 fn set_analysis_enabled(&self, enabled: bool) -> bool {
1482 self.note(format!("analysis {enabled}"));
1483 true
1484 }
1485
1486 fn set_session_metadata(&self, metadata: &MediaMetadata) {
1487 self.note(format!("session {}", metadata.title));
1488 }
1489
1490 fn equalizer_bands(&self) -> Vec<EqualizerBand> {
1491 vec![
1494 EqualizerBand::new(60.0, 6.0),
1495 EqualizerBand::new(1_000.0, 6.0),
1496 EqualizerBand::new(10_000.0, 6.0),
1497 ]
1498 }
1499
1500 fn set_equalizer(&self, settings: &EqualizerSettings) {
1501 self.note(format!(
1502 "equalizer {} preamp {} gains {:?}",
1503 settings.enabled, settings.preamp_db, settings.gains_db
1504 ));
1505 }
1506 }
1507
1508 fn install() -> (crate::registry::TestServiceGuard, Arc<FakePlayer>) {
1509 let guard = test_service_guard();
1510 clear_platform_media_player();
1511 let player = FakePlayer::new();
1512 set_platform_media_player(player.clone());
1513 (guard, player)
1514 }
1515
1516 fn track() -> MediaItem {
1517 MediaItem::new("file:///music/track.flac").with_metadata(
1518 MediaMetadata::titled("Track")
1519 .artist("Artist")
1520 .duration(Duration::from_secs(200)),
1521 )
1522 }
1523
1524 #[test]
1525 fn metadata_carries_everything_a_lock_screen_shows() {
1526 let artwork = MediaArtwork {
1527 bytes: vec![1, 2, 3].into(),
1528 mime: "image/png".to_string(),
1529 };
1530 let metadata = MediaMetadata::titled("Song")
1531 .artist("Band")
1532 .album("Record")
1533 .duration(Duration::from_secs(210))
1534 .artwork(artwork.clone());
1535
1536 assert_eq!(metadata.album, "Record");
1537 assert_eq!(metadata.artwork.as_ref(), Some(&artwork));
1538 assert!(!metadata.is_empty());
1539 assert!(!MediaMetadata::default().album("Record").is_empty());
1541 assert!(MediaMetadata::default().is_empty());
1542 }
1543
1544 #[test]
1545 fn a_band_reports_what_it_can_actually_do() {
1546 let band = EqualizerBand::new(1_000.0, 6.0);
1547 assert_eq!(band.clamp_gain(0.0), 0.0);
1548 assert_eq!(band.clamp_gain(6.0), 6.0);
1549 assert_eq!(band.clamp_gain(7.5), 6.0);
1550 assert_eq!(band.clamp_gain(-7.5), -6.0);
1551 }
1552
1553 #[test]
1554 fn an_equalizer_setting_is_clamped_to_the_bands_the_backend_has() {
1555 let bands = vec![
1556 EqualizerBand::new(60.0, 6.0),
1557 EqualizerBand::new(1_000.0, 6.0),
1558 ];
1559 let asked = EqualizerSettings {
1560 enabled: true,
1561 preamp_db: -3.0,
1562 gains_db: vec![12.0, -12.0, 4.0],
1564 };
1565 let applied = asked.clamped_to(&bands);
1566 assert_eq!(applied.gains_db, vec![6.0, -6.0]);
1567 assert_eq!(applied.preamp_db, -3.0);
1568 assert!(applied.enabled);
1569 }
1570
1571 #[test]
1572 fn a_setting_shorter_than_the_bands_leaves_the_rest_flat() {
1573 let bands = octave_equalizer_bands(12.0);
1574 let applied = EqualizerSettings {
1575 enabled: true,
1576 preamp_db: 0.0,
1577 gains_db: vec![3.0],
1578 }
1579 .clamped_to(&bands);
1580 assert_eq!(applied.gains_db.len(), bands.len());
1581 assert_eq!(applied.gains_db[0], 3.0);
1582 assert!(applied.gains_db[1..].iter().all(|gain| *gain == 0.0));
1583 }
1584
1585 #[test]
1586 fn a_curve_reaches_the_backend_clamped_to_its_own_bands() {
1587 let (_guard, player) = install();
1588
1589 assert_eq!(media_equalizer_bands().len(), 3);
1590 assert!(set_media_equalizer(EqualizerSettings {
1591 enabled: true,
1592 preamp_db: -2.0,
1593 gains_db: vec![9.0, 0.0, -9.0],
1594 }));
1595
1596 assert!(
1597 player
1598 .calls()
1599 .iter()
1600 .any(|call| call == "equalizer true preamp -2 gains [6.0, 0.0, -6.0]"),
1601 "the backend was not given the clamped curve: {:?}",
1602 player.calls()
1603 );
1604 }
1605
1606 #[test]
1607 fn a_curve_is_remembered_even_where_nothing_can_apply_it() {
1608 let _guard = test_service_guard();
1609 clear_platform_media_player();
1610
1611 let asked = EqualizerSettings {
1612 enabled: true,
1613 preamp_db: -1.0,
1614 gains_db: vec![4.0, -4.0],
1615 };
1616 assert!(!set_media_equalizer(asked.clone()));
1619 assert_eq!(media_equalizer(), asked);
1620 assert!(media_equalizer_bands().is_empty());
1621 }
1622
1623 #[test]
1624 fn a_backend_without_an_equalizer_says_so_rather_than_pretending() {
1625 let _guard = test_service_guard();
1626 clear_platform_media_player();
1627 set_platform_media_player(FakePlayer::with(MediaCapabilities {
1628 equalizer: false,
1629 ..MediaCapabilities::TRANSPORT
1630 }));
1631
1632 assert!(media_equalizer_bands().is_empty());
1633 assert!(!set_media_equalizer(EqualizerSettings::flat(10)));
1634 }
1635
1636 #[test]
1637 fn an_item_falls_back_to_its_file_name_for_a_title() {
1638 assert_eq!(
1639 MediaItem::new("file:///music/03 - Song.mp3").display_title(),
1640 "03 - Song.mp3"
1641 );
1642 assert_eq!(
1643 MediaItem::new("https://host/stream?token=1").display_title(),
1644 "stream"
1645 );
1646 assert_eq!(track().display_title(), "Track");
1647 }
1648
1649 #[test]
1650 fn progress_reports_fractions_only_for_items_that_have_a_length() {
1651 let known = PlaybackProgress::new(Duration::from_secs(30), Duration::from_secs(120));
1652 assert_eq!(known.fraction(), Some(0.25));
1653 assert_eq!(known.buffered_fraction(), Some(1.0));
1654
1655 let live = PlaybackProgress {
1656 position: Duration::from_secs(30),
1657 duration: None,
1658 buffered: Duration::from_secs(35),
1659 };
1660 assert_eq!(live.fraction(), None);
1661 assert_eq!(live.buffered_fraction(), None);
1662 }
1663
1664 #[test]
1665 fn progress_never_reads_past_the_end_of_the_item() {
1666 let progress = PlaybackProgress::new(Duration::from_secs(500), Duration::from_secs(120));
1667 assert_eq!(progress.position, Duration::from_secs(120));
1668 assert_eq!(progress.fraction(), Some(1.0));
1669 }
1670
1671 #[test]
1672 fn samples_reject_a_layout_that_does_not_describe_the_data() {
1673 assert!(MediaSamples::new(44_100, 2, 0, vec![0.0; 3]).is_none());
1674 assert!(MediaSamples::new(0, 2, 0, vec![0.0; 4]).is_none());
1675 assert!(MediaSamples::new(44_100, 0, 0, vec![0.0; 4]).is_none());
1676
1677 let block = MediaSamples::new(44_100, 2, 7, vec![0.0; 4410]).expect("well-formed block");
1678 assert_eq!(block.frames(), 2205);
1679 assert_eq!(block.span(), Duration::from_millis(50));
1680 assert_eq!(block.sequence, 7);
1681 }
1682
1683 #[test]
1684 fn without_a_backend_every_call_reports_that_it_is_unsupported() {
1685 let _guard = test_service_guard();
1686 clear_platform_media_player();
1687
1688 assert!(!media_playback_supported());
1689 assert_eq!(media_capabilities(), MediaCapabilities::default());
1690 assert_eq!(open_media(track()), Err(MediaError::Unsupported));
1691 assert_eq!(
1692 playback_state(),
1693 PlaybackState::Failed(MediaError::Unsupported)
1694 );
1695 assert_eq!(play_media(), Err(MediaError::Unsupported));
1696 assert_eq!(seek_media(Duration::ZERO), Err(MediaError::Unsupported));
1697 assert!(!set_media_speed(2.0));
1698 assert!(!set_media_analysis_enabled(true));
1699 }
1700
1701 #[test]
1702 fn opening_an_item_shows_the_wait_before_the_backend_is_asked() {
1703 let (_guard, player) = install();
1704 let seen = Arc::new(Mutex::new(Vec::new()));
1705 let recorder = Arc::clone(&seen);
1706 let _observer = observe_playback_state(move |state| recorder.lock().push(state));
1707
1708 open_media(track()).expect("the fake backend opens anything");
1709
1710 assert_eq!(
1711 *seen.lock(),
1712 vec![
1713 PlaybackState::Idle,
1714 PlaybackState::Loading,
1715 PlaybackState::Paused,
1716 ]
1717 );
1718 assert_eq!(
1719 player.calls(),
1720 vec!["session Track", "prepare file:///music/track.flac"]
1721 );
1722 assert_eq!(current_media_item().map(|item| item.uri), Some(track().uri));
1723 }
1724
1725 #[test]
1726 fn an_item_that_cannot_be_opened_publishes_the_failure() {
1727 let _guard = test_service_guard();
1728 clear_platform_media_player();
1729 set_platform_media_player(FakePlayer::failing());
1730
1731 let error = open_media(track()).expect_err("the failing backend refuses");
1732 assert_eq!(
1733 error,
1734 MediaError::UnsupportedSource("file:///music/track.flac".to_string())
1735 );
1736 assert_eq!(playback_state().failure(), Some(&error));
1737 }
1738
1739 #[test]
1740 fn the_transport_routes_to_the_backend_and_publishes_what_it_did() {
1741 let (_guard, player) = install();
1742
1743 open_media(track()).expect("opens");
1744 play_media().expect("plays");
1745 assert!(playback_state().is_playing());
1746
1747 toggle_media();
1748 assert_eq!(playback_state(), PlaybackState::Paused);
1749
1750 toggle_media();
1751 assert!(playback_state().is_playing());
1752
1753 stop_media();
1754 assert_eq!(playback_state(), PlaybackState::Idle);
1755 assert_eq!(current_media_item(), None);
1756
1757 assert_eq!(
1758 player.calls(),
1759 vec![
1760 "session Track",
1761 "prepare file:///music/track.flac",
1762 "play",
1763 "pause",
1764 "play",
1765 "stop",
1766 ]
1767 );
1768 }
1769
1770 #[test]
1771 fn playing_nothing_reports_that_nothing_is_loaded() {
1772 let (_guard, _player) = install();
1773
1774 assert_eq!(play_media(), Err(MediaError::NothingLoaded));
1775 assert_eq!(
1776 seek_media(Duration::from_secs(1)),
1777 Err(MediaError::NothingLoaded)
1778 );
1779 }
1780
1781 #[test]
1782 fn a_seek_is_clamped_to_the_item_rather_than_to_each_backend() {
1783 let (_guard, player) = install();
1784 open_media(track()).expect("opens");
1785
1786 seek_media(Duration::from_secs(1_000)).expect("seeks");
1787
1788 assert!(player.calls().contains(&"seek 200000".to_string()));
1789 }
1790
1791 #[test]
1792 fn a_seek_bar_fraction_maps_onto_the_item() {
1793 let (_guard, player) = install();
1794 open_media(track()).expect("opens");
1795
1796 seek_media_fraction(0.25).expect("seeks");
1797 seek_media_fraction(3.0).expect("clamps rather than refusing");
1798
1799 let calls = player.calls();
1800 assert!(calls.contains(&"seek 50000".to_string()));
1801 assert!(calls.contains(&"seek 200000".to_string()));
1802 }
1803
1804 #[test]
1805 fn a_stream_with_no_length_has_no_seek_bar_fraction() {
1806 let (_guard, _player) = install();
1807 open_media(MediaItem::new("https://host/live")).expect("opens");
1808
1809 assert_eq!(seek_media_fraction(0.5), Err(MediaError::NotSeekable));
1810 }
1811
1812 #[test]
1813 fn a_backend_that_cannot_seek_says_so_instead_of_moving_nothing() {
1814 let _guard = test_service_guard();
1815 clear_platform_media_player();
1816 set_platform_media_player(FakePlayer::with(MediaCapabilities {
1817 seeking: false,
1818 ..MediaCapabilities::TRANSPORT
1819 }));
1820
1821 open_media(track()).expect("opens");
1822 assert_eq!(
1823 seek_media(Duration::from_secs(1)),
1824 Err(MediaError::NotSeekable)
1825 );
1826 }
1827
1828 #[test]
1829 fn what_reaches_the_device_is_the_volume_combined_with_the_focus_gain() {
1830 let (_guard, player) = install();
1831 open_media(track()).expect("opens");
1832
1833 set_media_volume(0.5);
1834 assert_eq!(*player.volume.lock(), 0.5);
1835 assert_eq!(media_volume(), 0.5);
1836
1837 publish_audio_focus(AudioFocus::Ducked);
1838 assert_eq!(*player.volume.lock(), 0.5 * DUCKED_GAIN);
1839
1840 set_media_volume(1.0);
1843 assert_eq!(*player.volume.lock(), DUCKED_GAIN);
1844
1845 publish_audio_focus(AudioFocus::Gained);
1846 assert_eq!(*player.volume.lock(), 1.0);
1847 }
1848
1849 #[test]
1850 fn a_volume_outside_the_range_is_brought_back_into_it() {
1851 let (_guard, player) = install();
1852
1853 set_media_volume(4.0);
1854 assert_eq!(media_volume(), 1.0);
1855 set_media_volume(-1.0);
1856 assert_eq!(media_volume(), 0.0);
1857 assert_eq!(*player.volume.lock(), 0.0);
1858 }
1859
1860 #[test]
1861 fn a_transient_loss_pauses_and_the_next_gain_resumes() {
1862 let (_guard, _player) = install();
1863 open_media(track()).expect("opens");
1864 play_media().expect("plays");
1865
1866 publish_audio_focus(AudioFocus::LostTransient);
1867 assert_eq!(playback_state(), PlaybackState::Paused);
1868
1869 publish_audio_focus(AudioFocus::Gained);
1870 assert!(playback_state().is_playing());
1871 }
1872
1873 #[test]
1874 fn regaining_focus_does_not_resume_what_the_user_paused() {
1875 let (_guard, _player) = install();
1876 open_media(track()).expect("opens");
1877 play_media().expect("plays");
1878 pause_media();
1879
1880 publish_audio_focus(AudioFocus::LostTransient);
1881 publish_audio_focus(AudioFocus::Gained);
1882
1883 assert_eq!(playback_state(), PlaybackState::Paused);
1884 }
1885
1886 #[test]
1887 fn focus_lost_for_good_stops_and_does_not_come_back() {
1888 let (_guard, _player) = install();
1889 open_media(track()).expect("opens");
1890 play_media().expect("plays");
1891
1892 publish_audio_focus(AudioFocus::Lost);
1893 assert_eq!(playback_state(), PlaybackState::Idle);
1894
1895 publish_audio_focus(AudioFocus::Gained);
1896 assert_eq!(playback_state(), PlaybackState::Idle);
1897 }
1898
1899 #[test]
1900 fn session_commands_drive_the_transport_and_still_reach_the_application() {
1901 let (_guard, player) = install();
1902 open_media(track()).expect("opens");
1903 let seen = Arc::new(Mutex::new(Vec::new()));
1904 let recorder = Arc::clone(&seen);
1905 let _observer = observe_media_commands(move |command| recorder.lock().push(command));
1906
1907 publish_media_command(MediaCommand::Play);
1908 assert!(playback_state().is_playing());
1909 publish_media_command(MediaCommand::TogglePlayPause);
1910 assert_eq!(playback_state(), PlaybackState::Paused);
1911 publish_media_command(MediaCommand::SeekTo(Duration::from_secs(10)));
1912 publish_media_command(MediaCommand::Next);
1913
1914 assert_eq!(
1915 *seen.lock(),
1916 vec![
1917 MediaCommand::Play,
1918 MediaCommand::TogglePlayPause,
1919 MediaCommand::SeekTo(Duration::from_secs(10)),
1920 MediaCommand::Next,
1921 ]
1922 );
1923 assert!(player.calls().contains(&"seek 10000".to_string()));
1926 assert_eq!(playback_state(), PlaybackState::Paused);
1927 }
1928
1929 #[test]
1930 fn next_and_previous_are_the_commands_the_framework_leaves_alone() {
1931 assert!(MediaCommand::Play.is_transport());
1932 assert!(MediaCommand::SeekTo(Duration::ZERO).is_transport());
1933 assert!(!MediaCommand::Next.is_transport());
1934 assert!(!MediaCommand::Previous.is_transport());
1935 }
1936
1937 #[test]
1938 fn analysis_is_off_until_it_is_asked_for_and_only_where_it_exists() {
1939 let (_guard, player) = install();
1940 assert!(set_media_analysis_enabled(true));
1941 assert!(player.calls().contains(&"analysis true".to_string()));
1942
1943 clear_platform_media_player();
1944 set_platform_media_player(FakePlayer::with(MediaCapabilities::TRANSPORT));
1945 assert!(!set_media_analysis_enabled(true));
1946 }
1947
1948 #[test]
1949 fn the_newest_sample_block_replaces_the_stored_one() {
1950 let (_guard, _player) = install();
1951 let first = MediaSamples::new(48_000, 1, 1, vec![0.25; 8]).expect("block");
1952 let second = MediaSamples::new(48_000, 1, 2, vec![0.5; 8]).expect("block");
1953
1954 publish_media_samples(first);
1955 publish_media_samples(second.clone());
1956
1957 assert_eq!(latest_media_samples(), Some(second));
1958 record_dropped_media_samples();
1959 record_dropped_media_samples();
1960 assert_eq!(dropped_media_samples(), 2);
1961 }
1962
1963 #[test]
1964 fn turning_analysis_off_forgets_the_last_block() {
1965 let (_guard, _player) = install();
1966 publish_media_samples(MediaSamples::new(48_000, 1, 1, vec![0.25; 8]).expect("block"));
1967
1968 assert!(set_media_analysis_enabled(false));
1969
1970 assert_eq!(latest_media_samples(), None);
1971 }
1972
1973 #[test]
1974 fn observers_stop_being_called_once_they_are_dropped() {
1975 let (_guard, _player) = install();
1976 let seen = Arc::new(Mutex::new(0usize));
1977 let recorder = Arc::clone(&seen);
1978 let observer = observe_playback_progress(move |_| *recorder.lock() += 1);
1979
1980 publish_playback_progress(PlaybackProgress::new(
1981 Duration::from_secs(1),
1982 Duration::from_secs(10),
1983 ));
1984 let delivered = *seen.lock();
1985 drop(observer);
1986 publish_playback_progress(PlaybackProgress::new(
1987 Duration::from_secs(2),
1988 Duration::from_secs(10),
1989 ));
1990
1991 assert_eq!(*seen.lock(), delivered);
1992 }
1993
1994 #[test]
1995 fn published_progress_never_reads_past_the_end() {
1996 let (_guard, _player) = install();
1997 publish_playback_progress(PlaybackProgress {
1998 position: Duration::from_secs(99),
1999 duration: Some(Duration::from_secs(10)),
2000 buffered: Duration::from_secs(99),
2001 });
2002
2003 let progress = playback_progress();
2004 assert_eq!(progress.position, Duration::from_secs(10));
2005 assert_eq!(progress.buffered, Duration::from_secs(10));
2006 }
2007
2008 #[test]
2009 fn playing_holds_the_runtime_awake_and_stopping_lets_it_sleep() {
2010 let (_guard, _player) = install();
2011 assert!(!holds_background_work());
2012
2013 open_media(track()).expect("opens");
2014 assert!(
2015 !holds_background_work(),
2016 "an item that is open but not playing is not work the runtime must keep turning for"
2017 );
2018
2019 play_media().expect("plays");
2020 assert!(holds_background_work());
2021
2022 pause_media();
2023 assert!(!holds_background_work());
2024
2025 play_media().expect("plays");
2026 assert!(holds_background_work());
2027 stop_media();
2028 assert!(!holds_background_work());
2029 }
2030
2031 #[test]
2032 fn a_destroyed_host_stops_playback_but_a_backgrounded_one_does_not() {
2033 let (_guard, _player) = install();
2034 open_media(track()).expect("opens");
2035 play_media().expect("plays");
2036
2037 on_lifecycle(LifecycleEvent {
2038 from: LifecycleState::Resumed,
2039 to: LifecycleState::Stopped,
2040 });
2041 assert!(playback_state().is_playing());
2042
2043 on_lifecycle(LifecycleEvent {
2044 from: LifecycleState::Stopped,
2045 to: LifecycleState::Destroyed,
2046 });
2047 assert_eq!(playback_state(), PlaybackState::Idle);
2048 }
2049
2050 #[test]
2051 fn metadata_learned_after_playback_started_reaches_the_session() {
2052 let (_guard, player) = install();
2053 open_media(MediaItem::new("file:///music/untagged.mp3")).expect("opens");
2054
2055 set_media_metadata(MediaMetadata::titled("Late Tag").artist("Artist"));
2056
2057 assert!(player.calls().contains(&"session Late Tag".to_string()));
2058 assert_eq!(
2059 current_media_item().map(|item| item.metadata.title),
2060 Some("Late Tag".to_string())
2061 );
2062 }
2063
2064 #[test]
2065 fn metadata_with_nothing_in_it_is_metadata_a_lock_screen_can_skip() {
2066 assert!(MediaMetadata::default().is_empty());
2067 assert!(!MediaMetadata::titled("Track").is_empty());
2068 }
2069
2070 #[test]
2071 fn a_path_survives_the_round_trip_through_a_uri() {
2072 let path = PathBuf::from("/music/Sgt. Pepper's #1.mp3");
2073 let uri = uri_for_path(&path);
2074
2075 assert_eq!(uri, "file:///music/Sgt.%20Pepper%27s%20%231.mp3");
2076 assert_eq!(path_from_uri(&uri), Some(path));
2077 }
2078
2079 #[test]
2080 fn a_windows_path_keeps_its_drive_letter() {
2081 let uri = uri_for_path(Path::new("C:\\Music\\track.mp3"));
2082
2083 assert_eq!(uri, "file:///C%3A/Music/track.mp3");
2084 assert_eq!(
2085 path_from_uri(&uri),
2086 Some(PathBuf::from("C:/Music/track.mp3"))
2087 );
2088 }
2089
2090 #[test]
2091 fn a_bare_path_is_accepted_as_itself() {
2092 assert_eq!(
2093 path_from_uri("/music/track.mp3"),
2094 Some(PathBuf::from("/music/track.mp3"))
2095 );
2096 }
2097
2098 #[test]
2099 fn anything_that_is_not_a_local_file_has_no_path() {
2100 assert_eq!(path_from_uri("https://host/stream.mp3"), None);
2101 assert_eq!(path_from_uri("content://media/audio/1"), None);
2102 assert_eq!(path_from_uri("blob:https://host/abc"), None);
2103 assert_eq!(path_from_uri("file://host/share/track.mp3"), None);
2104 assert_eq!(path_from_uri(""), None);
2105 }
2106
2107 #[test]
2108 fn a_truncated_escape_is_not_guessed_at() {
2109 assert_eq!(path_from_uri("file:///music/track%2"), None);
2110 assert_eq!(path_from_uri("file:///music/track%zz.mp3"), None);
2111 }
2112
2113 #[test]
2114 fn speed_and_looping_reach_a_backend_that_has_them() {
2115 let (_guard, player) = install();
2116 assert!(set_media_speed(1.5));
2117 set_media_looping(true);
2118
2119 let calls = player.calls();
2120 assert!(calls.contains(&"speed 1.5".to_string()));
2121 assert!(calls.contains(&"looping true".to_string()));
2122 }
2123}