Skip to main content

freya_video/
player.rs

1use std::time::Duration;
2
3use async_io::Timer;
4use freya_core::prelude::*;
5
6use crate::{
7    VideoClient,
8    VideoEvent,
9    VideoFrame,
10    VideoSource,
11};
12
13/// Current playback state of a [`VideoPlayer`].
14#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
15pub enum PlaybackState {
16    #[default]
17    Idle,
18    Loading,
19    Playing,
20    Paused,
21    Ended,
22    Errored,
23}
24
25/// Wait window before a seek actually spawns ffmpeg.
26const SEEK_DEBOUNCE: Duration = Duration::from_millis(150);
27
28/// Reactive handle to a video decoding pipeline.
29#[derive(Clone, Copy, PartialEq, Eq)]
30pub struct VideoPlayer {
31    frame: State<Option<VideoFrame>>,
32    playback: State<PlaybackState>,
33    forwarder: State<Option<OwnedTaskHandle>>,
34    source: State<Option<VideoSource>>,
35    position: State<Duration>,
36    duration: State<Option<Duration>>,
37    client: State<Option<VideoClient>>,
38}
39
40impl VideoPlayer {
41    /// Allocate the reactive slots in the current scope.
42    pub fn create() -> Self {
43        Self {
44            frame: State::create(None),
45            playback: State::create(PlaybackState::Idle),
46            forwarder: State::create(None),
47            source: State::create(None),
48            position: State::create(Duration::ZERO),
49            duration: State::create(None),
50            client: State::create(None),
51        }
52    }
53
54    /// Latest decoded frame, if any.
55    pub fn frame(&self) -> Option<VideoFrame> {
56        self.frame.read().as_ref().cloned()
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    /// Playback progress in `0.0..=100.0`.
75    pub fn progress(&self) -> f64 {
76        let position = *self.position.read();
77        let Some(duration) = *self.duration.read() else {
78            return 0.0;
79        };
80        if duration.is_zero() {
81            return 0.0;
82        }
83        (position.as_secs_f64() / duration.as_secs_f64() * 100.0).clamp(0.0, 100.0)
84    }
85
86    /// Stop playback and reset to the beginning.
87    pub fn stop(&mut self) {
88        self.forwarder.set(None);
89        self.client.set(None);
90        self.frame.set(None);
91        self.playback.set(PlaybackState::Idle);
92        self.position.set(Duration::ZERO);
93    }
94
95    /// Resume playback.
96    pub fn play(&mut self) {
97        if (self.playback)() == PlaybackState::Paused {
98            self.playback.set(PlaybackState::Playing);
99            if let Some(client) = self.client.peek().as_ref() {
100                client.play();
101            }
102        }
103    }
104
105    /// Pause playback.
106    pub fn pause(&mut self) {
107        if (self.playback)() == PlaybackState::Playing {
108            self.playback.set(PlaybackState::Paused);
109            if let Some(client) = self.client.peek().as_ref() {
110                client.pause();
111            }
112        }
113    }
114
115    /// Toggle play/pause, or restart from the beginning when ended.
116    pub fn toggle(&mut self) {
117        match (self.playback)() {
118            PlaybackState::Playing => self.pause(),
119            PlaybackState::Paused => self.play(),
120            PlaybackState::Ended => self.seek(Duration::ZERO),
121            _ => {}
122        }
123    }
124
125    /// Seek to `position`.
126    pub fn seek(&mut self, position: Duration) {
127        self.position.set(position);
128        self.client.set(None);
129        self.playback.set(PlaybackState::Loading);
130
131        let Some(source) = self.source.peek().as_ref().cloned() else {
132            self.forwarder.set(None);
133            return;
134        };
135        let player = *self;
136        let handle = spawn(async move {
137            Timer::after(SEEK_DEBOUNCE).await;
138            run_playback(source, position, player).await;
139        })
140        .owned();
141        self.forwarder.set(Some(handle));
142    }
143}
144
145/// Create a [`VideoPlayer`] and start playing `init()`.
146pub fn use_video(init: impl FnOnce() -> VideoSource + 'static) -> VideoPlayer {
147    use_hook(move || {
148        let source = init();
149        let mut player = VideoPlayer::create();
150        player.source.set(Some(source.clone()));
151        player.playback.set(PlaybackState::Loading);
152        let handle = spawn(run_playback(source, Duration::ZERO, player)).owned();
153        player.forwarder.set(Some(handle));
154        player
155    })
156}
157
158/// Play `source` from `start_offset` into `player`.
159async fn run_playback(source: VideoSource, start_offset: Duration, mut player: VideoPlayer) {
160    let client = VideoClient::new(source, start_offset);
161    let events = client.events().clone();
162    player.client.set(Some(client));
163
164    while let Ok(event) = events.recv().await {
165        match event {
166            VideoEvent::Duration(duration) => {
167                player.duration.set(Some(duration));
168            }
169            VideoEvent::Frame { frame, position } => {
170                player.frame.set(Some(frame));
171                player.position.set(position);
172                if (player.playback)() == PlaybackState::Loading {
173                    player.playback.set(PlaybackState::Playing);
174                }
175            }
176            VideoEvent::Ended => {
177                player.playback.set(PlaybackState::Ended);
178                break;
179            }
180            VideoEvent::Errored => {
181                player.playback.set(PlaybackState::Errored);
182                break;
183            }
184        }
185    }
186}