1use std::{
34 fs::File,
35 path::{Path, PathBuf},
36 sync::{
37 Arc,
38 atomic::{AtomicBool, AtomicU64, Ordering},
39 },
40 time::Duration,
41};
42
43use cranpose_core::{EventStream, State, rememberEventStream};
44use parking_lot::Mutex;
45
46use crate::{
47 background::{BackgroundWorkLease, acquire_background_work},
48 host::{LifecycleEvent, LifecycleState},
49 registry::ServiceRegistry,
50};
51
52pub const DUCKED_GAIN: f32 = 0.2;
57
58#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct MediaArtwork {
61 pub bytes: Arc<[u8]>,
63 pub mime: String,
66}
67
68impl MediaArtwork {
69 pub fn new(bytes: impl Into<Arc<[u8]>>, mime: impl Into<String>) -> MediaArtwork {
71 MediaArtwork {
72 bytes: bytes.into(),
73 mime: mime.into(),
74 }
75 }
76}
77
78#[derive(Clone, Debug, Default, PartialEq, Eq)]
81pub struct MediaMetadata {
82 pub title: String,
83 pub artist: String,
84 pub album: String,
85 pub duration: Option<Duration>,
89 pub artwork: Option<MediaArtwork>,
90}
91
92impl MediaMetadata {
93 pub fn titled(title: impl Into<String>) -> MediaMetadata {
95 MediaMetadata {
96 title: title.into(),
97 ..MediaMetadata::default()
98 }
99 }
100
101 pub fn artist(mut self, artist: impl Into<String>) -> MediaMetadata {
103 self.artist = artist.into();
104 self
105 }
106
107 pub fn album(mut self, album: impl Into<String>) -> MediaMetadata {
109 self.album = album.into();
110 self
111 }
112
113 pub fn duration(mut self, duration: Duration) -> MediaMetadata {
115 self.duration = Some(duration);
116 self
117 }
118
119 pub fn artwork(mut self, artwork: MediaArtwork) -> MediaMetadata {
121 self.artwork = Some(artwork);
122 self
123 }
124
125 pub fn is_empty(&self) -> bool {
127 self.title.is_empty() && self.artist.is_empty() && self.album.is_empty()
128 }
129}
130
131#[derive(Clone, Debug, Default, PartialEq, Eq)]
139pub struct MediaItem {
140 pub uri: String,
141 pub metadata: MediaMetadata,
142}
143
144impl MediaItem {
145 pub fn new(uri: impl Into<String>) -> MediaItem {
147 MediaItem {
148 uri: uri.into(),
149 metadata: MediaMetadata::default(),
150 }
151 }
152
153 pub fn with_metadata(mut self, metadata: MediaMetadata) -> MediaItem {
155 self.metadata = metadata;
156 self
157 }
158
159 pub fn display_title(&self) -> &str {
162 if !self.metadata.title.is_empty() {
163 return &self.metadata.title;
164 }
165 let path = self.uri.split(['?', '#']).next().unwrap_or(&self.uri);
166 match path.rsplit(['/', '\\']).next() {
167 Some(name) if !name.is_empty() => name,
168 _ => &self.uri,
169 }
170 }
171}
172
173#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
178pub struct MediaCapabilities {
179 pub seeking: bool,
181 pub speed: bool,
183 pub looping: bool,
185 pub analysis: bool,
187 pub session: bool,
190 pub equalizer: bool,
194 pub probing: bool,
198}
199
200impl MediaCapabilities {
201 pub const TRANSPORT: MediaCapabilities = MediaCapabilities {
204 seeking: true,
205 speed: false,
206 looping: true,
207 analysis: false,
208 session: false,
209 equalizer: false,
210 probing: false,
211 };
212}
213
214#[derive(Clone, Copy, Debug, PartialEq)]
216pub struct EqualizerBand {
217 pub center_hz: f32,
219 pub min_gain_db: f32,
221 pub max_gain_db: f32,
223}
224
225impl EqualizerBand {
226 pub fn new(center_hz: f32, range_db: f32) -> EqualizerBand {
228 let range = range_db.abs();
229 EqualizerBand {
230 center_hz,
231 min_gain_db: -range,
232 max_gain_db: range,
233 }
234 }
235
236 pub fn clamp_gain(&self, gain_db: f32) -> f32 {
238 gain_db.clamp(self.min_gain_db, self.max_gain_db)
239 }
240}
241
242pub const OCTAVE_BAND_CENTERS_HZ: [f32; 10] = [
249 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,
250];
251
252pub fn octave_equalizer_bands(range_db: f32) -> Vec<EqualizerBand> {
254 OCTAVE_BAND_CENTERS_HZ
255 .iter()
256 .map(|center| EqualizerBand::new(*center, range_db))
257 .collect()
258}
259
260#[derive(Clone, Debug, Default, PartialEq)]
267pub struct EqualizerSettings {
268 pub enabled: bool,
271 pub preamp_db: f32,
273 pub gains_db: Vec<f32>,
275}
276
277impl EqualizerSettings {
278 pub fn flat(bands: usize) -> EqualizerSettings {
280 EqualizerSettings {
281 enabled: true,
282 preamp_db: 0.0,
283 gains_db: vec![0.0; bands],
284 }
285 }
286
287 pub fn clamped_to(&self, bands: &[EqualizerBand]) -> EqualizerSettings {
290 EqualizerSettings {
291 enabled: self.enabled,
292 preamp_db: self.preamp_db,
293 gains_db: bands
294 .iter()
295 .enumerate()
296 .map(|(index, band)| {
297 band.clamp_gain(self.gains_db.get(index).copied().unwrap_or(0.0))
298 })
299 .collect(),
300 }
301 }
302}
303
304#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
305pub enum MediaError {
306 #[error("media playback is not supported here")]
308 Unsupported,
309 #[error("cannot play {0}")]
312 UnsupportedSource(String),
313 #[error("no media item is loaded")]
315 NothingLoaded,
316 #[error("this item cannot be seeked")]
319 NotSeekable,
320 #[error("{0}")]
322 Failed(String),
323}
324
325#[derive(Clone, Debug, Default, PartialEq, Eq)]
327pub enum PlaybackState {
328 #[default]
330 Idle,
331 Loading,
335 Playing,
337 Paused,
339 Ended,
342 Failed(MediaError),
344}
345
346impl PlaybackState {
347 pub fn is_playing(&self) -> bool {
349 matches!(self, PlaybackState::Playing)
350 }
351
352 pub fn is_active(&self) -> bool {
354 matches!(
355 self,
356 PlaybackState::Loading | PlaybackState::Playing | PlaybackState::Paused
357 )
358 }
359
360 pub fn failure(&self) -> Option<&MediaError> {
362 match self {
363 PlaybackState::Failed(error) => Some(error),
364 _ => None,
365 }
366 }
367}
368
369#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
371pub struct PlaybackProgress {
372 pub position: Duration,
374 pub duration: Option<Duration>,
376 pub buffered: Duration,
379}
380
381impl PlaybackProgress {
382 pub fn new(position: Duration, duration: Duration) -> PlaybackProgress {
384 PlaybackProgress {
385 position: position.min(duration),
386 duration: Some(duration),
387 buffered: duration,
388 }
389 }
390
391 pub fn fraction(&self) -> Option<f32> {
393 let duration = self.duration?;
394 if duration.is_zero() {
395 return Some(0.0);
396 }
397 Some((self.position.as_secs_f32() / duration.as_secs_f32()).clamp(0.0, 1.0))
398 }
399
400 pub fn buffered_fraction(&self) -> Option<f32> {
402 let duration = self.duration?;
403 if duration.is_zero() {
404 return Some(0.0);
405 }
406 Some((self.buffered.as_secs_f32() / duration.as_secs_f32()).clamp(0.0, 1.0))
407 }
408}
409
410#[derive(Clone, Copy, Debug, PartialEq, Eq)]
413pub enum MediaCommand {
414 Play,
415 Pause,
416 TogglePlayPause,
418 Stop,
419 Next,
421 Previous,
423 SeekTo(Duration),
424}
425
426impl MediaCommand {
427 pub fn is_transport(self) -> bool {
433 !matches!(self, MediaCommand::Next | MediaCommand::Previous)
434 }
435}
436
437#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
439pub enum AudioFocus {
440 #[default]
442 Gained,
443 Ducked,
446 LostTransient,
449 Lost,
452}
453
454#[derive(Clone, Debug, PartialEq)]
456pub struct MediaSamples {
457 pub sample_rate: u32,
459 pub channels: u16,
461 pub samples: Arc<[f32]>,
463 pub sequence: u64,
466}
467
468impl MediaSamples {
469 pub fn new(
473 sample_rate: u32,
474 channels: u16,
475 sequence: u64,
476 samples: impl Into<Arc<[f32]>>,
477 ) -> Option<MediaSamples> {
478 let samples = samples.into();
479 if sample_rate == 0 || channels == 0 || samples.len() % channels as usize != 0 {
480 return None;
481 }
482 Some(MediaSamples {
483 sample_rate,
484 channels,
485 samples,
486 sequence,
487 })
488 }
489
490 pub fn frames(&self) -> usize {
492 self.samples.len() / self.channels.max(1) as usize
493 }
494
495 pub fn span(&self) -> Duration {
497 if self.sample_rate == 0 {
498 return Duration::ZERO;
499 }
500 Duration::from_secs_f64(self.frames() as f64 / self.sample_rate as f64)
501 }
502}
503
504pub trait MediaPlayer: Send + Sync {
517 fn capabilities(&self) -> MediaCapabilities;
520
521 fn prepare(&self, item: &MediaItem) -> Result<(), MediaError>;
527
528 fn play(&self) -> Result<(), MediaError>;
530
531 fn pause(&self);
533
534 fn stop(&self);
536
537 fn seek_to(&self, _position: Duration) -> Result<(), MediaError> {
539 Err(MediaError::NotSeekable)
540 }
541
542 fn set_volume(&self, volume: f32);
545
546 fn set_speed(&self, _speed: f32) -> bool {
549 false
550 }
551
552 fn set_looping(&self, _looping: bool) {}
554
555 fn set_analysis_enabled(&self, _enabled: bool) -> bool {
559 false
560 }
561
562 fn set_session_metadata(&self, _metadata: &MediaMetadata) {}
566
567 fn equalizer_bands(&self) -> Vec<EqualizerBand> {
575 Vec::new()
576 }
577
578 fn set_equalizer(&self, _settings: &EqualizerSettings) {}
580
581 fn audio_extensions(&self) -> Vec<&'static str> {
595 Vec::new()
596 }
597
598 fn probe_duration(&self, _item: &MediaItem) -> Option<Duration> {
606 None
607 }
608}
609
610pub type MediaPlayerRef = Arc<dyn MediaPlayer>;
612
613static PLATFORM_MEDIA: ServiceRegistry<dyn MediaPlayer> = ServiceRegistry::new();
614
615pub fn set_platform_media_player(player: MediaPlayerRef) {
617 PLATFORM_MEDIA.set(player);
618}
619
620pub fn clear_platform_media_player() {
622 if let Some(player) = PLATFORM_MEDIA.get() {
623 player.stop();
624 }
625 PLATFORM_MEDIA.clear();
626 STATE_OBSERVERS.clear();
627 PROGRESS_OBSERVERS.clear();
628 COMMAND_OBSERVERS.clear();
629 FOCUS_OBSERVERS.clear();
630 SAMPLE_OBSERVERS.clear();
631 *STATE.lock() = PlaybackState::Idle;
632 *PROGRESS.lock() = PlaybackProgress::default();
633 *CURRENT_ITEM.lock() = None;
634 *LATEST_SAMPLES.lock() = None;
635 *FOCUS.lock() = AudioFocus::Gained;
636 *VOLUME.lock() = 1.0;
637 PAUSED_BY_FOCUS.store(false, Ordering::Release);
638 DROPPED_SAMPLES.store(0, Ordering::Release);
639 release_background_lease();
640}
641
642pub fn media_player() -> Option<MediaPlayerRef> {
644 PLATFORM_MEDIA.get()
645}
646
647pub fn media_playback_supported() -> bool {
649 PLATFORM_MEDIA.get().is_some()
650}
651
652pub fn media_capabilities() -> MediaCapabilities {
655 media_player()
656 .map(|player| player.capabilities())
657 .unwrap_or_default()
658}
659
660static STATE: Mutex<PlaybackState> = Mutex::new(PlaybackState::Idle);
663static PROGRESS: Mutex<PlaybackProgress> = Mutex::new(PlaybackProgress {
664 position: Duration::ZERO,
665 duration: None,
666 buffered: Duration::ZERO,
667});
668static CURRENT_ITEM: Mutex<Option<MediaItem>> = Mutex::new(None);
669static LATEST_SAMPLES: Mutex<Option<MediaSamples>> = Mutex::new(None);
670static FOCUS: Mutex<AudioFocus> = Mutex::new(AudioFocus::Gained);
671static VOLUME: Mutex<f32> = Mutex::new(1.0);
673static EQUALIZER: Mutex<EqualizerSettings> = Mutex::new(EqualizerSettings {
677 enabled: false,
678 preamp_db: 0.0,
679 gains_db: Vec::new(),
680});
681static PAUSED_BY_FOCUS: AtomicBool = AtomicBool::new(false);
682
683static DROPPED_SAMPLES: AtomicU64 = AtomicU64::new(0);
687
688pub fn playback_state() -> PlaybackState {
690 STATE.lock().clone()
691}
692
693pub fn playback_progress() -> PlaybackProgress {
698 *PROGRESS.lock()
699}
700
701pub fn current_media_item() -> Option<MediaItem> {
703 CURRENT_ITEM.lock().clone()
704}
705
706pub fn latest_media_samples() -> Option<MediaSamples> {
709 LATEST_SAMPLES.lock().clone()
710}
711
712pub fn dropped_media_samples() -> u64 {
714 DROPPED_SAMPLES.load(Ordering::Acquire)
715}
716
717pub fn audio_focus() -> AudioFocus {
719 *FOCUS.lock()
720}
721
722pub fn media_volume() -> f32 {
724 *VOLUME.lock()
725}
726
727struct ObserverList<T: ?Sized> {
732 entries: Mutex<Vec<(u64, Arc<T>)>>,
733}
734
735impl<T: ?Sized> ObserverList<T> {
736 const fn new() -> ObserverList<T> {
737 ObserverList {
738 entries: Mutex::new(Vec::new()),
739 }
740 }
741
742 fn add(&self, observer: Arc<T>) -> u64 {
743 let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
744 self.entries.lock().push((id, observer));
745 id
746 }
747
748 fn remove(&self, id: u64) {
749 self.entries.lock().retain(|(entry, _)| *entry != id);
750 }
751
752 fn snapshot(&self) -> Vec<Arc<T>> {
753 self.entries
754 .lock()
755 .iter()
756 .map(|(_, observer)| Arc::clone(observer))
757 .collect()
758 }
759
760 fn clear(&self) {
761 self.entries.lock().clear();
762 }
763}
764
765static NEXT_OBSERVER: AtomicU64 = AtomicU64::new(1);
766
767type StateObserverFn = dyn Fn(PlaybackState) + Send + Sync;
768type ProgressObserverFn = dyn Fn(PlaybackProgress) + Send + Sync;
769type CommandObserverFn = dyn Fn(MediaCommand) + Send + Sync;
770type FocusObserverFn = dyn Fn(AudioFocus) + Send + Sync;
771type SampleObserverFn = dyn Fn(MediaSamples) + Send + Sync;
772
773static STATE_OBSERVERS: ObserverList<StateObserverFn> = ObserverList::new();
774static PROGRESS_OBSERVERS: ObserverList<ProgressObserverFn> = ObserverList::new();
775static COMMAND_OBSERVERS: ObserverList<CommandObserverFn> = ObserverList::new();
776static FOCUS_OBSERVERS: ObserverList<FocusObserverFn> = ObserverList::new();
777static SAMPLE_OBSERVERS: ObserverList<SampleObserverFn> = ObserverList::new();
778
779pub struct MediaObserver {
781 id: u64,
782 remove: fn(u64),
783}
784
785impl Drop for MediaObserver {
786 fn drop(&mut self) {
787 (self.remove)(self.id);
788 }
789}
790
791pub fn observe_playback_state(
795 observer: impl Fn(PlaybackState) + Send + Sync + 'static,
796) -> MediaObserver {
797 let observer: Arc<StateObserverFn> = Arc::new(observer);
798 let id = STATE_OBSERVERS.add(Arc::clone(&observer));
799 observer(playback_state());
800 MediaObserver {
801 id,
802 remove: |id| STATE_OBSERVERS.remove(id),
803 }
804}
805
806pub fn observe_playback_progress(
809 observer: impl Fn(PlaybackProgress) + Send + Sync + 'static,
810) -> MediaObserver {
811 let observer: Arc<ProgressObserverFn> = Arc::new(observer);
812 let id = PROGRESS_OBSERVERS.add(Arc::clone(&observer));
813 observer(playback_progress());
814 MediaObserver {
815 id,
816 remove: |id| PROGRESS_OBSERVERS.remove(id),
817 }
818}
819
820pub fn observe_media_commands(
822 observer: impl Fn(MediaCommand) + Send + Sync + 'static,
823) -> MediaObserver {
824 let id = COMMAND_OBSERVERS.add(Arc::new(observer));
825 MediaObserver {
826 id,
827 remove: |id| COMMAND_OBSERVERS.remove(id),
828 }
829}
830
831pub fn observe_audio_focus(observer: impl Fn(AudioFocus) + Send + Sync + 'static) -> MediaObserver {
834 let observer: Arc<FocusObserverFn> = Arc::new(observer);
835 let id = FOCUS_OBSERVERS.add(Arc::clone(&observer));
836 observer(audio_focus());
837 MediaObserver {
838 id,
839 remove: |id| FOCUS_OBSERVERS.remove(id),
840 }
841}
842
843pub fn observe_media_samples(
845 observer: impl Fn(MediaSamples) + Send + Sync + 'static,
846) -> MediaObserver {
847 let id = SAMPLE_OBSERVERS.add(Arc::new(observer));
848 MediaObserver {
849 id,
850 remove: |id| SAMPLE_OBSERVERS.remove(id),
851 }
852}
853
854pub fn publish_playback_state(state: PlaybackState) {
862 {
863 let mut current = STATE.lock();
864 if *current == state {
865 return;
866 }
867 *current = state.clone();
868 }
869 if state.is_playing() {
870 acquire_background_lease();
871 } else {
872 release_background_lease();
873 }
874 if !state.is_active() {
875 *PROGRESS.lock() = PlaybackProgress::default();
876 *LATEST_SAMPLES.lock() = None;
877 }
878 if matches!(state, PlaybackState::Idle) {
879 *CURRENT_ITEM.lock() = None;
880 DROPPED_SAMPLES.store(0, Ordering::Release);
881 }
882 for observer in STATE_OBSERVERS.snapshot() {
883 observer(state.clone());
884 }
885}
886
887pub fn publish_playback_progress(progress: PlaybackProgress) {
890 let progress = clamp_progress(progress);
891 {
892 let mut current = PROGRESS.lock();
893 if *current == progress {
894 return;
895 }
896 *current = progress;
897 }
898 for observer in PROGRESS_OBSERVERS.snapshot() {
899 observer(progress);
900 }
901}
902
903fn clamp_progress(mut progress: PlaybackProgress) -> PlaybackProgress {
904 if let Some(duration) = progress.duration {
905 progress.position = progress.position.min(duration);
906 progress.buffered = progress.buffered.min(duration);
907 }
908 progress
909}
910
911pub fn publish_media_command(command: MediaCommand) {
918 match command {
919 MediaCommand::Play => {
920 let _ = play_media();
921 }
922 MediaCommand::Pause => pause_media(),
923 MediaCommand::TogglePlayPause => toggle_media(),
924 MediaCommand::Stop => stop_media(),
925 MediaCommand::SeekTo(position) => {
926 let _ = seek_media(position);
927 }
928 MediaCommand::Next | MediaCommand::Previous => {}
929 }
930 for observer in COMMAND_OBSERVERS.snapshot() {
931 observer(command);
932 }
933}
934
935pub fn publish_audio_focus(focus: AudioFocus) {
949 {
950 let mut current = FOCUS.lock();
951 if *current == focus {
952 return;
953 }
954 *current = focus;
955 }
956 apply_volume();
957 match focus {
958 AudioFocus::Gained => {
959 if PAUSED_BY_FOCUS.swap(false, Ordering::AcqRel) {
960 let _ = play_media();
961 }
962 }
963 AudioFocus::Ducked => {}
964 AudioFocus::LostTransient => {
965 if playback_state().is_playing() {
966 PAUSED_BY_FOCUS.store(true, Ordering::Release);
967 pause_media();
968 }
969 }
970 AudioFocus::Lost => {
971 PAUSED_BY_FOCUS.store(false, Ordering::Release);
972 stop_media();
973 }
974 }
975 for observer in FOCUS_OBSERVERS.snapshot() {
976 observer(focus);
977 }
978}
979
980pub fn publish_media_samples(samples: MediaSamples) {
987 *LATEST_SAMPLES.lock() = Some(samples.clone());
988 let observers = SAMPLE_OBSERVERS.snapshot();
989 if observers.is_empty() {
990 return;
991 }
992 for observer in observers {
993 observer(samples.clone());
994 }
995}
996
997pub fn record_dropped_media_samples() {
999 DROPPED_SAMPLES.fetch_add(1, Ordering::AcqRel);
1000}
1001
1002pub fn open_media(item: MediaItem) -> Result<(), MediaError> {
1012 let Some(player) = media_player() else {
1013 publish_playback_state(PlaybackState::Failed(MediaError::Unsupported));
1014 return Err(MediaError::Unsupported);
1015 };
1016 PAUSED_BY_FOCUS.store(false, Ordering::Release);
1017 DROPPED_SAMPLES.store(0, Ordering::Release);
1018 *CURRENT_ITEM.lock() = Some(item.clone());
1019 publish_playback_progress(PlaybackProgress {
1020 position: Duration::ZERO,
1021 duration: item.metadata.duration,
1022 buffered: Duration::ZERO,
1023 });
1024 publish_playback_state(PlaybackState::Loading);
1025 if player.capabilities().session {
1026 player.set_session_metadata(&item.metadata);
1027 }
1028 player.prepare(&item).inspect_err(|error| {
1029 publish_playback_state(PlaybackState::Failed(error.clone()));
1030 })
1031}
1032
1033pub fn play_media() -> Result<(), MediaError> {
1035 let Some(player) = media_player() else {
1036 return Err(MediaError::Unsupported);
1037 };
1038 if CURRENT_ITEM.lock().is_none() {
1039 return Err(MediaError::NothingLoaded);
1040 }
1041 player.play().inspect_err(|error| {
1042 publish_playback_state(PlaybackState::Failed(error.clone()));
1043 })
1044}
1045
1046pub fn pause_media() {
1048 if let Some(player) = media_player() {
1049 player.pause();
1050 }
1051}
1052
1053pub fn stop_media() {
1055 PAUSED_BY_FOCUS.store(false, Ordering::Release);
1056 if let Some(player) = media_player() {
1057 player.stop();
1058 }
1059 publish_playback_state(PlaybackState::Idle);
1060}
1061
1062pub fn toggle_media() {
1065 if playback_state().is_playing() {
1066 pause_media();
1067 } else {
1068 let _ = play_media();
1069 }
1070}
1071
1072pub fn seek_media(position: Duration) -> Result<(), MediaError> {
1078 let Some(player) = media_player() else {
1079 return Err(MediaError::Unsupported);
1080 };
1081 if CURRENT_ITEM.lock().is_none() {
1082 return Err(MediaError::NothingLoaded);
1083 }
1084 if !player.capabilities().seeking {
1085 return Err(MediaError::NotSeekable);
1086 }
1087 let position = match playback_progress().duration {
1088 Some(duration) => position.min(duration),
1089 None => position,
1090 };
1091 player.seek_to(position)
1092}
1093
1094pub fn seek_media_fraction(fraction: f32) -> Result<(), MediaError> {
1099 let Some(duration) = playback_progress().duration else {
1100 return Err(MediaError::NotSeekable);
1101 };
1102 let fraction = fraction.clamp(0.0, 1.0) as f64;
1103 seek_media(Duration::from_secs_f64(duration.as_secs_f64() * fraction))
1104}
1105
1106pub fn set_media_volume(volume: f32) {
1112 *VOLUME.lock() = volume.clamp(0.0, 1.0);
1113 apply_volume();
1114}
1115
1116fn apply_volume() {
1117 let Some(player) = media_player() else {
1118 return;
1119 };
1120 let gain = match audio_focus() {
1121 AudioFocus::Ducked => DUCKED_GAIN,
1122 _ => 1.0,
1123 };
1124 player.set_volume(media_volume() * gain);
1125}
1126
1127pub fn set_media_speed(speed: f32) -> bool {
1130 match media_player() {
1131 Some(player) if player.capabilities().speed => player.set_speed(speed),
1132 _ => false,
1133 }
1134}
1135
1136pub fn set_media_looping(looping: bool) {
1138 if let Some(player) = media_player() {
1139 player.set_looping(looping);
1140 }
1141}
1142
1143pub fn set_media_analysis_enabled(enabled: bool) -> bool {
1149 match media_player() {
1150 Some(player) if player.capabilities().analysis => {
1151 if !enabled {
1152 *LATEST_SAMPLES.lock() = None;
1153 }
1154 player.set_analysis_enabled(enabled)
1155 }
1156 _ => false,
1157 }
1158}
1159
1160pub fn probe_media_duration(item: &MediaItem) -> Option<Duration> {
1165 media_player()?.probe_duration(item)
1166}
1167
1168pub fn media_equalizer_bands() -> Vec<EqualizerBand> {
1173 match media_player() {
1174 Some(player) if player.capabilities().equalizer => player.equalizer_bands(),
1175 _ => Vec::new(),
1176 }
1177}
1178
1179pub fn media_audio_extensions() -> Vec<&'static str> {
1185 media_player()
1186 .map(|player| player.audio_extensions())
1187 .unwrap_or_default()
1188}
1189
1190pub fn media_equalizer() -> EqualizerSettings {
1192 EQUALIZER.lock().clone()
1193}
1194
1195pub fn set_media_equalizer(settings: EqualizerSettings) -> bool {
1202 *EQUALIZER.lock() = settings.clone();
1203 let Some(player) = media_player() else {
1204 return false;
1205 };
1206 if !player.capabilities().equalizer {
1207 return false;
1208 }
1209 player.set_equalizer(&settings.clamped_to(&player.equalizer_bands()));
1210 true
1211}
1212
1213pub fn set_media_metadata(metadata: MediaMetadata) {
1217 {
1218 let mut item = CURRENT_ITEM.lock();
1219 let Some(item) = item.as_mut() else {
1220 return;
1221 };
1222 item.metadata = metadata.clone();
1223 }
1224 if let Some(player) = media_player()
1225 && player.capabilities().session
1226 {
1227 player.set_session_metadata(&metadata);
1228 }
1229}
1230
1231static BACKGROUND_LEASE: Mutex<Option<BackgroundWorkLease>> = Mutex::new(None);
1234
1235fn acquire_background_lease() {
1236 let mut lease = BACKGROUND_LEASE.lock();
1237 if lease.is_none() {
1238 *lease = Some(acquire_background_work());
1239 }
1240}
1241
1242fn release_background_lease() {
1243 BACKGROUND_LEASE.lock().take();
1244}
1245
1246#[cfg(test)]
1252fn holds_background_work() -> bool {
1253 BACKGROUND_LEASE.lock().is_some()
1254}
1255
1256pub(crate) fn on_lifecycle(event: LifecycleEvent) {
1263 if event.to == LifecycleState::Destroyed {
1264 stop_media();
1265 }
1266}
1267
1268pub fn uri_for_path(path: &Path) -> String {
1277 let text = path.to_string_lossy();
1278 let mut uri = String::with_capacity(text.len() + 8);
1279 uri.push_str("file://");
1280 if !text.starts_with('/') {
1281 uri.push('/');
1284 }
1285 for byte in text.bytes() {
1286 match byte {
1287 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' | b'/' => {
1288 uri.push(byte as char);
1289 }
1290 b'\\' => uri.push('/'),
1291 _ => uri.push_str(&format!("%{byte:02X}")),
1292 }
1293 }
1294 uri
1295}
1296
1297pub fn path_from_uri(uri: &str) -> Option<PathBuf> {
1303 let rest = match uri.split_once("://") {
1304 Some(("file", rest)) => rest,
1305 Some(_) => return None,
1306 None => return non_empty_path(uri),
1307 };
1308 let path = rest.strip_prefix('/')?;
1311 let decoded = crate::content::percent_decode(path)?;
1312 if decoded.starts_with('/') || decoded.is_empty() {
1313 return non_empty_path(&decoded);
1314 }
1315 if decoded.as_bytes().get(1) == Some(&b':') {
1318 non_empty_path(&decoded)
1319 } else {
1320 non_empty_path(&format!("/{decoded}"))
1321 }
1322}
1323
1324fn non_empty_path(text: &str) -> Option<PathBuf> {
1325 if text.is_empty() {
1326 return None;
1327 }
1328 Some(PathBuf::from(text))
1329}
1330
1331#[derive(Debug)]
1335pub struct MediaSourceHandle {
1336 pub stream: File,
1339 pub len: Option<u64>,
1346}
1347
1348pub trait MediaSourceOpener: Send + Sync {
1360 fn open(&self, uri: &str) -> std::io::Result<MediaSourceHandle>;
1362}
1363
1364pub type MediaSourceOpenerRef = Arc<dyn MediaSourceOpener>;
1366
1367static PLATFORM_MEDIA_SOURCE: ServiceRegistry<dyn MediaSourceOpener> = ServiceRegistry::new();
1368
1369pub fn set_platform_media_source_opener(opener: MediaSourceOpenerRef) {
1371 PLATFORM_MEDIA_SOURCE.set(opener);
1372}
1373
1374pub fn clear_platform_media_source_opener() {
1376 PLATFORM_MEDIA_SOURCE.clear();
1377}
1378
1379pub fn open_media_source(uri: &str) -> std::io::Result<MediaSourceHandle> {
1386 if let Some(path) = path_from_uri(uri) {
1387 let stream = File::open(path)?;
1388 let len = stream.metadata().ok().map(|metadata| metadata.len());
1389 return Ok(MediaSourceHandle { stream, len });
1390 }
1391 match PLATFORM_MEDIA_SOURCE.get() {
1392 Some(opener) => opener.open(uri),
1393 None => Err(std::io::Error::new(
1394 std::io::ErrorKind::Unsupported,
1395 format!("no platform opener for {uri}"),
1396 )),
1397 }
1398}
1399
1400#[allow(non_snake_case)]
1405#[track_caller]
1406pub fn rememberPlaybackState() -> State<PlaybackState> {
1407 let updates = rememberEventStream((), |sender| {
1408 observe_playback_state(move |state| sender.send(state))
1409 });
1410 cranpose_core::collectAsState(updates, (), playback_state())
1411}
1412
1413#[allow(non_snake_case)]
1420#[track_caller]
1421pub fn rememberPlaybackProgress() -> State<PlaybackProgress> {
1422 let updates = rememberEventStream((), |sender| {
1423 observe_playback_progress(move |progress| sender.send(progress))
1424 });
1425 cranpose_core::collectAsState(updates, (), playback_progress())
1426}
1427
1428#[allow(non_snake_case)]
1431#[track_caller]
1432pub fn rememberAudioFocus() -> State<AudioFocus> {
1433 let updates = rememberEventStream((), |sender| {
1434 observe_audio_focus(move |focus| sender.send(focus))
1435 });
1436 cranpose_core::collectAsState(updates, (), audio_focus())
1437}
1438
1439#[allow(non_snake_case)]
1446#[track_caller]
1447pub fn rememberMediaCommands() -> EventStream<MediaCommand> {
1448 rememberEventStream((), |sender| {
1449 observe_media_commands(move |command| sender.send(command))
1450 })
1451}
1452
1453#[allow(non_snake_case)]
1458#[track_caller]
1459pub fn rememberMediaSamples() -> EventStream<MediaSamples> {
1460 rememberEventStream((), |sender| {
1461 observe_media_samples(move |samples| sender.send(samples))
1462 })
1463}
1464
1465#[cfg(test)]
1466mod tests {
1467 use super::*;
1468 use crate::registry::test_service_guard;
1469
1470 struct FakePlayer {
1473 capabilities: MediaCapabilities,
1474 calls: Mutex<Vec<String>>,
1475 volume: Mutex<f32>,
1476 prepare_fails: bool,
1477 }
1478
1479 impl FakePlayer {
1480 fn new() -> Arc<FakePlayer> {
1481 Arc::new(FakePlayer {
1482 capabilities: MediaCapabilities {
1483 seeking: true,
1484 speed: true,
1485 looping: true,
1486 analysis: true,
1487 session: true,
1488 equalizer: true,
1489 probing: true,
1490 },
1491 calls: Mutex::new(Vec::new()),
1492 volume: Mutex::new(1.0),
1493 prepare_fails: false,
1494 })
1495 }
1496
1497 fn with(capabilities: MediaCapabilities) -> Arc<FakePlayer> {
1498 Arc::new(FakePlayer {
1499 capabilities,
1500 calls: Mutex::new(Vec::new()),
1501 volume: Mutex::new(1.0),
1502 prepare_fails: false,
1503 })
1504 }
1505
1506 fn failing() -> Arc<FakePlayer> {
1507 Arc::new(FakePlayer {
1508 capabilities: MediaCapabilities::TRANSPORT,
1509 calls: Mutex::new(Vec::new()),
1510 volume: Mutex::new(1.0),
1511 prepare_fails: true,
1512 })
1513 }
1514
1515 fn note(&self, call: impl Into<String>) {
1516 self.calls.lock().push(call.into());
1517 }
1518
1519 fn calls(&self) -> Vec<String> {
1520 self.calls.lock().clone()
1521 }
1522 }
1523
1524 impl MediaPlayer for FakePlayer {
1525 fn capabilities(&self) -> MediaCapabilities {
1526 self.capabilities
1527 }
1528
1529 fn prepare(&self, item: &MediaItem) -> Result<(), MediaError> {
1530 self.note(format!("prepare {}", item.uri));
1531 if self.prepare_fails {
1532 return Err(MediaError::UnsupportedSource(item.uri.clone()));
1533 }
1534 publish_playback_state(PlaybackState::Paused);
1535 Ok(())
1536 }
1537
1538 fn play(&self) -> Result<(), MediaError> {
1539 self.note("play");
1540 publish_playback_state(PlaybackState::Playing);
1541 Ok(())
1542 }
1543
1544 fn pause(&self) {
1545 self.note("pause");
1546 publish_playback_state(PlaybackState::Paused);
1547 }
1548
1549 fn stop(&self) {
1550 self.note("stop");
1551 }
1552
1553 fn seek_to(&self, position: Duration) -> Result<(), MediaError> {
1554 self.note(format!("seek {}", position.as_millis()));
1555 Ok(())
1556 }
1557
1558 fn set_volume(&self, volume: f32) {
1559 *self.volume.lock() = volume;
1560 }
1561
1562 fn set_speed(&self, speed: f32) -> bool {
1563 self.note(format!("speed {speed}"));
1564 true
1565 }
1566
1567 fn set_looping(&self, looping: bool) {
1568 self.note(format!("looping {looping}"));
1569 }
1570
1571 fn set_analysis_enabled(&self, enabled: bool) -> bool {
1572 self.note(format!("analysis {enabled}"));
1573 true
1574 }
1575
1576 fn set_session_metadata(&self, metadata: &MediaMetadata) {
1577 self.note(format!("session {}", metadata.title));
1578 }
1579
1580 fn equalizer_bands(&self) -> Vec<EqualizerBand> {
1581 vec![
1584 EqualizerBand::new(60.0, 6.0),
1585 EqualizerBand::new(1_000.0, 6.0),
1586 EqualizerBand::new(10_000.0, 6.0),
1587 ]
1588 }
1589
1590 fn set_equalizer(&self, settings: &EqualizerSettings) {
1591 self.note(format!(
1592 "equalizer {} preamp {} gains {:?}",
1593 settings.enabled, settings.preamp_db, settings.gains_db
1594 ));
1595 }
1596 }
1597
1598 fn install() -> (crate::registry::TestServiceGuard, Arc<FakePlayer>) {
1599 let guard = test_service_guard();
1600 clear_platform_media_player();
1601 let player = FakePlayer::new();
1602 set_platform_media_player(player.clone());
1603 (guard, player)
1604 }
1605
1606 fn track() -> MediaItem {
1607 MediaItem::new("file:///music/track.flac").with_metadata(
1608 MediaMetadata::titled("Track")
1609 .artist("Artist")
1610 .duration(Duration::from_secs(200)),
1611 )
1612 }
1613
1614 #[test]
1615 fn metadata_carries_everything_a_lock_screen_shows() {
1616 let artwork = MediaArtwork {
1617 bytes: vec![1, 2, 3].into(),
1618 mime: "image/png".to_string(),
1619 };
1620 let metadata = MediaMetadata::titled("Song")
1621 .artist("Band")
1622 .album("Record")
1623 .duration(Duration::from_secs(210))
1624 .artwork(artwork.clone());
1625
1626 assert_eq!(metadata.album, "Record");
1627 assert_eq!(metadata.artwork.as_ref(), Some(&artwork));
1628 assert!(!metadata.is_empty());
1629 assert!(!MediaMetadata::default().album("Record").is_empty());
1631 assert!(MediaMetadata::default().is_empty());
1632 }
1633
1634 #[test]
1635 fn a_band_reports_what_it_can_actually_do() {
1636 let band = EqualizerBand::new(1_000.0, 6.0);
1637 assert_eq!(band.clamp_gain(0.0), 0.0);
1638 assert_eq!(band.clamp_gain(6.0), 6.0);
1639 assert_eq!(band.clamp_gain(7.5), 6.0);
1640 assert_eq!(band.clamp_gain(-7.5), -6.0);
1641 }
1642
1643 #[test]
1644 fn an_equalizer_setting_is_clamped_to_the_bands_the_backend_has() {
1645 let bands = vec![
1646 EqualizerBand::new(60.0, 6.0),
1647 EqualizerBand::new(1_000.0, 6.0),
1648 ];
1649 let asked = EqualizerSettings {
1650 enabled: true,
1651 preamp_db: -3.0,
1652 gains_db: vec![12.0, -12.0, 4.0],
1654 };
1655 let applied = asked.clamped_to(&bands);
1656 assert_eq!(applied.gains_db, vec![6.0, -6.0]);
1657 assert_eq!(applied.preamp_db, -3.0);
1658 assert!(applied.enabled);
1659 }
1660
1661 #[test]
1662 fn a_setting_shorter_than_the_bands_leaves_the_rest_flat() {
1663 let bands = octave_equalizer_bands(12.0);
1664 let applied = EqualizerSettings {
1665 enabled: true,
1666 preamp_db: 0.0,
1667 gains_db: vec![3.0],
1668 }
1669 .clamped_to(&bands);
1670 assert_eq!(applied.gains_db.len(), bands.len());
1671 assert_eq!(applied.gains_db[0], 3.0);
1672 assert!(applied.gains_db[1..].iter().all(|gain| *gain == 0.0));
1673 }
1674
1675 #[test]
1676 fn a_curve_reaches_the_backend_clamped_to_its_own_bands() {
1677 let (_guard, player) = install();
1678
1679 assert_eq!(media_equalizer_bands().len(), 3);
1680 assert!(set_media_equalizer(EqualizerSettings {
1681 enabled: true,
1682 preamp_db: -2.0,
1683 gains_db: vec![9.0, 0.0, -9.0],
1684 }));
1685
1686 assert!(
1687 player
1688 .calls()
1689 .iter()
1690 .any(|call| call == "equalizer true preamp -2 gains [6.0, 0.0, -6.0]"),
1691 "the backend was not given the clamped curve: {:?}",
1692 player.calls()
1693 );
1694 }
1695
1696 #[test]
1697 fn a_curve_is_remembered_even_where_nothing_can_apply_it() {
1698 let _guard = test_service_guard();
1699 clear_platform_media_player();
1700
1701 let asked = EqualizerSettings {
1702 enabled: true,
1703 preamp_db: -1.0,
1704 gains_db: vec![4.0, -4.0],
1705 };
1706 assert!(!set_media_equalizer(asked.clone()));
1709 assert_eq!(media_equalizer(), asked);
1710 assert!(media_equalizer_bands().is_empty());
1711 }
1712
1713 #[test]
1714 fn a_backend_without_an_equalizer_says_so_rather_than_pretending() {
1715 let _guard = test_service_guard();
1716 clear_platform_media_player();
1717 set_platform_media_player(FakePlayer::with(MediaCapabilities {
1718 equalizer: false,
1719 ..MediaCapabilities::TRANSPORT
1720 }));
1721
1722 assert!(media_equalizer_bands().is_empty());
1723 assert!(!set_media_equalizer(EqualizerSettings::flat(10)));
1724 }
1725
1726 #[test]
1727 fn an_item_falls_back_to_its_file_name_for_a_title() {
1728 assert_eq!(
1729 MediaItem::new("file:///music/03 - Song.mp3").display_title(),
1730 "03 - Song.mp3"
1731 );
1732 assert_eq!(
1733 MediaItem::new("https://host/stream?token=1").display_title(),
1734 "stream"
1735 );
1736 assert_eq!(track().display_title(), "Track");
1737 }
1738
1739 #[test]
1740 fn progress_reports_fractions_only_for_items_that_have_a_length() {
1741 let known = PlaybackProgress::new(Duration::from_secs(30), Duration::from_secs(120));
1742 assert_eq!(known.fraction(), Some(0.25));
1743 assert_eq!(known.buffered_fraction(), Some(1.0));
1744
1745 let live = PlaybackProgress {
1746 position: Duration::from_secs(30),
1747 duration: None,
1748 buffered: Duration::from_secs(35),
1749 };
1750 assert_eq!(live.fraction(), None);
1751 assert_eq!(live.buffered_fraction(), None);
1752 }
1753
1754 #[test]
1755 fn progress_never_reads_past_the_end_of_the_item() {
1756 let progress = PlaybackProgress::new(Duration::from_secs(500), Duration::from_secs(120));
1757 assert_eq!(progress.position, Duration::from_secs(120));
1758 assert_eq!(progress.fraction(), Some(1.0));
1759 }
1760
1761 #[test]
1762 fn samples_reject_a_layout_that_does_not_describe_the_data() {
1763 assert!(MediaSamples::new(44_100, 2, 0, vec![0.0; 3]).is_none());
1764 assert!(MediaSamples::new(0, 2, 0, vec![0.0; 4]).is_none());
1765 assert!(MediaSamples::new(44_100, 0, 0, vec![0.0; 4]).is_none());
1766
1767 let block = MediaSamples::new(44_100, 2, 7, vec![0.0; 4410]).expect("well-formed block");
1768 assert_eq!(block.frames(), 2205);
1769 assert_eq!(block.span(), Duration::from_millis(50));
1770 assert_eq!(block.sequence, 7);
1771 }
1772
1773 #[test]
1774 fn without_a_backend_every_call_reports_that_it_is_unsupported() {
1775 let _guard = test_service_guard();
1776 clear_platform_media_player();
1777
1778 assert!(!media_playback_supported());
1779 assert_eq!(media_capabilities(), MediaCapabilities::default());
1780 assert_eq!(open_media(track()), Err(MediaError::Unsupported));
1781 assert_eq!(
1782 playback_state(),
1783 PlaybackState::Failed(MediaError::Unsupported)
1784 );
1785 assert_eq!(play_media(), Err(MediaError::Unsupported));
1786 assert_eq!(seek_media(Duration::ZERO), Err(MediaError::Unsupported));
1787 assert!(!set_media_speed(2.0));
1788 assert!(!set_media_analysis_enabled(true));
1789 }
1790
1791 #[test]
1792 fn opening_an_item_shows_the_wait_before_the_backend_is_asked() {
1793 let (_guard, player) = install();
1794 let seen = Arc::new(Mutex::new(Vec::new()));
1795 let recorder = Arc::clone(&seen);
1796 let _observer = observe_playback_state(move |state| recorder.lock().push(state));
1797
1798 open_media(track()).expect("the fake backend opens anything");
1799
1800 assert_eq!(
1801 *seen.lock(),
1802 vec![
1803 PlaybackState::Idle,
1804 PlaybackState::Loading,
1805 PlaybackState::Paused,
1806 ]
1807 );
1808 assert_eq!(
1809 player.calls(),
1810 vec!["session Track", "prepare file:///music/track.flac"]
1811 );
1812 assert_eq!(current_media_item().map(|item| item.uri), Some(track().uri));
1813 }
1814
1815 #[test]
1816 fn an_item_that_cannot_be_opened_publishes_the_failure() {
1817 let _guard = test_service_guard();
1818 clear_platform_media_player();
1819 set_platform_media_player(FakePlayer::failing());
1820
1821 let error = open_media(track()).expect_err("the failing backend refuses");
1822 assert_eq!(
1823 error,
1824 MediaError::UnsupportedSource("file:///music/track.flac".to_string())
1825 );
1826 assert_eq!(playback_state().failure(), Some(&error));
1827 }
1828
1829 #[test]
1830 fn the_transport_routes_to_the_backend_and_publishes_what_it_did() {
1831 let (_guard, player) = install();
1832
1833 open_media(track()).expect("opens");
1834 play_media().expect("plays");
1835 assert!(playback_state().is_playing());
1836
1837 toggle_media();
1838 assert_eq!(playback_state(), PlaybackState::Paused);
1839
1840 toggle_media();
1841 assert!(playback_state().is_playing());
1842
1843 stop_media();
1844 assert_eq!(playback_state(), PlaybackState::Idle);
1845 assert_eq!(current_media_item(), None);
1846
1847 assert_eq!(
1848 player.calls(),
1849 vec![
1850 "session Track",
1851 "prepare file:///music/track.flac",
1852 "play",
1853 "pause",
1854 "play",
1855 "stop",
1856 ]
1857 );
1858 }
1859
1860 #[test]
1861 fn playing_nothing_reports_that_nothing_is_loaded() {
1862 let (_guard, _player) = install();
1863
1864 assert_eq!(play_media(), Err(MediaError::NothingLoaded));
1865 assert_eq!(
1866 seek_media(Duration::from_secs(1)),
1867 Err(MediaError::NothingLoaded)
1868 );
1869 }
1870
1871 #[test]
1872 fn a_seek_is_clamped_to_the_item_rather_than_to_each_backend() {
1873 let (_guard, player) = install();
1874 open_media(track()).expect("opens");
1875
1876 seek_media(Duration::from_secs(1_000)).expect("seeks");
1877
1878 assert!(player.calls().contains(&"seek 200000".to_string()));
1879 }
1880
1881 #[test]
1882 fn a_seek_bar_fraction_maps_onto_the_item() {
1883 let (_guard, player) = install();
1884 open_media(track()).expect("opens");
1885
1886 seek_media_fraction(0.25).expect("seeks");
1887 seek_media_fraction(3.0).expect("clamps rather than refusing");
1888
1889 let calls = player.calls();
1890 assert!(calls.contains(&"seek 50000".to_string()));
1891 assert!(calls.contains(&"seek 200000".to_string()));
1892 }
1893
1894 #[test]
1895 fn a_stream_with_no_length_has_no_seek_bar_fraction() {
1896 let (_guard, _player) = install();
1897 open_media(MediaItem::new("https://host/live")).expect("opens");
1898
1899 assert_eq!(seek_media_fraction(0.5), Err(MediaError::NotSeekable));
1900 }
1901
1902 #[test]
1903 fn a_backend_that_cannot_seek_says_so_instead_of_moving_nothing() {
1904 let _guard = test_service_guard();
1905 clear_platform_media_player();
1906 set_platform_media_player(FakePlayer::with(MediaCapabilities {
1907 seeking: false,
1908 ..MediaCapabilities::TRANSPORT
1909 }));
1910
1911 open_media(track()).expect("opens");
1912 assert_eq!(
1913 seek_media(Duration::from_secs(1)),
1914 Err(MediaError::NotSeekable)
1915 );
1916 }
1917
1918 #[test]
1919 fn what_reaches_the_device_is_the_volume_combined_with_the_focus_gain() {
1920 let (_guard, player) = install();
1921 open_media(track()).expect("opens");
1922
1923 set_media_volume(0.5);
1924 assert_eq!(*player.volume.lock(), 0.5);
1925 assert_eq!(media_volume(), 0.5);
1926
1927 publish_audio_focus(AudioFocus::Ducked);
1928 assert_eq!(*player.volume.lock(), 0.5 * DUCKED_GAIN);
1929
1930 set_media_volume(1.0);
1933 assert_eq!(*player.volume.lock(), DUCKED_GAIN);
1934
1935 publish_audio_focus(AudioFocus::Gained);
1936 assert_eq!(*player.volume.lock(), 1.0);
1937 }
1938
1939 #[test]
1940 fn a_volume_outside_the_range_is_brought_back_into_it() {
1941 let (_guard, player) = install();
1942
1943 set_media_volume(4.0);
1944 assert_eq!(media_volume(), 1.0);
1945 set_media_volume(-1.0);
1946 assert_eq!(media_volume(), 0.0);
1947 assert_eq!(*player.volume.lock(), 0.0);
1948 }
1949
1950 #[test]
1951 fn a_transient_loss_pauses_and_the_next_gain_resumes() {
1952 let (_guard, _player) = install();
1953 open_media(track()).expect("opens");
1954 play_media().expect("plays");
1955
1956 publish_audio_focus(AudioFocus::LostTransient);
1957 assert_eq!(playback_state(), PlaybackState::Paused);
1958
1959 publish_audio_focus(AudioFocus::Gained);
1960 assert!(playback_state().is_playing());
1961 }
1962
1963 #[test]
1964 fn regaining_focus_does_not_resume_what_the_user_paused() {
1965 let (_guard, _player) = install();
1966 open_media(track()).expect("opens");
1967 play_media().expect("plays");
1968 pause_media();
1969
1970 publish_audio_focus(AudioFocus::LostTransient);
1971 publish_audio_focus(AudioFocus::Gained);
1972
1973 assert_eq!(playback_state(), PlaybackState::Paused);
1974 }
1975
1976 #[test]
1977 fn focus_lost_for_good_stops_and_does_not_come_back() {
1978 let (_guard, _player) = install();
1979 open_media(track()).expect("opens");
1980 play_media().expect("plays");
1981
1982 publish_audio_focus(AudioFocus::Lost);
1983 assert_eq!(playback_state(), PlaybackState::Idle);
1984
1985 publish_audio_focus(AudioFocus::Gained);
1986 assert_eq!(playback_state(), PlaybackState::Idle);
1987 }
1988
1989 #[test]
1990 fn session_commands_drive_the_transport_and_still_reach_the_application() {
1991 let (_guard, player) = install();
1992 open_media(track()).expect("opens");
1993 let seen = Arc::new(Mutex::new(Vec::new()));
1994 let recorder = Arc::clone(&seen);
1995 let _observer = observe_media_commands(move |command| recorder.lock().push(command));
1996
1997 publish_media_command(MediaCommand::Play);
1998 assert!(playback_state().is_playing());
1999 publish_media_command(MediaCommand::TogglePlayPause);
2000 assert_eq!(playback_state(), PlaybackState::Paused);
2001 publish_media_command(MediaCommand::SeekTo(Duration::from_secs(10)));
2002 publish_media_command(MediaCommand::Next);
2003
2004 assert_eq!(
2005 *seen.lock(),
2006 vec![
2007 MediaCommand::Play,
2008 MediaCommand::TogglePlayPause,
2009 MediaCommand::SeekTo(Duration::from_secs(10)),
2010 MediaCommand::Next,
2011 ]
2012 );
2013 assert!(player.calls().contains(&"seek 10000".to_string()));
2016 assert_eq!(playback_state(), PlaybackState::Paused);
2017 }
2018
2019 #[test]
2020 fn next_and_previous_are_the_commands_the_framework_leaves_alone() {
2021 assert!(MediaCommand::Play.is_transport());
2022 assert!(MediaCommand::SeekTo(Duration::ZERO).is_transport());
2023 assert!(!MediaCommand::Next.is_transport());
2024 assert!(!MediaCommand::Previous.is_transport());
2025 }
2026
2027 #[test]
2028 fn analysis_is_off_until_it_is_asked_for_and_only_where_it_exists() {
2029 let (_guard, player) = install();
2030 assert!(set_media_analysis_enabled(true));
2031 assert!(player.calls().contains(&"analysis true".to_string()));
2032
2033 clear_platform_media_player();
2034 set_platform_media_player(FakePlayer::with(MediaCapabilities::TRANSPORT));
2035 assert!(!set_media_analysis_enabled(true));
2036 }
2037
2038 #[test]
2047 fn the_backend_states_which_audio_formats_it_decodes() {
2048 let guard = test_service_guard();
2049 clear_platform_media_player();
2050 assert!(
2051 media_audio_extensions().is_empty(),
2052 "with no backend there is nothing to claim"
2053 );
2054
2055 struct Narrow;
2056 impl MediaPlayer for Narrow {
2057 fn capabilities(&self) -> MediaCapabilities {
2058 MediaCapabilities::TRANSPORT
2059 }
2060 fn prepare(&self, _item: &MediaItem) -> Result<(), MediaError> {
2061 Ok(())
2062 }
2063 fn play(&self) -> Result<(), MediaError> {
2064 Ok(())
2065 }
2066 fn pause(&self) {}
2067 fn stop(&self) {}
2068 fn set_volume(&self, _volume: f32) {}
2069 fn audio_extensions(&self) -> Vec<&'static str> {
2070 vec!["mp3", "wav"]
2071 }
2072 }
2073
2074 set_platform_media_player(Arc::new(Narrow));
2075 assert_eq!(media_audio_extensions(), vec!["mp3", "wav"]);
2076
2077 clear_platform_media_player();
2078 set_platform_media_player(FakePlayer::with(MediaCapabilities::TRANSPORT));
2079 assert!(
2080 media_audio_extensions().is_empty(),
2081 "a backend with no opinion says so rather than guessing"
2082 );
2083 drop(guard);
2084 }
2085
2086 #[test]
2087 fn the_newest_sample_block_replaces_the_stored_one() {
2088 let (_guard, _player) = install();
2089 let first = MediaSamples::new(48_000, 1, 1, vec![0.25; 8]).expect("block");
2090 let second = MediaSamples::new(48_000, 1, 2, vec![0.5; 8]).expect("block");
2091
2092 publish_media_samples(first);
2093 publish_media_samples(second.clone());
2094
2095 assert_eq!(latest_media_samples(), Some(second));
2096 record_dropped_media_samples();
2097 record_dropped_media_samples();
2098 assert_eq!(dropped_media_samples(), 2);
2099 }
2100
2101 #[test]
2102 fn turning_analysis_off_forgets_the_last_block() {
2103 let (_guard, _player) = install();
2104 publish_media_samples(MediaSamples::new(48_000, 1, 1, vec![0.25; 8]).expect("block"));
2105
2106 assert!(set_media_analysis_enabled(false));
2107
2108 assert_eq!(latest_media_samples(), None);
2109 }
2110
2111 #[test]
2112 fn observers_stop_being_called_once_they_are_dropped() {
2113 let (_guard, _player) = install();
2114 let seen = Arc::new(Mutex::new(0usize));
2115 let recorder = Arc::clone(&seen);
2116 let observer = observe_playback_progress(move |_| *recorder.lock() += 1);
2117
2118 publish_playback_progress(PlaybackProgress::new(
2119 Duration::from_secs(1),
2120 Duration::from_secs(10),
2121 ));
2122 let delivered = *seen.lock();
2123 drop(observer);
2124 publish_playback_progress(PlaybackProgress::new(
2125 Duration::from_secs(2),
2126 Duration::from_secs(10),
2127 ));
2128
2129 assert_eq!(*seen.lock(), delivered);
2130 }
2131
2132 #[test]
2133 fn published_progress_never_reads_past_the_end() {
2134 let (_guard, _player) = install();
2135 publish_playback_progress(PlaybackProgress {
2136 position: Duration::from_secs(99),
2137 duration: Some(Duration::from_secs(10)),
2138 buffered: Duration::from_secs(99),
2139 });
2140
2141 let progress = playback_progress();
2142 assert_eq!(progress.position, Duration::from_secs(10));
2143 assert_eq!(progress.buffered, Duration::from_secs(10));
2144 }
2145
2146 #[test]
2147 fn playing_holds_the_runtime_awake_and_stopping_lets_it_sleep() {
2148 let (_guard, _player) = install();
2149 assert!(!holds_background_work());
2150
2151 open_media(track()).expect("opens");
2152 assert!(
2153 !holds_background_work(),
2154 "an item that is open but not playing is not work the runtime must keep turning for"
2155 );
2156
2157 play_media().expect("plays");
2158 assert!(holds_background_work());
2159
2160 pause_media();
2161 assert!(!holds_background_work());
2162
2163 play_media().expect("plays");
2164 assert!(holds_background_work());
2165 stop_media();
2166 assert!(!holds_background_work());
2167 }
2168
2169 #[test]
2170 fn a_destroyed_host_stops_playback_but_a_backgrounded_one_does_not() {
2171 let (_guard, _player) = install();
2172 open_media(track()).expect("opens");
2173 play_media().expect("plays");
2174
2175 on_lifecycle(LifecycleEvent {
2176 from: LifecycleState::Resumed,
2177 to: LifecycleState::Stopped,
2178 });
2179 assert!(playback_state().is_playing());
2180
2181 on_lifecycle(LifecycleEvent {
2182 from: LifecycleState::Stopped,
2183 to: LifecycleState::Destroyed,
2184 });
2185 assert_eq!(playback_state(), PlaybackState::Idle);
2186 }
2187
2188 #[test]
2189 fn metadata_learned_after_playback_started_reaches_the_session() {
2190 let (_guard, player) = install();
2191 open_media(MediaItem::new("file:///music/untagged.mp3")).expect("opens");
2192
2193 set_media_metadata(MediaMetadata::titled("Late Tag").artist("Artist"));
2194
2195 assert!(player.calls().contains(&"session Late Tag".to_string()));
2196 assert_eq!(
2197 current_media_item().map(|item| item.metadata.title),
2198 Some("Late Tag".to_string())
2199 );
2200 }
2201
2202 #[test]
2203 fn metadata_with_nothing_in_it_is_metadata_a_lock_screen_can_skip() {
2204 assert!(MediaMetadata::default().is_empty());
2205 assert!(!MediaMetadata::titled("Track").is_empty());
2206 }
2207
2208 #[test]
2212 fn a_path_opens_here_and_everything_else_is_the_platforms() {
2213 struct Opener;
2214 impl MediaSourceOpener for Opener {
2215 fn open(&self, uri: &str) -> std::io::Result<MediaSourceHandle> {
2216 let path = crate::test_scratch_dir("media-source-opener").join("document.bin");
2217 std::fs::write(&path, uri.as_bytes())?;
2218 Ok(MediaSourceHandle {
2222 stream: File::open(path)?,
2223 len: Some(uri.len() as u64),
2224 })
2225 }
2226 }
2227 fn read(handle: MediaSourceHandle) -> String {
2228 let mut text = String::new();
2229 std::io::Read::read_to_string(&mut { handle.stream }, &mut text).expect("read");
2230 text
2231 }
2232
2233 clear_platform_media_source_opener();
2234 let path = crate::test_scratch_dir("media-source").join("track.bin");
2235 std::fs::write(&path, b"bytes").expect("write the fixture");
2236 let file = open_media_source(&uri_for_path(&path)).expect("the file");
2239 assert_eq!(file.len, Some(5), "a real file states its length");
2240 assert_eq!(read(file), "bytes");
2241 let error = open_media_source("content://provider/document/7").expect_err("no opener");
2244 assert_eq!(error.kind(), std::io::ErrorKind::Unsupported);
2245
2246 set_platform_media_source_opener(Arc::new(Opener));
2247 let opened = open_media_source("content://provider/document/7").expect("the opener");
2248 clear_platform_media_source_opener();
2249 assert_eq!(opened.len, Some(29));
2250 assert_eq!(read(opened), "content://provider/document/7");
2251 }
2252
2253 #[test]
2254 fn a_path_survives_the_round_trip_through_a_uri() {
2255 let path = PathBuf::from("/music/Sgt. Pepper's #1.mp3");
2256 let uri = uri_for_path(&path);
2257
2258 assert_eq!(uri, "file:///music/Sgt.%20Pepper%27s%20%231.mp3");
2259 assert_eq!(path_from_uri(&uri), Some(path));
2260 }
2261
2262 #[test]
2263 fn a_windows_path_keeps_its_drive_letter() {
2264 let uri = uri_for_path(Path::new("C:\\Music\\track.mp3"));
2265
2266 assert_eq!(uri, "file:///C%3A/Music/track.mp3");
2267 assert_eq!(
2268 path_from_uri(&uri),
2269 Some(PathBuf::from("C:/Music/track.mp3"))
2270 );
2271 }
2272
2273 #[test]
2274 fn a_bare_path_is_accepted_as_itself() {
2275 assert_eq!(
2276 path_from_uri("/music/track.mp3"),
2277 Some(PathBuf::from("/music/track.mp3"))
2278 );
2279 }
2280
2281 #[test]
2282 fn anything_that_is_not_a_local_file_has_no_path() {
2283 assert_eq!(path_from_uri("https://host/stream.mp3"), None);
2284 assert_eq!(path_from_uri("content://media/audio/1"), None);
2285 assert_eq!(path_from_uri("blob:https://host/abc"), None);
2286 assert_eq!(path_from_uri("file://host/share/track.mp3"), None);
2287 assert_eq!(path_from_uri(""), None);
2288 }
2289
2290 #[test]
2291 fn a_truncated_escape_is_not_guessed_at() {
2292 assert_eq!(path_from_uri("file:///music/track%2"), None);
2293 assert_eq!(path_from_uri("file:///music/track%zz.mp3"), None);
2294 }
2295
2296 #[test]
2297 fn speed_and_looping_reach_a_backend_that_has_them() {
2298 let (_guard, player) = install();
2299 assert!(set_media_speed(1.5));
2300 set_media_looping(true);
2301
2302 let calls = player.calls();
2303 assert!(calls.contains(&"speed 1.5".to_string()));
2304 assert!(calls.contains(&"looping true".to_string()));
2305 }
2306}