Skip to main content

cranpose_services/
media.rs

1//! Media playback: one item at a time, observable rather than polled.
2//!
3//! A media player is not a sound-effect engine. [`audio`](crate::audio) mixes
4//! short decoded cues; this plays one long encoded item — a track, a podcast,
5//! a recording — through whatever the platform already uses for media, and
6//! answers the four questions every player screen asks:
7//!
8//! * **What is it doing?** [`PlaybackState`] is published, not polled. A screen
9//!   that asks "is it playing yet?" every frame does that work whether or not
10//!   anything changed, and learns about a failure only by noticing that the
11//!   position stopped moving.
12//! * **Where is it?** [`PlaybackProgress`] carries position, duration and how
13//!   much is buffered, published by the backend as it moves. A seek bar reads
14//!   [`playback_progress`] while it drags and collects
15//!   [`rememberPlaybackProgress`] otherwise.
16//! * **May it be heard?** Audio focus is a contract with the rest of the
17//!   device, and every application gets it wrong in the same way: it ducks and
18//!   forgets to un-duck, or it resumes after a phone call it never paused for.
19//!   The policy lives here — see [`publish_audio_focus`] — so a backend only
20//!   has to report what the platform told it.
21//! * **What does the lock screen say?** [`MediaMetadata`] goes to the platform
22//!   media session, and the buttons on it come back as [`MediaCommand`]s. The
23//!   transport commands are carried out here; the ones that need a playlist are
24//!   handed to the application, because the framework does not have one.
25//!
26//! Analysis samples are **optional and capability-gated**. A visualiser wants
27//! the samples that are being heard; not every platform media stack will give
28//! them up, so [`MediaCapabilities::analysis`] says whether this one does
29//! instead of publishing silence that looks like a bug. When it does, samples
30//! are latest-wins and bounded exactly like camera frames: a visualiser that
31//! falls behind draws the sound that is playing now and counts what it missed.
32
33use 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
52/// The gain applied while another app is being heard over this one.
53///
54/// Ducking rather than pausing is what the platforms ask for on a transient
55/// interruption that can share the output — a navigation prompt over music.
56pub const DUCKED_GAIN: f32 = 0.2;
57
58/// Artwork for the platform media session.
59#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct MediaArtwork {
61    /// The encoded image, in whatever the tag carried.
62    pub bytes: Arc<[u8]>,
63    /// The image's media type, `image/jpeg` and `image/png` being what tags
64    /// actually contain.
65    pub mime: String,
66}
67
68impl MediaArtwork {
69    /// Artwork from encoded bytes.
70    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/// What the platform media session shows: the lock screen, the notification,
79/// the car head unit.
80#[derive(Clone, Debug, Default, PartialEq, Eq)]
81pub struct MediaMetadata {
82    pub title: String,
83    pub artist: String,
84    pub album: String,
85    /// The item's length when it is known before playback starts — from a tag,
86    /// or from a previous play. `None` means "ask the backend once it has
87    /// opened the item", which is what [`PlaybackProgress::duration`] reports.
88    pub duration: Option<Duration>,
89    pub artwork: Option<MediaArtwork>,
90}
91
92impl MediaMetadata {
93    /// Metadata carrying only a title, which is what a bare file name gives.
94    pub fn titled(title: impl Into<String>) -> MediaMetadata {
95        MediaMetadata {
96            title: title.into(),
97            ..MediaMetadata::default()
98        }
99    }
100
101    /// Sets the performer.
102    pub fn artist(mut self, artist: impl Into<String>) -> MediaMetadata {
103        self.artist = artist.into();
104        self
105    }
106
107    /// Sets the album.
108    pub fn album(mut self, album: impl Into<String>) -> MediaMetadata {
109        self.album = album.into();
110        self
111    }
112
113    /// Sets the length known ahead of playback.
114    pub fn duration(mut self, duration: Duration) -> MediaMetadata {
115        self.duration = Some(duration);
116        self
117    }
118
119    /// Sets the artwork.
120    pub fn artwork(mut self, artwork: MediaArtwork) -> MediaMetadata {
121        self.artwork = Some(artwork);
122        self
123    }
124
125    /// Whether there is anything worth showing on a lock screen.
126    pub fn is_empty(&self) -> bool {
127        self.title.is_empty() && self.artist.is_empty() && self.album.is_empty()
128    }
129}
130
131/// One playable item.
132///
133/// The source is a URI because that is the one form every platform media stack
134/// takes: `file:` and `content:` on Android, `file:` on desktop and iOS,
135/// `blob:` for a file the browser handed over, `http:` and `https:` everywhere.
136/// Handing the platform a URI is also what keeps a streamed item streaming
137/// instead of being read into memory first.
138#[derive(Clone, Debug, Default, PartialEq, Eq)]
139pub struct MediaItem {
140    pub uri: String,
141    pub metadata: MediaMetadata,
142}
143
144impl MediaItem {
145    /// An item at `uri`, with no metadata yet.
146    pub fn new(uri: impl Into<String>) -> MediaItem {
147        MediaItem {
148            uri: uri.into(),
149            metadata: MediaMetadata::default(),
150        }
151    }
152
153    /// The same item with metadata attached.
154    pub fn with_metadata(mut self, metadata: MediaMetadata) -> MediaItem {
155        self.metadata = metadata;
156        self
157    }
158
159    /// The title, falling back to the last path segment of the URI so a screen
160    /// always has something to show.
161    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/// What a platform media stack can actually do.
174///
175/// Reported rather than assumed: a screen greys out a speed control the device
176/// will not honour instead of offering one that silently does nothing.
177#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
178pub struct MediaCapabilities {
179    /// Whether [`seek_media`] moves the position.
180    pub seeking: bool,
181    /// Whether [`set_media_speed`] changes the rate.
182    pub speed: bool,
183    /// Whether [`set_media_looping`] repeats the item when it ends.
184    pub looping: bool,
185    /// Whether the backend can publish [`MediaSamples`] while it plays.
186    pub analysis: bool,
187    /// Whether metadata reaches a platform media session — the lock screen,
188    /// the notification, the headset buttons.
189    pub session: bool,
190    /// Whether the backend has an equalizer. The bands it has are reported by
191    /// [`media_equalizer_bands`], because a platform effect has the bands its
192    /// implementation has rather than the ones a screen would like.
193    pub equalizer: bool,
194    /// Whether [`probe_media_duration`] can read an item's length without
195    /// playing it. A playlist that shows durations for entries nobody has
196    /// opened needs this; one that does not, does not.
197    pub probing: bool,
198}
199
200impl MediaCapabilities {
201    /// A backend that plays, pauses and seeks and does nothing else, which is
202    /// the floor for anything worth calling a media player.
203    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/// One frequency band of a backend's equalizer.
215#[derive(Clone, Copy, Debug, PartialEq)]
216pub struct EqualizerBand {
217    /// The frequency the band is centred on, in hertz.
218    pub center_hz: f32,
219    /// The most this band can cut, in decibels — a negative number.
220    pub min_gain_db: f32,
221    /// The most this band can lift, in decibels.
222    pub max_gain_db: f32,
223}
224
225impl EqualizerBand {
226    /// A band centred on `center_hz` with a symmetric range.
227    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    /// Brings `gain_db` inside what this band can actually do.
237    pub fn clamp_gain(&self, gain_db: f32) -> f32 {
238        gain_db.clamp(self.min_gain_db, self.max_gain_db)
239    }
240}
241
242/// The octave centres a graphic equalizer is built on, in hertz.
243///
244/// The set a hardware graphic equalizer has had since long before software
245/// ones. A backend that builds its own filters — the desktop one, the browser
246/// one — reports these, so the same curve means the same thing on both. A
247/// platform effect reports whatever bands its implementation has instead.
248pub 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
252/// [`OCTAVE_BAND_CENTERS_HZ`] as bands, each able to lift or cut by `range_db`.
253pub 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/// What an equalizer is set to.
261///
262/// `gains_db` is read alongside the bands [`media_equalizer_bands`] reported:
263/// entry `n` is band `n`. A shorter list leaves the remaining bands flat, and a
264/// longer one is truncated, so a screen built for one band layout still says
265/// something sensible on a device with another.
266#[derive(Clone, Debug, Default, PartialEq)]
267pub struct EqualizerSettings {
268    /// Whether the equalizer is in circuit at all. A flat, disabled equalizer
269    /// is not the same as a flat, enabled one: the disabled one costs nothing.
270    pub enabled: bool,
271    /// Gain applied ahead of the bands, in decibels.
272    pub preamp_db: f32,
273    /// Per-band gain in decibels, in the order the bands were reported.
274    pub gains_db: Vec<f32>,
275}
276
277impl EqualizerSettings {
278    /// An enabled equalizer with every band flat.
279    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    /// This setting with every gain brought inside what `bands` can do, and
288    /// its length matched to theirs.
289    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    /// No media backend on this platform.
307    #[error("media playback is not supported here")]
308    Unsupported,
309    /// The backend cannot open this URI — an unknown scheme, a codec it has
310    /// no decoder for, a file that is not there.
311    #[error("cannot play {0}")]
312    UnsupportedSource(String),
313    /// A transport call arrived before anything was opened.
314    #[error("no media item is loaded")]
315    NothingLoaded,
316    /// The backend has no seek for this item — a live stream, or a container
317    /// without an index.
318    #[error("this item cannot be seeked")]
319    NotSeekable,
320    /// Any other failure the platform reported.
321    #[error("{0}")]
322    Failed(String),
323}
324
325/// What the player is doing.
326#[derive(Clone, Debug, Default, PartialEq, Eq)]
327pub enum PlaybackState {
328    /// Nothing is open.
329    #[default]
330    Idle,
331    /// An item is opening or refilling its buffer. A separate state rather
332    /// than a gap, because opening a network item takes long enough that a
333    /// screen has to say so.
334    Loading,
335    /// Sound is coming out.
336    Playing,
337    /// An item is open and positioned, and stopped.
338    Paused,
339    /// The item played to its end. Distinct from [`Paused`](Self::Paused):
340    /// this is what advances a playlist.
341    Ended,
342    /// The item could not be played, or playback ended in a failure.
343    Failed(MediaError),
344}
345
346impl PlaybackState {
347    /// Whether sound is coming out now.
348    pub fn is_playing(&self) -> bool {
349        matches!(self, PlaybackState::Playing)
350    }
351
352    /// Whether an item is open — playing, paused, or still opening.
353    pub fn is_active(&self) -> bool {
354        matches!(
355            self,
356            PlaybackState::Loading | PlaybackState::Playing | PlaybackState::Paused
357        )
358    }
359
360    /// The failure, if playback ended in one.
361    pub fn failure(&self) -> Option<&MediaError> {
362        match self {
363            PlaybackState::Failed(error) => Some(error),
364            _ => None,
365        }
366    }
367}
368
369/// Where the open item is.
370#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
371pub struct PlaybackProgress {
372    /// How far in the item playback has reached.
373    pub position: Duration,
374    /// The item's length, or `None` for a stream that has none.
375    pub duration: Option<Duration>,
376    /// How far ahead of the position the buffer reaches. Equal to `duration`
377    /// for a local file, which is what makes a local file's buffer bar full.
378    pub buffered: Duration,
379}
380
381impl PlaybackProgress {
382    /// Progress through an item of known length.
383    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    /// How far through the item this is, or `None` when it has no length.
392    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    /// How much of the item is buffered, or `None` when it has no length.
401    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/// A button pressed somewhere the application does not draw: a lock screen, a
411/// notification, a headset, a car.
412#[derive(Clone, Copy, Debug, PartialEq, Eq)]
413pub enum MediaCommand {
414    Play,
415    Pause,
416    /// The one button a headset has.
417    TogglePlayPause,
418    Stop,
419    /// Needs a playlist, so it is reported and not carried out.
420    Next,
421    /// Needs a playlist, so it is reported and not carried out.
422    Previous,
423    SeekTo(Duration),
424}
425
426impl MediaCommand {
427    /// Whether this command is one the framework carries out itself.
428    ///
429    /// The transport is player state and lives here. [`Next`](Self::Next) and
430    /// [`Previous`](Self::Previous) need an order the framework does not have,
431    /// so they are only reported.
432    pub fn is_transport(self) -> bool {
433        !matches!(self, MediaCommand::Next | MediaCommand::Previous)
434    }
435}
436
437/// What the rest of the device is doing with the output.
438#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
439pub enum AudioFocus {
440    /// This app may be heard at its own volume.
441    #[default]
442    Gained,
443    /// Something short is being said over the top — a navigation prompt.
444    /// Playback continues at [`DUCKED_GAIN`].
445    Ducked,
446    /// Something else has the output for a moment — a call, another player.
447    /// Playback pauses and resumes on the next [`Gained`](Self::Gained).
448    LostTransient,
449    /// Something else has the output for good. Playback stops and does not
450    /// come back on its own.
451    Lost,
452}
453
454/// Samples as they are being heard, for a visualiser.
455#[derive(Clone, Debug, PartialEq)]
456pub struct MediaSamples {
457    /// Samples per second per channel.
458    pub sample_rate: u32,
459    /// How many channels are interleaved in [`samples`](Self::samples).
460    pub channels: u16,
461    /// Interleaved samples, nominally in `[-1, 1]`.
462    pub samples: Arc<[f32]>,
463    /// Which block this is, so a visualiser can tell a repeat from a new one
464    /// and count what it missed.
465    pub sequence: u64,
466}
467
468impl MediaSamples {
469    /// A block of samples, or `None` when the layout does not describe the
470    /// data — which reads as a broken visualiser rather than as an error if it
471    /// is let through.
472    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    /// How many samples there are per channel.
491    pub fn frames(&self) -> usize {
492        self.samples.len() / self.channels.max(1) as usize
493    }
494
495    /// How long this block lasts.
496    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
504/// A platform media stack.
505///
506/// A backend opens items, drives the transport, and publishes what happens
507/// through [`publish_playback_state`], [`publish_playback_progress`],
508/// [`publish_audio_focus`], [`publish_media_command`] and
509/// [`publish_media_samples`]. Nothing here is polled, and no method blocks for
510/// the length of an item.
511///
512/// Applications call the free functions — [`open_media`], [`play_media`],
513/// [`seek_media`] — rather than this trait: the free functions are where volume
514/// is combined with the focus gain, where the background-work lease is held,
515/// and where a seek is clamped to the item.
516pub trait MediaPlayer: Send + Sync {
517    /// What this backend can do. Read by screens to decide which controls
518    /// exist at all.
519    fn capabilities(&self) -> MediaCapabilities;
520
521    /// Opens `item` and gets it ready to play, without playing it.
522    ///
523    /// Returns as soon as the request is accepted; the item's progress arrives
524    /// as [`PlaybackState`], because opening a network item takes as long as
525    /// the network does.
526    fn prepare(&self, item: &MediaItem) -> Result<(), MediaError>;
527
528    /// Starts, or resumes, the open item.
529    fn play(&self) -> Result<(), MediaError>;
530
531    /// Stops without giving up the position.
532    fn pause(&self);
533
534    /// Stops, closes the item and releases the output device.
535    fn stop(&self);
536
537    /// Moves the position within the open item.
538    fn seek_to(&self, _position: Duration) -> Result<(), MediaError> {
539        Err(MediaError::NotSeekable)
540    }
541
542    /// Sets the output gain, already combined with the audio-focus gain by
543    /// [`set_media_volume`]. `0.0` is silent, `1.0` is the item as recorded.
544    fn set_volume(&self, volume: f32);
545
546    /// Sets the playback rate, `1.0` being as recorded. Returns `false` where
547    /// the backend does not have one.
548    fn set_speed(&self, _speed: f32) -> bool {
549        false
550    }
551
552    /// Repeats the open item when it reaches its end.
553    fn set_looping(&self, _looping: bool) {}
554
555    /// Starts or stops publishing [`MediaSamples`]. Returns `false` where the
556    /// backend cannot produce them, which is also what
557    /// [`MediaCapabilities::analysis`] reports.
558    fn set_analysis_enabled(&self, _enabled: bool) -> bool {
559        false
560    }
561
562    /// Hands metadata to the platform media session. Called again whenever the
563    /// application learns more about the open item, because tags are often
564    /// parsed after playback has already started.
565    fn set_session_metadata(&self, _metadata: &MediaMetadata) {}
566
567    /// The equalizer bands this backend has, centre frequency and range.
568    ///
569    /// Empty where there is no equalizer, which is also what
570    /// [`MediaCapabilities::equalizer`] reports. A backend states its real
571    /// bands: a platform effect has the ones its implementation has, and a
572    /// screen that wants a different layout maps onto these rather than being
573    /// told a layout that is not there.
574    fn equalizer_bands(&self) -> Vec<EqualizerBand> {
575        Vec::new()
576    }
577
578    /// Applies an equalizer setting, already clamped to this backend's bands.
579    fn set_equalizer(&self, _settings: &EqualizerSettings) {}
580
581    /// The audio file extensions this backend can decode, lower case and
582    /// without the dot.
583    ///
584    /// An application that picks tracks off a disk decides what to offer from
585    /// this rather than from a list of its own. Which formats play is a
586    /// property of the stack underneath — the platform's decoders on a phone,
587    /// the ones compiled in on a desktop — and a list written next to the
588    /// picker is a claim about a backend it never asks. It goes stale the
589    /// moment the backend changes, and the failure is quiet: the tracks import
590    /// and then refuse to play.
591    ///
592    /// Empty where the backend cannot say, which a caller should read as "no
593    /// opinion, offer what you like" rather than as "nothing plays".
594    fn audio_extensions(&self) -> Vec<&'static str> {
595        Vec::new()
596    }
597
598    /// Reads how long `item` is without opening it for playback.
599    ///
600    /// A playlist shows the length of entries nobody has played yet, and the
601    /// only thing that can answer is the stack that reads the container.
602    /// `None` where this backend cannot tell, which is also what
603    /// [`MediaCapabilities::probing`] reports; a screen leaves the duration
604    /// blank rather than treating it as an error.
605    fn probe_duration(&self, _item: &MediaItem) -> Option<Duration> {
606        None
607    }
608}
609
610/// Shared handle to the platform media player.
611pub type MediaPlayerRef = Arc<dyn MediaPlayer>;
612
613static PLATFORM_MEDIA: ServiceRegistry<dyn MediaPlayer> = ServiceRegistry::new();
614
615/// Installs the platform media player, replacing any previous one.
616pub fn set_platform_media_player(player: MediaPlayerRef) {
617    PLATFORM_MEDIA.set(player);
618}
619
620/// Removes the platform media player and forgets everything it published.
621pub 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
642/// The installed media player, or `None` where this platform has none.
643pub fn media_player() -> Option<MediaPlayerRef> {
644    PLATFORM_MEDIA.get()
645}
646
647/// Whether this platform can play media at all.
648pub fn media_playback_supported() -> bool {
649    PLATFORM_MEDIA.get().is_some()
650}
651
652/// What the installed backend can do, or [`MediaCapabilities::default`] — every
653/// capability absent — when there is none.
654pub fn media_capabilities() -> MediaCapabilities {
655    media_player()
656        .map(|player| player.capabilities())
657        .unwrap_or_default()
658}
659
660// -- Published state ---------------------------------------------------------
661
662static 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);
671/// The volume the application asked for, before the focus gain is applied.
672static VOLUME: Mutex<f32> = Mutex::new(1.0);
673/// The equalizer curve the application asked for. Kept whether or not a
674/// platform can apply it, so a stored user setting survives a device that
675/// cannot honour it and reaches one that can.
676static 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
683/// Sample blocks produced while every observer was still busy with an earlier
684/// one. Counted rather than queued, for the same reason camera frames are: a
685/// visualiser that falls behind should draw the sound that is playing now.
686static DROPPED_SAMPLES: AtomicU64 = AtomicU64::new(0);
687
688/// What the player is doing.
689pub fn playback_state() -> PlaybackState {
690    STATE.lock().clone()
691}
692
693/// Where the open item is.
694///
695/// Read outside composition — while a seek bar is being dragged, or during
696/// draw — so a moving position costs no recomposition.
697pub fn playback_progress() -> PlaybackProgress {
698    *PROGRESS.lock()
699}
700
701/// The open item, or `None` when nothing is.
702pub fn current_media_item() -> Option<MediaItem> {
703    CURRENT_ITEM.lock().clone()
704}
705
706/// The last block of samples, or `None` when analysis is off or nothing has
707/// played yet. Read during draw, so a visualiser never draws a stale block.
708pub fn latest_media_samples() -> Option<MediaSamples> {
709    LATEST_SAMPLES.lock().clone()
710}
711
712/// How many sample blocks were produced while every observer was still busy.
713pub fn dropped_media_samples() -> u64 {
714    DROPPED_SAMPLES.load(Ordering::Acquire)
715}
716
717/// What the rest of the device is doing with the output.
718pub fn audio_focus() -> AudioFocus {
719    *FOCUS.lock()
720}
721
722/// The volume the application asked for, before the audio-focus gain.
723pub fn media_volume() -> f32 {
724    *VOLUME.lock()
725}
726
727// -- Observers ---------------------------------------------------------------
728
729/// One registry of callbacks. Every published signal has the same shape, so it
730/// is written once and instantiated per signal rather than copied per signal.
731struct 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
779/// Keeps a media observer registered until it is dropped.
780pub 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
791/// Registers `observer` for playback state. The current state is delivered at
792/// once, so a screen composed mid-item shows what is happening rather than
793/// waiting for the next change.
794pub 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
806/// Registers `observer` for position updates. The current position is
807/// delivered at once.
808pub 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
820/// Registers `observer` for media-session commands.
821pub 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
831/// Registers `observer` for audio-focus changes. The current focus is
832/// delivered at once.
833pub 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
843/// Registers `observer` for analysis samples.
844pub 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
854// -- Publishing (called by backends) -----------------------------------------
855
856/// Publishes what the player is doing.
857///
858/// This is also where the background-work lease is taken and given up: an app
859/// that is playing has work the runtime must keep turning for even with its
860/// surface gone, and an app that has stopped does not.
861pub 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
887/// Publishes where the open item is. Backends call this as the position moves,
888/// which for a local file is a handful of times a second.
889pub 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
911/// Publishes a button pressed outside the application's own UI.
912///
913/// The transport commands are carried out here before observers are told, so an
914/// application that only wants to advance its playlist has nothing to wire up:
915/// it collects [`rememberMediaCommands`] and reacts to
916/// [`MediaCommand::Next`] and [`MediaCommand::Previous`].
917pub 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
935/// Publishes what the rest of the device is doing with the output, and applies
936/// the policy that goes with it.
937///
938/// The policy is the whole point of this living in the framework:
939///
940/// * [`Ducked`](AudioFocus::Ducked) lowers the gain to [`DUCKED_GAIN`] and
941///   keeps playing; regaining focus puts the application's own volume back,
942///   whatever it changed to in the meantime.
943/// * [`LostTransient`](AudioFocus::LostTransient) pauses **and remembers that
944///   it did**, so the next [`Gained`](AudioFocus::Gained) resumes — and a
945///   [`Gained`](AudioFocus::Gained) that follows a user's own pause does not.
946/// * [`Lost`](AudioFocus::Lost) stops and forgets, because focus lost for good
947///   does not come back.
948pub 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
980/// Publishes a block of samples as it is heard.
981///
982/// The newest block always replaces the stored one, so a visualiser drawing
983/// [`latest_media_samples`] never draws a stale one; observers that keep up see
984/// every block, and blocks nobody could take are counted in
985/// [`dropped_media_samples`] rather than queued behind.
986pub 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
997/// Records that the backend produced a block nobody could take.
998pub fn record_dropped_media_samples() {
999    DROPPED_SAMPLES.fetch_add(1, Ordering::AcqRel);
1000}
1001
1002// -- Transport (called by applications) --------------------------------------
1003
1004/// Opens `item`, publishing [`PlaybackState::Loading`] before the backend is
1005/// asked so a screen shows the wait rather than a gap.
1006///
1007/// The item is not played: an application that wants it to start calls
1008/// [`play_media`] when the backend publishes [`PlaybackState::Paused`], or
1009/// simply calls it straight away — a backend queues the request against the
1010/// item it is opening.
1011pub 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
1033/// Starts, or resumes, the open item.
1034pub 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
1046/// Stops without giving up the position.
1047pub fn pause_media() {
1048    if let Some(player) = media_player() {
1049        player.pause();
1050    }
1051}
1052
1053/// Stops, closes the item and releases the output device.
1054pub 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
1062/// Pauses what is playing and plays what is paused — the one button a headset
1063/// has, and the space bar.
1064pub fn toggle_media() {
1065    if playback_state().is_playing() {
1066        pause_media();
1067    } else {
1068        let _ = play_media();
1069    }
1070}
1071
1072/// Moves the position within the open item.
1073///
1074/// Clamped to the item's length here rather than in every backend, because a
1075/// seek past the end means different things to different platform stacks and
1076/// none of them mean what the seek bar meant.
1077pub 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
1094/// Moves the position to a fraction of the item, which is what a seek bar has.
1095///
1096/// Reports [`MediaError::NotSeekable`] for an item with no length, because a
1097/// fraction of an unknown length is not a position.
1098pub 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
1106/// Sets the volume the application asks for, `1.0` being the item as recorded.
1107///
1108/// What reaches the device is this combined with the audio-focus gain, so an
1109/// application may set its volume freely while another app is being heard over
1110/// the top without undoing the duck.
1111pub 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
1127/// Sets the playback rate, `1.0` being as recorded. Returns `false` where the
1128/// backend has none — see [`MediaCapabilities::speed`].
1129pub 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
1136/// Repeats the open item when it reaches its end.
1137pub fn set_media_looping(looping: bool) {
1138    if let Some(player) = media_player() {
1139        player.set_looping(looping);
1140    }
1141}
1142
1143/// Starts or stops publishing [`MediaSamples`]. Returns `false` where the
1144/// backend cannot produce them — see [`MediaCapabilities::analysis`].
1145///
1146/// Off by default: producing samples costs the platform work on every block,
1147/// and a screen with no visualiser on it should not pay for one.
1148pub 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
1160/// Reads how long `item` is without playing it.
1161///
1162/// `None` where no backend is installed or the installed one cannot tell —
1163/// see [`MediaCapabilities::probing`].
1164pub fn probe_media_duration(item: &MediaItem) -> Option<Duration> {
1165    media_player()?.probe_duration(item)
1166}
1167
1168/// The equalizer bands this platform has, in the order gains are given in.
1169///
1170/// Empty where there is no equalizer. A screen reads this to know how many
1171/// controls to draw and what to label them, rather than assuming a layout.
1172pub 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
1179/// The audio file extensions the platform backend can decode, lower case and
1180/// without the dot.
1181///
1182/// Empty where there is no backend, or where the backend has no opinion. See
1183/// [`MediaPlayer::audio_extensions`].
1184pub fn media_audio_extensions() -> Vec<&'static str> {
1185    media_player()
1186        .map(|player| player.audio_extensions())
1187        .unwrap_or_default()
1188}
1189
1190/// The equalizer setting last applied.
1191pub fn media_equalizer() -> EqualizerSettings {
1192    EQUALIZER.lock().clone()
1193}
1194
1195/// Applies an equalizer setting, clamped to what the platform's bands can do.
1196///
1197/// Returns `false` where there is no equalizer — see
1198/// [`MediaCapabilities::equalizer`]. The setting is remembered either way, so a
1199/// screen that stores a user's curve reads back what the user chose rather than
1200/// what a device happened to support.
1201pub 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
1213/// Updates the metadata shown by the platform media session for the open item.
1214///
1215/// Called when tags finish parsing, which is usually after playback started.
1216pub 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
1231// -- Lifecycle ---------------------------------------------------------------
1232
1233static 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/// Whether playback is what is keeping the runtime turning.
1247///
1248/// The lease count is one number for the whole process — a durable save holds
1249/// leases too — so this asks about the one this service took rather than about
1250/// the total.
1251#[cfg(test)]
1252fn holds_background_work() -> bool {
1253    BACKGROUND_LEASE.lock().is_some()
1254}
1255
1256/// Applies a host lifecycle transition to playback.
1257///
1258/// Backgrounding does **not** stop a media player: that is the difference
1259/// between a media player and every other service, and it is why playback holds
1260/// a background-work lease while it runs. A host being destroyed does stop it,
1261/// because the device it holds outlives the surface that was drawing.
1262pub(crate) fn on_lifecycle(event: LifecycleEvent) {
1263    if event.to == LifecycleState::Destroyed {
1264        stop_media();
1265    }
1266}
1267
1268// -- Local-file URIs ---------------------------------------------------------
1269
1270/// The `file:` URI for a path, which is what [`MediaItem`] takes.
1271///
1272/// Percent-encodes everything a URI reserves, so a track called `Sgt. Pepper's
1273/// #1.mp3` survives the trip. Lives here rather than in a backend because
1274/// every backend that reads local files needs the same answer, and an
1275/// application building an item needs it too.
1276pub 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        // A Windows path (`C:\Music\x.mp3`) has no leading slash of its own,
1282        // and `file://C:/...` would read `C:` as a host.
1283        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
1297/// The path a media URI addresses, or `None` when it addresses something that
1298/// is not a local file — a stream, a content provider, a browser blob.
1299///
1300/// A bare path is accepted as itself: an application that already has a
1301/// `PathBuf` should not have to build a URI to hand it back.
1302pub 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    // `file:///path` has an empty authority; `file://host/path` names a host
1309    // no local backend can read.
1310    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    // A Windows path came through as `C:/Music/x.mp3`; anything else that has
1316    // lost its leading slash is put back where it was.
1317    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// -- Opening a URI for the in-process decoder --------------------------------
1332
1333/// A media stream the platform opened, and what it knows about it.
1334#[derive(Debug)]
1335pub struct MediaSourceHandle {
1336    /// The descriptor to read. Seekable for a real file; a pipe for a provider
1337    /// that streams.
1338    pub stream: File,
1339    /// How long the whole thing is, when the platform knows.
1340    ///
1341    /// A provider that streams cannot answer `stat` — that is what makes its
1342    /// descriptor a pipe — but it listed a size for the document all the same,
1343    /// and a decoder that knows the length can seek by it instead of waiting
1344    /// for the stream to end to find out where the end is.
1345    pub len: Option<u64>,
1346}
1347
1348/// Opens a media URI the decoder cannot open for itself.
1349///
1350/// A `file:` URI is a path and needs nobody, which is why
1351/// [`open_media_source`] answers those without asking. A `content://` document
1352/// belongs to an Android provider and only the platform layer can ask that
1353/// provider for a descriptor, so the platform layer registers this and the
1354/// decode thread calls it.
1355///
1356/// A descriptor rather than a reader because that is what both sides really
1357/// have: a real file is seekable, a provider that streams hands back a pipe,
1358/// and the decoder tells them apart by trying to seek.
1359pub trait MediaSourceOpener: Send + Sync {
1360    /// Opens `uri` for reading.
1361    fn open(&self, uri: &str) -> std::io::Result<MediaSourceHandle>;
1362}
1363
1364/// Shared handle to the platform media source opener.
1365pub type MediaSourceOpenerRef = Arc<dyn MediaSourceOpener>;
1366
1367static PLATFORM_MEDIA_SOURCE: ServiceRegistry<dyn MediaSourceOpener> = ServiceRegistry::new();
1368
1369/// Installs the platform media source opener, replacing any previous one.
1370pub fn set_platform_media_source_opener(opener: MediaSourceOpenerRef) {
1371    PLATFORM_MEDIA_SOURCE.set(opener);
1372}
1373
1374/// Removes the platform media source opener.
1375pub fn clear_platform_media_source_opener() {
1376    PLATFORM_MEDIA_SOURCE.clear();
1377}
1378
1379/// Opens `uri` for decoding.
1380///
1381/// `file:` URIs and bare paths are opened here. Anything else is the platform's
1382/// to answer, and a platform that registered no opener gets
1383/// [`ErrorKind::Unsupported`](std::io::ErrorKind::Unsupported) rather than a
1384/// guess.
1385pub 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// -- Composables -------------------------------------------------------------
1401
1402/// What the player is doing, observed for as long as this call stays in the
1403/// composition.
1404#[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/// Where the open item is, observed for as long as this call stays in the
1414/// composition.
1415///
1416/// This recomposes as the position moves, which is what a seek bar and a time
1417/// label want. A visualiser or a waveform that redraws every frame anyway reads
1418/// [`playback_progress`] during draw instead.
1419#[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/// What the rest of the device is doing with the output, observed for as long
1429/// as this call stays in the composition.
1430#[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/// Buttons pressed outside the application's own UI, as a stream this
1440/// composition collects.
1441///
1442/// The transport commands have already been carried out by the time they arrive
1443/// here; what an application acts on is [`MediaCommand::Next`] and
1444/// [`MediaCommand::Previous`], which need the playlist it owns.
1445#[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/// Samples as they are heard, as a stream this composition collects.
1454///
1455/// Enable them with [`set_media_analysis_enabled`] first; a backend that cannot
1456/// produce them says so through [`MediaCapabilities::analysis`].
1457#[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    /// A backend that records what it was asked and publishes what a real one
1471    /// would.
1472    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            // Three bands with a narrow range, so a test can tell a clamp from
1582            // a pass-through and a band count from a hard-coded ten.
1583            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        // An album on its own is still something to show.
1630        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            // Too loud for these bands, and one entry too many.
1653            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        // No backend: the user's curve is still theirs, and reaches the next
1707        // device that can honour it.
1708        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        // The application may still change its own volume while ducked, and
1931        // doing so must not undo the duck.
1932        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        // `Next` needs a playlist the framework does not have, so it reached
2014        // the application without touching the transport.
2015        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    /// A backend states the formats it decodes, so a picker offers what will
2039    /// play rather than what some list next to it once claimed.
2040    ///
2041    /// CranAmp shipped the second: a hardcoded list left over from decoding in
2042    /// process, kept after the tracks were handed to the platform's decoder.
2043    /// It went on offering AIFF and CAF on a phone whose `MediaPlayer` reads
2044    /// neither, so they imported and then refused to play with nothing on
2045    /// screen to say why.
2046    #[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    /// Who answers for a URI: a path is opened here, and anything else is the
2209    /// platform's. One test rather than three, because the opener is a process
2210    /// -wide registry and three would race each other clearing it.
2211    #[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                // A streaming provider knows the length it listed even though
2219                // its descriptor cannot be stat-ed, which is the case this
2220                // handle exists to carry.
2221                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        // A file is a file on every target that has a filesystem, so no
2237        // platform has to be asked about one.
2238        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        // A document URI on a platform that registered nothing says so rather
2242        // than guessing at a path.
2243        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}