use std::{num::NonZeroU32, sync::Arc, time::Duration};
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, Copy, Debug, Eq)]
pub struct Rate {
frames: NonZeroU32,
seconds: NonZeroU32,
}
impl Rate {
#[cfg(target_os = "linux")]
fn rounded(&self) -> u32 {
let frames = u64::from(self.frames.get());
let seconds = u64::from(self.seconds.get());
((frames + seconds / 2) / seconds).max(1) as u32
}
pub fn frames(&self) -> NonZeroU32 {
self.frames
}
pub fn interval(&self) -> Duration {
Duration::from_secs(u64::from(self.seconds.get()))
}
}
impl Ord for Rate {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(u64::from(self.frames.get()) * u64::from(other.seconds.get()))
.cmp(&(u64::from(other.frames.get()) * u64::from(self.seconds.get())))
}
}
impl PartialOrd for Rate {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Rate {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Mode {
pub width: u32,
pub height: u32,
pub framerates: Vec<Rate>,
}
impl Mode {
pub fn max_framerate(&self) -> Option<Rate> {
self.framerates.first().copied()
}
}
#[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 camera_modes(camera: Option<&str>) -> Result<Vec<Mode>, Error> {
let _ = camera;
#[cfg(target_os = "linux")]
{
let camera = camera.map(str::to_string);
blocking(move || v4l2::modes(camera.as_deref())).await
}
#[cfg(not(target_os = "linux"))]
{
Err(Error::Unsupported("listing camera modes".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));
}
}