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)) => {
87 match audio.next_audio(Some(Duration::ZERO)) {
91 Ok(frame) => return Ok(CaptureEvent::Audio(frame)),
92 Err(PinrayError::Timeout(_)) => {}
93 Err(error) => return Err(error),
94 }
95 match video.next_event(timeout) {
96 Ok(event) => Ok(event),
97 Err(PinrayError::Timeout(_)) => {
98 audio.next_audio(timeout).map(CaptureEvent::Audio)
99 }
100 Err(error) => Err(error),
101 }
102 }
103 (None, None) => Err(PinrayError::BackendNotSelected),
104 }
105 }
106
107 pub fn next_audio(&mut self, timeout: Option<Duration>) -> Result<AudioFrame> {
108 let audio = self
109 .audio_backend
110 .as_mut()
111 .ok_or(PinrayError::BackendNotSelected)?;
112 audio.next_audio(timeout)
113 }
114}
115
116#[derive(Debug, Clone, Default)]
117pub struct SessionBuilder {
118 config: SessionConfig,
119}
120
121impl SessionBuilder {
122 pub fn new() -> Self {
123 Self::default()
124 }
125
126 pub fn config(&self) -> &SessionConfig {
127 &self.config
128 }
129
130 pub fn backend_preference(
131 mut self,
132 backend_preference: crate::backend::BackendPreference,
133 ) -> Self {
134 self.config.backend_preference = backend_preference;
135 self
136 }
137
138 pub fn video_target(mut self, target: VideoCaptureTarget) -> Self {
139 self.config.video_target = Some(target);
140 self
141 }
142
143 pub fn audio(mut self, audio_capture: AudioCapture) -> Self {
144 self.config.audio_capture = Some(audio_capture);
145 self
146 }
147
148 pub fn pixel_format(mut self, pixel_format: PixelFormat) -> Self {
149 self.config.pixel_format = pixel_format;
150 self
151 }
152
153 pub fn restore_token(mut self, restore_token: impl Into<String>) -> Self {
154 self.config.restore_token = Some(restore_token.into());
155 self
156 }
157
158 pub fn color_space(mut self, color_space: Option<ColorSpace>) -> Self {
159 self.config.color_space = color_space;
160 self
161 }
162
163 pub fn cursor_mode(mut self, cursor_mode: CursorMode) -> Self {
164 self.config.cursor_mode = cursor_mode;
165 self
166 }
167
168 pub fn crop_rect(mut self, crop_rect: Option<Rect>) -> Self {
169 self.config.crop_rect = crop_rect;
170 self
171 }
172
173 pub fn frame_rate(mut self, frame_rate: Option<u32>) -> Self {
174 self.config.frame_rate = frame_rate;
175 self
176 }
177
178 pub fn queue_depth(mut self, queue_depth: u32) -> Self {
179 self.config.queue_depth = queue_depth;
180 self
181 }
182
183 #[instrument(skip(self, resolver))]
184 pub fn build_with_resolver<R>(self, resolver: &R) -> Result<CaptureSession>
185 where
186 R: BackendResolver,
187 {
188 self.config.validate()?;
189 let bundle = resolver.resolve(&self.config)?;
190 Ok(CaptureSession::new(self.config, bundle))
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use std::collections::VecDeque;
197 use std::time::Duration;
198
199 use super::{CaptureSession, SessionBuilder};
200 use crate::{
201 audio::{AudioData, AudioFrame, SampleFormat},
202 backend::{AudioBackend, BackendBundle, BackendInfo, BackendKind, VideoBackend},
203 config::SessionConfig,
204 error::{PinrayError, Result},
205 frame::{CaptureEvent, FrameData, PixelFormat, VideoFrame},
206 };
207
208 #[test]
209 fn builder_requires_video_or_audio() {
210 let config = SessionBuilder::new().config().clone();
211 assert!(config.validate().is_err());
212 }
213
214 #[test]
215 fn queue_depth_must_be_positive() {
216 let config = SessionBuilder::new().queue_depth(0).config().clone();
217 assert!(config.validate().is_err());
218 }
219
220 fn mock_info() -> BackendInfo {
221 BackendInfo {
222 kind: BackendKind::LinuxWaylandPortal,
223 supports_audio: true,
224 zero_copy: false,
225 notes: "mock",
226 }
227 }
228
229 struct BusyVideo;
231
232 impl VideoBackend for BusyVideo {
233 fn info(&self) -> BackendInfo {
234 mock_info()
235 }
236 fn start(&mut self) -> Result<()> {
237 Ok(())
238 }
239 fn stop(&mut self) -> Result<()> {
240 Ok(())
241 }
242 fn next_event(&mut self, _timeout: Option<Duration>) -> Result<CaptureEvent> {
243 Ok(CaptureEvent::Video(VideoFrame {
244 stream_time_ns: 0,
245 sequence: 0,
246 width: 1,
247 height: 1,
248 stride: 4,
249 pixel_format: PixelFormat::Bgra8888,
250 color_space: None,
251 data: FrameData::Host(vec![0; 4]),
252 damage: None,
253 }))
254 }
255 }
256
257 struct QueuedAudio(VecDeque<AudioFrame>);
258
259 impl AudioBackend for QueuedAudio {
260 fn info(&self) -> BackendInfo {
261 mock_info()
262 }
263 fn start(&mut self) -> Result<()> {
264 Ok(())
265 }
266 fn stop(&mut self) -> Result<()> {
267 Ok(())
268 }
269 fn next_audio(&mut self, timeout: Option<Duration>) -> Result<AudioFrame> {
270 self.0
271 .pop_front()
272 .ok_or(PinrayError::Timeout(timeout.unwrap_or_default()))
273 }
274 }
275
276 #[test]
277 fn repeated_start_stop_is_stable() {
278 let bundle = BackendBundle {
279 info: mock_info(),
280 video: Some(Box::new(BusyVideo)),
281 audio: None,
282 };
283 let mut session = CaptureSession::new(SessionConfig::default(), bundle);
284
285 for _ in 0..100 {
286 session.start().unwrap();
287 assert!(session.is_running());
288 assert!(matches!(
289 session.next_event(Some(Duration::from_millis(1))),
290 Ok(CaptureEvent::Video(_))
291 ));
292 session.stop().unwrap();
293 assert!(!session.is_running());
294 }
295 }
296
297 #[test]
298 fn busy_video_does_not_starve_audio() {
299 let audio_frame = AudioFrame {
300 stream_time_ns: 0,
301 sequence: 0,
302 sample_rate: 48_000,
303 channels: 2,
304 sample_format: SampleFormat::F32,
305 data: AudioData::Interleaved(vec![0; 8]),
306 };
307 let bundle = BackendBundle {
308 info: mock_info(),
309 video: Some(Box::new(BusyVideo)),
310 audio: Some(Box::new(QueuedAudio(VecDeque::from([
311 audio_frame.clone(),
312 audio_frame,
313 ])))),
314 };
315 let mut session = CaptureSession::new(SessionConfig::default(), bundle);
316
317 let mut audio_events = 0;
318 let mut video_events = 0;
319 for _ in 0..6 {
320 match session.next_event(Some(Duration::from_millis(1))).unwrap() {
321 CaptureEvent::Audio(_) => audio_events += 1,
322 CaptureEvent::Video(_) => video_events += 1,
323 other => panic!("unexpected event: {other:?}"),
324 }
325 }
326 assert_eq!(audio_events, 2, "queued audio must be drained");
327 assert_eq!(video_events, 4);
328 }
329}