server: stabilize uvc device selection

This commit is contained in:
Brad Stein 2026-01-06 12:11:48 -03:00
parent 3c1f647b04
commit 1bec61006d
2 changed files with 51 additions and 3 deletions

View File

@ -5,6 +5,7 @@
use anyhow::Context as _; use anyhow::Context as _;
use futures_util::{Stream, StreamExt}; use futures_util::{Stream, StreamExt};
use gstreamer as gst; use gstreamer as gst;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use std::{backtrace::Backtrace, panic, pin::Pin, sync::Arc}; use std::{backtrace::Backtrace, panic, pin::Pin, sync::Arc};
@ -141,7 +142,16 @@ fn pick_uvc_device() -> anyhow::Result<String> {
return Ok(path); return Ok(path);
} }
let ctrl = UsbGadget::find_controller().ok();
if let Some(ctrl) = ctrl.as_deref() {
let by_path = format!("/dev/v4l/by-path/platform-{ctrl}-video-index0");
if Path::new(&by_path).exists() {
return Ok(by_path);
}
}
// walk /dev/video* via udev and look for an outputcapable node (gadget exposes one) // walk /dev/video* via udev and look for an outputcapable node (gadget exposes one)
let mut fallback: Option<String> = None;
if let Ok(mut en) = udev::Enumerator::new() { if let Ok(mut en) = udev::Enumerator::new() {
let _ = en.match_subsystem("video4linux"); let _ = en.match_subsystem("video4linux");
if let Ok(devs) = en.scan_devices() { if let Ok(devs) = en.scan_devices() {
@ -150,14 +160,33 @@ fn pick_uvc_device() -> anyhow::Result<String> {
.property_value("ID_V4L_CAPABILITIES") .property_value("ID_V4L_CAPABILITIES")
.and_then(|v| v.to_str()) .and_then(|v| v.to_str())
.unwrap_or_default(); .unwrap_or_default();
if caps.contains(":video_output:") { if !caps.contains(":video_output:") {
if let Some(node) = dev.devnode() { continue;
return Ok(node.to_string_lossy().into_owned()); }
let Some(node) = dev.devnode() else { continue };
let node = node.to_string_lossy().into_owned();
let product = dev
.property_value("ID_V4L_PRODUCT")
.and_then(|v| v.to_str())
.unwrap_or_default();
let path = dev
.property_value("ID_PATH")
.and_then(|v| v.to_str())
.unwrap_or_default();
if let Some(ctrl) = ctrl.as_deref() {
if product == ctrl || path.contains(ctrl) {
return Ok(node);
} }
} }
if fallback.is_none() {
fallback = Some(node);
}
} }
} }
} }
if let Some(node) = fallback {
return Ok(node);
}
Err(anyhow::anyhow!( Err(anyhow::anyhow!(
"no video_output v4l2 node found; set LESAVKA_UVC_DEV" "no video_output v4l2 node found; set LESAVKA_UVC_DEV"

View File

@ -10,6 +10,8 @@ use gstreamer_app as gst_app;
use lesavka_common::lesavka::VideoPacket; use lesavka_common::lesavka::VideoPacket;
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
use tonic::Status; use tonic::Status;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use tracing::{Level, debug, enabled, error, info, trace, warn}; use tracing::{Level, debug, enabled, error, info, trace, warn};
const EYE_ID: [&str; 2] = ["l", "r"]; const EYE_ID: [&str; 2] = ["l", "r"];
@ -81,6 +83,14 @@ pub async fn eye_ball(dev: &str, id: u32, _max_bitrate_kbit: u32) -> anyhow::Res
let eye = EYE_ID[id as usize]; let eye = EYE_ID[id as usize];
gst::init().context("gst init")?; gst::init().context("gst init")?;
let target_fps = env_u32("LESAVKA_EYE_FPS", 25);
let frame_interval_us = if target_fps == 0 {
0
} else {
(1_000_000 / target_fps) as u64
};
let last_sent = Arc::new(AtomicU64::new(0));
let desc = format!( let desc = format!(
"v4l2src name=cam_{eye} device=\"{dev}\" io-mode=mmap do-timestamp=true ! \ "v4l2src name=cam_{eye} device=\"{dev}\" io-mode=mmap do-timestamp=true ! \
queue ! \ queue ! \
@ -166,6 +176,7 @@ pub async fn eye_ball(dev: &str, id: u32, _max_bitrate_kbit: u32) -> anyhow::Res
} }
}); });
let last_sent_cloned = last_sent.clone();
sink.set_callbacks( sink.set_callbacks(
gst_app::AppSinkCallbacks::builder() gst_app::AppSinkCallbacks::builder()
.new_sample(move |sink| { .new_sample(move |sink| {
@ -211,6 +222,14 @@ pub async fn eye_ball(dev: &str, id: u32, _max_bitrate_kbit: u32) -> anyhow::Res
.nseconds() .nseconds()
/ 1_000; / 1_000;
if frame_interval_us > 0 {
let last = last_sent_cloned.load(Ordering::Relaxed);
if last != 0 && pts_us.saturating_sub(last) < frame_interval_us {
return Ok(gst::FlowSuccess::Ok);
}
last_sent_cloned.store(pts_us, Ordering::Relaxed);
}
/* -------- ship over gRPC ----- */ /* -------- ship over gRPC ----- */
let data = map.as_slice().to_vec(); let data = map.as_slice().to_vec();
let size = data.len(); let size = data.len();