Skip to content
This repository has been archived by the owner on Oct 8, 2024. It is now read-only.

Linux wayland support #38

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ wgpu = { version = "0.20", optional = true, features = ["dx12", "hal"] }
d3d12 = "0.20"
winapi = { version = "0.3", optional = true }

[target.'cfg(target_os = "linux")'.dependencies]
ashpd = "0.9.1"
pipewire = "0.8.0"

[dev-dependencies]
futures = "0.3"
tokio = { version = "1.37", features = ["rt", "macros", "rt-multi-thread"] }
Expand Down
19 changes: 19 additions & 0 deletions src/feature/bitmap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ use windows::Win32::System::WinRT::Direct3D11::IDirect3DDxgiInterfaceAccess;
#[cfg(target_os = "windows")]
use windows::Win32::Graphics::Direct3D11::D3D11_USAGE_DYNAMIC;

#[cfg(target_os = "linux")]
use pipewire::spa::param::video::VideoFormat;

#[derive(Clone)]
struct BitmapPool<T: Sized + Zeroable + Copy> {
free_bitmaps_and_count: Arc<Mutex<(Vec<Box<[T]>>, usize)>>,
Expand Down Expand Up @@ -526,6 +529,22 @@ impl VideoFrameBitmapInternal for VideoFrame {
Err(VideoFrameBitmapError::Other("Failed to lock iosurface".to_string()))
}
}
#[cfg(target_os = "linux")]
{
match self.impl_video_frame.format.format() {
VideoFormat::BGRA | VideoFormat::BGRx => {
let plane_ptr = VideoFramePlanePtr {
ptr: self.impl_video_frame.data,
width: self.impl_video_frame.size.width as usize,
height: self.impl_video_frame.size.height as usize,
bytes_per_row: self.impl_video_frame.size.width as usize * 4,
};

output_mapping(VideoFrameDataCopyPtrs::Bgra8888(plane_ptr))
}
_ => Err(VideoFrameBitmapError::Other("Invalid pixel format".to_string())),
}
}
}
}

Expand Down
316 changes: 316 additions & 0 deletions src/platform/linux_wayland/capture_content.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
use std::rc::Rc;

use ashpd::{
desktop::{
screencast::{CursorMode, Screencast, SourceType},
Session,
},
enumflags2::BitFlags,
};

use crate::{
capturable_content::{CapturableContentError, CapturableContentFilter},
prelude::Rect,
};

#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct WaylandCapturableWindow {
pub pw_node_id: u32,
pub position: Option<(i32, i32)>,
pub size: Option<(i32, i32)>,
pub id: Option<String>,
pub mapping_id: Option<String>,
pub virt: bool,
pub cursor_as_metadata: bool,
}

impl WaylandCapturableWindow {
pub fn from_impl(window: Self) -> Self {
window
}

pub fn title(&self) -> String {
String::from("n/a")
}

pub fn rect(&self) -> Rect {
let origin = self.position.unwrap_or((0, 0));
let size = self.size.unwrap_or((0, 0));
Rect {
origin: crate::prelude::Point {
x: origin.0 as f64,
y: origin.1 as f64,
},
size: crate::prelude::Size {
width: size.0 as f64,
height: size.1 as f64,
},
}
}

pub fn application(&self) -> WaylandCapturableApplication {
WaylandCapturableApplication(())
}

pub fn is_visible(&self) -> bool {
true
}
}

#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct WaylandCapturableDisplay {
pub pw_node_id: u32,
pub position: Option<(i32, i32)>,
pub size: Option<(i32, i32)>,
pub id: Option<String>,
pub mapping_id: Option<String>,
pub cursor_as_metadata: bool,
}

impl WaylandCapturableDisplay {
pub fn from_impl(window: Self) -> Self {
window
}

pub fn rect(&self) -> Rect {
let origin = self.position.unwrap_or((0, 0));
let size = self.size.unwrap_or((0, 0));
Rect {
origin: crate::prelude::Point {
x: origin.0 as f64,
y: origin.1 as f64,
},
size: crate::prelude::Size {
width: size.0 as f64,
height: size.1 as f64,
},
}
}
}

pub struct WaylandCapturableApplication(());

impl WaylandCapturableApplication {
pub fn identifier(&self) -> String {
String::from("n/a")
}

pub fn name(&self) -> String {
String::from("n/a")
}

pub fn pid(&self) -> i32 {
-1
}
}

pub struct WaylandCapturableContent {
pub windows: Vec<WaylandCapturableWindow>,
pub displays: Vec<WaylandCapturableDisplay>,
_sc: Rc<Screencast<'static>>,
_sc_session: Rc<Session<'static, Screencast<'static>>>,
}

impl WaylandCapturableContent {
fn source_types_filter(filter: CapturableContentFilter) -> BitFlags<SourceType> {
let mut bitflags = BitFlags::empty();
if filter.displays {
bitflags |= SourceType::Monitor | SourceType::Virtual;
}
if let Some(windows_filter) = filter.windows {
if windows_filter.desktop_windows || windows_filter.onscreen_only {
bitflags |= SourceType::Window;
}
}
bitflags
}

pub async fn new(filter: CapturableContentFilter) -> Result<Self, CapturableContentError> {
let screencast = Screencast::new()
.await
.map_err(|e| CapturableContentError::Other(e.to_string()))?;

// TODO: Fix dual cursor in kwin when capture monitor and cursor as metadata
// let cursor_as_metadata = screencast
// .available_cursor_modes()
// .await
// .map_err(|e| CapturableContentError::Other(e.to_string()))?
// .contains(CursorMode::Metadata);
let cursor_as_metadata = false;

let source_types = Self::source_types_filter(filter)
// Some portal implementations freak out when we include supported an not supported source types
& screencast.available_source_types().await.map_err(|e| CapturableContentError::Other(e.to_string()))?;

if source_types.is_empty() {
return Err(CapturableContentError::Other(
"Unsupported content filter".to_string(),
));
}

let session = screencast
.create_session()
.await
.map_err(|e| CapturableContentError::Other(e.to_string()))?;

// INVESTIGATE: Show cursor as default when metadata-mode is not available?
let cursor_mode = if cursor_as_metadata {
CursorMode::Metadata
} else {
CursorMode::Embedded
};

screencast
.select_sources(
&session,
cursor_mode,
source_types,
false,
None,
ashpd::desktop::PersistMode::DoNot,
)
.await
.map_err(|e| CapturableContentError::Other(e.to_string()))?
.response()
.map_err(|e| CapturableContentError::Other(e.to_string()))?;
let streams = screencast
.start(&session, &ashpd::WindowIdentifier::None)
.await
.map_err(|e| CapturableContentError::Other(e.to_string()))?
.response()
.map_err(|e| CapturableContentError::Other(e.to_string()))?;

let mut sources = Self {
windows: Vec::new(),
displays: Vec::new(),
_sc: Rc::new(screencast),
_sc_session: Rc::new(session),
};
for stream in streams.streams() {
if let Some(source_type) = stream.source_type() {
match source_type {
SourceType::Window | SourceType::Virtual => {
sources.windows.push(WaylandCapturableWindow {
pw_node_id: stream.pipe_wire_node_id(),
position: stream.position(),
size: stream.size(),
id: stream.id().map(|id| id.to_string()),
mapping_id: stream.mapping_id().map(|id| id.to_string()),
virt: source_type == SourceType::Virtual,
cursor_as_metadata,
});
continue;
}
SourceType::Monitor => {}
}
}
// If the stream source_type is `None`, then treat it as a display
sources.displays.push(WaylandCapturableDisplay {
pw_node_id: stream.pipe_wire_node_id(),
position: stream.position(),
size: stream.size(),
id: stream.id().map(|id| id.to_string()),
mapping_id: stream.mapping_id().map(|id| id.to_string()),
cursor_as_metadata,
});
}

Ok(sources)
}
}

#[derive(Clone, Default)]
pub(crate) struct WaylandCapturableContentFilter(());

impl WaylandCapturableContentFilter {
pub(crate) const DEFAULT: Self = Self(());
pub(crate) const NORMAL_WINDOWS: Self = Self(());
}

#[cfg(test)]
mod tests {
use ashpd::{desktop::screencast::SourceType, enumflags2::BitFlags};

use crate::{
platform::platform_impl::{
capture_content::WaylandCapturableContent, ImplCapturableContentFilter,
},
prelude::{CapturableContentFilter, CapturableWindowFilter},
};

#[test]
fn source_type_filter_conversion_displays() {
assert_eq!(
WaylandCapturableContent::source_types_filter(CapturableContentFilter {
windows: None,
displays: true,
impl_capturable_content_filter: ImplCapturableContentFilter::default(),
}),
SourceType::Monitor | SourceType::Virtual
);
}

#[test]
fn source_type_filter_conversion_windows() {
assert_eq!(
WaylandCapturableContent::source_types_filter(CapturableContentFilter {
windows: Some(CapturableWindowFilter {
desktop_windows: true,
onscreen_only: false
}),
displays: false,
impl_capturable_content_filter: ImplCapturableContentFilter::default(),
}),
SourceType::Window
);
assert_eq!(
WaylandCapturableContent::source_types_filter(CapturableContentFilter {
windows: Some(CapturableWindowFilter {
desktop_windows: false,
onscreen_only: true
}),
displays: false,
impl_capturable_content_filter: ImplCapturableContentFilter::default(),
}),
SourceType::Window
);
assert_eq!(
WaylandCapturableContent::source_types_filter(CapturableContentFilter {
windows: Some(CapturableWindowFilter {
desktop_windows: true,
onscreen_only: true
}),
displays: false,
impl_capturable_content_filter: ImplCapturableContentFilter::default(),
}),
SourceType::Window
);
}

#[test]
fn source_type_filter_conversion_none() {
assert_eq!(
WaylandCapturableContent::source_types_filter(CapturableContentFilter {
windows: None,
displays: false,
impl_capturable_content_filter: ImplCapturableContentFilter::default(),
}),
BitFlags::empty()
);
}

#[test]
fn source_type_filter_conversion_all() {
assert_eq!(
WaylandCapturableContent::source_types_filter(CapturableContentFilter {
windows: Some(CapturableWindowFilter {
desktop_windows: true,
onscreen_only: true
}),
displays: true,
impl_capturable_content_filter: ImplCapturableContentFilter::default(),
}),
SourceType::Monitor | SourceType::Virtual | SourceType::Window
);
}
}
Loading