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"
|
serde_json = "1.0"
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
shell-escape = "0.1"
|
shell-escape = "0.1"
|
||||||
|
v4l = "0.14"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
prost-build = "0.13"
|
prost-build = "0.13"
|
||||||
|
|||||||
@ -7,6 +7,7 @@ use gstreamer as gst;
|
|||||||
use gstreamer_app as gst_app;
|
use gstreamer_app as gst_app;
|
||||||
use gst::prelude::*;
|
use gst::prelude::*;
|
||||||
use lesavka_common::lesavka::VideoPacket;
|
use lesavka_common::lesavka::VideoPacket;
|
||||||
|
use v4l::capability::Capabilities;
|
||||||
|
|
||||||
pub struct CameraCapture {
|
pub struct CameraCapture {
|
||||||
pipeline: gst::Pipeline,
|
pipeline: gst::Pipeline,
|
||||||
@ -17,10 +18,13 @@ impl CameraCapture {
|
|||||||
pub fn new(device_fragment: Option<&str>) -> anyhow::Result<Self> {
|
pub fn new(device_fragment: Option<&str>) -> anyhow::Result<Self> {
|
||||||
gst::init().ok();
|
gst::init().ok();
|
||||||
|
|
||||||
// Pick device
|
// Pick device (prefers V4L2 nodes with capture capability)
|
||||||
let dev = device_fragment
|
let dev = match device_fragment {
|
||||||
.and_then(Self::find_device)
|
Some(path) if path.starts_with("/dev/") => path.to_string(),
|
||||||
.unwrap_or_else(|| "/dev/video0".into());
|
Some(fragment) => Self::find_device(fragment)
|
||||||
|
.unwrap_or_else(|| "/dev/video0".into()),
|
||||||
|
None => "/dev/video0".into(),
|
||||||
|
};
|
||||||
|
|
||||||
// let (enc, raw_caps) = Self::pick_encoder();
|
// let (enc, raw_caps) = Self::pick_encoder();
|
||||||
// (NVIDIA → VA-API → software x264).
|
// (NVIDIA → VA-API → software x264).
|
||||||
@ -91,20 +95,51 @@ impl CameraCapture {
|
|||||||
Some(VideoPacket { id: 2, pts, data: map.as_slice().to_vec() })
|
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> {
|
fn find_device(substr: &str) -> Option<String> {
|
||||||
let dir = std::fs::read_dir("/dev/v4l/by-id").ok()?;
|
let wanted = substr.to_ascii_lowercase();
|
||||||
for e in dir.flatten() {
|
let mut matches: Vec<_> = std::fs::read_dir("/dev/v4l/by-id")
|
||||||
let p = e.path();
|
.ok()?
|
||||||
if p.file_name()?.to_string_lossy().contains(substr) {
|
.flatten()
|
||||||
if let Ok(target) = std::fs::read_link(&p) {
|
.filter_map(|e| {
|
||||||
return Some(format!("/dev/{}", target.file_name()?.to_string_lossy()));
|
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
|
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
|
/// Cheap stub used when the web‑cam is disabled
|
||||||
pub fn new_stub() -> Self {
|
pub fn new_stub() -> Self {
|
||||||
let pipeline = gst::Pipeline::new();
|
let pipeline = gst::Pipeline::new();
|
||||||
|
|||||||
@ -28,11 +28,14 @@ async fn main() -> Result<()> {
|
|||||||
/*------------- common filter & stderr layer ------------------------*/
|
/*------------- common filter & stderr layer ------------------------*/
|
||||||
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||||
EnvFilter::new(
|
EnvFilter::new(
|
||||||
"lesavka_client=trace,\
|
"warn,\
|
||||||
lesavka_server=trace,\
|
lesavka_client::app=info,\
|
||||||
tonic=debug,\
|
lesavka_client::input::camera=debug,\
|
||||||
h2=debug,\
|
lesavka_client::output::video=info,\
|
||||||
tower=debug",
|
lesavka_client::output::audio=info,\
|
||||||
|
tonic=warn,\
|
||||||
|
h2=warn,\
|
||||||
|
tower=warn",
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -62,6 +62,9 @@ impl MonitorWindow {
|
|||||||
|
|
||||||
/* -------- move/resize overlay ---------------------------------- */
|
/* -------- move/resize overlay ---------------------------------- */
|
||||||
if let Some(sink_elem) = pipeline.by_name("sink") {
|
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 Ok(overlay) = sink_elem.dynamic_cast::<VideoOverlay>() {
|
||||||
if let Some(r) = rects.get(id as usize) {
|
if let Some(r) = rects.get(id as usize) {
|
||||||
// 1. Tell glimagesink how to crop the texture in its own window
|
// 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>,
|
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")
|
let placer = if Command::new("swaymsg").arg("-t").arg("get_tree")
|
||||||
.output().is_ok()
|
.output().is_ok()
|
||||||
{
|
{
|
||||||
@ -100,6 +108,30 @@ impl MonitorWindow {
|
|||||||
.status()
|
.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 {
|
} else {
|
||||||
Placer {
|
Placer {
|
||||||
name: "noop",
|
name: "noop",
|
||||||
@ -113,24 +145,40 @@ impl MonitorWindow {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if placer.name != "noop" {
|
if placer.name != "noop" {
|
||||||
// Criteria string that works for i3-/sway-compatible IPC
|
let cmd = match placer.name {
|
||||||
let criteria = format!(
|
// Criteria string that works for i3-/sway-compatible IPC
|
||||||
r#"[title="^Lesavka-eye-{id}$"] \
|
"swaymsg" | "hyprctl" => format!(
|
||||||
resize set {w} {h}; \
|
r#"[title="^Lesavka-eye-{id}$"] \
|
||||||
move absolute position {x} {y}"#,
|
resize set {w} {h}; \
|
||||||
w = r.w,
|
move absolute position {x} {y}"#,
|
||||||
h = r.h,
|
w = r.w,
|
||||||
x = r.x,
|
h = r.h,
|
||||||
y = r.y,
|
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
|
// Retry in a detached thread - avoids blocking GStreamer
|
||||||
let placename = placer.name;
|
let placename = placer.name;
|
||||||
let runner = placer.run.clone();
|
let runner = placer.run.clone();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
for attempt in 1..=10 {
|
for attempt in 1..=10 {
|
||||||
thread::sleep(Duration::from_millis(300));
|
thread::sleep(Duration::from_millis(300));
|
||||||
match runner(&criteria) {
|
match runner(&cmd) {
|
||||||
Ok(st) if st.success() => {
|
Ok(st) if st.success() => {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"✅ {placename}: placed eye-{id} (attempt {attempt})"
|
"✅ {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 \
|
sudo pacman -Syq --needed --noconfirm \
|
||||||
git rustup protobuf gcc evtest base-devel \
|
git rustup protobuf gcc evtest base-devel \
|
||||||
gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav \
|
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)
|
# 1b. yay for AUR bits (run as the invoking user, never root)
|
||||||
if ! command -v yay >/dev/null 2>&1; then
|
if ! command -v yay >/dev/null 2>&1; then
|
||||||
|
|||||||
@ -147,7 +147,8 @@ Requires=sys-kernel-config.mount
|
|||||||
Type=oneshot
|
Type=oneshot
|
||||||
ExecStart=/usr/local/bin/lesavka-core.sh
|
ExecStart=/usr/local/bin/lesavka-core.sh
|
||||||
RemainAfterExit=yes
|
RemainAfterExit=yes
|
||||||
CapabilityBoundingSet=CAP_SYS_ADMIN
|
CapabilityBoundingSet=CAP_SYS_ADMIN CAP_SYS_MODULE
|
||||||
|
AmbientCapabilities=CAP_SYS_MODULE
|
||||||
MountFlags=slave
|
MountFlags=slave
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user