Skip to main content

freya_video/
player.rs

1use std::time::Duration;
2
3use async_io::Timer;
4use freya_core::{
5    elements::image::ImageHandle,
6    prelude::*,
7};
8
9use crate::{
10    VideoClient,
11    VideoEvent,
12    VideoSource,
13};
14
15/// Current playback state of a [`VideoPlayer`].
16#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
17pub enum PlaybackState {
18    #[default]
19    Idle,
20    Loading,
21    Playing,
22    Paused,
23    Ended,
24    Errored,
25}
26
27/// Reactive handle to a video decoding pipeline.
28#[derive(Clone, Copy, PartialEq, Eq)]
29pub struct VideoPlayer {
30    frame: State<Option<ImageHandle>>,
31    playback: State<PlaybackState>,
32    forwarder: State<Option<OwnedTaskHandle>>,
33    source: State<VideoSource>,
34    position: State<Duration>,
35    duration: State<Option<Duration>>,
36    volume: State<f32>,
37    client: State<Option<VideoClient>>,
38}
39
40impl VideoPlayer {
41    pub fn create(source: VideoSource) -> Self {
42        Self {
43            frame: State::create(None),
44            playback: State::create(PlaybackState::default()),
45            forwarder: State::create(None),
46            source: State::create(source),
47            position: State::create(Duration::ZERO),
48            duration: State::create(None),
49            volume: State::create(1.0),
50            client: State::create(None),
51        }
52    }
53
54    /// Latest decoded frame, if any.
55    pub fn frame(&self) -> Option<ImageHandle> {
56        self.frame.read().clone()
57    }
58
59    /// Current [`PlaybackState`].
60    pub fn state(&self) -> PlaybackState {
61        *self.playback.read()
62    }
63
64    /// Current playback position.
65    pub fn position(&self) -> Duration {
66        *self.position.read()
67    }
68
69    /// Total duration, if known.
70    pub fn duration(&self) -> Option<Duration> {
71        *self.duration.read()
72    }
73
74    /// Audio volume in `0.0..=1.0`, where `1.0` is the original level.
75    pub fn volume(&self) -> f32 {
76        *self.volume.read()
77    }
78
79    /// Set the audio volume, clamped to `0.0..=1.0`.
80    pub fn set_volume(&mut self, volume: f32) {
81        let volume = volume.clamp(0.0, 1.0);
82        self.volume.set(volume);
83        if let Some(client) = self.client.peek().as_ref() {
84            client.set_volume(volume);
85        }
86    }
87
88    /// Playback progress in `0.0..=100.0`.
89    pub fn progress(&self) -> f64 {
90        let Some(duration) = *self.duration.read() else {
91            return 0.0;
92        };
93        if duration.is_zero() {
94            return 0.0;
95        }
96        (self.position().as_secs_f64() / duration.as_secs_f64() * 100.0).clamp(0.0, 100.0)
97    }
98
99    /// Stop playback and reset to the beginning.
100    pub fn stop(&mut self) {
101        self.forwarder.set(None);
102        self.client.set(None);
103        self.frame.set(None);
104        self.playback.set(PlaybackState::Idle);
105        self.position.set(Duration::ZERO);
106    }
107
108    /// Resume playback, or start from the current position when idle.
109    pub fn play(&mut self) {
110        match self.state() {
111            PlaybackState::Paused => {
112                self.playback.set(PlaybackState::Playing);
113                if let Some(client) = self.client.peek().as_ref() {
114                    client.play();
115                }
116            }
117            PlaybackState::Idle => self.seek(self.position(), Duration::ZERO),
118            _ => {}
119        }
120    }
121
122    /// Pause playback.
123    pub fn pause(&mut self) {
124        if self.state() == PlaybackState::Playing {
125            self.playback.set(PlaybackState::Paused);
126            if let Some(client) = self.client.peek().as_ref() {
127                client.pause();
128            }
129        }
130    }
131
132    /// Toggle play/pause, or restart from the beginning when ended.
133    pub fn toggle(&mut self) {
134        match self.state() {
135            PlaybackState::Playing => self.pause(),
136            PlaybackState::Paused => self.play(),
137            PlaybackState::Ended => self.seek(Duration::ZERO, Duration::ZERO),
138            _ => {}
139        }
140    }
141
142    /// Replace the video source and start playing it from the beginning.
143    pub fn set_source(&mut self, source: impl Into<VideoSource>) {
144        self.source.set(source.into());
145        self.seek(Duration::ZERO, Duration::ZERO);
146    }
147
148    /// Seek to `position` after `debounce`, where a newer seek within the wait replaces it.
149    pub fn seek(&mut self, position: Duration, debounce: Duration) {
150        let start_paused = self.state() == PlaybackState::Paused;
151        self.position.set(position);
152        self.client.set(None);
153        self.playback.set(PlaybackState::Loading);
154
155        let source = self.source.peek().clone();
156        let player = *self;
157        let handle = spawn(async move {
158            Timer::after(debounce).await;
159            player.run(source, position, start_paused).await;
160        })
161        .owned();
162        self.forwarder.set(Some(handle));
163    }
164
165    /// Drive this player from a [`VideoClient`] decoding `source`.
166    async fn run(mut self, source: VideoSource, start_offset: Duration, start_paused: bool) {
167        let client = VideoClient::new(source, start_offset, start_paused, *self.volume.peek());
168        let events = client.events().clone();
169        self.client.set(Some(client));
170
171        while let Ok(event) = events.recv().await {
172            match event {
173                VideoEvent::Duration(duration) => {
174                    self.duration.set(Some(duration));
175                }
176                VideoEvent::Frame { frame, position } => {
177                    self.frame.set(Some(frame));
178                    self.position.set(position);
179                    if self.state() == PlaybackState::Loading {
180                        self.playback.set(if start_paused {
181                            PlaybackState::Paused
182                        } else {
183                            PlaybackState::Playing
184                        });
185                    }
186                }
187                VideoEvent::Ended => {
188                    self.playback.set(PlaybackState::Ended);
189                    break;
190                }
191                VideoEvent::Errored => {
192                    tracing::warn!("Video stream errored");
193                    self.playback.set(PlaybackState::Errored);
194                    break;
195                }
196            }
197        }
198    }
199}
200
201/// Create a [`VideoPlayer`] and start playing `video_source()`.
202///
203/// # Example
204///
205/// ```rust, no_run
206/// use freya::{
207///     elements::image::image,
208///     prelude::*,
209///     video::*,
210/// };
211///
212/// fn app() -> impl IntoElement {
213///     let player = use_video(|| "video.mp4");
214///
215///     rect().maybe_child(player.frame().map(image))
216/// }
217/// ```
218pub fn use_video<Source: Into<VideoSource>>(
219    video_source: impl FnOnce() -> Source + 'static,
220) -> VideoPlayer {
221    use_hook(move || {
222        let mut player = VideoPlayer::create(video_source().into());
223        player.play();
224        player
225    })
226}