Compare commits

..

No commits in common. "main" and "pre-tauri" have entirely different histories.

54 changed files with 2918 additions and 5943 deletions

4
.gitignore vendored
View file

@ -1,6 +1,4 @@
target
/target
settings.toml
.direnv/*
logs/*
ui/static/node_modules/*
ui/static/@tauri-apps/*

4414
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,40 +4,29 @@ version = "2.0.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.5.1", features = [] }
[features]
tokio-debug = []
tracker-state-debug = []
tokio-logging = []
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
# DO NOT REMOVE!!
custom-protocol = [ "tauri/custom-protocol" ]
[dependencies]
async-channel = "2.3.1"
bincode = "1.3.3"
async-channel = "2.2.0"
config = "0.14.0"
futures = "0.3.30"
futures-core = "0.3.30"
futures-util = { version = "0.3.30" }
gilrs = "0.11.0"
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.40", features = ["rt-multi-thread", "time", "sync"] }
tokio-tungstenite = "0.24.0"
toml = "0.8.19"
futures-util = { version = "0.3.30", features = ["tokio-io"] }
gilrs = "0.10.6"
gstreamer = { version = "0.22.4", features = ["v1_22"] }
gstreamer-app = { version = "0.22.0", features = ["v1_22"] }
gst-plugin-gtk4 = { version = "0.12.2", features = ["gtk_v4_12"] }
gtk = { version = "0.8.1", package = "gtk4", features = ["v4_12"] }
log = "0.4.21"
serde = { version = "1.0.197", features = ["derive"] }
tokio = { version = "1.37.0", features = ["rt-multi-thread", "time", "sync"] }
tokio-tungstenite = "0.21.0"
toml = "0.8.12"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["tracing-log"] }
tracing-appender = "0.2.3"
snafu = "0.8.4"
snafu = "0.8.2"
console-subscriber = "0.3.0"
tauri = { version = "1.8", features = [] }
lazy_static = "1.5.0"
vcs-common = { git = "https://git.nickiel.net/VCC/vcs-common.git", branch = "main" }
async-recursion = "1.1.1"

View file

@ -1,3 +0,0 @@
fn main() {
tauri_build::build();
}

View file

@ -1,12 +1,30 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1725103162,
"narHash": "sha256-Ym04C5+qovuQDYL/rKWSR+WESseQBbNAe5DsXNx5trY=",
"lastModified": 1711163522,
"narHash": "sha256-YN/Ciidm+A0fmJPWlHBGvVkcarYWSC+s3NTPk/P+q3c=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "12228ff1752d7b7624a54e9c1af4b222b3c1073b",
"rev": "44d0940ea560dee511026a53f0e2e2cde489b4d4",
"type": "github"
},
"original": {
@ -18,11 +36,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1718428119,
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
"lastModified": 1706487304,
"narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
"rev": "90f456026d284c22b3e3497be980b2e47d0b28ac",
"type": "github"
},
"original": {
@ -40,14 +58,15 @@
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1725330199,
"narHash": "sha256-oUkdPJIxP3r3YyVOBLkDVLIJiQV9YlrVqA+jNcdpCvM=",
"lastModified": 1711332768,
"narHash": "sha256-SFnlIwnrwJxEawLcrH7+zGb8spePcYyai5asMZnm0BM=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "a562172c72d00350f9f2ff830e6515b6e7bee6d5",
"rev": "8a8e3ea9a9a4b2225cb5e33e07c3a337f820168c",
"type": "github"
},
"original": {
@ -55,6 +74,21 @@
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View file

@ -36,9 +36,13 @@ Some utility commands:
src = ./.;
nativeBuildInputs = [ pkg-config ];
buildInputs = [
tailwindcss
openssl
systemd
gtk4
gst_all_1.gstreamer
gst_all_1.gst-plugins-base
gst_all_1.gst-plugins-good
gst_all_1.gst-plugins-bad
];
cargoHash = nixpkgs.lib.fakeHash;
};
@ -48,44 +52,16 @@ Some utility commands:
platforms = [ system ];
#maintainers = with maintainers; [ ];
};
libraries = with pkgs;[
gtk3
glib
dbus
openssl_3
librsvg
];
tauri_packages = with pkgs; [
curl
wget
pkg-config
dbus
openssl_3
glib
gtk3
libsoup
webkitgtk
librsvg
];
in {
devShells.${system}.default = with pkgs; mkShell {
packages = [
(pkgs.rust-bin.stable.latest.default.override {
extensions = [ "rust-src" ];
})
cargo-watch
cargo-tauri
cargo-edit
bacon
typescript # .js language server
] ++ tauri_packages;
];
inputsFrom = with self.packages.${system}; [ joystick-controller-client ];
shellHook =
''
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH
export XDG_DATA_DIRS=${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS
'';
};
packages.${system} = {
default = self.packages.${system}.joystick-controller-client;
@ -141,4 +117,3 @@ Some utility commands:
};
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

6
settings.toml Normal file
View file

@ -0,0 +1,6 @@
camera_ip = "10.0.0.33"
camera_port = 8765
tracker_ip = "localhost"
tracker_port = 6543
tracker_refresh_rate_millis = 10
tracker_jpeg_quality = 80

View file

@ -5,20 +5,10 @@ use std::fs::File;
use std::io::Write;
use tracing::{info, instrument};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ConnectionString {
pub ip: String,
pub port: u32,
}
impl ConnectionString {
pub fn build_conn_string(&self) -> String {
format!("ws://{}:{}", self.ip, self.port)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AppConfig {
pub cameras: Vec<ConnectionString>,
pub camera_ip: String,
pub camera_port: u32,
pub tracker_ip: String,
pub tracker_port: u32,
@ -29,10 +19,8 @@ pub struct AppConfig {
impl Default for AppConfig {
fn default() -> Self {
AppConfig {
cameras: vec![ConnectionString {
ip: "127.0.0.1".to_owned(),
port: 8765,
}],
camera_ip: "10.0.0.33".to_string(),
camera_port: 8765,
tracker_ip: "10.0.0.210".to_string(),
tracker_port: 6543,

View file

@ -0,0 +1,230 @@
use std::pin::Pin;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::time::Instant;
use async_channel::{Receiver, Sender};
use futures_util::{stream::SplitSink, SinkExt, StreamExt};
use tokio::net::TcpStream;
use tokio::runtime::Handle;
use tokio::sync::RwLock;
use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream};
use tracing::{debug, error, info, instrument};
use crate::config::AppConfig;
use crate::coordinator::socket_listen;
use crate::coordinator::tracker_state::TrackerState;
use crate::gstreamer_pipeline;
use crate::{sources::joystick_source::joystick_loop, ui::GuiUpdate};
use super::perf_state::TrackerMetrics;
use super::remote_video_processor::remote_video_loop;
use super::{ApplicationEvent, ConnectionType};
#[derive(Debug)]
pub struct SocketState {
pub is_connected: AtomicBool,
pub stay_connected: AtomicBool,
}
#[derive(Debug)]
pub struct CoordState<'a> {
pub settings: Arc<RwLock<AppConfig>>,
pub tracker_metrics: TrackerMetrics,
pub sck_outbound: Option<SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>>,
pub stay_alive_sck_recvr: Arc<AtomicBool>,
pub joystick_loop_alive: Arc<AtomicBool>,
pub current_priority: ConnectionType,
pub last_update_of_priority: Instant,
pub mec: Pin<&'a mut Receiver<ApplicationEvent>>,
pub to_mec: Sender<ApplicationEvent>,
pub to_gui: Sender<GuiUpdate>,
pub rt: Handle,
pub pipeline: gstreamer_pipeline::WebcamPipeline,
pub tracker_state: TrackerState,
pub tracker_connection_state: Arc<SocketState>,
}
impl<'a> CoordState<'a> {
pub fn new(
mec: Pin<&'a mut Receiver<ApplicationEvent>>,
to_mec: Sender<ApplicationEvent>,
to_gui: Sender<GuiUpdate>,
rt: Handle,
settings: Arc<RwLock<AppConfig>>,
jpeg_quality: i32,
) -> Self {
CoordState {
settings,
tracker_metrics: TrackerMetrics::new(to_gui.clone()),
sck_outbound: None,
stay_alive_sck_recvr: Arc::new(AtomicBool::new(false)),
joystick_loop_alive: Arc::new(AtomicBool::new(false)),
current_priority: ConnectionType::Local,
last_update_of_priority: Instant::now(),
mec,
to_mec,
to_gui,
rt,
pipeline: gstreamer_pipeline::WebcamPipeline::new(jpeg_quality).unwrap(),
tracker_state: TrackerState{
tracking_id: 0,
highlighted_id: None,
last_detect: Instant::now(),
enabled: true,
identity_boxes: vec![],
update_ids: false,
},
tracker_connection_state: Arc::new(SocketState {
stay_connected: AtomicBool::new(false),
is_connected: AtomicBool::new(false),
}),
}
}
#[instrument(skip(self))]
pub async fn socket_send(&mut self, message: Message) {
if let Some(mut socket) = self.sck_outbound.take() {
if let Err(e) = socket.send(message).await {
error!("There was an error sending to the socket: {:#?}", e);
} else {
self.sck_outbound = Some(socket);
}
}
}
pub fn socket_connected(&self) -> bool {
self.sck_outbound.is_some()
}
pub async fn socket_start(&mut self) {
self.stay_alive_sck_recvr.store(true, Ordering::SeqCst);
if let Err(e) = self.to_gui.send(GuiUpdate::SocketConnecting).await {
error!("Cannot send message to gui thread: {e}");
}
let conn_string: String = {
let read_settings = self.settings.read().await;
format!(
"ws://{}:{}",
read_settings.camera_ip,
read_settings.camera_port
)
};
match connect_async(conn_string).await {
Ok((val, _)) => {
info!("Socket connection to camera made successfully");
let (outbound, inbound) = val.split();
self.rt.spawn(socket_listen(
self.to_mec.clone(),
self.stay_alive_sck_recvr.clone(),
inbound,
));
self.sck_outbound = Some(outbound);
if let Err(e) = self.to_gui.send(GuiUpdate::SocketConnected).await {
error!("Cannot send message to gui thread: {e}");
}
}
Err(_) => {
error!("Couldn't connect to URL!");
if let Err(e) = self.to_gui.send(GuiUpdate::SocketDisconnected).await {
error!("Cannot send message to gui thread: {e}");
}
}
}
}
pub async fn socket_close(&mut self) {
debug!("Cleaning up camera socket state");
if let Some(mut socket) = self.sck_outbound.take() {
if let Err(e) = socket.close().await {
error!("Couldn't close socket during shutdown: {e}");
}
}
if let Err(e) = self.to_gui.send(GuiUpdate::SocketDisconnected).await {
error!("Cannot send message to gui thread: {e}");
}
self.stay_alive_sck_recvr.store(false, Ordering::SeqCst);
}
pub async fn start_video_loop(&mut self) {
let conn_string: String = {
let read_settings = self.settings.read().await;
format!(
"ws://{}:{}",
read_settings.tracker_ip,
read_settings.tracker_port
)
};
self.rt.spawn(remote_video_loop(
conn_string,
self.pipeline.sink_frame.clone(),
self.to_mec.clone(),
self.tracker_connection_state.clone(),
));
}
pub async fn check_states(&mut self) {
// This one needs to always be alive, and restart after a crash
if !self.joystick_loop_alive.load(Ordering::SeqCst) {
info!("Restarting joystick loop");
self.rt.spawn(joystick_loop(
self.to_mec.clone(),
self.joystick_loop_alive.clone(),
));
}
// If tracker state is not connected, and it should be
if !self
.tracker_connection_state
.is_connected
.load(Ordering::SeqCst)
&& self
.tracker_connection_state
.stay_connected
.load(Ordering::SeqCst)
{
self.start_video_loop().await;
}
// if stay alive is false, and there is a connection, kill it
if !self.stay_alive_sck_recvr.load(Ordering::SeqCst) && self.sck_outbound.is_some() {
self.socket_close().await;
}
}
pub async fn close(&mut self) {
info!("closing coord state");
self.tracker_connection_state
.stay_connected
.store(false, Ordering::SeqCst);
self.socket_close().await;
self.joystick_loop_alive.store(false, Ordering::SeqCst);
self.to_gui.close();
self.mec.close();
}
}

View file

@ -1,363 +1,268 @@
use std::fmt::Display;
use std::sync::{atomic::AtomicBool, Arc};
use std::time::Duration;
use std::pin::pin;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::time::{Duration, Instant};
use async_channel::{Receiver, Sender, TryRecvError};
use serde::{Deserialize, Serialize};
use tauri::Manager;
use async_channel::{Receiver, Sender};
use futures_util::{stream::SplitStream, StreamExt};
use gstreamer::prelude::ElementExt;
use gstreamer::State;
use tokio::net::TcpStream;
use tokio::runtime::Handle;
use tracing::{debug, error, info, warn};
use vcs_common::ApplicationMessage;
use tokio::sync::RwLock;
use tokio_tungstenite::{tungstenite::Message, MaybeTlsStream, WebSocketStream};
use tracing::{debug, error, info, instrument};
mod coord_state;
mod process_box_string;
mod remote_video_processor;
use crate::states::perf_state;
use crate::states::tracker_state;
use crate::config::AppConfig;
use crate::APP_HANDLE;
use crate::ui::{GuiUpdate, NormalizedBoxCoords};
pub use coord_state::{CoordState, SocketState};
mod joystick_source;
mod satellite_connection;
const PRIORITY_TIMEOUT: Duration = Duration::from_secs(2);
use joystick_source::joystick_loop;
use satellite_connection::SatelliteConnection;
pub enum ApplicationEvent {
SupportsWebRTC(bool),
WebRTCMessage((String, vcs_common::ApplicationMessage)),
ChangeTargetSatellite(String),
JoystickMove(Point),
RetryDisconnectedSatellites,
Close,
}
#[derive(Debug)]
pub struct Point {
#[derive(Clone)]
pub struct MoveEvent {
pub x: i32,
pub y: i32,
}
impl Display for Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
#[derive(Clone, Copy, PartialEq, PartialOrd, Debug)]
pub enum ConnectionType {
Local,
Remote,
Automated,
}
pub struct AppState {
to_mec: Sender<ApplicationEvent>,
mec: Receiver<ApplicationEvent>,
pub runtime: Handle,
pub has_webrtc_support: Option<bool>,
_config: Arc<tokio::sync::RwLock<AppConfig>>,
pub target_satellite: Option<usize>,
pub camera_satellites: Vec<SatelliteConnection>,
pub _endpoint_satellites: Vec<SatelliteConnection>,
pub joystick_task_is_alive: Arc<AtomicBool>,
pub enum TrackerUpdate {
Clear,
Fail,
Update(TrackerUpdatePackage),
HeaderUpdate(String),
}
impl AppState {
pub async fn new(
#[derive(Clone)]
pub struct TrackerUpdatePackage {
pub boxes: Vec<NormalizedBoxCoords>,
pub time: Instant,
pub request_duration: Duration,
}
pub enum ApplicationEvent {
CameraConnectionPress,
SocketMessage(Message),
MoveEvent(MoveEvent, ConnectionType),
TrackerUpdate(TrackerUpdate),
ChangeTracking(u32),
EnableAutomatic(bool),
}
#[instrument(skip_all)]
pub async fn start_coordinator(
// Main_Event_Channel
mec: Receiver<ApplicationEvent>,
to_mec: Sender<ApplicationEvent>,
config: Arc<tokio::sync::RwLock<AppConfig>>,
rt: Handle,
) -> Self {
let camera_satellites = config
.read()
.await
.cameras
.iter()
.map(|x| SatelliteConnection::new(x.clone()))
.collect::<Vec<SatelliteConnection>>();
AppState {
to_mec,
mec,
runtime: rt,
has_webrtc_support: None,
_config: config,
target_satellite: None,
camera_satellites,
_endpoint_satellites: vec![],
joystick_task_is_alive: Arc::new(AtomicBool::new(false)),
}
}
pub fn update_satellite_names(&self) {
let new_names = self
.camera_satellites
.iter()
.map(|x| SatelliteName {
name: x.name.clone(),
is_connected: x.is_connected(),
})
.collect::<Vec<SatelliteName>>();
send_ui_message(
"satellite_names".to_owned(),
serde_json::to_string(&new_names).unwrap(),
);
}
pub async fn check_alive_things(&mut self) {
/*
if !self
.joystick_task_is_alive
.load(std::sync::atomic::Ordering::SeqCst)
{
self.runtime.spawn(joystick_loop(
self.to_mec.clone(),
Arc::clone(&self.joystick_task_is_alive),
));
}
*/
let mut resend_names = false;
for i in self
.camera_satellites
.iter_mut()
.filter(|x| x.was_connected && !x.is_connected())
{
info!("new dead connection found, cleaning up!");
i.was_connected = false;
resend_names = true;
}
if resend_names {
info!("Refreshing UI connection names");
self.update_satellite_names();
}
for i in self
.camera_satellites
.iter_mut()
.filter(|x| !x.is_connected() && x.try_connecting)
{
i.connect(self.runtime.clone()).await;
}
}
}
pub async fn run_main_event_loop(
mec: Receiver<ApplicationEvent>,
to_mec: Sender<ApplicationEvent>,
to_ui: Sender<ApplicationEvent>,
config: Arc<tokio::sync::RwLock<AppConfig>>,
rt: Handle,
to_gui: Sender<GuiUpdate>,
runtime: Handle,
settings: Arc<RwLock<AppConfig>>,
) {
let mut state = AppState::new(mec, to_mec, config, rt).await;
loop {
state.check_alive_things().await;
info!("Starting coordinator!");
match state.mec.try_recv() {
Err(TryRecvError::Empty) => tokio::time::sleep(Duration::from_millis(50)).await,
Err(TryRecvError::Closed) => break,
Ok(msg) => {
match msg {
ApplicationEvent::SupportsWebRTC(has_support) => {
state.has_webrtc_support = Some(has_support);
state
.camera_satellites
.iter_mut()
.filter(|x| x.is_connected())
.for_each(|x| {
x.send_blocking(
ApplicationMessage::ConnectionSupportsWebRTC(has_support),
state.runtime.clone(),
);
});
// Use this as a 'ui is ready' event as well
state.update_satellite_names();
}
ApplicationEvent::Close => {
state.mec.close(); // cleanup is handled on reading from a closed mec
}
ApplicationEvent::ChangeTargetSatellite(new_target_name) => {
state.target_satellite = state
.camera_satellites
.iter()
.position(|x| x.name == new_target_name);
info!(
"Changed active satellite index to: {:?}",
state.target_satellite
);
}
ApplicationEvent::RetryDisconnectedSatellites => {
state.camera_satellites.iter_mut().for_each(|x| {
info!("Resetting connections");
x.try_connecting = true;
x.retry_attempts = 0;
});
}
ApplicationEvent::WebRTCMessage((name, msg)) => {
info!(
"Valid camera names are: {:?}",
state
.camera_satellites
.iter()
.map(|x| x.name.clone())
.collect::<Vec<String>>()
);
info!("Reqested name is: {}", name);
info!(
"Count of is_connected: {}",
state
.camera_satellites
.iter()
.filter(|x| x.is_connected())
.count()
);
for conn in state
.camera_satellites
.iter_mut()
.filter(|x| x.name == name && x.is_connected())
{
info!("Sending message");
if let Err(_) = conn.send(msg.clone()).await {
error!("The websocket gave an error when I tried to send a message! I hope your logging is good enough");
}
}
}
ApplicationEvent::JoystickMove(mut coord) => {
if coord.x < 10 && coord.x > -10 {
coord.x = 0;
}
if coord.y < 10 && coord.y > -10 {
coord.y = 0;
}
if let Some(target) = state.target_satellite {
if state.camera_satellites.len() >= target {
if let Err(e) = state.camera_satellites[target]
.send(ApplicationMessage::ManualMovementOverride((
coord.x, coord.y,
)))
.await
{
error!("There was an error sending the joystick movement message to the target! {:?}", e);
}
} else {
error!("That was not a valid target! Need to notifiy the UI about this");
debug!("Count of camera satellites: {}, target: {:?}", state.camera_satellites.len(), target);
}
}
}
}
}
}
let mec = pin!(mec);
let mut update_names: bool = false;
for connection in state.camera_satellites.iter_mut() {
match connection.try_next().await {
Some(msg) => match msg {
ApplicationMessage::NameRequest(None) => {
if let Err(e) = connection
.send(ApplicationMessage::NameRequest(Some(
"Controller".to_owned(),
)))
.await
{
info!("Was not able to send name to remote? {:?}", e);
}
}
ApplicationMessage::NameRequest(Some(name)) => {
warn!("Got a name update!");
connection.name = name;
update_names = true;
}
ApplicationMessage::ChangeTrackingID(_) => {}
ApplicationMessage::ManualMovementOverride(_) => {}
ApplicationMessage::TrackingBoxes(_update) => {}
let jpeg_quality = settings.read().await.tracker_jpeg_quality;
ApplicationMessage::ConnectionSupportsWebRTCRequest => {
let does_support_webrtc = true;
if let Err(e) = connection
.send(ApplicationMessage::ConnectionSupportsWebRTC(
does_support_webrtc,
let mut state = CoordState::new(
mec,
to_mec,
to_gui,
runtime,
settings,
jpeg_quality,
);
state
.pipeline
.pipeline
.set_state(State::Playing)
.expect("Could not set pipeline state to playing");
if let Err(e) = state
.to_gui
.send(GuiUpdate::UpdatePaintable(
state.pipeline.sink_paintable.clone(),
))
.await
{
info!(
"Was not able to send webrtc support status to remote: {:?}",
e
error!("Could not send new paintable to GUI: {e}");
}
state.check_states().await;
while let Some(msg) = state.mec.next().await {
state.check_states().await;
match msg {
ApplicationEvent::CameraConnectionPress => {
if state.socket_connected() {
state.socket_close().await;
} else {
state.socket_start().await;
}
}
ApplicationEvent::SocketMessage(socket_message) => {
state.socket_send(socket_message).await;
}
ApplicationEvent::ChangeTracking(new_id) => {
state.tracker_state.tracking_id = new_id;
}
ApplicationEvent::EnableAutomatic(do_enable) => {
state.tracker_state.enabled = do_enable;
state
.tracker_connection_state
.stay_connected
.store(do_enable, Ordering::SeqCst);
state.check_states().await;
}
ApplicationEvent::MoveEvent(coord, priority) => {
// If Automatic control, but local event happens, override the automatice events for 2 seconds
if priority <= state.current_priority
|| Instant::now() > state.last_update_of_priority + PRIORITY_TIMEOUT
{
state.last_update_of_priority = Instant::now();
state.current_priority = priority;
if let Err(e) = state.to_gui.send(GuiUpdate::MoveEvent(coord.clone())).await {
panic!("Could not set message to gui channel; Unrecoverable: {e}");
}
if state.socket_connected() {
let message = format!(
"{}{}:{}{}",
if coord.y > 0 { "D" } else { "U" },
coord.y.abs(),
if coord.x > 0 { "R" } else { "L" },
coord.x.abs()
);
state.socket_send(Message::Text(message)).await;
}
}
ApplicationMessage::ConnectionSupportsWebRTC(does_support) => {
info!(
"Cool, the camera satellite supports webrtc: {}",
does_support
);
}
ApplicationMessage::WebRTCIceCandidateInit(pkt) => {
send_frontend_message(serde_json::to_string(&pkt).unwrap())
ApplicationEvent::TrackerUpdate(update) => match update {
TrackerUpdate::HeaderUpdate(_) => {}
TrackerUpdate::Clear => {
state.tracker_state.clear();
state.tracker_metrics.clear_times();
if let Err(e) = state.to_gui.send(GuiUpdate::TrackerUpdate(TrackerUpdate::Clear)).await {
error!("Could not send message to GUI: {e}");
break;
}
ApplicationMessage::WebRTCIceCandidate(pkt) => {
send_frontend_message(serde_json::to_string(&pkt).unwrap())
}
ApplicationMessage::WebRTCPacket(pkt) => {
send_frontend_message(serde_json::to_string(&pkt).unwrap())
TrackerUpdate::Fail => {
let fail_count: usize = state.tracker_metrics.fail_count + 1;
state.tracker_metrics.starting_connection(Some(fail_count));
}
ApplicationMessage::CloseConnection => {
error!("Cannot handle close connection from satellite yet");
TrackerUpdate::Update(update) => {
let mut x_adj: i32 = 0;
let mut y_adj: i32 = 0;
if let Err(e) = state.to_gui
.send(GuiUpdate::TrackerUpdate(TrackerUpdate::Update(
update.clone(),
)))
.await
{
error!("Could not send message to the GUI: {e}");
break;
}
state.tracker_state.update_from_boxes(update.boxes);
state.tracker_state.last_detect = update.time;
match state.tracker_state.calculate_tracking() {
Ok((x, y, _tracker_enabled)) => {
x_adj = x;
y_adj = y;
}
Err(e) => {
if state.tracker_state.tracking_id > 0 {
info!("Could not calculate the tracking!: {e}");
}
}
}
let me = MoveEvent { x: x_adj, y: y_adj };
if let Err(e) = state
.to_mec
.send(ApplicationEvent::MoveEvent(
me.clone(),
ConnectionType::Automated,
))
.await
{
error!("Could not send to MEC... even though in the MEC?! {e}");
}
if let Err(e) = state.to_gui.send(GuiUpdate::MoveEvent(me)).await {
error!("Could not send to MEC... even though in the MEC?! {e}");
}
state.tracker_metrics.insert_time(update.request_duration);
}
},
None => {}
}
}
if update_names {
state.update_satellite_names();
}
}
info!("Closing the MEC loop");
state
.joystick_task_is_alive
.store(false, std::sync::atomic::Ordering::SeqCst);
.pipeline
.pipeline
.set_state(State::Null)
.expect("Could not set pipeline state to playing");
let close_handles: Vec<_> = state
.camera_satellites
.iter_mut()
.filter(|x| x.is_connected())
.map(|x| x.close())
.collect();
futures::future::join_all(close_handles).await;
info!("Satellite connections all closed.");
state.close().await;
info!("Stopping Coordinator");
}
pub fn send_frontend_message(message: String) {
if let Ok(mut e) = APP_HANDLE.lock() {
if e.is_none() {
return;
} else {
e.as_mut()
.unwrap()
.emit_all("frontend_message", message)
.expect("Could not send message to the tauri frontend!");
async fn socket_listen(
mec: Sender<ApplicationEvent>,
stay_alive_sck_recvr: Arc<AtomicBool>,
mut reader: SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>,
) {
if stay_alive_sck_recvr.load(std::sync::atomic::Ordering::SeqCst) {
while let Some(msg) = reader.next().await {
match msg {
Ok(val) => {
match val {
Message::Ping(_) => {
// Do nothing because pings are handled on reads and write by tungestenite
}
_ => {
info!("Received message from the camera websocket? {:#?}", val);
}
}
}
Err(e) => {
error!("Websocket error: {:#?}", e);
}
}
}
#[derive(Serialize, Deserialize)]
struct SatelliteName {
pub is_connected: bool,
pub name: String,
// setting this will call the internal state.socket_close next check states
stay_alive_sck_recvr.store(false, Ordering::SeqCst);
}
pub fn send_ui_message(handle_name: String, message: String) {
if let Ok(mut e) = APP_HANDLE.lock() {
if e.is_none() {
error!("Could not get app handle!");
return;
} else {
info!("sending event '{}' with payload '{}'", handle_name, message);
e.as_mut()
.unwrap()
.emit_all(&handle_name, message)
.expect("Could not send message to the tauri frontend!");
}
} else {
error!("Could not get lock on APP_HANDLE!");
}
// If the mec is closed or full, then this socket should be closing anyways
// as there was most likely an unrecoverable error
let _ = mec
.send(ApplicationEvent::SocketMessage(Message::Close(None)))
.await;
debug!("Closed socket reading thread");
}

View file

@ -0,0 +1,43 @@
use crate::ui::NormalizedBoxCoords;
pub fn process_incoming_string(message: String) -> Result<Vec<NormalizedBoxCoords>, String> {
let mut boxes: Vec<NormalizedBoxCoords> = Vec::new();
for line in message.lines() {
let parts: Vec<&str> = line.split(' ').collect();
let id = parts[0]
.replace(['[', ']'], "")
.parse()
.map_err(|_| "Invalid ID")?;
if parts.len() != 3 {
return Err("Invalid socket input format: number of parts".to_string());
}
let coords: Vec<&str> = parts[1].split(':').collect();
if coords.len() != 2 {
return Err("Invalid socket input format: coords 1".to_string());
}
let x1: u32 = coords[0].parse().map_err(|_| "Invalid x coordinate")?;
let y1: u32 = coords[1].parse().map_err(|_| "Invalid y coordinate")?;
let coords2: Vec<&str> = parts[2].split(':').collect();
if coords2.len() != 2 {
return Err("Invalid socket input format: coords 2".to_string());
}
let x2: u32 = coords2[0].parse().map_err(|_| "Invalid width")?;
let y2: u32 = coords2[1].parse().map_err(|_| "Invalid width")?;
boxes.push(NormalizedBoxCoords {
id,
x1: (x1 as f32 / 1000.0),
x2: (x2 as f32 / 1000.0),
y1: (y1 as f32 / 1000.0),
y2: (y2 as f32 / 1000.0),
});
}
Ok(boxes)
}

View file

@ -0,0 +1,196 @@
use std::{
sync::{atomic::Ordering, Arc},
time::{Duration, Instant},
};
use async_recursion::async_recursion;
use async_channel::Sender;
use futures_util::{stream::SplitStream, SinkExt, StreamExt, TryStreamExt};
use gstreamer_app::AppSink;
use tokio::{net::TcpStream, sync::Mutex, time::sleep_until};
use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream};
use tracing::{error, info, instrument, warn};
use super::{
process_box_string::process_incoming_string, ApplicationEvent, SocketState, TrackerUpdate,
TrackerUpdatePackage,
};
#[instrument(skip_all)]
pub async fn remote_video_loop(
conn_string: String,
appsink: Arc<Mutex<AppSink>>,
to_mec: Sender<ApplicationEvent>,
socket_state: Arc<SocketState>,
) {
info!(
"Starting remote tracker processing connection to: {}",
conn_string
);
socket_state.is_connected.store(true, Ordering::SeqCst);
match connect_async(&conn_string).await {
Err(e) => {
warn!("Could not connect to remote computer: {e}");
if let Err(e) = to_mec
.send(ApplicationEvent::TrackerUpdate(TrackerUpdate::Fail))
.await
{
error!("Could not send message to MEC! {e}");
}
}
Ok((connection, _)) => {
let (mut sender, mut recvr) = connection.split();
let mut last_iter: Instant;
loop {
last_iter = Instant::now();
// Do this in an encloser to not keep a lock on the appsink
let image_message = {
let res = {
let appsnk = appsink.lock().await;
get_video_frame(&appsnk)
};
match res {
Ok(e) => e,
Err(e) => {
error!("Could not get video frame! {e}");
if let Err(e) = sender.close().await {
error!("Could not close socket to remote computer: {e}")
}
socket_state.is_connected.store(false, Ordering::SeqCst);
break;
}
}
};
if let Err(e) = sender.send(image_message).await {
error!("There was an error sending the video frame to the server: {e}");
if let Err(e) = sender.close().await {
error!("Could not close socket to remote computer: {e}")
}
socket_state.is_connected.store(false, Ordering::SeqCst);
socket_state.stay_connected.store(false, Ordering::SeqCst);
break;
}
let do_not_break = handle_message(&mut recvr, &to_mec, last_iter).await;
if !do_not_break { break; }
if !socket_state.stay_connected.load(Ordering::SeqCst) {
info!("Shutting down remote video loop");
break;
}
// rate limit updates
sleep_until(tokio::time::Instant::now() + Duration::from_millis(10)).await;
}
}
}
info!("Shutting down remote video loop");
if let Err(e) = to_mec
.send(ApplicationEvent::TrackerUpdate(TrackerUpdate::Clear))
.await
{
error!("Could not send message to MEC! {e}");
}
{
// This message forces a redraw after clearing the queue
if let Err(e) = to_mec
.send(ApplicationEvent::MoveEvent(
crate::coordinator::MoveEvent { x: 0, y: 0 },
crate::coordinator::ConnectionType::Automated,
))
.await
{
error!(
"Error sending message to MEC during shutdown of tracker thread: {}",
e
);
}
}
socket_state.is_connected.store(false, Ordering::SeqCst);
}
fn get_video_frame(appsink: &AppSink) -> Result<Message, String> {
let sample = appsink
.pull_sample()
.map_err(|e| format!("Could not get sample: {e}"))?;
let buffer = sample.buffer().ok_or("Could not get buffer, was None")?;
let map = buffer
.map_readable()
.map_err(|e| format!("Could not get readable map: {e}"))?;
Ok(Message::binary(map.to_vec()))
}
#[async_recursion]
async fn handle_message(
recvr: &mut SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>,
// sender: &mut SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,
to_mec: &Sender<ApplicationEvent>,
last_iter: Instant,
) -> bool {
match recvr.try_next().await {
Ok(Some(message)) => {
match message {
Message::Close(_) => {
info!("Close packet received from remote computer: {:?}", message);
return false;
}
Message::Pong(_) | Message::Frame(_) | Message::Text(_) => {
warn!("There was an unhandled message type from the camera: {}\n{}", message, message.to_string());
// this was not the expected response, recursion!
return handle_message(recvr, to_mec, last_iter).await;
}
Message::Ping(_) => {
// Ping/Pongs are handled by tokio tungstenite on reads and writes
// this was not the expected response, recursion!
return handle_message(recvr, to_mec, last_iter).await;
}
Message::Binary(bin) => {
let message = std::str::from_utf8(&bin);
if let Err(e) = message {
error!("Could not decode binary message! Assuming corrupted response! {e}");
}
match process_incoming_string(message.unwrap().to_string()) {
Ok(v) => {
if let Err(e) = to_mec
.send(ApplicationEvent::TrackerUpdate(TrackerUpdate::Update(
TrackerUpdatePackage {
boxes: v,
time: Instant::now(),
request_duration: Instant::now() - last_iter,
},
)))
.await
{
error!("Could not send to MEC! {e}");
return false;
}
}
Err(e) => {
error!("Could not parse incoming string! {}\n{}", e, message.unwrap());
}
};
}
}
}
Ok(None) => {
info!("Recieved an empty message from the remote computer: Aborting");
return false;
}
Err(e) => {
error!("Got an error on while recieving from remote computer: {e}");
}
}
return true;
}

View file

@ -1,162 +0,0 @@
use std::sync::{atomic::AtomicBool, Arc};
use async_channel::TryRecvError;
use tokio::runtime::Handle;
use tracing::{error, info, instrument, warn};
use crate::config::ConnectionString;
use vcs_common::{AppReceiver, AppSender, ApplicationMessage};
#[derive(Debug)]
pub enum SatelliteConnectionError {
SocketIsClosed,
}
pub struct SatelliteConnection {
pub name: String,
pub retry_attempts: usize,
pub try_connecting: bool,
pub was_connected: bool,
connection: ConnectionString,
currently_connecting: bool,
to_socket: Option<AppSender>,
from_socket: Option<AppReceiver>,
socket_is_dead: Arc<AtomicBool>,
}
impl SatelliteConnection {
#[instrument]
pub fn new(conn_string: ConnectionString) -> Self {
SatelliteConnection {
name: String::new(),
connection: conn_string.clone(),
retry_attempts: 0,
try_connecting: false,
// external flag for 'have a checked this since it disocnnected'
was_connected: false,
currently_connecting: false,
to_socket: None,
from_socket: None,
socket_is_dead: Arc::new(AtomicBool::new(true)),
}
}
#[instrument(skip(self))]
pub async fn close(&mut self) {
if self.to_socket.is_some() {
if let Err(_) = self
.to_socket
.as_ref()
.unwrap()
.send(ApplicationMessage::CloseConnection)
.await
{
info!("Could not send close connection to active satellite");
}
}
self.socket_is_dead
.store(true, std::sync::atomic::Ordering::SeqCst);
self.to_socket.take(); // closing all senders will dispose of the
self.from_socket.take(); // channel
}
#[instrument(skip(self))]
pub fn is_connected(&self) -> bool {
return !self
.socket_is_dead
.load(std::sync::atomic::Ordering::SeqCst);
}
#[instrument(skip(self, rt))]
pub async fn connect(&mut self, rt: Handle) {
if self.currently_connecting
|| !self
.socket_is_dead
.load(std::sync::atomic::Ordering::SeqCst)
{
return;
}
self.currently_connecting = true;
match vcs_common::connect_to_server(self.connection.build_conn_string(), rt).await {
Ok((sender, recvr, is_alive)) => {
if let Err(e) = sender.send(ApplicationMessage::NameRequest(None)).await {
error!(
"Couldn't send message to fresh socket sender! '{}' \n {}",
self.connection.build_conn_string(),
e
);
}
self.to_socket = Some(sender);
self.from_socket = Some(recvr);
self.socket_is_dead = is_alive;
self.currently_connecting = false;
self.was_connected = true;
}
Err(e) => {
self.retry_attempts += 1;
self.currently_connecting = false;
if self.retry_attempts > 5 {
info!("Retry attempts maxed out. Stopping the retries");
self.try_connecting = false;
}
error!(
"Could not connect to socket remote: '{}' \nRetry Attempt: {} \n {}",
self.connection.build_conn_string(),
self.retry_attempts,
e
);
}
}
}
/// This is not the recommneded method, as it ignores error, but sometimes
/// you just really don't want to be async
#[instrument(skip(self))]
pub fn send_blocking(&mut self, msg: ApplicationMessage, rt: tokio::runtime::Handle) {
if self.to_socket.is_some() {
let sender = self.to_socket.as_ref().unwrap().clone();
rt.spawn(async move {
if let Err(e) = sender.send(msg).await {
error!("Unhandled send error while in send_blocking! {e}");
}
});
}
}
#[instrument(skip(self))]
pub async fn send(&mut self, msg: ApplicationMessage) -> Result<(), SatelliteConnectionError> {
if self.to_socket.is_some() {
if let Err(_) = self.to_socket.as_ref().unwrap().send(msg).await {
self.close().await;
return Err(SatelliteConnectionError::SocketIsClosed);
}
return Ok(());
}
Err(SatelliteConnectionError::SocketIsClosed)
}
#[instrument(skip(self))]
pub async fn try_next(&mut self) -> Option<ApplicationMessage> {
if self.from_socket.is_some() {
match self.from_socket.as_ref().unwrap().try_recv() {
Ok(msg) => match msg {
_ => Some(msg),
},
Err(TryRecvError::Empty) => None,
Err(TryRecvError::Closed) => {
self.close().await;
return None;
}
}
} else {
None
}
}
}

228
src/gstreamer_pipeline.rs Normal file
View file

@ -0,0 +1,228 @@
use gstreamer::{prelude::*, PadLinkError};
use gstreamer::{Element, ElementFactory, Pipeline};
use gstreamer_app::AppSink;
use gtk::glib::BoolError;
use snafu::prelude::*;
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::Mutex;
#[derive(Debug)]
pub struct WebcamPipeline {
pub pipeline: Pipeline,
pub sink_paintable: Element,
pub sink_frame: Arc<Mutex<AppSink>>,
}
impl WebcamPipeline {
pub fn new(jpeg_quality: i32) -> Result<WebcamPipeline, PipelineError> {
let pipeline = Pipeline::with_name("webcam_pipeline");
// All of the following errors are unrecoverable
let source = ElementFactory::make("mfvideosrc")
.build()
.context(BuildSnafu {
element: "mfvideosrc",
})?;
let convert = ElementFactory::make("videoconvert")
.build()
.context(BuildSnafu {
element: "videoconvert",
})?;
let rate = ElementFactory::make("videorate")
.build()
.context(BuildSnafu {
element: "videorate",
})?;
let tee = ElementFactory::make("tee")
.build()
.context(BuildSnafu { element: "tee" })?;
let queue_app = ElementFactory::make("queue")
.property("max-size-time", 1u64)
.property("max-size-buffers", 0u32)
.property("max-size-bytes", 0u32)
.build()
.context(BuildSnafu {
element: "paintable queue",
})?;
let sink_paintable = ElementFactory::make("gtk4paintablesink")
.name("gtk4_output")
.build()
.context(BuildSnafu {
element: "gtkpaintablesink",
})?;
// queue.connect_closure("overrun", false, glib::closure!(|queue: Element| {
// println!("The queue is full!");
// }));
let appsink_queue = ElementFactory::make("queue")
.property("max-size-time", 1u64)
.property("max-size-buffers", 0u32)
.property("max-size-bytes", 0u32)
.build()
.context(BuildSnafu {
element: "appsink queue",
})?;
let resize = ElementFactory::make("videoscale")
.build()
.context(BuildSnafu {
element: "videoscale",
})?;
let jpeg_enc = ElementFactory::make("jpegenc")
.property("quality", jpeg_quality)
.build()
.context(BuildSnafu { element: "jpegenc" })?;
let caps_string = "image/jpeg,width=640,height=640";
let appsrc_caps = gstreamer::Caps::from_str(caps_string).context(BuildSnafu {
element: "appsink caps",
})?;
let sink_frame = AppSink::builder()
.name("frame_appsink")
.sync(false)
.max_buffers(1u32)
.drop(true)
.caps(&appsrc_caps)
.build();
sink_frame.set_property("caps", &appsrc_caps.to_value());
pipeline
.add_many([
&source,
&convert,
&rate,
&tee,
&queue_app,
&sink_paintable,
&appsink_queue,
&resize,
&jpeg_enc,
&sink_frame.upcast_ref(),
])
.context(LinkSnafu {
from: "all",
to: "pipeline",
})?;
Element::link_many([&source, &convert, &rate]).context(LinkSnafu {
from: "source et. al.",
to: "rate",
})?;
// -- BEGIN PAINTABLE SINK PIPELINE
let tee_caps =
// gstreamer::caps::Caps::from_str("video/x-raw,framerate=15/1").context(BuildSnafu {
gstreamer::caps::Caps::from_str("video/x-raw").context(BuildSnafu {
element: "tee caps",
})?;
rate.link_filtered(&tee, &tee_caps).context(LinkSnafu {
from: "videorate",
to: "tee",
})?;
let tee_src_1 = tee
.request_pad_simple("src_%u")
.ok_or(PipelineError::PadRequest {
element: "tee pad 1".to_string(),
})?;
let paintable_queue_sinkpad =
queue_app
.static_pad("sink")
.ok_or(PipelineError::PadRequest {
element: "gtk4 sink".to_string(),
})?;
tee_src_1
.link(&paintable_queue_sinkpad)
.context(PadLinkSnafu {
from: "tee src pad",
to: "gtk4 paintable queue",
})?;
queue_app.link(&sink_paintable).context(LinkSnafu {
from: "gtk4 paintable queue",
to: "gtk4 paintable",
})?;
// -- END PAINTABLE SINK PIPELINE
// -- BEGIN APPSINK PIPELINE
let tee_src_2 = tee
.request_pad_simple("src_%u")
.ok_or(PipelineError::PadRequest {
element: "tee pad 2".to_string(),
})?;
let appsink_queue_sinkpad =
appsink_queue
.static_pad("sink")
.ok_or(PipelineError::PadRequest {
element: "appsink queue".to_string(),
})?;
tee_src_2
.link(&appsink_queue_sinkpad)
.context(PadLinkSnafu {
from: "tee src pad 2",
to: "appsink queue sinkpad",
})?;
appsink_queue.link(&resize).context(LinkSnafu {
from: "appsink_queue",
to: "resize",
})?;
let resize_caps =
gstreamer::caps::Caps::from_str("video/x-raw,format=RGB,width=640,height=640")
.context(BuildSnafu {
element: "resize_caps",
})?;
resize
.link_filtered(&jpeg_enc, &resize_caps)
.context(LinkSnafu {
from: "jpeg_enc",
to: "resize_caps",
})?;
Element::link_many([&jpeg_enc, &sink_frame.upcast_ref()]).context(LinkSnafu {
from: "jpeg_enc",
to: "appsink",
})?;
Ok(WebcamPipeline {
pipeline,
sink_paintable,
sink_frame: Arc::new(Mutex::new(sink_frame)),
})
}
}
#[derive(Debug, Snafu)]
pub enum PipelineError {
#[snafu(display("Error during element linking"))]
Link {
source: BoolError,
from: String,
to: String,
},
#[snafu(display("Error linking pads"))]
PadLink {
source: PadLinkError,
from: String,
to: String,
},
#[snafu(display("Error creating element"))]
Build { source: BoolError, element: String },
#[snafu(display("Error getting pad from element"))]
PadRequest { element: String },
}

View file

@ -1,32 +1,26 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use async_channel::Sender;
use lazy_static::lazy_static;
use std::sync::{Arc, Mutex};
use tauri::{AppHandle, Manager};
use gtk::prelude::{ApplicationExt, ApplicationExtManual};
use gtk::{glib, Application};
use std::{env, sync::Arc};
use tokio::{runtime, sync::RwLock};
use tracing::{self, debug, error, info};
use tracing::{self, info, Level};
#[cfg(not(debug_assertions))]
use tracing_subscriber;
use vcs_common::ApplicationMessage;
use crate::config::{load_config, AppConfig};
mod config;
mod coordinator;
mod tauri_functions;
mod webrtc_remote;
mod gstreamer_pipeline;
mod sources;
mod states;
mod ui;
const APP_ID: &str = "net.nickiel.joystick-controller-client";
use coordinator::{run_main_event_loop, ApplicationEvent};
fn main() -> glib::ExitCode {
// set the environment var to make gtk use window's default action bar
env::set_var("gtk_csd", "0");
lazy_static! {
static ref TO_MEC_REF: Mutex<Option<Sender<ApplicationEvent>>> = Mutex::new(None);
static ref APP_HANDLE: Mutex<Option<AppHandle>> = Mutex::new(None);
}
fn main() {
#[cfg(not(debug_assertions))]
{
let file_appender = tracing_appender::rolling::daily(".\\logs", "camera-controller");
@ -39,125 +33,45 @@ fn main() {
}
#[cfg(all(not(feature = "tokio-debug"), debug_assertions))]
{
let _sub = tracing_subscriber::fmt()
.with_max_level(tracing_subscriber::filter::LevelFilter::DEBUG)
.init();
let sub = tracing_subscriber::FmtSubscriber::new();
if let Err(e) = tracing::subscriber::set_global_default(sub) {
panic!("Could not set tracing global: {e}");
}
}
#[cfg(feature = "tokio-debug")]
{
console_subscriber::init();
}
let (to_mec, mec) = async_channel::bounded::<ApplicationEvent>(10);
let (to_ui, ui_ec) = async_channel::bounded::<ApplicationEvent>(10);
let span = tracing::span!(Level::TRACE, "main");
let _enter = span.enter();
info!("Logging intialized");
let config: Arc<RwLock<AppConfig>> = Arc::new(RwLock::new(load_config()));
gstreamer::init().expect("Unable to start gstreamer");
gstgtk4::plugin_register_static().expect("Unable to register gtk4 plugin");
let rt = runtime::Runtime::new().expect("Could not start tokio runtime");
let handle = rt.handle().clone();
let handle2 = handle.clone();
let _coordinator = rt.handle().spawn(run_main_event_loop(
mec,
to_mec.clone(),
to_ui,
config,
handle,
));
let app = Application::builder().application_id(APP_ID).build();
*TO_MEC_REF.lock().unwrap() = Some(to_mec.clone());
app.connect_startup(ui::on_activate);
tauri::Builder::default()
.manage(tauri_functions::TauriState {
to_mec: to_mec.clone(),
rt: handle2.clone(),
})
.invoke_handler(tauri::generate_handler![
tauri_functions::connect_to_camera,
tauri_functions::supports_webrtc,
tauri_functions::change_target_satellite,
tauri_functions::send_joystick_event,
])
.setup(|app| {
*APP_HANDLE.lock().unwrap() = Some(app.handle());
let _id2 = app.listen_global("webrtc-message", |event| {
debug!("Got webrtc-message event from Tauri client! {:#?}", event);
match event.payload() {
Some(payload) => {
if let Ok(e) = TO_MEC_REF.lock() {
match e.as_ref() {
Some(to_mec) => {
debug!("Sending message to the webrtc connection");
let message: Option<ApplicationMessage> = match payload {
s if s.starts_with("{\"type") => {
Some(ApplicationMessage::WebRTCPacket(
serde_json::from_str(payload)
.expect("Could not decode the browser's sdp"),
))
}
s if s.starts_with("{\"candidate") => {
Some(ApplicationMessage::WebRTCIceCandidateInit(
serde_json::from_str(payload).expect(
"Could not decode the browser's Ice Candidate",
),
))
}
_ => None,
};
if message.is_some() {
if let Err(e) =
to_mec.send_blocking(ApplicationEvent::WebRTCMessage((
"CameraSatellite_1".to_owned(),
message.unwrap(),
)))
{
error!("Could not send to mec! {e}");
}
} else {
error!("Could not deserialize ui webrtc message");
}
}
None => {
error!("TO_MEC_REF was none!");
}
}
}
}
None => {
info!("There was an empty payload!");
}
}
app.connect_activate(move |app| {
ui::build_ui(app, config.clone(), handle.clone());
});
// let _id = app.listen_global("webrtc-event", |event| {
// match event.payload() {
// Some(payload) => {
// if let Ok(e) = TO_MEC_REF.lock() {
// match e.as_ref() {
// Some(to_mec) => {
// if let Err(e) = to_mec.send_blocking(ApplicationEvent::WebRTCMessage(payload.to_string())) {
// error!("Could not send to mec! {e}");
// }
// },
// None => {
// error!("TO_MEC_REF was none!");
// }
// }
// }
// }
// None => {
// info!("There was an empty payload!");
// }
// }
// });
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
let exit_code = app.run();
let _ = to_mec.send_blocking(ApplicationEvent::Close);
info!("Gtk application has closed");
rt.block_on(async {});
info!("Tokio runtime has shut down");
exit_code
}

View file

@ -1,4 +1,4 @@
use crate::coordinator::{ApplicationEvent, Point};
use crate::coordinator::{ApplicationEvent, MoveEvent};
use async_channel::Sender;
use gilrs::{ev::filter::FilterFn, Axis, Button, Event, EventType, Filter, Gilrs, GilrsBuilder};
@ -53,7 +53,6 @@ pub async fn joystick_loop(tx: Sender<ApplicationEvent>, is_alive: Arc<AtomicBoo
// get the next event, and if it is an axis we are interested in, update the
// corresponding variable
while let Some(evt) = gilrs.next_event().filter_ev(&UnknownSlayer {}, &mut gilrs) {
info!("got a new joystick event");
match evt.event {
gilrs::EventType::AxisChanged(gilrs::Axis::LeftStickY, val, _) => {
curr_y = (val * 100.0) as i32;
@ -87,10 +86,13 @@ pub async fn joystick_loop(tx: Sender<ApplicationEvent>, is_alive: Arc<AtomicBoo
count_zeros = 0;
}
match tx.try_send(ApplicationEvent::JoystickMove(Point {
match tx.try_send(ApplicationEvent::MoveEvent(
MoveEvent {
x: curr_x,
y: curr_y,
})) {
},
crate::coordinator::ConnectionType::Local,
)) {
Ok(_) => {}
Err(async_channel::TrySendError::Closed(_)) => {
info!("MEC is closed, stopping Joystick loop");

2
src/sources/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod joystick_source;

4
src/states/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod perf_state;
pub mod tracker_state;

89
src/states/perf_state.rs Normal file
View file

@ -0,0 +1,89 @@
use std::{collections::VecDeque, time::Duration};
use async_channel::Sender;
use tracing::error;
use crate::coordinator::TrackerUpdate;
use crate::ui::GuiUpdate;
const MAX_RECORDED_TIMES: usize = 10;
const DEGRADED_TRACKER_TIME: u128 = 150;
#[derive(Debug)]
pub struct TrackerMetrics {
pub header_text: String,
pub fail_count: usize,
tracker_times: VecDeque<u128>,
to_gui: Sender<GuiUpdate>,
}
impl TrackerMetrics {
pub fn new(to_gui: Sender<GuiUpdate>) -> Self {
let mut ret = TrackerMetrics {
header_text: String::from(""),
fail_count: 0,
tracker_times: VecDeque::with_capacity(MAX_RECORDED_TIMES),
to_gui,
};
ret.clear_times();
ret
}
fn update_gui(&mut self) {
if let Err(e) = self.to_gui.send_blocking(GuiUpdate::TrackerUpdate(TrackerUpdate::HeaderUpdate(self.header_text.clone()))) {
error!("TrackerMetrics couldnt' send update to GUI: {e}");
}
}
pub fn starting_connection(&mut self, fail_count: Option<usize>) {
self.clear_times();
self.header_text.clear();
match fail_count {
None => self.header_text.push_str("Status: Connecting ..."),
Some(v) => self.header_text.push_str(&format!("Status: Attempt {}/5", v)),
}
self.update_gui();
}
pub fn clear_times(&mut self) {
for _ in 0..10 {
self.tracker_times.pop_front();
}
self.header_text = "Status: Disconnected".to_string();
self.update_gui();
}
pub fn insert_time(&mut self, new_measurement: Duration) {
if self.tracker_times.len() > MAX_RECORDED_TIMES {
let _ = self.tracker_times.pop_front();
}
self.tracker_times.push_back(new_measurement.as_millis());
let avg_time = self.tracker_times.iter().sum::<u128>() / self.tracker_times.len() as u128;
if avg_time == 0 {
self.header_text = format!(
"Status: Failed Avg Response: {} ms",
avg_time
);
}
if avg_time > DEGRADED_TRACKER_TIME {
self.header_text = format!(
"Status: Degraded Avg Response: {} ms",
avg_time
);
} else {
self.header_text = format!(
"Status: Nominal Avg Response: {} ms",
avg_time
);
}
self.update_gui();
}
}

View file

@ -0,0 +1,79 @@
use std::{
cmp::{max, min},
time::Instant,
};
use crate::ui::NormalizedBoxCoords;
#[derive(Debug)]
pub struct TrackerState {
pub tracking_id: u32,
pub highlighted_id: Option<u32>,
pub last_detect: Instant,
pub enabled: bool,
pub update_ids: bool,
pub identity_boxes: Vec<NormalizedBoxCoords>,
}
impl TrackerState {
pub fn clear(&mut self) {
self.tracking_id = 0;
self.highlighted_id = None;
self.last_detect = Instant::now();
self.enabled = false;
self.update_ids = false;
self.identity_boxes.clear();
}
pub fn update_from_boxes(&mut self, new_boxes: Vec<NormalizedBoxCoords>) {
let mut old_ids: Vec<u32> = self.identity_boxes.iter().map(|x| x.id).collect();
old_ids.sort();
let mut new_ids: Vec<u32> = new_boxes.iter().map(|x| x.id).collect();
new_ids.sort();
self.update_ids = new_ids == old_ids;
self.identity_boxes = new_boxes;
}
// TODO: have a return type that says "highlighted ID not found" and "no highlighted id selected"
// It's not really worth logging...
pub fn calculate_tracking(&mut self) -> core::result::Result<(i32, i32, bool), String> {
if let Some(target_box) = self
.identity_boxes
.iter()
.find(|e| e.id == self.tracking_id)
{
let x_adjust = calc_x_adjust(target_box.x1, target_box.x2);
let y_adjust = calc_y_adjust(target_box.y1);
self.last_detect = std::time::Instant::now();
Ok((x_adjust, y_adjust, self.enabled))
} else {
Err("Couldn't find target in results".to_string())
}
}
}
fn calc_x_adjust(x1: f32, x2: f32) -> i32 {
let dist_from_center = ((x1 + x2) / 2.0) - 0.5;
let mut x_adjust = ((dist_from_center / 0.5 * 2.0) * 100.0) as i32;
if x_adjust < 15 && x_adjust > -15 {
x_adjust = 0;
}
min(max(x_adjust, -100), 100)
}
fn calc_y_adjust(y1: f32) -> i32 {
// All values are normalized, then multiplied by 1000. 500 == 50% of the screen
let mut y_adjust = ((y1 - 0.1) * 250.0) as i32;
if y_adjust < 0 {
y_adjust -= 20;
} else if y_adjust < 30 {
y_adjust = 0;
} else {
y_adjust = (y_adjust as f32 * 0.75) as i32;
}
min(max(y_adjust, -100), 100)
}

View file

@ -1,46 +0,0 @@
use async_channel::Sender;
use tauri::State;
use tokio::runtime::Handle;
use tracing::info;
use crate::coordinator::{ApplicationEvent, Point};
pub struct TauriState {
pub to_mec: Sender<ApplicationEvent>,
pub rt: Handle,
}
#[tauri::command]
pub fn connect_to_camera(state: State<'_, TauriState>) {
let mec = state.to_mec.clone();
state.rt.spawn(async move {
let _ = mec.send_blocking(ApplicationEvent::RetryDisconnectedSatellites);
});
}
#[tauri::command(rename_all = "snake_case")]
pub fn supports_webrtc(has_support: bool, state: State<'_, TauriState>) {
let mec = state.to_mec.clone();
state.rt.spawn(async move {
let _ = mec.send_blocking(ApplicationEvent::SupportsWebRTC(has_support));
});
}
#[tauri::command(rename_all = "snake_case")]
pub fn change_target_satellite(new_target_name: String, state: State<'_, TauriState>) {
let mec = state.to_mec.clone();
state.rt.spawn(async move {
let _ = mec.send_blocking(ApplicationEvent::ChangeTargetSatellite(new_target_name));
});
}
#[tauri::command(rename_all = "snake_case")]
pub fn send_joystick_event(x: f32, y: f32, state: State<'_, TauriState>) {
let mec = state.to_mec.clone();
state.rt.spawn(async move {
let _ = mec.send_blocking(ApplicationEvent::JoystickMove(Point {
x: (x * 100.0) as i32,
y: (y * 100.0) as i32,
}));
});
}

188
src/ui/control_panel.rs Normal file
View file

@ -0,0 +1,188 @@
use std::sync::{Arc, Mutex};
use async_channel::Sender;
use gtk::{
glib::{self, object::CastNone},
prelude::{
BoxExt, ButtonExt, Cast, GObjectPropertyExpressionExt, ListItemExt, ToggleButtonExt,
},
Box, Button, Expander, Label, ListItem, ListView, ScrolledWindow, SignalListItemFactory,
SingleSelection, StringList, StringObject, ToggleButton, Widget,
};
use tracing::{error, event, span, Level};
#[cfg(feature = "tracker-state-debug")]
use tracing::debug;
use crate::coordinator::ApplicationEvent;
use crate::states::tracker_state::TrackerState;
#[derive(Debug)]
pub struct ControlPanel {
top_level: Box,
pub connection_buttons: ExpanderMenu,
pub current_id: Label,
pub items: StringList,
pub list_view: ListView,
}
#[derive(Debug)]
pub struct ExpanderMenu {
pub top_level: Expander,
pub camera_connection: Button,
pub tracker_enable_toggle: ToggleButton,
}
impl ControlPanel {
pub fn new(tracker_state: Arc<Mutex<TrackerState>>) -> ControlPanel {
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
let list_item = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be a List Item");
let label = Label::new(None);
list_item.set_child(Some(&label));
list_item
.property_expression("item")
.chain_property::<StringObject>("string")
.bind(&label, "label", Widget::NONE);
});
let items = StringList::new(&["Please connect automatic source"]);
let model = SingleSelection::builder()
.model(&items)
.autoselect(false)
.can_unselect(false)
.build();
model.connect_selected_item_notify(move |x| {
let item = x.selected_item().and_downcast::<StringObject>();
if let Some(item) = item {
if let Ok(id) = item.string().parse::<u32>() {
#[cfg(feature = "tracker-state-debug")]
debug!("Getting lock on tracker state for setting active tracking id!");
if let Ok(mut ts) = tracker_state.lock() {
ts.tracking_id = id;
}
} else {
error!("An unparsable ID was clicked");
}
} else {
error!("An invalid id was selected from the selection");
}
});
let list_view = ListView::new(Some(model), Some(factory));
let scrolled_window = ScrolledWindow::builder()
.child(&list_view)
.hscrollbar_policy(gtk::PolicyType::Never)
.height_request(200)
.build();
let top_level = Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(5)
.margin_top(24)
.margin_start(24)
.margin_end(24)
.margin_bottom(12)
.build();
let expander = ExpanderMenu::new();
let current_id = Label::builder()
.label("Not Tracking")
.can_focus(false)
.can_target(false)
.css_classes(["current-id"])
.build();
top_level.append(&expander.top_level);
top_level.append(&current_id);
top_level.append(&scrolled_window);
ControlPanel {
top_level,
connection_buttons: expander,
current_id,
items,
list_view,
}
}
pub fn get_top_level(&self) -> &Box {
&self.top_level
}
pub fn connect_button_callbacks(&self, to_mec: Sender<ApplicationEvent>) {
self.connection_buttons
.tracker_enable_toggle
.connect_clicked(glib::clone!(@strong to_mec => move |button| {
let span = span!(Level::TRACE, "tracker_enable_toggle callback");
let _enter = span.enter();
if let Err(e) =
to_mec.send_blocking(ApplicationEvent::EnableAutomatic(button.is_active()))
{
event!(Level::ERROR, error = ?e, "Could not send message to the MEC");
}
}));
self.connection_buttons.camera_connection.connect_clicked(glib::clone!(@strong to_mec => move |_button| {
let span = span!(Level::TRACE, "camera_connection callback");
let _enter = span.enter();
match to_mec.try_send(ApplicationEvent::CameraConnectionPress) {
Ok(_) => {},
Err(async_channel::TrySendError::Closed(_)) => panic!("Coordinator MEC is closed. Unrecoverable error."),
Err(e) => event!(Level::ERROR, error = ?e, message = "There was an error sending to the MEC"),
}
}));
}
}
impl ExpanderMenu {
pub fn new() -> Self {
let content_box = Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(10)
.margin_top(12)
.margin_start(24)
.margin_end(24)
.margin_bottom(12)
.build();
let expander = Expander::builder()
.child(&content_box)
.expanded(true)
.label("Connections")
.build();
let camera_connection = Button::builder()
.label("Connect to Camera")
.margin_top(12)
.build();
let tracker_enable_toggle = ToggleButton::builder()
.label("Connect to Tracker Computer")
.active(false)
.margin_top(12)
.build();
content_box.append(&camera_connection);
content_box.append(&tracker_enable_toggle);
ExpanderMenu {
top_level: expander,
camera_connection,
tracker_enable_toggle,
}
}
}

214
src/ui/liveview_panel.rs Normal file
View file

@ -0,0 +1,214 @@
use std::{
cmp::Ordering,
sync::{Arc, Mutex},
};
use async_channel::Sender;
use gtk::{
gdk::Paintable,
prelude::{BoxExt, GestureExt, WidgetExt},
AspectFrame, Box, DrawingArea, EventControllerMotion, GestureClick, Label, Overlay, Picture,
};
use crate::coordinator::ApplicationEvent;
use crate::states::tracker_state::TrackerState;
use super::NormalizedBoxCoords;
pub struct LiveViewPanel {
top_level: gtk::Box,
pub tracker_status_label: Label,
pub cam_status_label: Label,
pub adjustment_label: Label,
picture: Picture,
overlay: Overlay,
}
impl LiveViewPanel {
pub fn new(tracker_state: Arc<Mutex<TrackerState>>, to_mec: Sender<ApplicationEvent>) -> Self {
let right_box = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.hexpand(true)
.valign(gtk::Align::Center)
.build();
let tracker_status_label = Label::builder()
.label("No Status Yet".to_string())
.can_focus(true)
.css_classes(vec!["large-label", "NoConnection"])
.build();
let cam_status_label = Label::builder()
.label("No Connection".to_string())
.css_classes(vec!["NoConnection"])
.can_focus(true)
.build();
let adjustment_label = Label::builder()
.label("X: 0 Y: )")
.justify(gtk::Justification::Center)
.css_classes(vec!["JoystickCurrent"])
.build();
let webcam_picture = gtk::Picture::builder().can_focus(false).build();
let overlay_box = gtk::Overlay::builder().build();
overlay_box.set_child(Some(&webcam_picture));
let click_handler = GestureClick::builder()
.button(gtk::gdk::ffi::GDK_BUTTON_PRIMARY as u32)
.propagation_limit(gtk::PropagationLimit::SameNative)
.build();
let move_handler = EventControllerMotion::builder()
.propagation_limit(gtk::PropagationLimit::SameNative)
.build();
let tracker_state_2 = tracker_state.clone();
let handler_picture = webcam_picture.clone();
move_handler.connect_motion(move |_motion_handler, x, y| {
LiveViewPanel::motion_callback(&handler_picture, &tracker_state_2, x, y);
});
let handler_picture = webcam_picture.clone();
click_handler.connect_pressed(move |gesture, _id, x, y| {
gesture.set_state(gtk::EventSequenceState::Claimed);
LiveViewPanel::click_gesture_callback(&handler_picture, &tracker_state, &to_mec, x, y)
});
overlay_box.add_controller(click_handler);
overlay_box.add_controller(move_handler);
let aspect = AspectFrame::builder()
.ratio(16.0 / 9.0)
.obey_child(false)
.child(&overlay_box)
.build();
right_box.append(&tracker_status_label);
right_box.append(&aspect);
right_box.append(&cam_status_label);
right_box.append(&adjustment_label);
LiveViewPanel {
adjustment_label,
tracker_status_label,
cam_status_label,
overlay: overlay_box,
picture: webcam_picture,
top_level: right_box,
}
}
fn click_gesture_callback(
overlay: &Picture,
tracker_state: &Arc<Mutex<TrackerState>>,
to_mec: &Sender<ApplicationEvent>,
x_coord: f64,
y_coord: f64,
) {
let x_size = overlay.size(gtk::Orientation::Horizontal);
let y_size = overlay.size(gtk::Orientation::Vertical);
let x_coord = x_coord as f32 / x_size as f32;
let y_coord = y_coord as f32 / y_size as f32;
if let Ok(mut ts) = tracker_state.lock() {
if let Some(v) = calc_box_under_mouse(&ts.identity_boxes, x_coord, y_coord) {
ts.tracking_id = v;
if let Err(e) = to_mec.send_blocking(ApplicationEvent::ChangeTracking(v)) {
panic!("Could not send message to MEC, unrecoverable: {e}");
}
}
}
}
fn motion_callback(
overlay: &Picture,
tracker_state: &Arc<Mutex<TrackerState>>,
x_coord: f64,
y_coord: f64,
) {
let x_size = overlay.size(gtk::Orientation::Horizontal);
let y_size = overlay.size(gtk::Orientation::Vertical);
let x_coord = x_coord as f32 / x_size as f32;
let y_coord = y_coord as f32 / y_size as f32;
if let Ok(mut ts) = tracker_state.lock() {
ts.highlighted_id = calc_box_under_mouse(&ts.identity_boxes, x_coord, y_coord);
}
}
pub fn get_top_level(&self) -> &Box {
&self.top_level
}
pub fn set_drawable(&self, new_drawable: &DrawingArea) {
self.overlay.add_overlay(new_drawable);
}
pub fn set_paintable(&self, new_paintable: &Paintable) {
self.picture.set_paintable(Some(new_paintable));
}
}
fn calc_box_under_mouse(
boxes: &[NormalizedBoxCoords],
x_coord: f32,
y_coord: f32,
) -> Option<u32> {
let mut mouse_over: Vec<NormalizedBoxCoords> = vec![];
for nb in boxes.iter() {
if nb.x1 < x_coord
&& nb.y1 < y_coord
&& nb.x2 > x_coord
&& nb.y2 > y_coord
{
mouse_over.push(*nb);
}
}
if mouse_over.len() > 1 {
let mut x_coords = mouse_over
.iter()
.flat_map(|b| [b.x1, b.x2])
.collect::<Vec<f32>>();
x_coords.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mid = x_coords.len() / 2;
let median_two_x = &x_coords[mid - 1..mid + 1];
let mut y_coords = mouse_over
.iter()
.flat_map(|b| [b.y1, b.y2])
.collect::<Vec<f32>>();
y_coords.sort_by(|a, b| a.partial_cmp(b).unwrap());
let median_two_y = &y_coords[mid - 1..mid + 1];
let overlap_area =
(median_two_x[1] - median_two_x[0]) * (median_two_y[1] - median_two_y[0]);
// We want the one with the largest percentage of it's area as overlap
// to be the highlighted one
mouse_over.sort_by(|a, b| {
let result =
((a.area() - overlap_area) / a.area()) - ((b.area() - overlap_area) / b.area());
if 0.0001 > result && result > -0.0001 {
Ordering::Equal
} else if result > 0.0 {
Ordering::Greater
} else {
Ordering::Less
}
});
}
mouse_over
.iter()
.map(|x| x.id)
.collect::<Vec<u32>>()
.first()
.copied()
}

411
src/ui/mod.rs Normal file
View file

@ -0,0 +1,411 @@
use std::fmt::Display;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use gtk::cairo::Context;
use gtk::gdk::Paintable;
use gtk::glib::clone;
use gtk::{gio, glib, prelude::*, CssProvider};
use gtk::{Application, ApplicationWindow};
use log::{error, info};
use tokio::runtime::Handle;
use tokio::sync::RwLock;
use crate::config::AppConfig;
use crate::states::tracker_state::TrackerState;
use crate::coordinator::{start_coordinator, ApplicationEvent, MoveEvent, TrackerUpdate};
mod control_panel;
mod liveview_panel;
mod settings_modal;
use control_panel::ControlPanel;
use liveview_panel::LiveViewPanel;
pub enum GuiUpdate {
SocketDisconnected,
SocketConnecting,
SocketConnected,
MoveEvent(MoveEvent),
UpdatePaintable(gstreamer::Element),
TrackerUpdate(TrackerUpdate),
}
#[derive(Debug, Clone, Copy)]
pub struct BoxCoords {
pub id: u32,
pub x1: u32,
pub y1: u32,
pub x2: u32,
pub y2: u32,
}
impl Display for BoxCoords {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Absolute Box {}, x1: {}, y1: {}, x2: {}, y2: {}",
self.id, self.x1, self.y1, self.x2, self.y2
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct NormalizedBoxCoords {
pub id: u32,
pub x1: f32,
pub y1: f32,
pub x2: f32,
pub y2: f32,
}
impl NormalizedBoxCoords {
fn absolute_coords(&self, width: i32, height: i32) -> BoxCoords {
BoxCoords {
id: self.id,
x1: (self.x1 * width as f32) as u32,
y1: (self.y1 * height as f32) as u32,
x2: (self.x2 * width as f32) as u32,
y2: (self.y2 * height as f32) as u32,
}
}
fn area(&self) -> f32 {
(self.x2 - self.x1) * (self.y2 - self.y1)
}
}
impl Display for NormalizedBoxCoords {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Normalized Box {}, x1: {}, y1: {}, x2: {}, y2: {}",
self.id, self.x1, self.y1, self.x2, self.y2
)
}
}
pub fn on_activate(app: &Application) {
let provider = CssProvider::new();
provider.load_from_string(include_str!("../../style.css"));
gtk::style_context_add_provider_for_display(
&gtk::gdk::Display::default().expect("Could not connect to a display"),
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
let menubar = {
let connections_menu = {
let settings = gio::MenuItem::new(Some("Edit IPs"), Some("app.connections"));
let connections_menu = gio::Menu::new();
connections_menu.append_item(&settings);
connections_menu
};
let menubar = gio::Menu::new();
menubar.append_submenu(Some("Settings"), &connections_menu);
menubar
};
app.set_menubar(Some(&menubar));
info!("Menu bar set up");
}
pub fn build_ui(app: &Application, config: Arc<RwLock<AppConfig>>, runtime: Handle) {
let main_box = gtk::Box::new(gtk::Orientation::Horizontal, 0);
let left_box = gtk::Box::builder()
.width_request(300)
.orientation(gtk::Orientation::Vertical)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("VCC Camera Controller")
.show_menubar(true)
.default_width(840)
.default_height(480)
.child(&main_box)
.build();
let rt = runtime.clone();
let connections_modal_config = config.clone();
let connections_activate = gio::ActionEntry::builder("connections")
.activate(clone!(@weak window => move |app: &gtk::Application, _, _| {
let connections_modal = settings_modal::ConnectionsModal::new(app, &window, &rt, &connections_modal_config);
connections_modal.window.present();
}))
.build();
app.add_action_entries([connections_activate]);
// Main Event Channel
let (to_mec, mec) = async_channel::bounded::<ApplicationEvent>(10);
let (to_gui, gui_recv) = async_channel::bounded::<GuiUpdate>(10);
let tracker_state = Arc::new(Mutex::new(TrackerState {
tracking_id: 0,
highlighted_id: None,
last_detect: Instant::now(),
enabled: true,
identity_boxes: vec![],
update_ids: false,
}));
let coord_config = config.clone();
runtime.spawn(start_coordinator(
mec,
to_mec.clone(),
to_gui,
runtime.clone(),
coord_config,
));
let control_panel = ControlPanel::new(tracker_state.clone());
control_panel.connect_button_callbacks(to_mec.clone());
left_box.append(control_panel.get_top_level());
main_box.append(&left_box);
let liveview_panel = LiveViewPanel::new(tracker_state.clone(), to_mec.clone());
main_box.append(liveview_panel.get_top_level());
let drawable = gtk::DrawingArea::builder().build();
let drawable_ts = tracker_state.clone();
drawable.set_draw_func(move |_, ctx, width, height| {
draw_boxes(width, height, ctx, &drawable_ts);
});
liveview_panel.set_drawable(&drawable);
let items = control_panel.items.clone();
let id_label = control_panel.current_id.clone();
let model = control_panel
.list_view
.model()
.expect("The list view should have a model!");
let tracker_state_2 = tracker_state.clone();
glib::timeout_add_local(Duration::from_millis(500), move || {
#[cfg(feature = "tracker-state-debug")]
debug!("Getting lock on tracker state for checking identity boxes");
// don't update the stringlist until after letting go of the tracker state
// due to async interweaving causing a mutex deadlock
let mut ids: Option<Vec<String>> = None;
let mut current_id: u32 = 0;
if let Ok(ts) = tracker_state.lock() {
current_id = ts.tracking_id;
if ts.update_ids {
ids = Some(ts.identity_boxes.iter().map(|t| t.id.to_string()).collect());
}
}
if current_id > 0 {
id_label.set_text(current_id.to_string().as_str());
}
let current_id: String = current_id.to_string();
if let Some(mut ids) = ids {
let mut active_index = 0;
let mut to_delete_indexes: Vec<u32> = vec![];
// find all indexes where matching item does not exist in
// the ids, list, and remove all items in the ids list
// that already exists in the stringList
for i in 0..items.n_items() {
let item = items.string(i).unwrap_or("Empty".into()).to_string();
if item == current_id {
active_index = i;
}
if !ids.contains(&item) {
to_delete_indexes.push(i);
} else if let Some(pos) = ids.iter().position(|x| **x == item) {
ids.remove(pos);
}
}
if active_index > 0 {
model.select_item(active_index, true);
}
// invert the order so we don't have to adjust after each deletion
to_delete_indexes.sort();
to_delete_indexes.reverse();
to_delete_indexes.iter().for_each(|x| {
items.remove(*x);
});
items.splice(
items.n_items(),
0,
&ids.iter().map(|x| x.as_str()).collect::<Vec<&str>>()[0..],
)
}
glib::ControlFlow::Continue
});
let mut tracker_status_label = liveview_panel.tracker_status_label.clone();
let mut tracker_enable_toggle = control_panel
.connection_buttons
.tracker_enable_toggle
.clone();
glib::spawn_future_local(glib::clone!(@weak drawable => async move {
while let Ok(d) = gui_recv.recv().await {
drawable.queue_draw();
match d {
GuiUpdate::MoveEvent(msg) => {
liveview_panel.adjustment_label.set_text(
format!("X: {:>4} Y: {:>4}", msg.x, msg.y).as_str()
);
}
GuiUpdate::SocketConnected => {
control_panel.connection_buttons.camera_connection.set_sensitive(true);
control_panel.connection_buttons.camera_connection.set_label("Disconnect Camera");
liveview_panel.cam_status_label.set_label("Connected");
liveview_panel.cam_status_label.set_css_classes(&["YesConnection"]);
},
GuiUpdate::SocketConnecting => {
control_panel.connection_buttons.camera_connection.set_sensitive(false);
control_panel.connection_buttons.camera_connection.set_label("Please wait");
liveview_panel.cam_status_label.set_label("Connecting");
liveview_panel.cam_status_label.set_css_classes(&["LoadingConnection"]);
},
GuiUpdate::SocketDisconnected => {
control_panel.connection_buttons.camera_connection.set_sensitive(true);
control_panel.connection_buttons.camera_connection.set_label("Connect to Camera");
liveview_panel.cam_status_label.set_label("Not Connected to Camera");
liveview_panel.cam_status_label.set_css_classes(&["NoConnection"]);
}
GuiUpdate::UpdatePaintable(sink) => {
let paintable = sink.property::<Paintable>("paintable");
liveview_panel.set_paintable(&paintable);
}
GuiUpdate::TrackerUpdate(update) => match update {
TrackerUpdate::Fail => {}
TrackerUpdate::HeaderUpdate(new_header) => {
update_tracker_header(new_header, &mut tracker_status_label, &mut tracker_enable_toggle)
}
TrackerUpdate::Clear => {
if let Ok(mut ts) = tracker_state_2.lock() {
ts.clear();
}
}
TrackerUpdate::Update(update) => {
if let Ok(mut ts) = tracker_state_2.lock() {
ts.update_from_boxes(update.boxes);
ts.last_detect = update.time;
}
}
}
}
}
info!("Closing update loop");
}));
window.connect_close_request(move |_| glib::Propagation::Proceed);
// Present window
window.present();
}
fn update_tracker_header(new_header: String, tracker_status_label: &mut gtk::Label, tracker_enable_toggle: &mut gtk::ToggleButton) {
tracker_status_label.set_text(&new_header);
if new_header.contains("Failed") || new_header.contains("Disconnected") {
tracker_status_label.set_css_classes(&["NoConnection"]);
tracker_enable_toggle.set_label("Connect to Tracker Computer");
tracker_enable_toggle.set_active(false);
tracker_enable_toggle.set_sensitive(true);
} else if new_header.contains("Degraded") {
tracker_status_label.set_css_classes(&["LoadingConnection"]);
tracker_enable_toggle.set_label("Disconnect Tracker Computer");
tracker_enable_toggle.set_active(true);
tracker_enable_toggle.set_sensitive(true);
} else if new_header.contains("Connecting") {
tracker_status_label.set_css_classes(&["LoadingConnection"]);
tracker_enable_toggle.set_label("Please Wait");
tracker_enable_toggle.set_active(false);
tracker_enable_toggle.set_sensitive(false);
} else if new_header.contains("Nominal") {
tracker_status_label.set_css_classes(&["YesConnection"]);
tracker_enable_toggle.set_label("Disconnect Tracker Computer");
tracker_enable_toggle.set_active(true);
tracker_enable_toggle.set_sensitive(true);
}
}
fn draw_boxes(width: i32, height: i32, ctx: &Context, tracker_state: &Arc<Mutex<TrackerState>>) {
ctx.set_line_width(5.0);
ctx.select_font_face(
"Arial",
gtk::cairo::FontSlant::Normal,
gtk::cairo::FontWeight::Bold,
);
ctx.set_font_size(24.0);
#[cfg(feature = "tracker-state-debug")]
debug!("Getting tracker state for drawing boxes");
if let Ok(ts) = tracker_state.lock() {
let active = ts.tracking_id;
let highlighted_id = ts.highlighted_id.unwrap_or(0);
for nb in ts.identity_boxes.iter() {
if nb.id == active {
ctx.set_source_rgb(1.0, 0.0, 0.0);
} else if nb.id == highlighted_id {
ctx.set_source_rgb(0.0, 1.0, 1.0);
} else {
ctx.set_source_rgb(0.0, 0.0, 1.0);
}
if nb.id == active && nb.id == highlighted_id {
ctx.set_source_rgb(1.0, 1.0, 0.0);
}
let b = nb.absolute_coords(width, height);
ctx.rectangle(
b.x1 as f64,
b.y1 as f64,
(b.x2 - b.x1) as f64,
(b.y2 - b.y1) as f64,
);
if let Err(e) = ctx.stroke() {
error!("Could not draw cairo stroke: {e}");
}
ctx.stroke()
.expect("GTK Cario context did not have a stroke!");
ctx.move_to(b.x1 as f64 + 5.0, b.y1 as f64 + 18.0);
ctx.set_source_rgb(0.0, 0.0, 0.0);
ctx.show_text(&format!("[{}]", b.id))
.expect("Couldn't show text!");
ctx.stroke()
.expect("GTK Cario context did not have a stroke!");
}
} else {
error!("Could not get lock on boxes for drawing on the draw area!");
}
}

161
src/ui/settings_modal.rs Normal file
View file

@ -0,0 +1,161 @@
use std::sync::Arc;
use gtk::glib::{self, clone};
use gtk::{
prelude::{BoxExt, ButtonExt, EditableExt, GtkWindowExt},
Application, ApplicationWindow, Button, Entry, Label, Window,
};
use log::{error, info};
use tokio::runtime::Handle;
use tokio::sync::RwLock;
use crate::config::{save_config, AppConfig};
pub struct ConnectionsModal {
pub window: Window,
}
impl ConnectionsModal {
pub fn new(
app: &Application,
parent: &ApplicationWindow,
rt: &Handle,
app_config: &Arc<RwLock<AppConfig>>,
) -> Self {
// Send help :(
let config_read = rt.block_on(async { app_config.read().await });
let main_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
let window = Window::builder()
.application(app)
.title("Change Connection Information")
.modal(true)
.transient_for(parent)
.destroy_with_parent(true)
.resizable(false)
.child(&main_box)
.build();
let camera_label = Label::builder()
.label("Camera Connection")
.margin_end(5)
.margin_start(5)
.build();
let camera_ip_entry = Entry::builder()
.placeholder_text("IP Address")
.text(config_read.camera_ip.clone())
.margin_top(5)
.margin_end(5)
.margin_start(5)
.margin_bottom(5)
.can_focus(true)
.build();
let camera_port_entry = Entry::builder()
.placeholder_text("Port")
.text(config_read.camera_port.to_string())
.margin_top(5)
.margin_end(5)
.margin_start(5)
.margin_bottom(5)
.can_focus(true)
.build();
let tracker_label = Label::builder()
.label("Tracker Connection")
.margin_end(5)
.margin_start(5)
.build();
let tracker_ip_entry = Entry::builder()
.placeholder_text("IP Address")
.text(config_read.tracker_ip.clone())
.margin_top(5)
.margin_end(5)
.margin_start(5)
.margin_bottom(5)
.can_focus(true)
.build();
let tracker_port_entry = Entry::builder()
.placeholder_text("Port")
.text(config_read.tracker_port.to_string())
.margin_top(5)
.margin_end(5)
.margin_start(5)
.margin_bottom(5)
.can_focus(true)
.build();
let tracker_refresh_millis = Entry::builder()
.placeholder_text("Refresh timeout")
.text(config_read.tracker_refresh_rate_millis.to_string())
.margin_top(5)
.margin_end(5)
.margin_start(5)
.margin_bottom(5)
.can_focus(true)
.build();
let quit_button = Button::builder()
.label("Save")
// .action_name("window.close")
.margin_top(5)
.margin_end(5)
.margin_start(5)
.margin_bottom(5)
.build();
main_box.append(&camera_label);
main_box.append(&camera_ip_entry);
main_box.append(&camera_port_entry);
main_box.append(&tracker_label);
main_box.append(&tracker_ip_entry);
main_box.append(&tracker_port_entry);
main_box.append(&tracker_refresh_millis);
main_box.append(&quit_button);
let new_ref = app_config.clone();
quit_button.connect_clicked(clone!(
@strong rt, @weak window,
@weak camera_ip_entry, @weak camera_port_entry,
@weak tracker_ip_entry, @weak tracker_port_entry,
@weak tracker_refresh_millis => move |_| {
let new_camera_ip = camera_ip_entry.text().to_string();
let new_camera_port = camera_port_entry.text().parse::<u32>().unwrap();
let new_tracker_ip = tracker_ip_entry.text().to_string();
let new_tracker_port = tracker_port_entry.text().parse::<u32>().unwrap();
let new_tracker_millis = tracker_refresh_millis.text().parse::<u32>().unwrap();
info!("Starting config save");
{
// maybe just send the police.
let mut write_lock = rt.block_on(async {new_ref.write().await});
write_lock.camera_ip = new_camera_ip;
write_lock.camera_port = new_camera_port;
write_lock.tracker_ip = new_tracker_ip;
write_lock.tracker_port = new_tracker_port;
write_lock.tracker_refresh_rate_millis = new_tracker_millis;
// why does this feel like the borrow checker gave me a pass?
if let Err(e) = save_config(&write_lock) {
error!("Could not save config! {e}");
}
// FBI!!! OPEN UP!!!!
}
window.close();
info!("Please nicholas, add a non-crashing parse");
}));
ConnectionsModal {
window,
}
}
}

View file

@ -1,120 +0,0 @@
use futures_util::{SinkExt, StreamExt};
use tokio::net::TcpListener;
use tokio_tungstenite::{accept_async, tungstenite::Message};
use tracing::{debug, error, info, instrument};
use vcs_common::{AppReceiver, AppSender, ApplicationMessage};
#[instrument(skip_all)]
pub async fn start_listener(from_app: AppReceiver, to_ui: AppSender) {
info!("starting tcplistener");
let listener = TcpListener::bind("localhost:7891").await.unwrap();
info!("Listening for webrtc connections on localhost:7891");
if let Ok((stream, _)) = listener.accept().await {
match accept_async(stream).await {
Err(e) => error!("Could not convert incoming stream to websocket: {e}"),
Ok(ws_stream) => {
let (mut ws_sender, mut ws_receiver) = ws_stream.split();
tokio::spawn(async move {
while let Ok(msg) = from_app.recv().await {
#[cfg(debug_assertions)]
{
// serialized message
match serde_json::to_string(&msg) {
Err(e) => {
error!("Could not serialize ApplicationMessage to JSON! {e}")
}
Ok(msg) => {
if let Err(e) = ws_sender.send(Message::text(msg)).await {
error!("Could not send text ApplicationMessage to websocket! Closing websocket\n{e}");
break;
}
}
}
}
#[cfg(not(debug_assertions))]
{
match bincode::serialize(&msg) {
Err(e) => error!(
"Could not serialize ApplicationMessage into binary! {e}"
),
Ok(e) => {
if let Err(e) = sender.send(Message::binary(msg)).await {
error!("Could not send binary ApplicationMessage to websocket! Closing websocket\n{e}");
break;
}
}
}
}
}
});
while let Some(msg) = ws_receiver.next().await {
match msg {
Err(e) => {
error!("There was an error getting a message from the remote! {e}");
}
Ok(msg) => match msg {
Message::Ping(_) | Message::Pong(_) => {}
Message::Close(_) => {
info!("Received WebSocket close message! Closing the websocket");
break;
}
Message::Frame(_) => {
info!("Received a Frame websocket message?");
}
Message::Text(text) => {
debug!("Recieved text from websocket: {text}");
#[cfg(debug_assertions)]
{
match serde_json::from_str(&text) {
Ok(msg) => {
if let Err(e) = to_ui.send(msg).await {
error!("Could not send message from ws to application! Closing and exiting\n{e}");
break;
}
}
Err(e) => {
error!("Received a malformed JSON message from the websocket!\n{text}\nmsg: {e}");
}
}
}
#[cfg(not(debug_assertions))]
{
warn!("Recieved a `Text` message from the remote while running in release mode! " +
"Was the other endpoint running release mode?\n msg: {text}");
}
}
Message::Binary(msg) => {
#[cfg(debug_assertions)]
{
match bincode::deserialize::<ApplicationMessage>(&msg) {
Ok(m) => {
if let Err(e) = to_ui.send(m).await {
error!("Could not send message to application! Closing and exiting\n{e}");
break;
}
}
Err(e) => {
error!("Received a malformed binary message from the websocket!\n{e}");
}
}
}
#[cfg(not(debug_assertions))]
{
warn!("Recieved a `Binary` message from the remote while running in debug mode! " +
"Was the other endpoing running debug mode?");
}
}
},
}
}
}
}
}
}

View file

@ -1,13 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./ui/**/*.html", "./ui/**/*.js"],
theme: {
extend: {
aspectRatio: {
albumCarousel: "1/1.25",
},
},
},
plugins: [],
}

View file

@ -1,72 +0,0 @@
{
"build": {
"beforeBuildCommand": {
"script": "tailwindcss -i ./ui/input.css -o ./ui/static/main.css;",
"cwd": "."
},
"beforeDevCommand": {
"script": "tailwindcss -i ./ui/input.css -o ./ui/static/main.css;",
"cwd": "."
},
"devPath": "./ui",
"distDir": "./ui",
"withGlobalTauri": true
},
"package": {
"productName": "VCS-Controller",
"version": "0.1.0"
},
"tauri": {
"allowlist": {
"all": false
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"deb": {
"depends": []
},
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "com.tauri.dev",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [
{
"fullscreen": false,
"height": 600,
"resizable": true,
"title": "VCS Controller",
"width": 800
}
]
}
}

View file

@ -1,14 +1,6 @@
# Functional
- Up-direction maxes at -50 instead of 100
# Tauri Rewrite Todo
- propogate changes to the UI from internal state - replace GuiUpdate
- Draw boxes onto video feed
- UI Can update config options
- UI get's 'socket connected/failed' and tracker metrics
- UI can select active tracking target
## QoL
- Fine tuning the tracking speeds to be non-linear, make sure the pi doesn't have that speed cap (remember could be expecting 6v max speed).
- left and right need to hit 50 fast

View file

@ -1,55 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link href="/static/main.css" rel="stylesheet"/>
<script src="/static/feather.min.js"></script>
</head>
<body>
<nav class="bg-fuchsia-300">
<div class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8">
<div class="relative flex h-16 items-center justify-between">
<div class="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
<div class="hidden sm:ml-6 sm:block">
<div class="flex space-x-4">
<!-- Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" -->
<a href="#" class="rounded-md bg-slate-800 px-3 py-2 text-sm font-medium text-white" aria-current="page">Dashboard</a>
</div>
</div>
<div class="me-0 ms-auto py-2">
<button class="rounded-full p-2 hover:bg-cyan-700 transition-colors">
<i data-feather="menu"></i>
</button>
</div>
</div>
</div>
</div>
</nav>
<div class="flex flex-row h-dvh w-100 text-neutral-400">
<div class="flex flex-col bg-neutral-300 h-dvh w-1/8">
<button id="camera_connect_button" class="rounded-full font-semibold mx-3 mt-2 px-4 py-2 text-white bg-cyan-600">Connect to Camera</button>
<button class="rounded-full font-semibold mx-3 mt-2 px-4 py-2 text-white bg-cyan-600">Connect to Computer</button>
<ul id="connections_list" class="m-3 border-y-1 border-neutral-800 bg-neutral-400">
<li>No Camera Connections</li>
</ul>
</div>
<div class="bg-emerald-700 h-dvh w-7/8">
<video style="width: 320px; height: 240px;" autoplay id="remoteview"></video>
<video style="width: 320px; height: 240px;" id="capture"></video>
</div>
</div>
<script type=module>
import { init } from "/static/index.js";
feather.replace();
init();
</script>
</body>
</html>

View file

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -1,72 +0,0 @@
import { invoke, event } from "./node_modules/@tauri-apps/api/index.js";
async function setup_listeners() {
await listen_for_new_satellites();
start_joystick_callback();
}
async function listen_for_new_satellites() {
const new_satellite = await event.listen("satellite_names", async (msg) => {
const payload = JSON.parse(msg.payload);
console.log(payload);
let cam_button = document.getElementById("camera_connect_button");
cam_button.innerText = "Connect to Camera";
cam_button.classList.add("text-semibold");
cam_button.classList.add("text-white");
cam_button.classList.remove("text-neutral-400")
let out_ul = document.getElementById("connections_list");
out_ul.innerHTML = "";
payload.forEach((item) => {
let li = document.createElement("li");
li.classList.add("flex", "py-2");
if (!item.is_connected) {
li.classList.add("text-neutral-300", "bg-neutral-500");
li.innerHTML = `<span class="ms-2 text-sm"><i class="inline-block ms-1 me-2" data-feather="wifi-off"></i>${item.name}</span>`;
} else {
li.innerHTML = `<span class="ms-2 text-sm"><i class="inline-block ms-1 me-2" data-feather="wifi"></i>${item.name}</span>`;
li.classList.add("text-white");
}
out_ul.appendChild(li);
feather.replace();
})
register_list_elements();
});
}
function register_list_elements() {
Array.from(document.getElementById("connections_list").children).forEach((li) => {
const target_name = li.querySelector("span").innerText;
li.addEventListener("click", () => {
invoke("change_target_satellite", { new_target_name: target_name })
.catch((e) => console.log(e));
});
});
}
const denoise_level = 0.01;
function start_joystick_callback() {
window.addEventListener("gamepadconnected", (e) => {
setInterval(() => {
var cur_obj = {};
const gp = navigator.getGamepads()[e.gamepad.index];
if (Math.abs(gp.axes[0]) > denoise_level || Math.abs(gp.axes[1]) > denoise_level) {
invoke("send_joystick_event", {x: gp.axes[0], y: gp.axes[1]});
}
// THIS DISABLES THE RIGHT STICK
// if (Math.abs(gp.axes[2]) > denoise_level || Math.abs(gp.axes[3]) > denoise_level) {
// invoke("send_joystick_event", {x: gp.axes[2], y: gp.axes[3]});
//}
}, 100);
});
}
export { setup_listeners };

File diff suppressed because one or more lines are too long

View file

@ -1,46 +0,0 @@
import { invoke, event } from "./node_modules/@tauri-apps/api/index.js";
import { setup_listeners } from "./async_listeners.js";
import { rtc_init } from "./rtc.js";
function call_camera_connect() {
invoke("connect_to_camera", {})
.then(() => {
let cam_button = document.getElementById("camera_connect_button");
cam_button.innerText = "Connecting to Camera";
cam_button.classList.remove("text-semibold");
cam_button.classList.remove("text-white");
cam_button.classList.add("text-neutral-400")
})
.catch((e) => console.error(e));
}
function supports_webrtc() {
var isWebRTCSupported = false;
['RTCPeerConnection', 'webkitRTCPeerConnection', 'mozRTCPeerConnection', 'RTCIceGatherer'].forEach(function(item) {
if (isWebRTCSupported) {
return;
}
if (item in window) {
isWebRTCSupported = true;
}
});
return isWebRTCSupported;
}
async function init() {
console.log("Setting up");
document.getElementById("camera_connect_button").addEventListener("click", call_camera_connect);
await setup_listeners();
let webrtc_support = supports_webrtc();
invoke("supports_webrtc", { has_support: webrtc_support });
if (webrtc_support) {
await rtc_init();
}
}
export { init };

View file

@ -1,928 +0,0 @@
/*
! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/
html,
:host {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
-webkit-tap-highlight-color: transparent;
/* 7 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-feature-settings: normal;
/* 2 */
font-variation-settings: normal;
/* 3 */
font-size: 1em;
/* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-feature-settings: inherit;
/* 1 */
font-variation-settings: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
letter-spacing: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
input:where([type='button']),
input:where([type='reset']),
input:where([type='submit']) {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Reset default styling for dialogs.
*/
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
.visible {
visibility: visible;
}
.collapse {
visibility: collapse;
}
.static {
position: static;
}
.fixed {
position: fixed;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.m-3 {
margin: 0.75rem;
}
.mx-3 {
margin-left: 0.75rem;
margin-right: 0.75rem;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.mx-1 {
margin-left: 0.25rem;
margin-right: 0.25rem;
}
.me-0 {
margin-inline-end: 0px;
}
.ms-auto {
margin-inline-start: auto;
}
.mt-2 {
margin-top: 0.5rem;
}
.ms-2 {
margin-inline-start: 0.5rem;
}
.me-2 {
margin-inline-end: 0.5rem;
}
.ms-1 {
margin-inline-start: 0.25rem;
}
.block {
display: block;
}
.inline-block {
display: inline-block;
}
.inline {
display: inline;
}
.flex {
display: flex;
}
.table {
display: table;
}
.grid {
display: grid;
}
.contents {
display: contents;
}
.hidden {
display: none;
}
.h-16 {
height: 4rem;
}
.h-dvh {
height: 100dvh;
}
.max-w-7xl {
max-width: 80rem;
}
.flex-1 {
flex: 1 1 0%;
}
.transform {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.resize {
resize: both;
}
.flex-row {
flex-direction: row;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
.rounded-full {
border-radius: 9999px;
}
.rounded-md {
border-radius: 0.375rem;
}
.border-neutral-800 {
--tw-border-opacity: 1;
border-color: rgb(38 38 38 / var(--tw-border-opacity));
}
.bg-cyan-600 {
--tw-bg-opacity: 1;
background-color: rgb(8 145 178 / var(--tw-bg-opacity));
}
.bg-emerald-700 {
--tw-bg-opacity: 1;
background-color: rgb(4 120 87 / var(--tw-bg-opacity));
}
.bg-fuchsia-300 {
--tw-bg-opacity: 1;
background-color: rgb(240 171 252 / var(--tw-bg-opacity));
}
.bg-gray-900 {
--tw-bg-opacity: 1;
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
.bg-neutral-300 {
--tw-bg-opacity: 1;
background-color: rgb(212 212 212 / var(--tw-bg-opacity));
}
.bg-neutral-400 {
--tw-bg-opacity: 1;
background-color: rgb(163 163 163 / var(--tw-bg-opacity));
}
.bg-neutral-500 {
--tw-bg-opacity: 1;
background-color: rgb(115 115 115 / var(--tw-bg-opacity));
}
.bg-slate-800 {
--tw-bg-opacity: 1;
background-color: rgb(30 41 59 / var(--tw-bg-opacity));
}
.p-2 {
padding: 0.5rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.font-medium {
font-weight: 500;
}
.font-semibold {
font-weight: 600;
}
.italic {
font-style: italic;
}
.text-gray-300 {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity));
}
.text-neutral-300 {
--tw-text-opacity: 1;
color: rgb(212 212 212 / var(--tw-text-opacity));
}
.text-neutral-400 {
--tw-text-opacity: 1;
color: rgb(163 163 163 / var(--tw-text-opacity));
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.underline {
text-decoration-line: underline;
}
.shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.ring {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.blur {
--tw-blur: blur(8px);
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.hover\:bg-cyan-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(14 116 144 / var(--tw-bg-opacity));
}
.hover\:bg-gray-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
}
.hover\:text-white:hover {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
@media (min-width: 640px) {
.sm\:ml-6 {
margin-left: 1.5rem;
}
.sm\:block {
display: block;
}
.sm\:items-stretch {
align-items: stretch;
}
.sm\:justify-start {
justify-content: flex-start;
}
.sm\:px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
}
@media (min-width: 1024px) {
.lg\:px-8 {
padding-left: 2rem;
padding-right: 2rem;
}
}

View file

@ -1,56 +0,0 @@
{
"name": "vcs-controller",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "vcs-controller",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@floating-ui/dom": "^1.6.11",
"@tauri-apps/api": "^1.6.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz",
"integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.8"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz",
"integrity": "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.8"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.8",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
"license": "MIT"
},
"node_modules/@tauri-apps/api": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.6.0.tgz",
"integrity": "sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg==",
"engines": {
"node": ">= 14.6.0",
"npm": ">= 6.6.0",
"yarn": ">= 1.19.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
}
}
}
}

View file

@ -1,15 +0,0 @@
{
"name": "vcs-controller",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@floating-ui/dom": "^1.6.11",
"@tauri-apps/api": "^1.6.0"
}
}

View file

@ -1,101 +0,0 @@
import { event } from "./node_modules/@tauri-apps/api/index.js";
async function rtc_init() {
const videoview = document.getElementById("remoteview");
const config = {
iceServers: [{ urls: "stun:localhost" }]
};
const polite = true;
const pc = new RTCPeerConnection(config);
window.pc = pc;
pc.ontrack = (e) => {
console.log(e);
e.track.onunmute = () => {
console.log("Unmuted?");
if (remoteview.srcObject) {
console.log("Skipping srcobject");
return;
}
videoview.srcObject = e.streams[0];
Object.assign(videoview.style, { "background-color": "black"});
};
};
let makingOffer = false;
pc.onnegotionationneeded = async () => {
try {
makingOffer = true;
await pc.setLocalDescription();
console.log("emitting response webrtc packet");
event.emit("webrtc-message", pc.localDescription );
} catch (err) {
console.error(err);
} finally {
makingOffer = false;
}
};
pc.onicecandidate = ({ candidate }) => {
console.log("emitting response webrtc packet");
event.emit("webrtc-message", candidate);
};
pc.oniceconnectionstatechange = () => {
console.log('ICE state: ',pc.iceConnectionState);
if (pc.iceConnectionState === "failed") {
pc.restartIce();
}
};
let ignoreOffer = false;
console.log("registering listener");
const application_message = await event.listen('frontend_message', async (msg) => {
const payload = JSON.parse(msg.payload);
console.log(payload);
try {
if (payload.type) {
const offerCollision =
payload.type === "offer" &&
(makingOffer || pc.signalingState !== "stable");
ignoreOffer = !polite && offerCollision;
if (ignoreOffer) {
return;
}
await pc.setRemoteDescription(payload);
if (payload.type === "offer") {
console.log("Settings local description");
await pc.setLocalDescription();
console.log("emitting response webrtc packet");
event.emit( "webrtc-message", pc.localDescription);
}
} else if (payload.candidate) {
try {
console.log("Adding trickle ICE candidate");
await pc.addIceCandidate(payload.candidate);
} catch (err) {
if (!ignoreOffer) {
throw err;
}
}
}
} catch (err) {
console.error(err);
}
});
}
export { rtc_init };