AV fix - more or less
This commit is contained in:
parent
e62f588cf5
commit
6241715c57
@ -20,8 +20,9 @@ futures = "0.3"
|
||||
evdev = "0.13"
|
||||
gstreamer = { version = "0.23", features = ["v1_22"] }
|
||||
gstreamer-app = "0.23"
|
||||
gstreamer-video = "0.23"
|
||||
gstreamer-video = { package = "gstreamer-video", version = "0.23" }
|
||||
gstreamer-gl-wayland = "0.23"
|
||||
gtk = { version = "0.8", package = "gtk4", features = ["v4_6"] }
|
||||
winit = "0.30"
|
||||
raw-window-handle = "0.6"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
@ -70,10 +70,10 @@ impl LesavkaClientApp {
|
||||
let kbd_loop = self.stream_loop_keyboard(hid_ep.clone());
|
||||
let mou_loop = self.stream_loop_mouse(hid_ep.clone());
|
||||
|
||||
/*────────── optional 30 s auto-exit in dev mode */
|
||||
/*───────── optional 300 s auto-exit in dev mode */
|
||||
let suicide = async {
|
||||
if self.dev_mode {
|
||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
||||
tokio::time::sleep(Duration::from_secs(300)).await;
|
||||
warn!("💀 dev-mode timeout");
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
@ -85,6 +85,7 @@ impl LesavkaClientApp {
|
||||
let (video_tx, mut video_rx) = tokio::sync::mpsc::unbounded_channel::<VideoPacket>();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
gtk::init().expect("GTK initialisation failed");
|
||||
let el = EventLoopBuilder::<()>::new().with_any_thread(true).build().unwrap();
|
||||
let win0 = MonitorWindow::new(0).expect("win0");
|
||||
let win1 = MonitorWindow::new(1).expect("win1");
|
||||
|
||||
@ -49,7 +49,7 @@ impl InputAggregator {
|
||||
continue;
|
||||
}
|
||||
|
||||
// ─── open the event node read‑write *without* unsafe ──────────
|
||||
// ─── open the event node read-write *without* unsafe ──────────
|
||||
let mut dev = match Device::open(&path) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// client/src/layout.rs – Wayland‑only window placement utilities
|
||||
// client/src/layout.rs – Wayland-only window placement utilities
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use std::process::Command;
|
||||
@ -9,13 +9,13 @@ use serde_json::Value;
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Layout {
|
||||
SideBySide, // two halves on the current monitor
|
||||
FullLeft, // left eye full‑screen, right hidden
|
||||
FullRight, // right eye full‑screen, left hidden
|
||||
FullLeft, // left eye full-screen, right hidden
|
||||
FullRight, // right eye full-screen, left hidden
|
||||
}
|
||||
|
||||
/// Move/resize a window titled “Lesavka‑eye‑{eye}” using `swaymsg`.
|
||||
/// Move/resize a window titled “Lesavka-eye-{eye}” using `swaymsg`.
|
||||
fn place_window(eye: u32, x: i32, y: i32, w: i32, h: i32) {
|
||||
let title = format!("Lesavka‑eye‑{eye}");
|
||||
let title = format!("Lesavka-eye-{eye}");
|
||||
let cmd = format!(
|
||||
r#"[title="^{title}$"] resize set {w} {h}; move position {x} {y}"#
|
||||
);
|
||||
@ -27,7 +27,7 @@ fn place_window(eye: u32, x: i32, y: i32, w: i32, h: i32) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a layout on the **currently‑focused** output.
|
||||
/// Apply a layout on the **currently-focused** output.
|
||||
/// No `?` operators → the function can stay `-> ()`.
|
||||
pub fn apply(layout: Layout) {
|
||||
let out = match Command::new("swaymsg")
|
||||
@ -57,7 +57,7 @@ pub fn apply(layout: Layout) {
|
||||
}
|
||||
Layout::FullLeft => {
|
||||
place_window(0, x, y, w, h);
|
||||
place_window(1, x + w * 2, y + h * 2, 1, 1); // off‑screen
|
||||
place_window(1, x + w * 2, y + h * 2, 1, 1); // off-screen
|
||||
}
|
||||
Layout::FullRight => {
|
||||
place_window(1, x, y, w, h);
|
||||
|
||||
@ -14,12 +14,12 @@ impl AudioOut {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
gst::init()?;
|
||||
|
||||
// Auto‑audiosink picks PipeWire / Pulse etc.
|
||||
// Auto-audiosink picks PipeWire / Pulse etc.
|
||||
const PIPE: &str =
|
||||
"appsrc name=src is-live=true format=time do-timestamp=true block=false ! \
|
||||
queue leaky=downstream ! aacparse ! avdec_aac ! audioresample ! autoaudiosink";
|
||||
|
||||
// `parse_launch()` returns `gst::Element`; down‑cast manually and
|
||||
// `parse_launch()` returns `gst::Element`; down-cast manually and
|
||||
// map the error into anyhow ourselves (no `?` on the downcast).
|
||||
let pipeline: gst::Pipeline = gst::parse::launch(PIPE)?
|
||||
.downcast::<gst::Pipeline>()
|
||||
|
||||
54
client/src/output/display.rs
Normal file
54
client/src/output/display.rs
Normal file
@ -0,0 +1,54 @@
|
||||
// client/src/output/display.rs
|
||||
|
||||
use gtk::prelude::ListModelExt;
|
||||
use gtk::prelude::*;
|
||||
use gtk::gdk;
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MonitorInfo {
|
||||
pub geometry: gdk::Rectangle,
|
||||
pub scale_factor: i32,
|
||||
pub is_internal: bool,
|
||||
}
|
||||
|
||||
/// Enumerate monitors sorted by our desired priority.
|
||||
pub fn enumerate_monitors() -> Vec<MonitorInfo> {
|
||||
let Some(display) = gdk::Display::default() else {
|
||||
tracing::warn!("⚠️ no GDK display – falling back to single-monitor 0,0");
|
||||
return vec![MonitorInfo {
|
||||
geometry: gdk::Rectangle::new(0, 0, 1920, 1080),
|
||||
scale_factor: 1,
|
||||
is_internal: false,
|
||||
}];
|
||||
};
|
||||
let model = display.monitors(); // gio::ListModel
|
||||
|
||||
let mut list: Vec<_> = (0..model.n_items())
|
||||
.filter_map(|i| model.item(i))
|
||||
.filter_map(|obj| obj.downcast::<gdk::Monitor>().ok())
|
||||
.map(|m| {
|
||||
// -------- internal vs external ----------------------------------
|
||||
let connector = m.connector().unwrap_or_default(); // e.g. "eDP-1"
|
||||
let is_internal = connector.starts_with("eDP")
|
||||
|| connector.starts_with("LVDS")
|
||||
|| connector.starts_with("DSI")
|
||||
|| connector.to_ascii_lowercase().contains("internal");
|
||||
|
||||
// -------- geometry / scale --------------------------------------
|
||||
let geometry = m.geometry();
|
||||
let scale_factor = m.scale_factor();
|
||||
|
||||
debug!(
|
||||
"🖥️ monitor: {:?}, connector={:?}, geom={:?}, scale={}",
|
||||
m.model(), connector, geometry, scale_factor
|
||||
);
|
||||
MonitorInfo { geometry, scale_factor, is_internal }
|
||||
})
|
||||
.collect();
|
||||
|
||||
// external first, built-in last
|
||||
list.sort_by_key(|m| m.is_internal);
|
||||
debug!("🖥️ sorted monitors = {:?}", list);
|
||||
list
|
||||
}
|
||||
61
client/src/output/layout.rs
Normal file
61
client/src/output/layout.rs
Normal file
@ -0,0 +1,61 @@
|
||||
// client/src/output/layout.rs
|
||||
|
||||
use super::display::MonitorInfo;
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Rect { pub x: i32, pub y: i32, pub w: i32, pub h: i32 }
|
||||
|
||||
/// Compute rectangles for N video streams (all 16:9 here).
|
||||
pub fn assign_rectangles(
|
||||
monitors: &[MonitorInfo],
|
||||
streams: &[(&str, i32, i32)], // (name, w, h)
|
||||
) -> Vec<Rect> {
|
||||
let mut rects = vec![Rect { x:0, y:0, w:0, h:0 }; streams.len()];
|
||||
|
||||
match monitors.len() {
|
||||
0 => return rects, // impossible, but keep compiler happy
|
||||
1 => {
|
||||
// One monitor: side-by-side layout
|
||||
let m = &monitors[0].geometry;
|
||||
let total_native_width: i32 = streams.iter().map(|(_,w,_)| *w).sum();
|
||||
let scale = f64::min(
|
||||
m.width() as f64 / total_native_width as f64,
|
||||
m.height() as f64 / streams[0].2 as f64,
|
||||
);
|
||||
debug!("one-monitor scale = {}", scale);
|
||||
|
||||
let mut x = m.x();
|
||||
for (idx, &(_, w, h)) in streams.iter().enumerate() {
|
||||
let ww = (w as f64 * scale).round() as i32;
|
||||
let hh = (h as f64 * scale).round() as i32;
|
||||
rects[idx] = Rect { x, y: m.y(), w: ww, h: hh };
|
||||
x += ww;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// ≥2 monitors: map 1-to-1 until we run out
|
||||
for (idx, stream) in streams.iter().enumerate() {
|
||||
if idx >= monitors.len() { break; }
|
||||
|
||||
let m = &monitors[idx];
|
||||
let geom = m.geometry;
|
||||
let (w, h) = (stream.1, stream.2);
|
||||
|
||||
let scale = f64::min(
|
||||
geom.width() as f64 / w as f64,
|
||||
geom.height() as f64 / h as f64,
|
||||
);
|
||||
debug!("monitor#{idx} scale = {scale}");
|
||||
|
||||
let ww = (w as f64 * scale).round() as i32;
|
||||
let hh = (h as f64 * scale).round() as i32;
|
||||
let xx = geom.x() + (geom.width() - ww) / 2;
|
||||
let yy = geom.y() + (geom.height() - hh) / 2;
|
||||
rects[idx] = Rect { x: xx, y: yy, w: ww, h: hh };
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!("📐 final rectangles = {:?}", rects);
|
||||
rects
|
||||
}
|
||||
@ -2,3 +2,5 @@
|
||||
|
||||
pub mod audio;
|
||||
pub mod video;
|
||||
pub mod layout;
|
||||
pub mod display;
|
||||
|
||||
@ -1,13 +1,20 @@
|
||||
// client/src/output/video.rs
|
||||
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::thread;
|
||||
use anyhow::Context;
|
||||
use gstreamer as gst;
|
||||
use gstreamer_app as gst_app;
|
||||
use gstreamer_video::{prelude::*, VideoOverlay};
|
||||
use gst::prelude::*;
|
||||
use lesavka_common::lesavka::VideoPacket;
|
||||
use tracing::{error, info, warn, debug};
|
||||
use gstreamer_video as gst_video;
|
||||
use gstreamer_video::glib::Type;
|
||||
|
||||
use crate::output::{display, layout};
|
||||
|
||||
/* ---------- pipeline ----------------------------------------------------
|
||||
* ┌────────────┐ H.264/AU ┌─────────┐ Decoded ┌─────────────┐
|
||||
* │ AppSrc │────────────► decodebin ├──────────► glimagesink │
|
||||
@ -27,32 +34,20 @@ pub struct MonitorWindow {
|
||||
}
|
||||
|
||||
impl MonitorWindow {
|
||||
pub fn new(_id: u32) -> anyhow::Result<Self> {
|
||||
pub fn new(id: u32) -> anyhow::Result<Self> {
|
||||
gst::init().context("initialising GStreamer")?;
|
||||
|
||||
// --- Build pipeline ------------------------------------------------
|
||||
// --- Build pipeline ---------------------------------------------------
|
||||
let pipeline: gst::Pipeline = gst::parse::launch(PIPELINE_DESC)?
|
||||
.downcast::<gst::Pipeline>()
|
||||
.expect("not a pipeline");
|
||||
|
||||
// Optional: turn the sink full‑screen when LESAVKA_FULLSCREEN=1
|
||||
let fullscreen = std::env::var("LESAVKA_FULLSCREEN").is_ok();
|
||||
if let Some(sink) = pipeline.by_name("sink") {
|
||||
// glimagesink: title / fullscreen / force‑aspect‑ratio
|
||||
let title = format!("Lesavka‑eye‑{_id}");
|
||||
if sink.has_property("title", None::<Type>) {
|
||||
sink.set_property("title", &title);
|
||||
}
|
||||
if sink.has_property("window-title", None::<Type>) {
|
||||
sink.set_property("window-title", &title);
|
||||
}
|
||||
if sink.has_property("fullscreen", None::<Type>) && fullscreen {
|
||||
let _ = sink.set_property("fullscreen", &true);
|
||||
}
|
||||
let _ = sink.set_property("force-aspect-ratio", &true);
|
||||
}
|
||||
/* -------- placement maths -------------------------------------- */
|
||||
let monitors = display::enumerate_monitors();
|
||||
let stream_defs = &[("eye-0", 1920, 1080), ("eye-1", 1920, 1080)];
|
||||
let rects = layout::assign_rectangles(&monitors, stream_defs);
|
||||
|
||||
/* ---------- AppSrc ------------------------------------------------- */
|
||||
// --- AppSrc------------------------------------------------------------
|
||||
let src: gst_app::AppSrc = pipeline
|
||||
.by_name("src")
|
||||
.unwrap()
|
||||
@ -64,12 +59,99 @@ impl MonitorWindow {
|
||||
.field("alignment", &"au")
|
||||
.build()));
|
||||
src.set_format(gst::Format::Time);
|
||||
pipeline.set_state(gst::State::Playing)?;
|
||||
if let Some(sink) = pipeline.by_name("sink") {
|
||||
let title = format!("Lesavka‑eye‑{_id}");
|
||||
sink.set_property("force-aspect-ratio", &true);
|
||||
|
||||
/* -------- move/resize overlay ---------------------------------- */
|
||||
if let Some(sink_elem) = pipeline.by_name("sink") {
|
||||
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
|
||||
overlay.set_render_rectangle(r.x, r.y, r.w, r.h);
|
||||
debug!(
|
||||
"🔲 eye-{id} → render_rectangle({}, {}, {}, {})",
|
||||
r.x, r.y, r.w, r.h
|
||||
);
|
||||
|
||||
// 2. **Compositor-level** placement (Wayland only)
|
||||
if std::env::var_os("WAYLAND_DISPLAY").is_some() {
|
||||
use std::process::{Command, ExitStatus};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
// A small helper struct so the two branches return the same type
|
||||
struct Placer {
|
||||
name: &'static str,
|
||||
run: Arc<dyn Fn(&str) -> std::io::Result<ExitStatus> + Send + Sync>,
|
||||
}
|
||||
|
||||
let placer = if Command::new("swaymsg").arg("-t").arg("get_tree")
|
||||
.output().is_ok()
|
||||
{
|
||||
Placer {
|
||||
name: "swaymsg",
|
||||
run: Arc::new(|cmd| Command::new("swaymsg").arg(cmd).status()),
|
||||
}
|
||||
} else if Command::new("hyprctl").arg("version").output().is_ok() {
|
||||
Placer {
|
||||
name: "hyprctl",
|
||||
run: Arc::new(|cmd| {
|
||||
Command::new("hyprctl")
|
||||
.args(["dispatch", "exec", cmd])
|
||||
.status()
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
Placer {
|
||||
name: "noop",
|
||||
run: Arc::new(|_| {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"no swaymsg/hyprctl found",
|
||||
))
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
// 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) {
|
||||
Ok(st) if st.success() => {
|
||||
tracing::info!(
|
||||
"✅ {placename}: placed eye-{id} (attempt {attempt})"
|
||||
);
|
||||
break;
|
||||
}
|
||||
_ => tracing::debug!(
|
||||
"⌛ {placename}: eye-{id} not mapped yet (attempt {attempt})"
|
||||
),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pipeline.set_state(gst::State::Playing)?;
|
||||
|
||||
Ok(Self { _pipeline: pipeline, src })
|
||||
}
|
||||
|
||||
|
||||
@ -92,7 +92,7 @@ pub async fn eye_ball(
|
||||
} else {
|
||||
warn!(target:"lesavka_server::video",
|
||||
eye = %eye,
|
||||
"🍪 cam_{eye} not found – skipping pad‑probe");
|
||||
"🍪 cam_{eye} not found – skipping pad-probe");
|
||||
}
|
||||
|
||||
let eye_clone = eye.to_owned();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user