AV fix - more or less
This commit is contained in:
parent
e62f588cf5
commit
6241715c57
@ -20,8 +20,9 @@ futures = "0.3"
|
|||||||
evdev = "0.13"
|
evdev = "0.13"
|
||||||
gstreamer = { version = "0.23", features = ["v1_22"] }
|
gstreamer = { version = "0.23", features = ["v1_22"] }
|
||||||
gstreamer-app = "0.23"
|
gstreamer-app = "0.23"
|
||||||
gstreamer-video = "0.23"
|
gstreamer-video = { package = "gstreamer-video", version = "0.23" }
|
||||||
gstreamer-gl-wayland = "0.23"
|
gstreamer-gl-wayland = "0.23"
|
||||||
|
gtk = { version = "0.8", package = "gtk4", features = ["v4_6"] }
|
||||||
winit = "0.30"
|
winit = "0.30"
|
||||||
raw-window-handle = "0.6"
|
raw-window-handle = "0.6"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|||||||
@ -70,10 +70,10 @@ impl LesavkaClientApp {
|
|||||||
let kbd_loop = self.stream_loop_keyboard(hid_ep.clone());
|
let kbd_loop = self.stream_loop_keyboard(hid_ep.clone());
|
||||||
let mou_loop = self.stream_loop_mouse(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 {
|
let suicide = async {
|
||||||
if self.dev_mode {
|
if self.dev_mode {
|
||||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
tokio::time::sleep(Duration::from_secs(300)).await;
|
||||||
warn!("💀 dev-mode timeout");
|
warn!("💀 dev-mode timeout");
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
} else {
|
} else {
|
||||||
@ -85,6 +85,7 @@ impl LesavkaClientApp {
|
|||||||
let (video_tx, mut video_rx) = tokio::sync::mpsc::unbounded_channel::<VideoPacket>();
|
let (video_tx, mut video_rx) = tokio::sync::mpsc::unbounded_channel::<VideoPacket>();
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
|
gtk::init().expect("GTK initialisation failed");
|
||||||
let el = EventLoopBuilder::<()>::new().with_any_thread(true).build().unwrap();
|
let el = EventLoopBuilder::<()>::new().with_any_thread(true).build().unwrap();
|
||||||
let win0 = MonitorWindow::new(0).expect("win0");
|
let win0 = MonitorWindow::new(0).expect("win0");
|
||||||
let win1 = MonitorWindow::new(1).expect("win1");
|
let win1 = MonitorWindow::new(1).expect("win1");
|
||||||
|
|||||||
@ -49,7 +49,7 @@ impl InputAggregator {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── open the event node read‑write *without* unsafe ──────────
|
// ─── open the event node read-write *without* unsafe ──────────
|
||||||
let mut dev = match Device::open(&path) {
|
let mut dev = match Device::open(&path) {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
Err(e) => {
|
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)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@ -9,13 +9,13 @@ use serde_json::Value;
|
|||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum Layout {
|
pub enum Layout {
|
||||||
SideBySide, // two halves on the current monitor
|
SideBySide, // two halves on the current monitor
|
||||||
FullLeft, // left eye full‑screen, right hidden
|
FullLeft, // left eye full-screen, right hidden
|
||||||
FullRight, // right eye full‑screen, left 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) {
|
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!(
|
let cmd = format!(
|
||||||
r#"[title="^{title}$"] resize set {w} {h}; move position {x} {y}"#
|
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 `-> ()`.
|
/// No `?` operators → the function can stay `-> ()`.
|
||||||
pub fn apply(layout: Layout) {
|
pub fn apply(layout: Layout) {
|
||||||
let out = match Command::new("swaymsg")
|
let out = match Command::new("swaymsg")
|
||||||
@ -57,7 +57,7 @@ pub fn apply(layout: Layout) {
|
|||||||
}
|
}
|
||||||
Layout::FullLeft => {
|
Layout::FullLeft => {
|
||||||
place_window(0, x, y, w, h);
|
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 => {
|
Layout::FullRight => {
|
||||||
place_window(1, x, y, w, h);
|
place_window(1, x, y, w, h);
|
||||||
|
|||||||
@ -14,12 +14,12 @@ impl AudioOut {
|
|||||||
pub fn new() -> anyhow::Result<Self> {
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
gst::init()?;
|
gst::init()?;
|
||||||
|
|
||||||
// Auto‑audiosink picks PipeWire / Pulse etc.
|
// Auto-audiosink picks PipeWire / Pulse etc.
|
||||||
const PIPE: &str =
|
const PIPE: &str =
|
||||||
"appsrc name=src is-live=true format=time do-timestamp=true block=false ! \
|
"appsrc name=src is-live=true format=time do-timestamp=true block=false ! \
|
||||||
queue leaky=downstream ! aacparse ! avdec_aac ! audioresample ! autoaudiosink";
|
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).
|
// map the error into anyhow ourselves (no `?` on the downcast).
|
||||||
let pipeline: gst::Pipeline = gst::parse::launch(PIPE)?
|
let pipeline: gst::Pipeline = gst::parse::launch(PIPE)?
|
||||||
.downcast::<gst::Pipeline>()
|
.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
|
||||||
|
}
|
||||||
@ -1,4 +1,6 @@
|
|||||||
// client/src/output/mod.rs
|
// client/src/output/mod.rs
|
||||||
|
|
||||||
pub mod audio;
|
pub mod audio;
|
||||||
pub mod video;
|
pub mod video;
|
||||||
|
pub mod layout;
|
||||||
|
pub mod display;
|
||||||
|
|||||||
@ -1,13 +1,20 @@
|
|||||||
// client/src/output/video.rs
|
// client/src/output/video.rs
|
||||||
|
|
||||||
|
use std::process::Command;
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::thread;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use gstreamer as gst;
|
use gstreamer as gst;
|
||||||
use gstreamer_app as gst_app;
|
use gstreamer_app as gst_app;
|
||||||
|
use gstreamer_video::{prelude::*, VideoOverlay};
|
||||||
use gst::prelude::*;
|
use gst::prelude::*;
|
||||||
use lesavka_common::lesavka::VideoPacket;
|
use lesavka_common::lesavka::VideoPacket;
|
||||||
use tracing::{error, info, warn, debug};
|
use tracing::{error, info, warn, debug};
|
||||||
|
use gstreamer_video as gst_video;
|
||||||
use gstreamer_video::glib::Type;
|
use gstreamer_video::glib::Type;
|
||||||
|
|
||||||
|
use crate::output::{display, layout};
|
||||||
|
|
||||||
/* ---------- pipeline ----------------------------------------------------
|
/* ---------- pipeline ----------------------------------------------------
|
||||||
* ┌────────────┐ H.264/AU ┌─────────┐ Decoded ┌─────────────┐
|
* ┌────────────┐ H.264/AU ┌─────────┐ Decoded ┌─────────────┐
|
||||||
* │ AppSrc │────────────► decodebin ├──────────► glimagesink │
|
* │ AppSrc │────────────► decodebin ├──────────► glimagesink │
|
||||||
@ -27,32 +34,20 @@ pub struct MonitorWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MonitorWindow {
|
impl MonitorWindow {
|
||||||
pub fn new(_id: u32) -> anyhow::Result<Self> {
|
pub fn new(id: u32) -> anyhow::Result<Self> {
|
||||||
gst::init().context("initialising GStreamer")?;
|
gst::init().context("initialising GStreamer")?;
|
||||||
|
|
||||||
// --- Build pipeline ------------------------------------------------
|
// --- Build pipeline ---------------------------------------------------
|
||||||
let pipeline: gst::Pipeline = gst::parse::launch(PIPELINE_DESC)?
|
let pipeline: gst::Pipeline = gst::parse::launch(PIPELINE_DESC)?
|
||||||
.downcast::<gst::Pipeline>()
|
.downcast::<gst::Pipeline>()
|
||||||
.expect("not a pipeline");
|
.expect("not a pipeline");
|
||||||
|
|
||||||
// Optional: turn the sink full‑screen when LESAVKA_FULLSCREEN=1
|
/* -------- placement maths -------------------------------------- */
|
||||||
let fullscreen = std::env::var("LESAVKA_FULLSCREEN").is_ok();
|
let monitors = display::enumerate_monitors();
|
||||||
if let Some(sink) = pipeline.by_name("sink") {
|
let stream_defs = &[("eye-0", 1920, 1080), ("eye-1", 1920, 1080)];
|
||||||
// glimagesink: title / fullscreen / force‑aspect‑ratio
|
let rects = layout::assign_rectangles(&monitors, stream_defs);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- AppSrc ------------------------------------------------- */
|
// --- AppSrc------------------------------------------------------------
|
||||||
let src: gst_app::AppSrc = pipeline
|
let src: gst_app::AppSrc = pipeline
|
||||||
.by_name("src")
|
.by_name("src")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@ -64,11 +59,98 @@ impl MonitorWindow {
|
|||||||
.field("alignment", &"au")
|
.field("alignment", &"au")
|
||||||
.build()));
|
.build()));
|
||||||
src.set_format(gst::Format::Time);
|
src.set_format(gst::Format::Time);
|
||||||
pipeline.set_state(gst::State::Playing)?;
|
|
||||||
if let Some(sink) = pipeline.by_name("sink") {
|
/* -------- move/resize overlay ---------------------------------- */
|
||||||
let title = format!("Lesavka‑eye‑{_id}");
|
if let Some(sink_elem) = pipeline.by_name("sink") {
|
||||||
sink.set_property("force-aspect-ratio", &true);
|
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 })
|
Ok(Self { _pipeline: pipeline, src })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -92,7 +92,7 @@ pub async fn eye_ball(
|
|||||||
} else {
|
} else {
|
||||||
warn!(target:"lesavka_server::video",
|
warn!(target:"lesavka_server::video",
|
||||||
eye = %eye,
|
eye = %eye,
|
||||||
"🍪 cam_{eye} not found – skipping pad‑probe");
|
"🍪 cam_{eye} not found – skipping pad-probe");
|
||||||
}
|
}
|
||||||
|
|
||||||
let eye_clone = eye.to_owned();
|
let eye_clone = eye.to_owned();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user