Skip to main content

orinium_browser/engine/bridge/audio/
mod.rs

1//! Audio playback abstraction for the engine's media widgets.
2//!
3//! The engine lays out and drives UI widgets such as the `<audio>` control,
4//! but it must not own an audio device: decoding, output device selection
5//! and stream management are platform concerns. This module defines the
6//! interface the engine uses to talk to whatever the host environment
7//! provides, mirroring [`crate::engine::bridge::text`] for text measurement.
8//!
9//! # Responsibilities
10//!
11//! - Accept a media source (fetched bytes and/or a URI) and start playback
12//! - Expose transport controls (pause/resume) and playback position
13//! - Report whether the decoded source has finished
14//!
15//! # Non-Responsibilities
16//!
17//! - Deciding *when* to play (the engine's widget drives that)
18//! - Fetching media bytes (that goes through the resource loader)
19//!
20//! # Data Flow
21//!
22//! ```text
23//! <audio> element → AudioSinkFactory → AudioSink → platform audio device
24//! ```
25
26use std::fmt;
27use std::sync::Arc;
28
29/* ============================
30 * Errors
31 * ============================ */
32
33/// Failures surfaced by an [`AudioSink`].
34#[derive(Debug)]
35pub enum AudioError {
36    /// The media bytes could not be decoded.
37    Decode(String),
38    /// The platform audio device rejected the request.
39    Device(String),
40    /// Playback was requested before any source was loaded.
41    NotLoaded,
42}
43
44impl fmt::Display for AudioError {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            Self::Decode(s) => write!(f, "Failed to decode audio: {s}"),
48            Self::Device(s) => write!(f, "Audio device error: {s}"),
49            Self::NotLoaded => write!(f, "No audio source is loaded"),
50        }
51    }
52}
53
54impl std::error::Error for AudioError {}
55
56/* ============================
57 * Traits
58 * ============================ */
59
60/// A single playable audio source.
61///
62/// Implementations own whatever playback state the platform needs, so a sink
63/// is **not** shared between two `<audio>` elements — use
64/// [`AudioSinkFactory`] to obtain one instance per element.
65pub trait AudioSink: Send + Sync {
66    /// Decodes `data` and rewinds to the start, without starting playback.
67    fn preload(&self, data: &[u8]) -> Result<(), AudioError>;
68
69    /// Starts playback of `source`, restarting from the beginning if the
70    /// source is not currently playing.
71    ///
72    /// `data` carries the already-fetched media bytes when they are
73    /// available. Sources that the platform must resolve itself (for example
74    /// `file:` or `resource:` URIs) may ignore `data` and load `source`.
75    fn play(&self, source: &str, data: Option<&[u8]>) -> Result<(), AudioError>;
76
77    /// Pauses playback, preserving the current position.
78    fn pause(&self) -> Result<(), AudioError>;
79
80    /// Resumes playback from the current position.
81    fn resume(&self) -> Result<(), AudioError>;
82
83    /// The current playback position, in seconds.
84    fn current_seconds(&self) -> f32;
85
86    /// The decoded duration in seconds, or `0.0` when nothing is loaded.
87    fn duration_seconds(&self) -> f32;
88
89    /// Whether the decoded source reached its end.
90    fn is_finished(&self) -> bool;
91}
92
93/// Creates one [`AudioSink`] per media element.
94///
95/// Audio widgets are built off the UI thread during layout, so the engine
96/// cannot construct a platform sink itself. The host injects this factory
97/// through the layout task instead.
98pub trait AudioSinkFactory: Send + Sync {
99    /// Builds a sink for one `<audio>` element.
100    fn create(&self) -> Arc<dyn AudioSink>;
101}
102
103/* ============================
104 * Null (headless / test)
105 * ============================ */
106
107/// A factory producing sinks that decode and play nothing.
108///
109/// Keeps the engine usable in builds without an audio device, such as
110/// headless test runs.
111#[derive(Debug, Clone, Copy, Default)]
112pub struct NullAudioSinkFactory;
113
114impl AudioSinkFactory for NullAudioSinkFactory {
115    fn create(&self) -> Arc<dyn AudioSink> {
116        Arc::new(NullAudioSink)
117    }
118}
119
120#[derive(Debug)]
121struct NullAudioSink;
122
123impl AudioSink for NullAudioSink {
124    fn preload(&self, _data: &[u8]) -> Result<(), AudioError> {
125        Ok(())
126    }
127
128    fn play(&self, _source: &str, _data: Option<&[u8]>) -> Result<(), AudioError> {
129        Ok(())
130    }
131
132    fn pause(&self) -> Result<(), AudioError> {
133        Ok(())
134    }
135
136    fn resume(&self) -> Result<(), AudioError> {
137        Ok(())
138    }
139
140    fn current_seconds(&self) -> f32 {
141        0.0
142    }
143
144    fn duration_seconds(&self) -> f32 {
145        0.0
146    }
147
148    fn is_finished(&self) -> bool {
149        true
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn null_sink_accepts_everything_and_reports_no_progress() {
159        let factory = NullAudioSinkFactory;
160        let sink = factory.create();
161
162        assert!(sink.preload(b"not really audio").is_ok());
163        assert!(sink.play("resource:///audio/birds.mp3", None).is_ok());
164        assert!(sink.pause().is_ok());
165        assert!(sink.resume().is_ok());
166
167        assert_eq!(sink.current_seconds(), 0.0);
168        assert_eq!(sink.duration_seconds(), 0.0);
169        assert!(sink.is_finished());
170    }
171
172    #[test]
173    fn each_created_sink_is_distinct() {
174        let factory = NullAudioSinkFactory;
175        assert!(!Arc::ptr_eq(&factory.create(), &factory.create()));
176    }
177}