Skip to content
This repository was archived by the owner on Oct 8, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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