install updates - relay service update
This commit is contained in:
parent
4f629facca
commit
d2677afc46
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,3 +9,4 @@ override.toml
|
||||
**/*~
|
||||
*.swp
|
||||
*.swo
|
||||
*.md
|
||||
|
||||
@ -5,8 +5,19 @@ set -euo pipefail
|
||||
ORIG_USER=${SUDO_USER:-$(id -un)}
|
||||
|
||||
# 1. packages (Arch)
|
||||
sudo pacman -Syq --needed --noconfirm git rustup protobuf gcc evtest gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav pipewire pipewire-pulse
|
||||
yay -S --noconfirm grpcurl-bin
|
||||
sudo pacman -Syq --needed --noconfirm \
|
||||
git rustup protobuf gcc evtest base-devel \
|
||||
gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav \
|
||||
pipewire pipewire-pulse
|
||||
|
||||
# 1b. yay for AUR bits (run as the invoking user, never root)
|
||||
if ! command -v yay >/dev/null 2>&1; then
|
||||
sudo -u "$ORIG_USER" bash -c 'cd /tmp && git clone --depth 1 https://aur.archlinux.org/yay.git &&
|
||||
cd yay && makepkg -si --noconfirm'
|
||||
fi
|
||||
sudo -u "$ORIG_USER" yay -S --needed --noconfirm grpcurl-bin
|
||||
|
||||
# 1c. input access
|
||||
sudo usermod -aG input "$ORIG_USER"
|
||||
|
||||
# 2. Rust tool-chain for both root & user
|
||||
@ -14,7 +25,9 @@ sudo rustup default stable
|
||||
sudo -u "$ORIG_USER" rustup default stable
|
||||
|
||||
# 3. clone / update into a user-writable dir
|
||||
SRC="$HOME/.local/src/lesavka"
|
||||
USER_HOME=$(getent passwd "$ORIG_USER" | cut -d: -f6)
|
||||
SRC="$USER_HOME/.local/src/lesavka"
|
||||
sudo -u "$ORIG_USER" mkdir -p "$(dirname "$SRC")"
|
||||
if [[ -d $SRC/.git ]]; then
|
||||
sudo -u "$ORIG_USER" git -C "$SRC" pull --ff-only
|
||||
else
|
||||
@ -41,7 +54,7 @@ Group=root
|
||||
|
||||
Environment=RUST_LOG=debug
|
||||
Environment=LESAVKA_DEV_MODE=1
|
||||
Environment=LESAVKA_SERVER_ADDR=http://64.25.10.31:50051
|
||||
Environment=LESAVKA_SERVER_ADDR=http://38.28.125.112:50051
|
||||
|
||||
ExecStart=/usr/local/bin/lesavka-client
|
||||
Restart=no
|
||||
|
||||
@ -24,6 +24,7 @@ sudo pacman -Syq --needed --noconfirm git \
|
||||
pipewire-pulse \
|
||||
tailscale \
|
||||
base-devel \
|
||||
v4l-utils \
|
||||
gstreamer \
|
||||
gst-plugins-base \
|
||||
gst-plugins-base-libs \
|
||||
@ -48,12 +49,30 @@ options uvcvideo quirks=0x200 timeout=10000
|
||||
EOF
|
||||
|
||||
echo "==> 2b. Predictable /dev names for each capture card"
|
||||
# probe all v4l2 devices, keep only the two GC311 capture cards
|
||||
# ensure relay (GPIO power) is on if present
|
||||
if systemctl list-unit-files | grep -q '^relay.service'; then
|
||||
sudo systemctl enable --now relay.service
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# probe v4l2 devices for GC311s (07ca:3311)
|
||||
mapfile -t GC_VIDEOS < <(
|
||||
sudo v4l2-ctl --list-devices |
|
||||
sudo v4l2-ctl --list-devices 2>/dev/null |
|
||||
awk '/Live Gamer MINI/{getline; print $1}'
|
||||
)
|
||||
|
||||
# fallback via udev if v4l2-ctl output is empty/partial
|
||||
if [ "${#GC_VIDEOS[@]}" -ne 2 ]; then
|
||||
mapfile -t GC_VIDEOS < <(
|
||||
for dev in /dev/video*; do
|
||||
props=$(sudo udevadm info -q property -n "$dev" 2>/dev/null || true)
|
||||
if echo "$props" | grep -q 'ID_VENDOR_ID=07ca' && echo "$props" | grep -q 'ID_MODEL_ID=3311'; then
|
||||
echo "$dev"
|
||||
fi
|
||||
done | sort -u
|
||||
)
|
||||
fi
|
||||
|
||||
if [ "${#GC_VIDEOS[@]}" -ne 2 ]; then
|
||||
echo "❌ Exactly two GC311 capture cards (index0) must be attached!" >&2
|
||||
printf ' Detected: %s\n' "${GC_VIDEOS[@]}"
|
||||
@ -89,7 +108,7 @@ sudo -u "$ORIG_USER" rustup default stable
|
||||
|
||||
echo "==> 4a. Source checkout"
|
||||
SRC_DIR=/var/src/lesavka
|
||||
REPO_URL=ssh://git@scm.bstein.dev:2242/brad_stein/lesavka.git
|
||||
REPO_URL=ssh://git@scm.bstein.dev:2242/bstein/lesavka.git
|
||||
if [[ ! -d $SRC_DIR ]]; then
|
||||
sudo mkdir -p /var/src
|
||||
sudo chown "$ORIG_USER":"$ORIG_USER" /var/src
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
// server/src/audio.rs
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use chrono::Local;
|
||||
use futures_util::Stream;
|
||||
use gstreamer as gst;
|
||||
use gstreamer_app as gst_app;
|
||||
use gst::prelude::*;
|
||||
use gst::ElementFactory;
|
||||
use gst::MessageView::*;
|
||||
use gst::prelude::*;
|
||||
use gstreamer as gst;
|
||||
use gstreamer_app as gst_app;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tonic::Status;
|
||||
use tracing::{debug, error, warn};
|
||||
use std::time::{Instant, Duration, SystemTime, UNIX_EPOCH};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use lesavka_common::lesavka::AudioPacket;
|
||||
|
||||
@ -58,9 +58,7 @@ pub async fn ear(alsa_dev: &str, id: u32) -> anyhow::Result<AudioStream> {
|
||||
*/
|
||||
let desc = build_pipeline_desc(alsa_dev)?;
|
||||
|
||||
let pipeline: gst::Pipeline = gst::parse::launch(&desc)?
|
||||
.downcast()
|
||||
.expect("pipeline");
|
||||
let pipeline: gst::Pipeline = gst::parse::launch(&desc)?.downcast().expect("pipeline");
|
||||
|
||||
let sink: gst_app::AppSink = pipeline
|
||||
.by_name("asink")
|
||||
@ -68,7 +66,10 @@ pub async fn ear(alsa_dev: &str, id: u32) -> anyhow::Result<AudioStream> {
|
||||
.downcast()
|
||||
.expect("appsink");
|
||||
|
||||
let tap = Arc::new(Mutex::new(ClipTap::new("🎧 - ear", Duration::from_secs(60))));
|
||||
let tap = Arc::new(Mutex::new(ClipTap::new(
|
||||
"🎧 - ear",
|
||||
Duration::from_secs(60),
|
||||
)));
|
||||
// sink.connect("underrun", false, |_| {
|
||||
// tracing::warn!("⚠️ USB playback underrun – host muted or not reading");
|
||||
// None
|
||||
@ -80,12 +81,19 @@ pub async fn ear(alsa_dev: &str, id: u32) -> anyhow::Result<AudioStream> {
|
||||
std::thread::spawn(move || {
|
||||
for msg in bus.iter_timed(gst::ClockTime::NONE) {
|
||||
match msg.view() {
|
||||
Error(e) => error!("💥 audio pipeline: {} ({})",
|
||||
e.error(), e.debug().unwrap_or_default()),
|
||||
Warning(w) => warn!("⚠️ audio pipeline: {} ({})",
|
||||
w.error(), w.debug().unwrap_or_default()),
|
||||
StateChanged(s) if s.current() == gst::State::Playing =>
|
||||
debug!("🎶 audio pipeline PLAYING"),
|
||||
Error(e) => error!(
|
||||
"💥 audio pipeline: {} ({})",
|
||||
e.error(),
|
||||
e.debug().unwrap_or_default()
|
||||
),
|
||||
Warning(w) => warn!(
|
||||
"⚠️ audio pipeline: {} ({})",
|
||||
w.error(),
|
||||
w.debug().unwrap_or_default()
|
||||
),
|
||||
StateChanged(s) if s.current() == gst::State::Playing => {
|
||||
debug!("🎶 audio pipeline PLAYING")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@ -104,24 +112,23 @@ pub async fn ear(alsa_dev: &str, id: u32) -> anyhow::Result<AudioStream> {
|
||||
// -------- clip‑tap (minute dumps) ------------
|
||||
tap.lock().unwrap().feed(map.as_slice());
|
||||
|
||||
static CNT: std::sync::atomic::AtomicU64 =
|
||||
std::sync::atomic::AtomicU64::new(0);
|
||||
static CNT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
let n = CNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
if n < 10 || n % 300 == 0 {
|
||||
debug!("🎧 ear #{n}: {} bytes", map.len());
|
||||
}
|
||||
|
||||
let pts_us = buffer
|
||||
.pts()
|
||||
.unwrap_or(gst::ClockTime::ZERO)
|
||||
.nseconds() / 1_000;
|
||||
let pts_us = buffer.pts().unwrap_or(gst::ClockTime::ZERO).nseconds() / 1_000;
|
||||
|
||||
// push non‑blocking; drop oldest on overflow
|
||||
if tx.try_send(Ok(AudioPacket {
|
||||
if tx
|
||||
.try_send(Ok(AudioPacket {
|
||||
id,
|
||||
pts: pts_us,
|
||||
data: map.as_slice().to_vec(),
|
||||
})).is_err() {
|
||||
}))
|
||||
.is_err()
|
||||
{
|
||||
static DROPS: std::sync::atomic::AtomicU64 =
|
||||
std::sync::atomic::AtomicU64::new(0);
|
||||
let d = DROPS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
@ -131,10 +138,12 @@ pub async fn ear(alsa_dev: &str, id: u32) -> anyhow::Result<AudioStream> {
|
||||
}
|
||||
Ok(gst::FlowSuccess::Ok)
|
||||
}
|
||||
}).build(),
|
||||
})
|
||||
.build(),
|
||||
);
|
||||
|
||||
pipeline.set_state(gst::State::Playing)
|
||||
pipeline
|
||||
.set_state(gst::State::Playing)
|
||||
.context("starting audio pipeline")?;
|
||||
|
||||
Ok(AudioStream {
|
||||
@ -152,9 +161,7 @@ fn build_pipeline_desc(dev: &str) -> anyhow::Result<String> {
|
||||
.into_iter()
|
||||
.find(|&e| {
|
||||
reg.find_plugin(e).is_some()
|
||||
|| reg
|
||||
.find_feature(e, ElementFactory::static_type())
|
||||
.is_some()
|
||||
|| reg.find_feature(e, ElementFactory::static_type()).is_some()
|
||||
})
|
||||
.ok_or_else(|| anyhow!("no AAC encoder plugin available"))?;
|
||||
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
// server/src/gadget.rs
|
||||
use std::{fs::{self, OpenOptions}, io::Write, path::Path, thread, time::Duration};
|
||||
use anyhow::{Context, Result};
|
||||
use tracing::{info, warn, trace};
|
||||
use std::{
|
||||
fs::{self, OpenOptions},
|
||||
io::Write,
|
||||
path::Path,
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
use tracing::{info, trace, warn};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UsbGadget {
|
||||
@ -46,8 +52,10 @@ impl UsbGadget {
|
||||
}
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
Err(anyhow::anyhow!("UDC never reached '{wanted}' (last = {:?})",
|
||||
fs::read_to_string(&path).unwrap_or_default()))
|
||||
Err(anyhow::anyhow!(
|
||||
"UDC never reached '{wanted}' (last = {:?})",
|
||||
fs::read_to_string(&path).unwrap_or_default()
|
||||
))
|
||||
}
|
||||
|
||||
pub fn wait_state_any(ctrl: &str, limit_ms: u64) -> anyhow::Result<String> {
|
||||
@ -61,7 +69,9 @@ impl UsbGadget {
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
Err(anyhow::anyhow!("UDC state did not settle within {limit_ms} ms"))
|
||||
Err(anyhow::anyhow!(
|
||||
"UDC state did not settle within {limit_ms} ms"
|
||||
))
|
||||
}
|
||||
|
||||
/// Write `value` (plus “\n”) into a sysfs attribute
|
||||
@ -81,14 +91,18 @@ impl UsbGadget {
|
||||
}
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
Err(anyhow::anyhow!("⚠️ UDC {ctrl} did not re-appear within {limit_ms} ms"))
|
||||
Err(anyhow::anyhow!(
|
||||
"⚠️ UDC {ctrl} did not re-appear within {limit_ms} ms"
|
||||
))
|
||||
}
|
||||
|
||||
/// Scan platform devices when /sys/class/udc is empty
|
||||
fn probe_platform_udc() -> Result<Option<String>> {
|
||||
for entry in fs::read_dir("/sys/bus/platform/devices")? {
|
||||
let p = entry?.file_name().into_string().unwrap();
|
||||
if p.ends_with(".usb") { return Ok(Some(p)); }
|
||||
if p.ends_with(".usb") {
|
||||
return Ok(Some(p));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@ -98,9 +112,9 @@ impl UsbGadget {
|
||||
/// Hard-reset the gadget → identical to a physical cable re-plug
|
||||
pub fn cycle(&self) -> Result<()> {
|
||||
/* 0 - ensure we *know* the controller even after a previous crash */
|
||||
let ctrl = Self::find_controller()
|
||||
.or_else(|_| Self::probe_platform_udc()?
|
||||
.ok_or_else(|| anyhow::anyhow!("no UDC present")))?;
|
||||
let ctrl = Self::find_controller().or_else(|_| {
|
||||
Self::probe_platform_udc()?.ok_or_else(|| anyhow::anyhow!("no UDC present"))
|
||||
})?;
|
||||
|
||||
/* 1 - detach gadget */
|
||||
info!("🔌 detaching gadget from {ctrl}");
|
||||
@ -112,12 +126,15 @@ impl UsbGadget {
|
||||
for attempt in 1..=10 {
|
||||
match Self::write_attr(self.udc_file, "") {
|
||||
Ok(_) => break,
|
||||
Err(err) if {
|
||||
Err(err)
|
||||
if {
|
||||
// only swallow EBUSY
|
||||
err.downcast_ref::<std::io::Error>()
|
||||
.and_then(|io| io.raw_os_error())
|
||||
== Some(libc::EBUSY) && attempt < 10
|
||||
} => {
|
||||
== Some(libc::EBUSY)
|
||||
&& attempt < 10
|
||||
} =>
|
||||
{
|
||||
trace!("⏳ UDC busy (attempt {attempt}/10) - retrying…");
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
@ -157,14 +174,13 @@ impl UsbGadget {
|
||||
}
|
||||
|
||||
/* 5 - wait for host (but tolerate sleep) */
|
||||
Self::wait_state(&ctrl, "configured", 6_000)
|
||||
.or_else(|e| {
|
||||
Self::wait_state(&ctrl, "configured", 6_000).or_else(|e| {
|
||||
// If the host is physically absent (sleep / KVM paused)
|
||||
// we allow 'not attached' and continue - we can still
|
||||
// accept keyboard/mouse data and the host will enumerate
|
||||
// later without another reset.
|
||||
let last = fs::read_to_string(format!("/sys/class/udc/{ctrl}/state"))
|
||||
.unwrap_or_default();
|
||||
let last =
|
||||
fs::read_to_string(format!("/sys/class/udc/{ctrl}/state")).unwrap_or_default();
|
||||
if last.trim() == "not attached" {
|
||||
warn!("⚠️ host did not enumerate within 6 s - continuing (state = {last:?})");
|
||||
Ok(())
|
||||
@ -182,7 +198,9 @@ impl UsbGadget {
|
||||
let cand = ["dwc2", "dwc3"];
|
||||
for drv in cand {
|
||||
let root = format!("/sys/bus/platform/drivers/{drv}");
|
||||
if !Path::new(&root).exists() { continue }
|
||||
if !Path::new(&root).exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*----------- unbind ------------------------------------------------*/
|
||||
info!("🔧 unbinding UDC driver ({drv})");
|
||||
@ -193,8 +211,7 @@ impl UsbGadget {
|
||||
trace!("unbind in-progress (#{attempt}) - waiting…");
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
Err(err) => return Err(err)
|
||||
.context("UDC unbind failed irrecoverably"),
|
||||
Err(err) => return Err(err).context("UDC unbind failed irrecoverably"),
|
||||
}
|
||||
}
|
||||
thread::sleep(Duration::from_millis(150)); // let the core quiesce
|
||||
@ -208,8 +225,7 @@ impl UsbGadget {
|
||||
trace!("bind busy (#{attempt}) - retrying…");
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
Err(err) => return Err(err)
|
||||
.context("UDC bind failed irrecoverably"),
|
||||
Err(err) => return Err(err).context("UDC bind failed irrecoverably"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,9 @@ impl Handshake for HandshakeSvc {
|
||||
|
||||
impl HandshakeSvc {
|
||||
pub fn server() -> HandshakeServer<Self> {
|
||||
HandshakeServer::new(Self { camera: true, microphone: true })
|
||||
HandshakeServer::new(Self {
|
||||
camera: true,
|
||||
microphone: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// server/src/lib.rs
|
||||
|
||||
pub mod audio;
|
||||
pub mod video;
|
||||
pub mod gadget;
|
||||
pub mod handshake;
|
||||
pub mod video;
|
||||
|
||||
@ -2,33 +2,27 @@
|
||||
// server/src/main.rs
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use std::{panic, backtrace::Backtrace, pin::Pin, sync::Arc};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use anyhow::Context as _;
|
||||
use futures_util::{Stream, StreamExt};
|
||||
use tokio::{
|
||||
fs::{OpenOptions},
|
||||
io::AsyncWriteExt,
|
||||
sync::Mutex,
|
||||
};
|
||||
use gstreamer as gst;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use std::{backtrace::Backtrace, panic, pin::Pin, sync::Arc};
|
||||
use tokio::{fs::OpenOptions, io::AsyncWriteExt, sync::Mutex};
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tonic::{Request, Response, Status};
|
||||
use tonic::transport::Server;
|
||||
use tonic_reflection::server::{Builder as ReflBuilder};
|
||||
use tracing::{info, warn, error, trace, debug};
|
||||
use tracing_subscriber::{filter::EnvFilter, fmt, prelude::*};
|
||||
use tonic::{Request, Response, Status};
|
||||
use tonic_reflection::server::Builder as ReflBuilder;
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::{filter::EnvFilter, fmt, prelude::*};
|
||||
|
||||
use lesavka_common::lesavka::{
|
||||
Empty, ResetUsbReply,
|
||||
AudioPacket, Empty, KeyboardReport, MonitorRequest, MouseReport, ResetUsbReply, VideoPacket,
|
||||
relay_server::{Relay, RelayServer},
|
||||
KeyboardReport, MouseReport,
|
||||
MonitorRequest, VideoPacket, AudioPacket
|
||||
};
|
||||
|
||||
use lesavka_server::{gadget::UsbGadget, video, audio, handshake::HandshakeSvc};
|
||||
use lesavka_server::{audio, gadget::UsbGadget, handshake::HandshakeSvc, video};
|
||||
|
||||
/*──────────────── constants ────────────────*/
|
||||
/// **false** = never reset automatically.
|
||||
@ -39,22 +33,19 @@ const PKG_NAME: &str = env!("CARGO_PKG_NAME");
|
||||
/*──────────────── logging ───────────────────*/
|
||||
fn init_tracing() -> anyhow::Result<WorkerGuard> {
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.create(true).truncate(true).write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open("/tmp/lesavka-server.log")?;
|
||||
let (file_writer, guard) = tracing_appender::non_blocking(file);
|
||||
|
||||
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
EnvFilter::new("lesavka_server=info,lesavka_server::video=warn")
|
||||
});
|
||||
let env_filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("lesavka_server=info,lesavka_server::video=warn"));
|
||||
let filter_str = env_filter.to_string();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(env_filter)
|
||||
.with(
|
||||
fmt::layer()
|
||||
.with_target(true)
|
||||
.with_thread_ids(true),
|
||||
)
|
||||
.with(fmt::layer().with_target(true).with_thread_ids(true))
|
||||
.with(
|
||||
fmt::layer()
|
||||
.with_writer(file_writer)
|
||||
@ -69,9 +60,13 @@ fn init_tracing() -> anyhow::Result<WorkerGuard> {
|
||||
|
||||
/*──────────────── helpers ───────────────────*/
|
||||
async fn open_with_retry(path: &str) -> anyhow::Result<tokio::fs::File> {
|
||||
for attempt in 1..=200 { // ≈10 s
|
||||
for attempt in 1..=200 {
|
||||
// ≈10 s
|
||||
match OpenOptions::new()
|
||||
.write(true).custom_flags(libc::O_NONBLOCK).open(path).await
|
||||
.write(true)
|
||||
.custom_flags(libc::O_NONBLOCK)
|
||||
.open(path)
|
||||
.await
|
||||
{
|
||||
Ok(f) => {
|
||||
info!("✅ {path} opened on attempt #{attempt}");
|
||||
@ -88,8 +83,7 @@ async fn open_with_retry(path: &str) -> anyhow::Result<tokio::fs::File> {
|
||||
}
|
||||
|
||||
fn next_minute() -> SystemTime {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH).unwrap();
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
let secs = now.as_secs();
|
||||
let next = (secs / 60 + 1) * 60;
|
||||
UNIX_EPOCH + Duration::from_secs(next)
|
||||
@ -191,9 +185,9 @@ impl Relay for Handler {
|
||||
&self,
|
||||
req: Request<tonic::Streaming<AudioPacket>>,
|
||||
) -> Result<Response<Self::StreamMicrophoneStream>, Status> {
|
||||
|
||||
// 1 ─ build once, early
|
||||
let mut sink = audio::Voice::new("hw:UAC2Gadget,0").await
|
||||
let mut sink = audio::Voice::new("hw:UAC2Gadget,0")
|
||||
.await
|
||||
.map_err(|e| Status::internal(format!("{e:#}")))?;
|
||||
|
||||
// 2 ─ dummy outbound stream (same trick as before)
|
||||
@ -202,8 +196,7 @@ impl Relay for Handler {
|
||||
// 3 ─ drive the sink in a background task
|
||||
tokio::spawn(async move {
|
||||
let mut inbound = req.into_inner();
|
||||
static CNT: std::sync::atomic::AtomicU64 =
|
||||
std::sync::atomic::AtomicU64::new(0);
|
||||
static CNT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
while let Some(pkt) = inbound.next().await.transpose()? {
|
||||
let n = CNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
@ -225,12 +218,11 @@ impl Relay for Handler {
|
||||
req: Request<tonic::Streaming<VideoPacket>>,
|
||||
) -> Result<Response<Self::StreamCameraStream>, Status> {
|
||||
// map gRPC camera id → UVC device
|
||||
let uvc = std::env::var("LESAVKA_UVC_DEV")
|
||||
.unwrap_or_else(|_| "/dev/video4".into());
|
||||
let uvc = std::env::var("LESAVKA_UVC_DEV").unwrap_or_else(|_| "/dev/video4".into());
|
||||
|
||||
// build once
|
||||
let relay = video::CameraRelay::new(0, &uvc)
|
||||
.map_err(|e| Status::internal(format!("{e:#}")))?;
|
||||
let relay =
|
||||
video::CameraRelay::new(0, &uvc).map_err(|e| Status::internal(format!("{e:#}")))?;
|
||||
|
||||
// dummy outbound (same pattern as other streams)
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(1);
|
||||
@ -271,8 +263,7 @@ impl Relay for Handler {
|
||||
// Only one speaker stream for now; both 0/1 → same ALSA dev.
|
||||
let _id = req.into_inner().id;
|
||||
// Allow override (`LESAVKA_ALSA_DEV=hw:2,0` for debugging).
|
||||
let dev = std::env::var("LESAVKA_ALSA_DEV")
|
||||
.unwrap_or_else(|_| "hw:UAC2Gadget,0".into());
|
||||
let dev = std::env::var("LESAVKA_ALSA_DEV").unwrap_or_else(|_| "hw:UAC2Gadget,0".into());
|
||||
|
||||
let s = audio::ear(&dev, 0)
|
||||
.await
|
||||
@ -282,10 +273,7 @@ impl Relay for Handler {
|
||||
}
|
||||
|
||||
/*────────────── USB-reset RPC ────────────*/
|
||||
async fn reset_usb(
|
||||
&self,
|
||||
_req: Request<Empty>,
|
||||
) -> Result<Response<ResetUsbReply>, Status> {
|
||||
async fn reset_usb(&self, _req: Request<Empty>) -> Result<Response<ResetUsbReply>, Status> {
|
||||
info!("🔴 explicit ResetUsb() called");
|
||||
match self.gadget.cycle() {
|
||||
Ok(_) => {
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
// server/src/video.rs
|
||||
|
||||
use anyhow::Context;
|
||||
use futures_util::Stream;
|
||||
use gst::MessageView::*;
|
||||
use gst::prelude::*;
|
||||
use gst::{MessageView, log};
|
||||
use gstreamer as gst;
|
||||
use gstreamer_app as gst_app;
|
||||
use gst::prelude::*;
|
||||
use gst::{log, MessageView};
|
||||
use gst::MessageView::*;
|
||||
use lesavka_common::lesavka::VideoPacket;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tonic::Status;
|
||||
use tracing::{debug, warn, error, info, enabled, trace, Level};
|
||||
use futures_util::Stream;
|
||||
use tracing::{Level, debug, enabled, error, info, trace, warn};
|
||||
|
||||
const EYE_ID: [&str; 2] = ["l", "r"];
|
||||
static START: std::sync::OnceLock<gst::ClockTime> = std::sync::OnceLock::new();
|
||||
@ -37,11 +37,7 @@ impl Drop for VideoStream {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn eye_ball(
|
||||
dev: &str,
|
||||
id: u32,
|
||||
_max_bitrate_kbit: u32,
|
||||
) -> anyhow::Result<VideoStream> {
|
||||
pub async fn eye_ball(dev: &str, id: u32, _max_bitrate_kbit: u32) -> anyhow::Result<VideoStream> {
|
||||
let eye = EYE_ID[id as usize];
|
||||
gst::init().context("gst init")?;
|
||||
|
||||
@ -79,8 +75,10 @@ pub async fn eye_ball(
|
||||
|
||||
/* ----- BUS WATCH: show errors & warnings immediately --------------- */
|
||||
let bus = pipeline.bus().expect("bus");
|
||||
if let Some(src_pad) = pipeline.by_name(&format!("cam_{eye}"))
|
||||
.and_then(|e| e.static_pad("src")) {
|
||||
if let Some(src_pad) = pipeline
|
||||
.by_name(&format!("cam_{eye}"))
|
||||
.and_then(|e| e.static_pad("src"))
|
||||
{
|
||||
src_pad.add_probe(gst::PadProbeType::EVENT_DOWNSTREAM, |pad, info| {
|
||||
if let Some(gst::PadProbeData::Event(ref ev)) = info.data {
|
||||
if let gst::EventView::Caps(c) = ev.view() {
|
||||
@ -139,8 +137,7 @@ pub async fn eye_ball(
|
||||
let map = buffer.map_readable().map_err(|_| gst::FlowError::Error)?;
|
||||
|
||||
/* -------- basic counters ------ */
|
||||
static FRAME: std::sync::atomic::AtomicU64 =
|
||||
std::sync::atomic::AtomicU64::new(0);
|
||||
static FRAME: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
let n = FRAME.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
if n % 120 == 0 {
|
||||
trace!(target: "lesavka_server::video", "eye-{eye}: delivered {n} frames");
|
||||
@ -157,7 +154,9 @@ pub async fn eye_ball(
|
||||
/* -------- detect SPS / IDR ---- */
|
||||
if enabled!(Level::DEBUG) {
|
||||
if let Some(&nal) = map.as_slice().get(4) {
|
||||
if (nal & 0x1F) == 0x05 /* IDR */ {
|
||||
if (nal & 0x1F) == 0x05
|
||||
/* IDR */
|
||||
{
|
||||
debug!("eye-{eye}: IDR");
|
||||
}
|
||||
}
|
||||
@ -175,7 +174,11 @@ pub async fn eye_ball(
|
||||
/* -------- ship over gRPC ----- */
|
||||
let data = map.as_slice().to_vec();
|
||||
let size = data.len();
|
||||
let pkt = VideoPacket { id, pts: pts_us, data };
|
||||
let pkt = VideoPacket {
|
||||
id,
|
||||
pts: pts_us,
|
||||
data,
|
||||
};
|
||||
match tx.try_send(Ok(pkt)) {
|
||||
Ok(_) => {
|
||||
trace!(target:"lesavka_server::video",
|
||||
@ -202,17 +205,26 @@ pub async fn eye_ball(
|
||||
.build(),
|
||||
);
|
||||
|
||||
pipeline.set_state(gst::State::Playing).context("🎥 starting video pipeline eye-{eye}")?;
|
||||
pipeline
|
||||
.set_state(gst::State::Playing)
|
||||
.context("🎥 starting video pipeline eye-{eye}")?;
|
||||
let bus = pipeline.bus().unwrap();
|
||||
loop {
|
||||
match bus.timed_pop(gst::ClockTime::NONE) {
|
||||
Some(msg) if matches!(msg.view(), MessageView::StateChanged(s)
|
||||
if s.current() == gst::State::Playing) => break,
|
||||
Some(msg)
|
||||
if matches!(msg.view(), MessageView::StateChanged(s)
|
||||
if s.current() == gst::State::Playing) =>
|
||||
{
|
||||
break;
|
||||
}
|
||||
Some(_) => continue,
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
Ok(VideoStream { _pipeline: pipeline, inner: ReceiverStream::new(rx) })
|
||||
Ok(VideoStream {
|
||||
_pipeline: pipeline,
|
||||
inner: ReceiverStream::new(rx),
|
||||
})
|
||||
}
|
||||
|
||||
pub struct WebcamSink {
|
||||
@ -225,16 +237,36 @@ impl WebcamSink {
|
||||
gst::init()?;
|
||||
|
||||
let pipeline = gst::Pipeline::new();
|
||||
|
||||
let caps_h264 = gst::Caps::builder("video/x-h264")
|
||||
.field("stream-format", "byte-stream")
|
||||
.field("alignment", "au")
|
||||
.build();
|
||||
let raw_caps = gst::Caps::builder("video/x-raw")
|
||||
.field("format", "YUY2")
|
||||
.field("width", 1280i32)
|
||||
.field("height", 720i32)
|
||||
.field("framerate", gst::Fraction::new(30, 1))
|
||||
.build();
|
||||
|
||||
let src = gst::ElementFactory::make("appsrc")
|
||||
.build()?
|
||||
.downcast::<gst_app::AppSrc>()
|
||||
.expect("appsrc");
|
||||
src.set_is_live(true);
|
||||
src.set_format(gst::Format::Time);
|
||||
src.set_caps(Some(&caps_h264));
|
||||
src.set_property("block", &true);
|
||||
|
||||
let h264parse = gst::ElementFactory::make("h264parse").build()?;
|
||||
let decoder = gst::ElementFactory::make("v4l2h264dec").build()?;
|
||||
let decoder_name = Self::pick_decoder();
|
||||
let decoder = gst::ElementFactory::make(decoder_name)
|
||||
.build()
|
||||
.with_context(|| format!("building decoder element {decoder_name}"))?;
|
||||
let convert = gst::ElementFactory::make("videoconvert").build()?;
|
||||
let caps = gst::ElementFactory::make("capsfilter")
|
||||
.property("caps", &raw_caps)
|
||||
.build()?;
|
||||
let sink = gst::ElementFactory::make("v4l2sink")
|
||||
.property("device", &uvc_dev)
|
||||
.property("sync", &false)
|
||||
@ -242,22 +274,48 @@ impl WebcamSink {
|
||||
|
||||
// Up‑cast to &gst::Element for the collection macros
|
||||
pipeline.add_many(&[
|
||||
src.upcast_ref(), &h264parse, &decoder, &convert, &sink
|
||||
src.upcast_ref(),
|
||||
&h264parse,
|
||||
&decoder,
|
||||
&convert,
|
||||
&caps,
|
||||
&sink,
|
||||
])?;
|
||||
gst::Element::link_many(&[
|
||||
src.upcast_ref(), &h264parse, &decoder, &convert, &sink
|
||||
src.upcast_ref(),
|
||||
&h264parse,
|
||||
&decoder,
|
||||
&convert,
|
||||
&caps,
|
||||
&sink,
|
||||
])?;
|
||||
pipeline.set_state(gst::State::Playing)?;
|
||||
|
||||
Ok(Self { appsrc: src, _pipe: pipeline })
|
||||
Ok(Self {
|
||||
appsrc: src,
|
||||
_pipe: pipeline,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn push(&self, pkt: VideoPacket) {
|
||||
let mut buf = gst::Buffer::from_slice(pkt.data);
|
||||
buf.get_mut().unwrap()
|
||||
buf.get_mut()
|
||||
.unwrap()
|
||||
.set_pts(Some(gst::ClockTime::from_useconds(pkt.pts)));
|
||||
let _ = self.appsrc.push_buffer(buf);
|
||||
}
|
||||
|
||||
fn pick_decoder() -> &'static str {
|
||||
if gst::ElementFactory::find("v4l2h264dec").is_some() {
|
||||
"v4l2h264dec"
|
||||
} else if gst::ElementFactory::find("v4l2slh264dec").is_some() {
|
||||
"v4l2slh264dec"
|
||||
} else if gst::ElementFactory::find("omxh264dec").is_some() {
|
||||
"omxh264dec"
|
||||
} else {
|
||||
"avdec_h264"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*─────────────────────────────────*/
|
||||
@ -281,7 +339,9 @@ impl CameraRelay {
|
||||
|
||||
/// Push one VideoPacket coming from the client
|
||||
pub fn feed(&self, pkt: VideoPacket) {
|
||||
let n = self.frames.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let n = self
|
||||
.frames
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
if n < 10 || n % 60 == 0 {
|
||||
tracing::debug!(target:"lesavka_server::video",
|
||||
cam_id = self.id,
|
||||
|
||||
@ -4,11 +4,21 @@ async fn hid_roundtrip() {
|
||||
use lesavka_server::RelaySvc; // export the struct in lib.rs
|
||||
let svc = RelaySvc::default();
|
||||
let (mut cli, srv) = tonic::transport::Channel::balance_channel(1);
|
||||
tokio::spawn(tonic::transport::server::Server::builder()
|
||||
tokio::spawn(
|
||||
tonic::transport::server::Server::builder()
|
||||
.add_service(relay_server::RelayServer::new(svc))
|
||||
.serve_with_incoming(srv));
|
||||
.serve_with_incoming(srv),
|
||||
);
|
||||
|
||||
let (mut tx, mut rx) = relay_client::RelayClient::new(cli).stream().await.unwrap().into_inner();
|
||||
tx.send(HidReport { data: vec![0,0,4,0,0,0,0,0] }).await.unwrap();
|
||||
let (mut tx, mut rx) = relay_client::RelayClient::new(cli)
|
||||
.stream()
|
||||
.await
|
||||
.unwrap()
|
||||
.into_inner();
|
||||
tx.send(HidReport {
|
||||
data: vec![0, 0, 4, 0, 0, 0, 0, 0],
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(rx.message().await.unwrap().is_none()); // nothing echoed yet
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user