core: permissions change, client: weyland window support
This commit is contained in:
parent
4f69556235
commit
3cdf5c0718
@ -29,6 +29,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
async-stream = "0.3"
|
||||
shell-escape = "0.1"
|
||||
v4l = "0.14"
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.13"
|
||||
|
||||
@ -7,6 +7,7 @@ use gstreamer as gst;
|
||||
use gstreamer_app as gst_app;
|
||||
use gst::prelude::*;
|
||||
use lesavka_common::lesavka::VideoPacket;
|
||||
use v4l::capability::Capabilities;
|
||||
|
||||
pub struct CameraCapture {
|
||||
pipeline: gst::Pipeline,
|
||||
@ -17,10 +18,13 @@ impl CameraCapture {
|
||||
pub fn new(device_fragment: Option<&str>) -> anyhow::Result<Self> {
|
||||
gst::init().ok();
|
||||
|
||||
// Pick device
|
||||
let dev = device_fragment
|
||||
.and_then(Self::find_device)
|
||||
.unwrap_or_else(|| "/dev/video0".into());
|
||||
// Pick device (prefers V4L2 nodes with capture capability)
|
||||
let dev = match device_fragment {
|
||||
Some(path) if path.starts_with("/dev/") => path.to_string(),
|
||||
Some(fragment) => Self::find_device(fragment)
|
||||
.unwrap_or_else(|| "/dev/video0".into()),
|
||||
None => "/dev/video0".into(),
|
||||
};
|
||||
|
||||
// let (enc, raw_caps) = Self::pick_encoder();
|
||||
// (NVIDIA → VA-API → software x264).
|
||||
@ -91,20 +95,51 @@ impl CameraCapture {
|
||||
Some(VideoPacket { id: 2, pts, data: map.as_slice().to_vec() })
|
||||
}
|
||||
|
||||
/// Fuzzy‑match devices under `/dev/v4l/by-id`
|
||||
/// Fuzzy‑match devices under `/dev/v4l/by-id`, preferring capture nodes
|
||||
fn find_device(substr: &str) -> Option<String> {
|
||||
let dir = std::fs::read_dir("/dev/v4l/by-id").ok()?;
|
||||
for e in dir.flatten() {
|
||||
let p = e.path();
|
||||
if p.file_name()?.to_string_lossy().contains(substr) {
|
||||
if let Ok(target) = std::fs::read_link(&p) {
|
||||
return Some(format!("/dev/{}", target.file_name()?.to_string_lossy()));
|
||||
let wanted = substr.to_ascii_lowercase();
|
||||
let mut matches: Vec<_> = std::fs::read_dir("/dev/v4l/by-id")
|
||||
.ok()?
|
||||
.flatten()
|
||||
.filter_map(|e| {
|
||||
let p = e.path();
|
||||
let name = p.file_name()?.to_string_lossy().to_ascii_lowercase();
|
||||
if name.contains(&wanted) {
|
||||
Some(p)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// deterministic order
|
||||
matches.sort();
|
||||
|
||||
for p in matches {
|
||||
if let Ok(target) = std::fs::read_link(&p) {
|
||||
let dev = format!("/dev/{}", target.file_name()?.to_string_lossy());
|
||||
if Self::is_capture(&dev) {
|
||||
return Some(dev);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn is_capture(dev: &str) -> bool {
|
||||
match v4l::Device::with_path(dev) {
|
||||
Ok(d) => d
|
||||
.query_caps()
|
||||
.map(|caps| {
|
||||
let c = caps.capabilities;
|
||||
c.contains(Capabilities::VIDEO_CAPTURE)
|
||||
|| c.contains(Capabilities::VIDEO_CAPTURE_MPLANE)
|
||||
})
|
||||
.unwrap_or(false),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Cheap stub used when the web‑cam is disabled
|
||||
pub fn new_stub() -> Self {
|
||||
let pipeline = gst::Pipeline::new();
|
||||
|
||||
@ -28,11 +28,14 @@ async fn main() -> Result<()> {
|
||||
/*------------- common filter & stderr layer ------------------------*/
|
||||
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
EnvFilter::new(
|
||||
"lesavka_client=trace,\
|
||||
lesavka_server=trace,\
|
||||
tonic=debug,\
|
||||
h2=debug,\
|
||||
tower=debug",
|
||||
"warn,\
|
||||
lesavka_client::app=info,\
|
||||
lesavka_client::input::camera=debug,\
|
||||
lesavka_client::output::video=info,\
|
||||
lesavka_client::output::audio=info,\
|
||||
tonic=warn,\
|
||||
h2=warn,\
|
||||
tower=warn",
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
@ -62,6 +62,9 @@ impl MonitorWindow {
|
||||
|
||||
/* -------- move/resize overlay ---------------------------------- */
|
||||
if let Some(sink_elem) = pipeline.by_name("sink") {
|
||||
// give the native window a predictable title for wmctrl/placement
|
||||
sink_elem.set_property("window-title", &format!("Lesavka-eye-{id}")).ok();
|
||||
|
||||
if let Ok(overlay) = sink_elem.dynamic_cast::<VideoOverlay>() {
|
||||
if let Some(r) = rects.get(id as usize) {
|
||||
// 1. Tell glimagesink how to crop the texture in its own window
|
||||
@ -84,6 +87,11 @@ impl MonitorWindow {
|
||||
run: Arc<dyn Fn(&str) -> std::io::Result<ExitStatus> + Send + Sync>,
|
||||
}
|
||||
|
||||
let is_kde = std::env::var("XDG_CURRENT_DESKTOP")
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase()
|
||||
.contains("kde");
|
||||
|
||||
let placer = if Command::new("swaymsg").arg("-t").arg("get_tree")
|
||||
.output().is_ok()
|
||||
{
|
||||
@ -100,6 +108,30 @@ impl MonitorWindow {
|
||||
.status()
|
||||
}),
|
||||
}
|
||||
} else if is_kde {
|
||||
// KWin DBus script: move/resize by title
|
||||
Placer {
|
||||
name: "kwin",
|
||||
run: Arc::new(|script| {
|
||||
let dbus = if Command::new("qdbus6")
|
||||
.arg("--version")
|
||||
.status()
|
||||
.is_ok()
|
||||
{
|
||||
"qdbus6"
|
||||
} else {
|
||||
"qdbus"
|
||||
};
|
||||
Command::new(dbus)
|
||||
.args([
|
||||
"org.kde.KWin",
|
||||
"/KWin",
|
||||
"org.kde.KWin.evaluateScript",
|
||||
script,
|
||||
])
|
||||
.status()
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
Placer {
|
||||
name: "noop",
|
||||
@ -113,24 +145,40 @@ impl MonitorWindow {
|
||||
};
|
||||
|
||||
if placer.name != "noop" {
|
||||
// Criteria string that works for i3-/sway-compatible IPC
|
||||
let criteria = format!(
|
||||
r#"[title="^Lesavka-eye-{id}$"] \
|
||||
resize set {w} {h}; \
|
||||
move absolute position {x} {y}"#,
|
||||
w = r.w,
|
||||
h = r.h,
|
||||
x = r.x,
|
||||
y = r.y,
|
||||
);
|
||||
|
||||
let cmd = match placer.name {
|
||||
// Criteria string that works for i3-/sway-compatible IPC
|
||||
"swaymsg" | "hyprctl" => format!(
|
||||
r#"[title="^Lesavka-eye-{id}$"] \
|
||||
resize set {w} {h}; \
|
||||
move absolute position {x} {y}"#,
|
||||
w = r.w,
|
||||
h = r.h,
|
||||
x = r.x,
|
||||
y = r.y,
|
||||
),
|
||||
// KWin JS via DBus
|
||||
"kwin" => format!(
|
||||
"var clients = workspace.clientList(); \
|
||||
clients.forEach(function(c) {{ \
|
||||
if (c.caption === \"Lesavka-eye-{id}\") {{ \
|
||||
c.geometry = {{x:{x}, y:{y}, width:{w}, height:{h}}}; \
|
||||
}} \
|
||||
}});",
|
||||
x = r.x,
|
||||
y = r.y,
|
||||
w = r.w,
|
||||
h = r.h,
|
||||
),
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
// Retry in a detached thread - avoids blocking GStreamer
|
||||
let placename = placer.name;
|
||||
let runner = placer.run.clone();
|
||||
thread::spawn(move || {
|
||||
for attempt in 1..=10 {
|
||||
thread::sleep(Duration::from_millis(300));
|
||||
match runner(&criteria) {
|
||||
match runner(&cmd) {
|
||||
Ok(st) if st.success() => {
|
||||
tracing::info!(
|
||||
"✅ {placename}: placed eye-{id} (attempt {attempt})"
|
||||
@ -145,6 +193,38 @@ impl MonitorWindow {
|
||||
});
|
||||
}
|
||||
}
|
||||
// 3. X11 placement (kwin/i3/etc.) via wmctrl
|
||||
else if std::env::var_os("DISPLAY").is_some() {
|
||||
let title = format!("Lesavka-eye-{id}");
|
||||
let w = r.w;
|
||||
let h = r.h;
|
||||
let x = r.x;
|
||||
let y = r.y;
|
||||
std::thread::spawn(move || {
|
||||
for attempt in 1..=10 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
let status = Command::new("wmctrl")
|
||||
.args([
|
||||
"-r",
|
||||
&title,
|
||||
"-e",
|
||||
&format!("0,{x},{y},{w},{h}"),
|
||||
])
|
||||
.status();
|
||||
match status {
|
||||
Ok(st) if st.success() => {
|
||||
tracing::info!(
|
||||
"✅ wmctrl placed eye-{id} (attempt {attempt})"
|
||||
);
|
||||
break;
|
||||
}
|
||||
_ => tracing::debug!(
|
||||
"⌛ wmctrl: eye-{id} not mapped yet (attempt {attempt})"
|
||||
),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,8 @@ ORIG_USER=${SUDO_USER:-$(id -un)}
|
||||
sudo pacman -Syq --needed --noconfirm \
|
||||
git rustup protobuf gcc evtest base-devel \
|
||||
gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav \
|
||||
pipewire pipewire-pulse
|
||||
pipewire pipewire-pulse \
|
||||
wmctrl qt6-tools
|
||||
|
||||
# 1b. yay for AUR bits (run as the invoking user, never root)
|
||||
if ! command -v yay >/dev/null 2>&1; then
|
||||
|
||||
@ -147,7 +147,8 @@ Requires=sys-kernel-config.mount
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/lesavka-core.sh
|
||||
RemainAfterExit=yes
|
||||
CapabilityBoundingSet=CAP_SYS_ADMIN
|
||||
CapabilityBoundingSet=CAP_SYS_ADMIN CAP_SYS_MODULE
|
||||
AmbientCapabilities=CAP_SYS_MODULE
|
||||
MountFlags=slave
|
||||
|
||||
[Install]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user