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