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}