Skip to main content

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);