use std::sync::Arc;
use crate::{Error, Frame, Rate};
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 mode;
#[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, 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<Rate>,
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<Rate>,
color: Option<crate::Color>,
label: String,
pending: Option<Frame>,
_backend: Keepalive,
}
impl Stream {
fn new(
chan: Arc<FrameChannel>,
width: u32,
height: u32,
framerate: Option<Rate>,
label: String,
pending: Option<Frame>,
backend: Keepalive,
) -> Self {
let color = pending.as_ref().and_then(|frame| frame.surface.color());
Self {
chan,
width,
height,
framerate,
color,
label,
pending,
_backend: backend,
}
}
pub async fn read(&mut self) -> Result<Option<Frame>, 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<Rate> {
self.framerate
}
pub fn color(&self) -> Option<crate::Color> {
self.color
}
pub fn label(&self) -> &str {
&self.label
}
pub(crate) fn now(&self) -> moq_net::Timestamp {
self.chan.now()
}
}
pub async fn open(config: &Config) -> Result<Stream, Error> {
match &config.source {
Source::Camera(device) => {
let _ = device;
#[cfg(target_os = "macos")]
{
avfoundation::open(config, device.as_deref()).await
}
#[cfg(target_os = "linux")]
{
match LinuxCamera::select(device.as_deref(), sandboxed()) {
LinuxCamera::V4l2(device) => v4l2::open(config, device).await,
#[cfg(feature = "pipewire")]
LinuxCamera::PipeWire(node) => pipewire::camera::open(config, node).await,
#[cfg(not(feature = "pipewire"))]
LinuxCamera::PipeWire(_) => Err(Error::Unsupported(
"PipeWire camera capture without the `pipewire` feature".to_string(),
)),
}
}
#[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()))
}
}
}
}
pub async fn cameras() -> Result<Vec<Camera>, Error> {
#[cfg(target_os = "macos")]
{
avfoundation::cameras()
}
#[cfg(target_os = "linux")]
{
let cameras = blocking(v4l2::cameras).await?;
#[cfg(feature = "pipewire")]
let cameras = [cameras, pipewire::camera::cameras().await?].concat();
Ok(cameras)
}
#[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")]
{
match LinuxCamera::select(camera, sandboxed()) {
LinuxCamera::V4l2(camera) => {
let camera = camera.map(str::to_string);
blocking(move || v4l2::modes(camera.as_deref())).await
}
#[cfg(feature = "pipewire")]
LinuxCamera::PipeWire(node) => pipewire::camera::modes(node).await,
#[cfg(not(feature = "pipewire"))]
LinuxCamera::PipeWire(_) => Err(Error::Unsupported(
"PipeWire camera modes without the `pipewire` feature".to_string(),
)),
}
}
#[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", test))]
const PIPEWIRE: &str = "pipewire";
#[cfg(any(target_os = "linux", test))]
#[derive(Debug, PartialEq, Eq)]
enum LinuxCamera<'a> {
V4l2(Option<&'a str>),
PipeWire(Option<&'a str>),
}
#[cfg(any(target_os = "linux", test))]
impl<'a> LinuxCamera<'a> {
fn select(selector: Option<&'a str>, sandboxed: bool) -> Self {
match selector {
None if sandboxed => Self::PipeWire(None),
None => Self::V4l2(None),
Some(PIPEWIRE) => Self::PipeWire(None),
Some(selector) => match selector.strip_prefix(PIPEWIRE).and_then(|rest| rest.strip_prefix(':')) {
Some(name) => Self::PipeWire(Some(name)),
None => Self::V4l2(Some(selector)),
},
}
}
}
#[cfg(target_os = "linux")]
fn sandboxed() -> bool {
#[cfg(feature = "pipewire")]
{
ashpd::is_sandboxed()
}
#[cfg(not(feature = "pipewire"))]
{
false
}
}
#[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 camera_selectors_name_their_backend() {
assert_eq!(LinuxCamera::select(None, false), LinuxCamera::V4l2(None));
assert_eq!(LinuxCamera::select(None, true), LinuxCamera::PipeWire(None));
for sandboxed in [false, true] {
assert_eq!(
LinuxCamera::select(Some("/dev/video2"), sandboxed),
LinuxCamera::V4l2(Some("/dev/video2"))
);
assert_eq!(LinuxCamera::select(Some("1"), sandboxed), LinuxCamera::V4l2(Some("1")));
assert_eq!(
LinuxCamera::select(Some("pipewire"), sandboxed),
LinuxCamera::PipeWire(None)
);
assert_eq!(
LinuxCamera::select(Some("pipewire:v4l2_input.pci-0000_00_14.0-usb-0_4_1.0"), sandboxed),
LinuxCamera::PipeWire(Some("v4l2_input.pci-0000_00_14.0-usb-0_4_1.0"))
);
}
assert_eq!(
LinuxCamera::select(Some("pipewired"), false),
LinuxCamera::V4l2(Some("pipewired"))
);
}
}