use std::sync::Arc;
use crate::Error;
use crate::frame::Surface;
const MAX_FRAMERATE: u32 = 1_000_000;
mod channel;
use channel::FrameChannel;
#[cfg(not(target_os = "macos"))]
type Keepalive = Box<dyn std::any::Any + Send>;
#[cfg(target_os = "macos")]
type Keepalive = Box<dyn std::any::Any>;
#[cfg(target_os = "macos")]
mod avfoundation;
#[cfg(target_os = "macos")]
mod screencapture;
#[cfg(target_os = "macos")]
mod surface;
#[cfg(target_os = "linux")]
mod v4l2;
#[cfg(target_os = "linux")]
mod x11;
#[cfg(all(target_os = "linux", feature = "pipewire"))]
mod pipewire;
#[cfg(target_os = "windows")]
mod mediafoundation;
#[cfg(target_os = "windows")]
mod desktopduplication;
#[cfg(target_os = "windows")]
mod window;
#[cfg(any(target_os = "linux", target_os = "windows"))]
mod pump;
#[cfg(any(target_os = "linux", target_os = "windows", test))]
mod settle;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Source {
Camera(Option<String>),
Display(Option<String>),
Window(String),
App(String),
}
impl Default for Source {
fn default() -> Self {
Self::Camera(None)
}
}
impl Source {
#[cfg(target_os = "macos")]
pub(crate) fn label(&self) -> String {
match self {
Self::Camera(None) => "camera".to_string(),
Self::Camera(Some(id)) => format!("camera:{id}"),
Self::Display(None) => "display".to_string(),
Self::Display(Some(id)) => format!("display:{id}"),
Self::Window(id) => format!("window:{id}"),
Self::App(id) => format!("app:{id}"),
}
}
}
#[derive(Clone, Debug)]
pub struct Camera {
pub id: String,
pub name: String,
}
impl Camera {
pub fn source(&self) -> Source {
Source::Camera(Some(self.id.clone()))
}
}
#[derive(Clone, Debug)]
pub struct Display {
pub id: String,
pub name: String,
pub width: u32,
pub height: u32,
}
impl Display {
pub fn source(&self) -> Source {
Source::Display(Some(self.id.clone()))
}
}
#[derive(Clone, Debug)]
pub struct Window {
pub id: String,
pub title: String,
pub app: String,
pub width: u32,
pub height: u32,
}
impl Window {
pub fn source(&self) -> Source {
Source::Window(self.id.clone())
}
}
#[derive(Clone, Debug)]
pub struct App {
pub id: String,
pub name: String,
}
impl App {
pub fn source(&self) -> Source {
Source::App(self.id.clone())
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Config {
pub source: Source,
pub width: Option<u32>,
pub height: Option<u32>,
pub framerate: Option<u32>,
pub cursor: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
source: Source::default(),
width: None,
height: None,
framerate: None,
cursor: true,
}
}
}
pub struct Stream {
chan: Arc<FrameChannel>,
width: u32,
height: u32,
framerate: Option<u32>,
color: Option<crate::Color>,
label: String,
pending: Option<Surface>,
_backend: Keepalive,
}
impl Stream {
fn new(
chan: Arc<FrameChannel>,
width: u32,
height: u32,
framerate: Option<u32>,
label: String,
pending: Option<Surface>,
backend: Keepalive,
) -> Self {
let color = pending.as_ref().and_then(Surface::color);
Self {
chan,
width,
height,
framerate,
color,
label,
pending,
_backend: backend,
}
}
pub async fn read(&mut self) -> Result<Option<Surface>, Error> {
if let Some(frame) = self.pending.take() {
return Ok(Some(frame));
}
self.chan.recv().await
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn framerate(&self) -> Option<u32> {
self.framerate
}
pub fn color(&self) -> Option<crate::Color> {
self.color
}
pub fn label(&self) -> &str {
&self.label
}
}
pub async fn open(config: &Config) -> Result<Stream, Error> {
validate(config)?;
match &config.source {
Source::Camera(device) => {
let _ = device;
#[cfg(target_os = "macos")]
{
avfoundation::open(config, device.as_deref()).await
}
#[cfg(target_os = "linux")]
{
v4l2::open(config, device.as_deref()).await
}
#[cfg(target_os = "windows")]
{
mediafoundation::open(config, device.as_deref()).await
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
Err(Error::Unsupported("camera capture".to_string()))
}
}
Source::Display(device) => {
let _ = device;
#[cfg(target_os = "macos")]
{
screencapture::open_display(config, device.as_deref()).await
}
#[cfg(target_os = "windows")]
{
desktopduplication::open(config, device.as_deref()).await
}
#[cfg(all(target_os = "linux", feature = "pipewire"))]
{
if x11::selected(device.as_deref()) {
x11::open_display(config, device.as_deref()).await
} else {
pipewire::open(config, device.as_deref()).await
}
}
#[cfg(all(target_os = "linux", not(feature = "pipewire")))]
{
x11::open_display(config, device.as_deref()).await
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
{
Err(Error::Unsupported("screen capture".to_string()))
}
}
Source::Window(id) => {
let _ = id;
#[cfg(target_os = "macos")]
{
screencapture::open_window(config, id).await
}
#[cfg(target_os = "linux")]
{
x11::open_window(config, id).await
}
#[cfg(target_os = "windows")]
{
window::open(config, id).await
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
Err(Error::Unsupported("window capture".to_string()))
}
}
Source::App(id) => {
let _ = id;
#[cfg(target_os = "macos")]
{
screencapture::open_app(config, id).await
}
#[cfg(not(target_os = "macos"))]
{
Err(Error::Unsupported("application capture".to_string()))
}
}
}
}
fn validate(config: &Config) -> Result<(), Error> {
match config.framerate {
Some(value) if value == 0 || value > MAX_FRAMERATE => Err(Error::InvalidFramerate(value)),
_ => Ok(()),
}
}
pub async fn cameras() -> Result<Vec<Camera>, Error> {
#[cfg(target_os = "macos")]
{
avfoundation::cameras()
}
#[cfg(target_os = "linux")]
{
blocking(v4l2::cameras).await
}
#[cfg(target_os = "windows")]
{
blocking(mediafoundation::cameras).await
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
Err(Error::Unsupported("listing cameras".to_string()))
}
}
pub async fn displays() -> Result<Vec<Display>, Error> {
#[cfg(target_os = "macos")]
{
screencapture::displays().await
}
#[cfg(target_os = "windows")]
{
blocking(desktopduplication::displays).await
}
#[cfg(target_os = "linux")]
{
blocking(x11::displays).await
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
{
Err(Error::Unsupported("listing displays".to_string()))
}
}
pub async fn windows() -> Result<Vec<Window>, Error> {
#[cfg(target_os = "macos")]
{
screencapture::windows().await
}
#[cfg(target_os = "linux")]
{
blocking(x11::windows).await
}
#[cfg(target_os = "windows")]
{
blocking(window::windows).await
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
Err(Error::Unsupported("listing windows".to_string()))
}
}
pub async fn apps() -> Result<Vec<App>, Error> {
#[cfg(target_os = "macos")]
{
screencapture::apps().await
}
#[cfg(not(target_os = "macos"))]
{
Err(Error::Unsupported("listing applications".to_string()))
}
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
async fn blocking<T, F>(f: F) -> Result<T, Error>
where
F: FnOnce() -> Result<T, Error> + Send + 'static,
T: Send + 'static,
{
tokio::task::spawn_blocking(f)
.await
.map_err(|err| Error::Codec(anyhow::anyhow!("capture enumeration thread failed: {err}")))?
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validates_framerate_range() {
let mut config = Config::default();
assert!(validate(&config).is_ok());
config.framerate = Some(1);
assert!(validate(&config).is_ok());
config.framerate = Some(MAX_FRAMERATE);
assert!(validate(&config).is_ok());
config.framerate = Some(0);
assert!(matches!(validate(&config), Err(Error::InvalidFramerate(0))));
config.framerate = Some(MAX_FRAMERATE + 1);
assert!(matches!(validate(&config), Err(Error::InvalidFramerate(value)) if value == MAX_FRAMERATE + 1));
}
}