feat: PipeWire capture on X11
Co-authored-by: Yuxuan Shui <yshuiv7@gmail.com>
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -4277,7 +4277,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "wlx-capture"
|
name = "wlx-capture"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
source = "git+https://github.com/galister/wlx-capture?tag=v0.3.9#0552577c45ce135a2f42d11399bcca0034762e2a"
|
source = "git+https://github.com/galister/wlx-capture?tag=v0.3.10#dacce353e4a0a4778323e96540c053ffefe82bfe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ashpd",
|
"ashpd",
|
||||||
"drm-fourcc",
|
"drm-fourcc",
|
||||||
|
|||||||
16
Cargo.toml
16
Cargo.toml
@@ -53,26 +53,24 @@ sysinfo = { version = "0.30.0", optional = true }
|
|||||||
thiserror = "1.0.56"
|
thiserror = "1.0.56"
|
||||||
vulkano = { git = "https://github.com/vulkano-rs/vulkano", rev = "94f50f1" }
|
vulkano = { git = "https://github.com/vulkano-rs/vulkano", rev = "94f50f1" }
|
||||||
vulkano-shaders = { git = "https://github.com/vulkano-rs/vulkano", rev = "94f50f1" }
|
vulkano-shaders = { git = "https://github.com/vulkano-rs/vulkano", rev = "94f50f1" }
|
||||||
wlx-capture = { git = "https://github.com/galister/wlx-capture", tag = "v0.3.9", default-features = false }
|
wlx-capture = { git = "https://github.com/galister/wlx-capture", tag = "v0.3.10", default-features = false }
|
||||||
winit = { version = "0.29.15", optional = true }
|
winit = { version = "0.29.15", optional = true }
|
||||||
xdg = "2.5.2"
|
xdg = "2.5.2"
|
||||||
log-panics = { version = "2.1.0", features = ["with-backtrace"] }
|
log-panics = { version = "2.1.0", features = ["with-backtrace"] }
|
||||||
serde_json5 = "0.1.0"
|
serde_json5 = "0.1.0"
|
||||||
xkbcommon = { version = "0.7.0" }
|
xkbcommon = { version = "0.7.0" }
|
||||||
xcb = { version = "1.4.0", optional = true, features = ["as-raw-xcb-connection"] }
|
xcb = { version = "1.4.0", optional = true, features = [
|
||||||
|
"as-raw-xcb-connection",
|
||||||
|
] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["openvr", "openxr", "osc", "x11", "wayland"]
|
default = ["openvr", "openxr", "osc", "x11", "wayland"]
|
||||||
openvr = ["dep:ovr_overlay", "dep:json"]
|
openvr = ["dep:ovr_overlay", "dep:json"]
|
||||||
openxr = ["dep:openxr", "dep:sysinfo"]
|
openxr = ["dep:openxr", "dep:sysinfo"]
|
||||||
osc = ["dep:rosc"]
|
osc = ["dep:rosc"]
|
||||||
x11 = [
|
x11 = ["dep:xcb", "wlx-capture/xshm", "xkbcommon/x11"]
|
||||||
"dep:xcb",
|
wayland = ["pipewire", "wlx-capture/wlr", "xkbcommon/wayland"]
|
||||||
|
pipewire = ["wlx-capture/pipewire"]
|
||||||
"wlx-capture/xshm",
|
|
||||||
"xkbcommon/x11",
|
|
||||||
]
|
|
||||||
wayland = ["wlx-capture/pipewire", "wlx-capture/wlr", "xkbcommon/wayland"]
|
|
||||||
uidev = ["dep:winit"]
|
uidev = ["dep:winit"]
|
||||||
no-dmabuf = []
|
no-dmabuf = []
|
||||||
xcb = ["dep:xcb"]
|
xcb = ["dep:xcb"]
|
||||||
|
|||||||
@@ -72,7 +72,13 @@ where
|
|||||||
crate::overlays::screen::create_screens_wayland(wl, app)?
|
crate::overlays::screen::create_screens_wayland(wl, app)?
|
||||||
} else {
|
} else {
|
||||||
keymap = get_keymap_x11().ok();
|
keymap = get_keymap_x11().ok();
|
||||||
crate::overlays::screen::create_screens_x11(app)?
|
match crate::overlays::screen::create_screens_x11pw(app) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(e) => {
|
||||||
|
log::info!("Will not use PipeWire capture: {:?}", e);
|
||||||
|
crate::overlays::screen::create_screens_xshm(app)?
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut show_screens = app.session.config.show_screens.clone();
|
let mut show_screens = app.session.config.show_screens.clone();
|
||||||
|
|||||||
@@ -584,10 +584,14 @@ impl XkbKeymap {
|
|||||||
pub use wayland::get_keymap_wl;
|
pub use wayland::get_keymap_wl;
|
||||||
|
|
||||||
#[cfg(not(feature = "wayland"))]
|
#[cfg(not(feature = "wayland"))]
|
||||||
pub fn get_keymap_wl() -> anyhow::Result<XkbKeymap> {}
|
pub fn get_keymap_wl() -> anyhow::Result<XkbKeymap> {
|
||||||
|
anyhow::bail!("Wayland support not enabled.")
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "x11")]
|
#[cfg(feature = "x11")]
|
||||||
pub use x11::get_keymap_x11;
|
pub use x11::get_keymap_x11;
|
||||||
|
|
||||||
#[cfg(not(feature = "x11"))]
|
#[cfg(not(feature = "x11"))]
|
||||||
pub fn get_keymap_x11() -> anyhow::Result<XkbKeymap> {}
|
pub fn get_keymap_x11() -> anyhow::Result<XkbKeymap> {
|
||||||
|
anyhow::bail!("X11 support not enabled.")
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ pub struct MirrorRenderer {
|
|||||||
}
|
}
|
||||||
impl MirrorRenderer {
|
impl MirrorRenderer {
|
||||||
pub fn new(name: Arc<str>) -> Self {
|
pub fn new(name: Arc<str>) -> Self {
|
||||||
let selector = Box::pin(pipewire_select_screen(None, false, false, false));
|
let selector = Box::pin(pipewire_select_screen(None, false, false, false, false));
|
||||||
Self {
|
Self {
|
||||||
name,
|
name,
|
||||||
renderer: None,
|
renderer: None,
|
||||||
@@ -58,12 +58,9 @@ impl OverlayRenderer for MirrorRenderer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(pw_result) = maybe_pw_result {
|
if let Ok(pw_result) = maybe_pw_result {
|
||||||
log::info!(
|
let node_id = pw_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element
|
||||||
"{}: PipeWire node selected: {}",
|
log::info!("{}: PipeWire node selected: {}", self.name.clone(), node_id,);
|
||||||
self.name.clone(),
|
let capture = PipewireCapture::new(self.name.clone(), node_id);
|
||||||
pw_result.node_id
|
|
||||||
);
|
|
||||||
let capture = PipewireCapture::new(self.name.clone(), pw_result.node_id);
|
|
||||||
self.renderer = Some(ScreenRenderer::new_raw(
|
self.renderer = Some(ScreenRenderer::new_raw(
|
||||||
self.name.clone(),
|
self.name.clone(),
|
||||||
Box::new(capture),
|
Box::new(capture),
|
||||||
|
|||||||
@@ -21,13 +21,18 @@ use wlx_capture::{
|
|||||||
WlxCapture,
|
WlxCapture,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "pipewire")]
|
||||||
|
use {
|
||||||
|
crate::config_io,
|
||||||
|
std::error::Error,
|
||||||
|
std::{ops::Deref, path::PathBuf},
|
||||||
|
wlx_capture::pipewire::{pipewire_select_screen, PipewireCapture, PipewireStream},
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "wayland")]
|
||||||
use {
|
use {
|
||||||
crate::config::AStrMapExt,
|
crate::config::AStrMapExt,
|
||||||
crate::config_io,
|
|
||||||
std::{error::Error, ops::Deref, path::PathBuf},
|
|
||||||
wlx_capture::{
|
wlx_capture::{
|
||||||
pipewire::{pipewire_select_screen, PipewireCapture},
|
|
||||||
wayland::{wayland_client::protocol::wl_output, WlxClient, WlxOutput},
|
wayland::{wayland_client::protocol::wl_output, WlxClient, WlxOutput},
|
||||||
wlr_dmabuf::WlrDmabufCapture,
|
wlr_dmabuf::WlrDmabufCapture,
|
||||||
wlr_screencopy::WlrScreencopyCapture,
|
wlr_screencopy::WlrScreencopyCapture,
|
||||||
@@ -327,10 +332,17 @@ impl ScreenRenderer {
|
|||||||
)> {
|
)> {
|
||||||
let name = output.name.clone();
|
let name = output.name.clone();
|
||||||
let embed_mouse = !session.config.double_cursor_fix;
|
let embed_mouse = !session.config.double_cursor_fix;
|
||||||
let select_screen_result =
|
let select_screen_result = futures::executor::block_on(pipewire_select_screen(
|
||||||
futures::executor::block_on(pipewire_select_screen(token, embed_mouse, true, true))?;
|
token,
|
||||||
|
embed_mouse,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
))?;
|
||||||
|
|
||||||
let capture = PipewireCapture::new(name, select_screen_result.node_id);
|
let node_id = select_screen_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element
|
||||||
|
|
||||||
|
let capture = PipewireCapture::new(name, node_id);
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
ScreenRenderer {
|
ScreenRenderer {
|
||||||
@@ -667,14 +679,14 @@ pub struct TokenConf {
|
|||||||
pub pw_tokens: PwTokenMap,
|
pub pw_tokens: PwTokenMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "pipewire")]
|
||||||
fn get_pw_token_path() -> PathBuf {
|
fn get_pw_token_path() -> PathBuf {
|
||||||
let mut path = config_io::get_conf_d_path();
|
let mut path = config_io::get_conf_d_path();
|
||||||
path.push("pw_tokens.yaml");
|
path.push("pw_tokens.yaml");
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "pipewire")]
|
||||||
pub fn save_pw_token_config(tokens: PwTokenMap) -> Result<(), Box<dyn Error>> {
|
pub fn save_pw_token_config(tokens: PwTokenMap) -> Result<(), Box<dyn Error>> {
|
||||||
let conf = TokenConf { pw_tokens: tokens };
|
let conf = TokenConf { pw_tokens: tokens };
|
||||||
let yaml = serde_yaml::to_string(&conf)?;
|
let yaml = serde_yaml::to_string(&conf)?;
|
||||||
@@ -683,7 +695,7 @@ pub fn save_pw_token_config(tokens: PwTokenMap) -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "pipewire")]
|
||||||
pub fn load_pw_token_config() -> Result<PwTokenMap, Box<dyn Error>> {
|
pub fn load_pw_token_config() -> Result<PwTokenMap, Box<dyn Error>> {
|
||||||
let yaml = std::fs::read_to_string(get_pw_token_path())?;
|
let yaml = std::fs::read_to_string(get_pw_token_path())?;
|
||||||
let conf: TokenConf = serde_yaml::from_str(yaml.as_str())?;
|
let conf: TokenConf = serde_yaml::from_str(yaml.as_str())?;
|
||||||
@@ -782,12 +794,102 @@ pub fn create_screens_wayland(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "x11"))]
|
#[cfg(not(feature = "x11"))]
|
||||||
pub fn create_screens_x11(_app: &mut AppState) -> anyhow::Result<ScreenCreateData> {
|
pub fn create_screens_xshm(_app: &mut AppState) -> anyhow::Result<ScreenCreateData> {
|
||||||
anyhow::bail!("X11 support not enabled")
|
anyhow::bail!("X11 support not enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(all(feature = "x11", feature = "pipewire")))]
|
||||||
|
pub fn create_screens_x11pw(_app: &mut AppState) -> anyhow::Result<ScreenCreateData> {
|
||||||
|
anyhow::bail!("Pipewire support not enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "x11", feature = "pipewire"))]
|
||||||
|
pub fn create_screens_x11pw(app: &mut AppState) -> anyhow::Result<ScreenCreateData> {
|
||||||
|
use crate::config::{AStrMap, AStrMapExt};
|
||||||
|
use anyhow::bail;
|
||||||
|
|
||||||
|
// Load existing Pipewire tokens from file
|
||||||
|
let mut pw_tokens: PwTokenMap = if let Ok(conf) = load_pw_token_config() {
|
||||||
|
conf
|
||||||
|
} else {
|
||||||
|
AStrMap::new()
|
||||||
|
};
|
||||||
|
let pw_tokens_copy = pw_tokens.clone();
|
||||||
|
let token = pw_tokens.arc_get("x11").map(|s| s.as_str());
|
||||||
|
let embed_mouse = !app.session.config.double_cursor_fix;
|
||||||
|
let select_screen_result =
|
||||||
|
futures::executor::block_on(pipewire_select_screen(token, embed_mouse, true, true, true))?;
|
||||||
|
if let Some(restore_token) = select_screen_result.restore_token {
|
||||||
|
if pw_tokens.arc_set("x11".into(), restore_token.clone()) {
|
||||||
|
log::info!("Adding Pipewire token {}", restore_token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pw_tokens_copy != pw_tokens {
|
||||||
|
// Token list changed, re-create token config file
|
||||||
|
if let Err(err) = save_pw_token_config(pw_tokens) {
|
||||||
|
log::error!("Failed to save Pipewire token config: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let monitors = match XshmCapture::get_monitors() {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
bail!(e.to_string());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
log::info!("Got {} monitors", monitors.len());
|
||||||
|
log::info!("Got {} streams", select_screen_result.streams.len());
|
||||||
|
|
||||||
|
let mut extent = vec2(0., 0.);
|
||||||
|
let screens = select_screen_result
|
||||||
|
.streams
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, s)| {
|
||||||
|
let m = best_match(&s, monitors.iter().map(AsRef::as_ref)).unwrap();
|
||||||
|
log::info!("Stream {i} is {}", m.name);
|
||||||
|
extent.x = extent.x.max((m.monitor.x() + m.monitor.width()) as f32);
|
||||||
|
extent.y = extent.y.max((m.monitor.y() + m.monitor.height()) as f32);
|
||||||
|
|
||||||
|
let size = (m.monitor.width(), m.monitor.height());
|
||||||
|
let interaction = create_screen_interaction(
|
||||||
|
vec2(m.monitor.x() as f32, m.monitor.y() as f32),
|
||||||
|
vec2(m.monitor.width() as f32, m.monitor.height() as f32),
|
||||||
|
Transform::Normal,
|
||||||
|
);
|
||||||
|
|
||||||
|
let state = create_screen_state(m.name.clone(), size, Transform::Normal, &app.session);
|
||||||
|
|
||||||
|
let meta = ScreenMeta {
|
||||||
|
name: m.name.clone(),
|
||||||
|
id: state.id,
|
||||||
|
native_handle: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let renderer = ScreenRenderer {
|
||||||
|
name: m.name.clone(),
|
||||||
|
capture: Box::new(PipewireCapture::new(m.name.clone(), s.node_id)),
|
||||||
|
pipeline: None,
|
||||||
|
last_view: None,
|
||||||
|
extent: extent_from_res(size),
|
||||||
|
};
|
||||||
|
|
||||||
|
let backend = Box::new(SplitOverlayBackend {
|
||||||
|
renderer: Box::new(renderer),
|
||||||
|
interaction: Box::new(interaction),
|
||||||
|
});
|
||||||
|
(meta, state, backend)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
app.hid_provider.set_desktop_extent(extent);
|
||||||
|
app.hid_provider.set_desktop_origin(vec2(0.0, 0.0));
|
||||||
|
|
||||||
|
Ok(ScreenCreateData { screens })
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "x11")]
|
#[cfg(feature = "x11")]
|
||||||
pub fn create_screens_x11(app: &mut AppState) -> anyhow::Result<ScreenCreateData> {
|
pub fn create_screens_xshm(app: &mut AppState) -> anyhow::Result<ScreenCreateData> {
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
|
|
||||||
let mut extent = vec2(0., 0.);
|
let mut extent = vec2(0., 0.);
|
||||||
@@ -839,6 +941,7 @@ pub fn create_screens_x11(app: &mut AppState) -> anyhow::Result<ScreenCreateData
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
app.hid_provider.set_desktop_extent(extent);
|
app.hid_provider.set_desktop_extent(extent);
|
||||||
|
app.hid_provider.set_desktop_origin(vec2(0.0, 0.0));
|
||||||
|
|
||||||
Ok(ScreenCreateData { screens })
|
Ok(ScreenCreateData { screens })
|
||||||
}
|
}
|
||||||
@@ -880,3 +983,31 @@ fn extent_from_res(res: (i32, i32)) -> [u32; 3] {
|
|||||||
let h = (res.1 as f32 / res.0 as f32 * w as f32) as u32;
|
let h = (res.1 as f32 / res.0 as f32 * w as f32) as u32;
|
||||||
[w, h, 1]
|
[w, h, 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "pipewire", feature = "x11"))]
|
||||||
|
fn best_match<'a>(
|
||||||
|
stream: &PipewireStream,
|
||||||
|
mut streams: impl Iterator<Item = &'a XshmScreen>,
|
||||||
|
) -> Option<&'a XshmScreen> {
|
||||||
|
let mut best = streams.next();
|
||||||
|
log::debug!("stream: {:?}", stream.position);
|
||||||
|
log::debug!("first: {:?}", best.map(|b| &b.monitor));
|
||||||
|
let Some(position) = stream.position else {
|
||||||
|
return best;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut best_dist = best
|
||||||
|
.map(|b| (b.monitor.x() - position.0).abs() + (b.monitor.y() - position.1).abs())
|
||||||
|
.unwrap_or(i32::MAX);
|
||||||
|
for stream in streams {
|
||||||
|
log::debug!("checking: {:?}", stream.monitor);
|
||||||
|
let dist =
|
||||||
|
(stream.monitor.x() - position.0).abs() + (stream.monitor.y() - position.1).abs();
|
||||||
|
if dist < best_dist {
|
||||||
|
best = Some(stream);
|
||||||
|
best_dist = dist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::debug!("best: {:?}", best.map(|b| &b.monitor));
|
||||||
|
best
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user