server: stabilize uvc device selection
This commit is contained in:
parent
3c1f647b04
commit
1bec61006d
@ -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 output‑capable node (gadget exposes one)
|
// walk /dev/video* via udev and look for an output‑capable 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"
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user