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}