Skip to main content

pinray_core/
session.rs

1use std::time::Duration;
2
3use tracing::{debug, instrument};
4
5use crate::{
6    audio::AudioFrame,
7    backend::{AudioBackend, BackendBundle, BackendInfo, BackendResolver, VideoBackend},
8    config::{AudioCapture, CursorMode, SessionConfig, VideoCaptureTarget},
9    error::{PinrayError, Result},
10    frame::{CaptureEvent, ColorSpace, PixelFormat, Rect},
11};
12
13pub struct CaptureSession {
14    config: SessionConfig,
15    backend_info: BackendInfo,
16    video_backend: Option<Box<dyn VideoBackend>>,
17    audio_backend: Option<Box<dyn AudioBackend>>,
18    running: bool,
19}
20
21impl CaptureSession {
22    pub fn new(config: SessionConfig, bundle: BackendBundle) -> Self {
23        Self {
24            config,
25            backend_info: bundle.info,
26            video_backend: bundle.video,
27            audio_backend: bundle.audio,
28            running: false,
29        }
30    }
31
32    pub fn config(&self) -> &SessionConfig {
33        &self.config
34    }
35
36    pub fn backend_info(&self) -> &BackendInfo {
37        &self.backend_info
38    }
39
40    pub fn is_running(&self) -> bool {
41        self.running
42    }
43
44    #[instrument(skip(self), fields(backend = ?self.backend_info.kind))]
45    pub fn start(&mut self) -> Result<()> {
46        if self.running {
47            return Ok(());
48        }
49
50        if let Some(video) = self.video_backend.as_mut() {
51            video.start()?;
52        }
53
54        if let Some(audio) = self.audio_backend.as_mut() {
55            audio.start()?;
56        }
57
58        self.running = true;
59        debug!("capture session started");
60        Ok(())
61    }
62
63    #[instrument(skip(self), fields(backend = ?self.backend_info.kind))]
64    pub fn stop(&mut self) -> Result<()> {
65        if !self.running {
66            return Ok(());
67        }
68
69        if let Some(audio) = self.audio_backend.as_mut() {
70            audio.stop()?;
71        }
72
73        if let Some(video) = self.video_backend.as_mut() {
74            video.stop()?;
75        }
76
77        self.running = false;
78        debug!("capture session stopped");
79        Ok(())
80    }
81
82    pub fn next_event(&mut self, timeout: Option<Duration>) -> Result<CaptureEvent> {
83        match (self.video_backend.as_mut(), self.audio_backend.as_mut()) {
84            (Some(video), None) => video.next_event(timeout),
85            (None, Some(audio)) => audio.next_audio(timeout).map(CaptureEvent::Audio),
86            (Some(video), Some(audio)) => match video.next_event(timeout) {
87                Ok(event) => Ok(event),
88                Err(PinrayError::Timeout(_)) => audio.next_audio(timeout).map(CaptureEvent::Audio),
89                Err(error) => Err(error),
90            },
91            (None, None) => Err(PinrayError::BackendNotSelected),
92        }
93    }
94
95    pub fn next_audio(&mut self, timeout: Option<Duration>) -> Result<AudioFrame> {
96        let audio = self
97            .audio_backend
98            .as_mut()
99            .ok_or(PinrayError::BackendNotSelected)?;
100        audio.next_audio(timeout)
101    }
102}
103
104#[derive(Debug, Clone, Default)]
105pub struct SessionBuilder {
106    config: SessionConfig,
107}
108
109impl SessionBuilder {
110    pub fn new() -> Self {
111        Self::default()
112    }
113
114    pub fn config(&self) -> &SessionConfig {
115        &self.config
116    }
117
118    pub fn backend_preference(
119        mut self,
120        backend_preference: crate::backend::BackendPreference,
121    ) -> Self {
122        self.config.backend_preference = backend_preference;
123        self
124    }
125
126    pub fn video_target(mut self, target: VideoCaptureTarget) -> Self {
127        self.config.video_target = Some(target);
128        self
129    }
130
131    pub fn audio(mut self, audio_capture: AudioCapture) -> Self {
132        self.config.audio_capture = Some(audio_capture);
133        self
134    }
135
136    pub fn pixel_format(mut self, pixel_format: PixelFormat) -> Self {
137        self.config.pixel_format = pixel_format;
138        self
139    }
140
141    pub fn restore_token(mut self, restore_token: impl Into<String>) -> Self {
142        self.config.restore_token = Some(restore_token.into());
143        self
144    }
145
146    pub fn color_space(mut self, color_space: Option<ColorSpace>) -> Self {
147        self.config.color_space = color_space;
148        self
149    }
150
151    pub fn cursor_mode(mut self, cursor_mode: CursorMode) -> Self {
152        self.config.cursor_mode = cursor_mode;
153        self
154    }
155
156    pub fn crop_rect(mut self, crop_rect: Option<Rect>) -> Self {
157        self.config.crop_rect = crop_rect;
158        self
159    }
160
161    pub fn frame_rate(mut self, frame_rate: Option<u32>) -> Self {
162        self.config.frame_rate = frame_rate;
163        self
164    }
165
166    pub fn queue_depth(mut self, queue_depth: u32) -> Self {
167        self.config.queue_depth = queue_depth;
168        self
169    }
170
171    #[instrument(skip(self, resolver))]
172    pub fn build_with_resolver<R>(self, resolver: &R) -> Result<CaptureSession>
173    where
174        R: BackendResolver,
175    {
176        self.config.validate()?;
177        let bundle = resolver.resolve(&self.config)?;
178        Ok(CaptureSession::new(self.config, bundle))
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::SessionBuilder;
185
186    #[test]
187    fn builder_requires_video_or_audio() {
188        let config = SessionBuilder::new().config().clone();
189        assert!(config.validate().is_err());
190    }
191
192    #[test]
193    fn queue_depth_must_be_positive() {
194        let config = SessionBuilder::new().queue_depth(0).config().clone();
195        assert!(config.validate().is_err());
196    }
197}