azul_core/media_player.rs
1//! Media playback POD types - the state the six media events describe.
2//!
3//! Stateful manager lives in `azul_layout::managers::media_player`; this is
4//! only the value a callback reads back through
5//! `CallbackInfo::get_media_state`.
6//!
7//! `EventType::{Play, Pause, Ended, TimeUpdate, VolumeChange, MediaError}`
8//! shipped with no player behind them - the unified layer has a decoder, an
9//! encoder and an audio sink, but no transport, no position, no duration and
10//! no volume - so the six events described a state machine that did not
11//! exist and dispatched to nothing. This struct is that state (11c).
12
13/// What a media node's playback looks like right now.
14///
15/// Field order is by descending alignment (the `f32`s, then the `bool`s):
16/// the repo's alignment-order check is a hard error, and a `bool` wedged
17/// between two `f32`s is what trips it.
18#[repr(C)]
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct PlaybackState {
21 /// Playback position, in seconds from the start.
22 pub position_s: f32,
23 /// Total length in seconds, or `0.0` when it is not known yet (a live
24 /// stream, or metadata that has not arrived). An unknown duration means
25 /// playback never reaches an end, so `Ended` cannot fire.
26 pub duration_s: f32,
27 /// Output gain, `0.0..=1.0`.
28 pub volume: f32,
29 /// Whether the transport is running.
30 pub playing: bool,
31 /// Muted independently of `volume`, exactly as the web models it - so
32 /// unmuting restores the level the user had chosen.
33 pub muted: bool,
34}
35
36impl Default for PlaybackState {
37 fn default() -> Self {
38 Self {
39 position_s: 0.0,
40 duration_s: 0.0,
41 // Full volume, unmuted: the state a `<video>` starts in.
42 volume: 1.0,
43 playing: false,
44 muted: false,
45 }
46 }
47}
48
49impl PlaybackState {
50 /// How far through the media we are, `0.0..=1.0`, or `None` while the
51 /// duration is unknown - a progress bar with no known length is a real
52 /// state, not a zero.
53 #[must_use]
54 pub fn progress(&self) -> Option<f32> {
55 if self.duration_s > 0.0 {
56 Some((self.position_s / self.duration_s).clamp(0.0, 1.0))
57 } else {
58 None
59 }
60 }
61
62 /// The gain that actually reaches the sink: `0.0` while muted.
63 #[must_use]
64 pub fn effective_volume(&self) -> f32 {
65 if self.muted {
66 0.0
67 } else {
68 self.volume
69 }
70 }
71}
72
73// FFI Option wrapper for `CallbackInfo::get_media_state(node) ->
74// Option<PlaybackState>` (mirrors `OptionSensorReading`). `None` means "this
75// node is not a media node", which is a different answer from a default
76// state and the app can act on the difference.
77impl_option!(
78 PlaybackState,
79 OptionPlaybackState,
80 [Debug, Clone, Copy, PartialEq]
81);