Skip to main content

kozan_core/html/
media_element.rs

1//! Media element trait — shared behavior for audio and video.
2//!
3//! Chrome equivalent: `HTMLMediaElement`.
4//! Intermediate trait between `HtmlElement` and concrete media elements.
5//!
6//! # Chrome hierarchy
7//!
8//! ```text
9//! HTMLElement
10//!   └── HTMLMediaElement         ← THIS TRAIT
11//!         ├── HTMLAudioElement
12//!         └── HTMLVideoElement   ← also ReplacedElement
13//! ```
14//!
15//! # What it provides
16//!
17//! - `src` / `set_src` — media source URL
18//! - `autoplay`, `loop_playback`, `muted`, `controls` — boolean attributes
19//! - `preload` — loading hint
20//! - Playback API stubs (play, pause, `current_time` — future)
21
22use super::html_element::HtmlElement;
23
24/// Media element ready states.
25///
26/// Chrome equivalent: `HTMLMediaElement::ReadyState`.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum MediaReadyState {
29    /// No information about the media resource.
30    #[default]
31    HaveNothing = 0,
32    /// Enough data to know duration and dimensions.
33    HaveMetadata = 1,
34    /// Data for the current playback position, but not enough for playback.
35    HaveCurrentData = 2,
36    /// Enough data to play at current position for a short while.
37    HaveFutureData = 3,
38    /// Enough data to play through to the end without buffering.
39    HaveEnoughData = 4,
40}
41
42/// Media element network states.
43///
44/// Chrome equivalent: `HTMLMediaElement::NetworkState`.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum MediaNetworkState {
47    /// No network activity.
48    #[default]
49    Empty = 0,
50    /// Fetching idle (no current fetch).
51    Idle = 1,
52    /// Actively downloading media data.
53    Loading = 2,
54    /// No suitable source found.
55    NoSource = 3,
56}
57
58/// Shared behavior for media elements (audio, video).
59///
60/// Chrome equivalent: `HTMLMediaElement`.
61///
62/// # Unimplemented — requires media backend
63///
64/// `play()`, `pause()`, `current_time()`, `duration()`, and `paused()` return
65/// safe defaults per the HTML spec until a media backend is wired in.
66pub trait MediaElement: HtmlElement {
67    // ---- Source ----
68
69    /// The media source URL.
70    fn src(&self) -> String {
71        self.attribute("src").unwrap_or_default()
72    }
73
74    fn set_src(&self, src: impl Into<String>) {
75        self.set_attribute("src", src);
76    }
77
78    // ---- Boolean attributes ----
79
80    /// Whether playback should start automatically.
81    fn autoplay(&self) -> bool {
82        self.attribute("autoplay").is_some()
83    }
84
85    fn set_autoplay(&self, autoplay: bool) {
86        if autoplay {
87            self.set_attribute("autoplay", "");
88        } else {
89            self.remove_attribute("autoplay");
90        }
91    }
92
93    /// Whether playback should loop.
94    fn loop_playback(&self) -> bool {
95        self.attribute("loop").is_some()
96    }
97
98    fn set_loop_playback(&self, looping: bool) {
99        if looping {
100            self.set_attribute("loop", "");
101        } else {
102            self.remove_attribute("loop");
103        }
104    }
105
106    /// Whether the audio is muted.
107    fn muted(&self) -> bool {
108        self.attribute("muted").is_some()
109    }
110
111    fn set_muted(&self, muted: bool) {
112        if muted {
113            self.set_attribute("muted", "");
114        } else {
115            self.remove_attribute("muted");
116        }
117    }
118
119    /// Whether the user agent should show controls.
120    fn controls(&self) -> bool {
121        self.attribute("controls").is_some()
122    }
123
124    fn set_controls(&self, controls: bool) {
125        if controls {
126            self.set_attribute("controls", "");
127        } else {
128            self.remove_attribute("controls");
129        }
130    }
131
132    // ---- Preload ----
133
134    /// The preload hint ("none", "metadata", "auto").
135    fn preload(&self) -> String {
136        self.attribute("preload")
137            .unwrap_or_else(|| "auto".to_string())
138    }
139
140    fn set_preload(&self, preload: impl Into<String>) {
141        self.set_attribute("preload", preload);
142    }
143
144    // ---- Playback state (stubs for now) ----
145
146    /// Current playback position in seconds.
147    fn current_time(&self) -> f64 {
148        0.0
149    }
150
151    /// Total duration in seconds. NaN if unknown.
152    fn duration(&self) -> f64 {
153        f64::NAN
154    }
155
156    /// Whether the media is currently paused.
157    fn paused(&self) -> bool {
158        true
159    }
160
161    /// Current ready state.
162    fn ready_state(&self) -> MediaReadyState {
163        MediaReadyState::HaveNothing
164    }
165
166    /// Current network state.
167    fn network_state(&self) -> MediaNetworkState {
168        MediaNetworkState::Empty
169    }
170
171    // ---- Playback actions (stubs for now) ----
172
173    /// Start playback. No-op until a media backend is wired in.
174    fn play(&self) {}
175
176    /// Pause playback. No-op until a media backend is wired in.
177    fn pause(&self) {}
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::dom::document::Document;
184    use crate::html::HtmlAudioElement;
185
186    #[test]
187    fn audio_src() {
188        let doc = Document::new();
189        let audio = doc.create::<HtmlAudioElement>();
190        assert_eq!(audio.src(), "");
191
192        audio.set_src("music.mp3");
193        assert_eq!(audio.src(), "music.mp3");
194    }
195
196    #[test]
197    fn audio_boolean_attrs() {
198        let doc = Document::new();
199        let audio = doc.create::<HtmlAudioElement>();
200
201        assert!(!audio.autoplay());
202        assert!(!audio.loop_playback());
203        assert!(!audio.muted());
204        assert!(!audio.controls());
205
206        audio.set_autoplay(true);
207        audio.set_controls(true);
208        assert!(audio.autoplay());
209        assert!(audio.controls());
210    }
211
212    #[test]
213    fn audio_default_ready_and_network_state() {
214        let doc = Document::new();
215        let audio = doc.create::<HtmlAudioElement>();
216        // paused/duration/current_time are unimplemented — no backend yet.
217        assert_eq!(audio.ready_state(), MediaReadyState::HaveNothing);
218        assert_eq!(audio.network_state(), MediaNetworkState::Empty);
219    }
220}