1use std::sync::Arc;
14
15use crate::Error;
16use crate::frame::Surface;
17
18const MAX_FRAMERATE: u32 = 1_000_000;
19
20mod channel;
21use channel::FrameChannel;
22
23#[cfg(not(target_os = "macos"))]
31type Keepalive = Box<dyn std::any::Any + Send>;
32#[cfg(target_os = "macos")]
33type Keepalive = Box<dyn std::any::Any>;
34
35#[cfg(target_os = "macos")]
36mod avfoundation;
37#[cfg(target_os = "macos")]
38mod screencapture;
39#[cfg(target_os = "macos")]
40mod surface;
41
42#[cfg(target_os = "linux")]
44mod v4l2;
45#[cfg(target_os = "linux")]
47mod x11;
48
49#[cfg(all(target_os = "linux", feature = "pipewire"))]
51mod pipewire;
52
53#[cfg(target_os = "windows")]
55mod mediafoundation;
56
57#[cfg(target_os = "windows")]
59mod desktopduplication;
60#[cfg(target_os = "windows")]
62mod window;
63
64#[cfg(any(target_os = "linux", target_os = "windows"))]
66mod pump;
67
68#[derive(Clone, Debug, PartialEq, Eq)]
75#[non_exhaustive]
76pub enum Source {
77 Camera(Option<String>),
83
84 Display(Option<String>),
89
90 Window(String),
93
94 App(String),
97}
98
99impl Default for Source {
101 fn default() -> Self {
102 Self::Camera(None)
103 }
104}
105
106impl Source {
107 #[cfg(target_os = "macos")]
115 pub(crate) fn label(&self) -> String {
116 match self {
117 Self::Camera(None) => "camera".to_string(),
118 Self::Camera(Some(id)) => format!("camera:{id}"),
119 Self::Display(None) => "display".to_string(),
120 Self::Display(Some(id)) => format!("display:{id}"),
121 Self::Window(id) => format!("window:{id}"),
122 Self::App(id) => format!("app:{id}"),
123 }
124 }
125}
126
127#[derive(Clone, Debug)]
129pub struct Camera {
130 pub id: String,
132 pub name: String,
134}
135
136impl Camera {
137 pub fn source(&self) -> Source {
139 Source::Camera(Some(self.id.clone()))
140 }
141}
142
143#[derive(Clone, Debug)]
145pub struct Display {
146 pub id: String,
148 pub name: String,
150 pub width: u32,
153 pub height: u32,
156}
157
158impl Display {
159 pub fn source(&self) -> Source {
161 Source::Display(Some(self.id.clone()))
162 }
163}
164
165#[derive(Clone, Debug)]
167pub struct Window {
168 pub id: String,
170 pub title: String,
172 pub app: String,
174 pub width: u32,
177 pub height: u32,
180}
181
182impl Window {
183 pub fn source(&self) -> Source {
185 Source::Window(self.id.clone())
186 }
187}
188
189#[derive(Clone, Debug)]
191pub struct App {
192 pub id: String,
194 pub name: String,
196}
197
198impl App {
199 pub fn source(&self) -> Source {
201 Source::App(self.id.clone())
202 }
203}
204
205#[derive(Clone, Debug)]
211#[non_exhaustive]
212pub struct Config {
213 pub source: Source,
215 pub width: Option<u32>,
217 pub height: Option<u32>,
219 pub framerate: Option<u32>,
221 pub cursor: bool,
224}
225
226impl Default for Config {
227 fn default() -> Self {
228 Self {
229 source: Source::default(),
230 width: None,
231 height: None,
232 framerate: None,
233 cursor: true,
234 }
235 }
236}
237
238pub struct Stream {
247 chan: Arc<FrameChannel>,
248 width: u32,
249 height: u32,
250 framerate: Option<u32>,
251 color: Option<crate::Color>,
252 label: String,
253 pending: Option<Surface>,
256 _backend: Keepalive,
259}
260
261impl Stream {
262 fn new(
264 chan: Arc<FrameChannel>,
265 width: u32,
266 height: u32,
267 framerate: Option<u32>,
268 label: String,
269 pending: Option<Surface>,
270 backend: Keepalive,
271 ) -> Self {
272 let color = pending.as_ref().and_then(Surface::color);
273 Self {
274 chan,
275 width,
276 height,
277 framerate,
278 color,
279 label,
280 pending,
281 _backend: backend,
282 }
283 }
284
285 pub async fn read(&mut self) -> Result<Option<Surface>, Error> {
295 if let Some(frame) = self.pending.take() {
296 return Ok(Some(frame));
297 }
298 self.chan.recv().await
299 }
300
301 pub fn width(&self) -> u32 {
303 self.width
304 }
305
306 pub fn height(&self) -> u32 {
308 self.height
309 }
310
311 pub fn framerate(&self) -> Option<u32> {
313 self.framerate
314 }
315
316 pub fn color(&self) -> Option<crate::Color> {
318 self.color
319 }
320
321 pub fn label(&self) -> &str {
323 &self.label
324 }
325}
326
327pub async fn open(config: &Config) -> Result<Stream, Error> {
329 validate(config)?;
330 match &config.source {
331 Source::Camera(device) => {
332 let _ = device;
333 #[cfg(target_os = "macos")]
334 {
335 avfoundation::open(config, device.as_deref()).await
336 }
337 #[cfg(target_os = "linux")]
338 {
339 v4l2::open(config, device.as_deref()).await
340 }
341 #[cfg(target_os = "windows")]
342 {
343 mediafoundation::open(config, device.as_deref()).await
344 }
345 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
346 {
347 Err(Error::Unsupported("camera capture".to_string()))
348 }
349 }
350 Source::Display(device) => {
351 let _ = device;
352 #[cfg(target_os = "macos")]
353 {
354 screencapture::open_display(config, device.as_deref()).await
355 }
356 #[cfg(target_os = "windows")]
357 {
358 desktopduplication::open(config, device.as_deref()).await
359 }
360 #[cfg(all(target_os = "linux", feature = "pipewire"))]
361 {
362 if x11::selected(device.as_deref()) {
363 x11::open_display(config, device.as_deref()).await
364 } else {
365 pipewire::open(config, device.as_deref()).await
366 }
367 }
368 #[cfg(all(target_os = "linux", not(feature = "pipewire")))]
369 {
370 x11::open_display(config, device.as_deref()).await
371 }
372 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
373 {
374 Err(Error::Unsupported("screen capture".to_string()))
375 }
376 }
377 Source::Window(id) => {
378 let _ = id;
379 #[cfg(target_os = "macos")]
380 {
381 screencapture::open_window(config, id).await
382 }
383 #[cfg(target_os = "linux")]
384 {
385 x11::open_window(config, id).await
386 }
387 #[cfg(target_os = "windows")]
388 {
389 window::open(config, id).await
390 }
391 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
392 {
393 Err(Error::Unsupported("window capture".to_string()))
394 }
395 }
396 Source::App(id) => {
397 let _ = id;
398 #[cfg(target_os = "macos")]
399 {
400 screencapture::open_app(config, id).await
401 }
402 #[cfg(not(target_os = "macos"))]
403 {
404 Err(Error::Unsupported("application capture".to_string()))
405 }
406 }
407 }
408}
409
410fn validate(config: &Config) -> Result<(), Error> {
411 match config.framerate {
412 Some(value) if value == 0 || value > MAX_FRAMERATE => Err(Error::InvalidFramerate(value)),
413 _ => Ok(()),
414 }
415}
416
417pub async fn cameras() -> Result<Vec<Camera>, Error> {
419 #[cfg(target_os = "macos")]
420 {
421 avfoundation::cameras()
422 }
423 #[cfg(target_os = "linux")]
424 {
425 blocking(v4l2::cameras).await
426 }
427 #[cfg(target_os = "windows")]
428 {
429 blocking(mediafoundation::cameras).await
430 }
431 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
432 {
433 Err(Error::Unsupported("listing cameras".to_string()))
434 }
435}
436
437pub async fn displays() -> Result<Vec<Display>, Error> {
443 #[cfg(target_os = "macos")]
444 {
445 screencapture::displays().await
446 }
447 #[cfg(target_os = "windows")]
448 {
449 blocking(desktopduplication::displays).await
450 }
451 #[cfg(target_os = "linux")]
452 {
453 blocking(x11::displays).await
454 }
455 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
456 {
457 Err(Error::Unsupported("listing displays".to_string()))
458 }
459}
460
461pub async fn windows() -> Result<Vec<Window>, Error> {
463 #[cfg(target_os = "macos")]
464 {
465 screencapture::windows().await
466 }
467 #[cfg(target_os = "linux")]
468 {
469 blocking(x11::windows).await
470 }
471 #[cfg(target_os = "windows")]
472 {
473 blocking(window::windows).await
474 }
475 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
476 {
477 Err(Error::Unsupported("listing windows".to_string()))
478 }
479}
480
481pub async fn apps() -> Result<Vec<App>, Error> {
483 #[cfg(target_os = "macos")]
484 {
485 screencapture::apps().await
486 }
487 #[cfg(not(target_os = "macos"))]
488 {
489 Err(Error::Unsupported("listing applications".to_string()))
490 }
491}
492
493#[cfg(any(target_os = "linux", target_os = "windows"))]
495async fn blocking<T, F>(f: F) -> Result<T, Error>
496where
497 F: FnOnce() -> Result<T, Error> + Send + 'static,
498 T: Send + 'static,
499{
500 tokio::task::spawn_blocking(f)
501 .await
502 .map_err(|err| Error::Codec(anyhow::anyhow!("capture enumeration thread failed: {err}")))?
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508
509 #[test]
510 fn validates_framerate_range() {
511 let mut config = Config::default();
512 assert!(validate(&config).is_ok());
513
514 config.framerate = Some(1);
515 assert!(validate(&config).is_ok());
516 config.framerate = Some(MAX_FRAMERATE);
517 assert!(validate(&config).is_ok());
518
519 config.framerate = Some(0);
520 assert!(matches!(validate(&config), Err(Error::InvalidFramerate(0))));
521 config.framerate = Some(MAX_FRAMERATE + 1);
522 assert!(matches!(validate(&config), Err(Error::InvalidFramerate(value)) if value == MAX_FRAMERATE + 1));
523 }
524}