diff --git a/build.rs b/build.rs index c5ecafe..4a5a99e 100644 --- a/build.rs +++ b/build.rs @@ -44,6 +44,10 @@ fn main() { panic!("Running `trunk build` failed, reason {err}"); } + if let Ok(trunk_output_string) = String::from_utf8(trunk_build_output.stdout) { + println!("{trunk_output_string}"); + } + let dist_paths = fs::read_dir("./frontend/dist").expect("Could not read dist directory"); let paths: Vec> = dist_paths.collect(); let paths: Vec<(String, PathBuf)> = paths diff --git a/frontend/src/app.rs b/frontend/src/app.rs index 7afc5f5..f7ea3b1 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -1,17 +1,16 @@ use std::{ - collections::VecDeque, - fmt::Display, + collections::{HashMap, VecDeque}, mem, net::{IpAddr, Ipv4Addr}, }; -use egui::{Button, Color32, RichText, Sense, Stroke, Vec2}; +use egui::{Button, Color32, RichText, Rounding, Sense, Stroke, Vec2}; use ewebsock::{Options, WsReceiver, WsSender}; use wasm_timer::Instant; use crate::websocket::{ - AddHyperdeckRequest, ClientRequest, HyperdeckConnectionState, RemoveHyperdeckRequest, - ServerEvent, + AddHyperdeckRequest, ClientRequest, HyperdeckConnectionState, HyperdeckRecordBay, + RecordingState, RemoveHyperdeckRequest, ServerEvent, }; pub struct HyperdeckMonitorApp { @@ -37,28 +36,69 @@ impl Default for HyperdeckMonitorApp { new_hyperdeck_name: "".to_owned(), new_hyperdeck_port: 9993.to_string(), hyperdecks: vec![ - // Hyperdeck { - // id: "test-1".to_string(), - // name: "Test Hyperdeck 1".to_string(), - // ip: IpAddr::V4(Ipv4Addr::new(192, 168, 10, 1)), - // status: HyperdeckStatus::Connected, - // recording_bays: vec![HyperdeckRecordBay { - // status: RecordingStatus::NotRecording, - // storage_capacity_mb: 500_000, - // recording_time_remaining: TimeRemaining(60), - // }], - // }, - // Hyperdeck { - // id: "test-2".to_string(), - // name: "Test Hyperdeck 2".to_string(), - // ip: IpAddr::V4(Ipv4Addr::new(192, 168, 10, 2)), - // status: HyperdeckStatus::Disconnected, - // recording_bays: vec![HyperdeckRecordBay { - // status: RecordingStatus::NotRecording, - // storage_capacity_mb: 500_000, - // recording_time_remaining: TimeRemaining(3600 * 5), // 5 Hours - // }], - // }, + Hyperdeck { + id: "test-1".to_string(), + name: "Description: Connected Hyperdeck - Not Recording - Not Much Time" + .to_string(), + ip: IpAddr::V4(Ipv4Addr::new(192, 168, 10, 1)), + status: HyperdeckStatus::Connected, + recording_status: RecordingState::NotRecording, + slots: vec![( + 0usize, + HyperdeckRecordBay { + recording_time_remaining: 60, + }, + )] + .into_iter() + .collect::>(), + }, + Hyperdeck { + id: "test-2".to_string(), + name: "Description: Connected Hyperdeck - Recording - Not Much Time" + .to_string(), + ip: IpAddr::V4(Ipv4Addr::new(192, 168, 10, 2)), + status: HyperdeckStatus::Connected, + recording_status: RecordingState::Recording, + slots: vec![( + 0usize, + HyperdeckRecordBay { + recording_time_remaining: 60, + }, + )] + .into_iter() + .collect::>(), + }, + Hyperdeck { + id: "test-3".to_string(), + name: "Description: Connected Hyperdeck - Recording - Plenty of Time" + .to_string(), + ip: IpAddr::V4(Ipv4Addr::new(192, 168, 10, 3)), + status: HyperdeckStatus::Connected, + recording_status: RecordingState::Recording, + slots: vec![( + 0usize, + HyperdeckRecordBay { + recording_time_remaining: 60 * 30, + }, + )] + .into_iter() + .collect::>(), + }, + Hyperdeck { + id: "test-4".to_string(), + name: "Description: Disconnected Hyperdeck".to_string(), + ip: IpAddr::V4(Ipv4Addr::new(192, 168, 10, 4)), + status: HyperdeckStatus::Disconnected, + recording_status: RecordingState::NotRecording, + slots: vec![( + 0usize, + HyperdeckRecordBay { + recording_time_remaining: 3600 * 5, // 5 Hours + }, + )] + .into_iter() + .collect::>(), + }, ], websocket_message_queue: VecDeque::new(), ws_sender, @@ -87,18 +127,14 @@ impl eframe::App for HyperdeckMonitorApp { name: hyperdeck.name, ip: hyperdeck.ip.parse().unwrap(), status: hyperdeck.connection_state.into(), - recording_bays: vec![], + recording_status: hyperdeck.recording_status, + slots: hyperdeck.slots, }) } } } } } - egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { - egui::menu::bar(ui, |ui| { - egui::widgets::global_dark_light_mode_buttons(ui); - }); - }); egui::CentralPanel::default().show(ctx, |ui| { add_hyperdeck_panel( @@ -118,11 +154,6 @@ impl eframe::App for HyperdeckMonitorApp { &mut self.websocket_message_queue, ); }); - - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { - connection_status(ui); - egui::warn_if_debug_build(ui); - }); }); if self.last_blink_change.elapsed().as_secs() >= 1 { @@ -195,27 +226,41 @@ fn hyperdeck_list( )); } }); - if !hyperdeck.recording_bays.is_empty() - && matches!(hyperdeck.status, HyperdeckStatus::Connected) - { - let recording_bays_text: RichText = "Recording Bays".into(); - ui.label(recording_bays_text.size(16.0).strong()); - for (index, bay) in hyperdeck.recording_bays.iter().enumerate() { - ui.horizontal(|ui| { - let bay_label: RichText = format!("Bay {}", index + 1).into(); - ui.label(bay_label.strong()); - match bay.status { - RecordingStatus::Recording => ui.label("Recording"), - RecordingStatus::NotRecording => ui.label("Not Recording"), - }; - ui.label(format!( - "Total Storage Capacity: {}GB", - bay.storage_capacity_mb / 1000, - )); - let time_remaining_text: RichText = - format!("Time remaining: {}", bay.recording_time_remaining).into(); + if matches!(hyperdeck.status, HyperdeckStatus::Connected) { + ui.horizontal(|ui| { + match hyperdeck.recording_status { + RecordingState::Recording => { + let (response, painter) = + ui.allocate_painter(Vec2 { x: 16.0, y: 16.0 }, Sense::hover()); + let rect = response.rect; + painter.rect( + rect, + Rounding::ZERO, + Color32::from_rgb(255, 255, 255), + Stroke::NONE, + ); + let recording_text: RichText = "[Recording]".into(); + ui.label( + recording_text + .color(Color32::from_rgb(255, 255, 255)) + .strong(), + ); + } + RecordingState::NotRecording => { + ui.label("[Not Recording]"); + } + }; + }); - if bay.recording_time_remaining.0 > 15 * 60 || !blink { + for (index, slot) in hyperdeck.slots.iter() { + ui.horizontal(|ui| { + let slot_label: RichText = format!("Slot {}", index + 1).into(); + ui.label(slot_label.strong()); + + let time_remaining_text: RichText = + format!("Time remaining: {}", slot.recording_time_remaining).into(); + + if slot.recording_time_remaining > 15 * 60 || !blink { ui.label(time_remaining_text); } else { ui.label(time_remaining_text.color(Color32::RED)); @@ -228,20 +273,14 @@ fn hyperdeck_list( } } -fn connection_status(ui: &mut egui::Ui) { - ui.horizontal(|ui| { - // TODO: Make it real - ui.label("Connected"); - }); -} - #[derive(serde::Deserialize, serde::Serialize)] struct Hyperdeck { id: String, name: String, ip: IpAddr, status: HyperdeckStatus, - recording_bays: Vec, + recording_status: RecordingState, + slots: HashMap, } #[derive(serde::Deserialize, serde::Serialize)] @@ -258,28 +297,3 @@ impl From for HyperdeckStatus { } } } - -#[derive(serde::Deserialize, serde::Serialize)] -struct HyperdeckRecordBay { - status: RecordingStatus, - /// Storage capacity in MB. - storage_capacity_mb: u64, - /// Recording time available in seconds. - recording_time_remaining: TimeRemaining, -} - -#[derive(serde::Deserialize, serde::Serialize)] -struct TimeRemaining(u64); - -impl Display for TimeRemaining { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let time = hrtime::from_sec_padded(self.0); - write!(f, "{time}") - } -} - -#[derive(serde::Deserialize, serde::Serialize)] -enum RecordingStatus { - Recording, - NotRecording, -} diff --git a/index.ts b/index.ts index 25be9b5..80918a4 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,6 @@ import { Hyperdeck, Commands } from 'hyperdeck-connection'; import WebSocket from 'ws'; +import { v4 as uuidv4 } from 'uuid'; interface WrappedHyperdeck { ip: String, @@ -26,8 +27,11 @@ type WebSocketMessage = { const wss = new WebSocket.Server({ port: 7867 }); -wss.on('connection', function connection(ws) { - ws.on('message', function message(data) { +const connected_clients: Map = new Map(); + +wss.on('connection', (ws) => { + const clientId = uuidv4(); + ws.on('message', (data) => { try { const message = JSON.parse(data.toString()) as Partial; handle_message(message) @@ -35,11 +39,16 @@ wss.on('connection', function connection(ws) { return; } }); + ws.on('close', () => { + connected_clients.delete(clientId) + }) ws.send(JSON.stringify({ event: "log", message: "Hello" })); + + connected_clients.set(clientId, ws); }); function exhaustiveMatch(_never: never) { @@ -68,24 +77,64 @@ function handle_message(message: Partial) { hyperdeck: newHyperdeck }); - newHyperdeck.on('connected', (info) => { - console.log(JSON.stringify(info)) + newHyperdeck.on('connected', (_info) => { + notifyClients({ + event: "hyperdeck_connected", + id: message.id + }) - newHyperdeck.sendCommand(new Commands.TransportInfoCommand()).then((transportInfo) => { - console.log(JSON.stringify(transportInfo)) + setInterval(() => { + newHyperdeck.sendCommand(new Commands.TransportInfoCommand()).then((transportInfo) => { + notifyClients({ + event: "record_state", + hyperdeckId: message.id, + state: transportInfo.status, + }) + }) + }) + + newHyperdeck.sendCommand(new Commands.DeviceInfoCommand()).then((info) => { + for (let index = 0; index < info.slots; index++) { + setInterval(() => { + newHyperdeck.sendCommand(new Commands.SlotInfoCommand(index)).then((slot) => { + notifyClients({ + event: "record_time_remaining", + hyperdeckId: message.id, + slotId: slot.slotId, + remaining: slot.recordingTime + }) + }) + }) + } }) }) - newHyperdeck.on('notify.slot', function (state) { - console.log(JSON.stringify(state)) // catch the slot state change. + newHyperdeck.on('notify.slot', function (slot) { + notifyClients({ + event: "record_time_remaining", + hyperdeckId: message.id, + slotId: slot.slotId, + remaining: slot.recordingTime + }) }) newHyperdeck.on('notify.transport', function (state) { - console.log(JSON.stringify(state)) // catch the transport state change. + notifyClients({ + event: "record_state", + hyperdeckId: message.id, + state: state.status + }) }) newHyperdeck.on('error', (err) => { console.log('Hyperdeck error', JSON.stringify(err)) }) + newHyperdeck.on('disconnected', () => { + notifyClients({ + event: "hyperdeck_disconnected", + id: message.id + }) + }) + newHyperdeck.connect(message.ip, message.port) break; @@ -105,3 +154,9 @@ function handle_message(message: Partial) { exhaustiveMatch(message) } } + +function notifyClients(message: object) { + connected_clients.forEach((client) => { + client.send(JSON.stringify(message)) + }) +} diff --git a/package-lock.json b/package-lock.json index dd7cdff..ddb5a59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "hyperdeck-connection": "^2.0.1", + "uuid": "^9.0.1", "ws": "^8.17.0" }, "devDependencies": { @@ -75,6 +76,18 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/ws": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", diff --git a/package.json b/package.json index 2bc9b4e..5857ebc 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ }, "dependencies": { "hyperdeck-connection": "^2.0.1", + "uuid": "^9.0.1", "ws": "^8.17.0" }, "devDependencies": { diff --git a/src/api.rs b/src/api.rs index 607d1f2..17dcee1 100644 --- a/src/api.rs +++ b/src/api.rs @@ -4,7 +4,6 @@ use axum::response::Html; use axum::Json; use axum::{ body::Bytes, - extract::Path, http::{header, HeaderValue, Method}, response::IntoResponse, routing::get, @@ -66,7 +65,7 @@ pub async fn initialize_api( let clients = state_clients.lock().await; let state_json = serde_json::to_string(&ServerEvent::HyperdeckMonitorState( - hyperdeck_monitor_state.into(), + hyperdeck_monitor_state, )) .unwrap(); for (_, client) in clients.iter() { diff --git a/src/api/message.rs b/src/api/message.rs index b2a8a78..cd288bd 100644 --- a/src/api/message.rs +++ b/src/api/message.rs @@ -40,6 +40,9 @@ pub struct HyperdeckState { pub ip: String, pub port: u16, pub connection_state: HyperdeckConnectionState, + pub recording_status: RecordingState, + // HashMap to allow for sparse entries. + pub slots: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -47,3 +50,15 @@ pub enum HyperdeckConnectionState { Connected, Disconnected, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum RecordingState { + Recording, + NotRecording, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HyperdeckRecordBay { + /// Recording time available in seconds. + pub recording_time_remaining: u64, +} diff --git a/src/api/ws.rs b/src/api/ws.rs index 408b6db..bb3a5a3 100644 --- a/src/api/ws.rs +++ b/src/api/ws.rs @@ -32,7 +32,7 @@ pub async fn client_connection( let current_state = state.read().await.clone(); let state_json = - serde_json::to_string(&ServerEvent::HyperdeckMonitorState(current_state.into())).unwrap(); + serde_json::to_string(&ServerEvent::HyperdeckMonitorState(current_state)).unwrap(); client_sender.send(Message::Text(state_json.clone())).ok(); client.sender = Some(client_sender); diff --git a/src/main.rs b/src/main.rs index 7403078..955ad13 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ -use std::{process::Stdio, time::Duration}; +use std::{collections::HashMap, process::Stdio, time::Duration}; use api::message::{ AddHyperdeckRequest, ClientRequest, HyperdeckConnectionState, HyperdeckMonitorState, - HyperdeckState, RemoveHyperdeckRequest, + HyperdeckRecordBay, HyperdeckState, RecordingState, RemoveHyperdeckRequest, }; use color_eyre::Report; use futures_util::{ @@ -31,9 +31,8 @@ async fn main() { let (node_ws_message_tx, node_ws_message_rx) = tokio::sync::mpsc::unbounded_channel(); let (node_commands_tx, node_commands_rx) = tokio::sync::mpsc::unbounded_channel(); - let state = AppState::default(); let node_ws_communication = - talk_to_node_ws(state, node_ws_message_tx, node_commands_rx, cancel.clone()).fuse(); + talk_to_node_ws(node_ws_message_tx, node_commands_rx, cancel.clone()).fuse(); let (state_tx, state_rx) = tokio::sync::broadcast::channel(1); let (client_request_tx, client_request_rx) = tokio::sync::mpsc::unbounded_channel(); @@ -67,7 +66,7 @@ async fn main() { async fn run( mut node_commands_tx: tokio::sync::mpsc::UnboundedSender, mut node_ws_message_rx: tokio::sync::mpsc::UnboundedReceiver, - mut state_tx: tokio::sync::broadcast::Sender, + state_tx: tokio::sync::broadcast::Sender, mut client_request_rx: tokio::sync::mpsc::UnboundedReceiver, cancel: CancellationToken, ) { @@ -100,7 +99,7 @@ async fn run( async fn handle_message_from_node( msg: NodeWsMessageReceived, - node_commands_tx: &mut tokio::sync::mpsc::UnboundedSender, + _node_commands_tx: &mut tokio::sync::mpsc::UnboundedSender, state: &mut HyperdeckMonitorState, ) -> bool { match msg { @@ -120,6 +119,38 @@ async fn handle_message_from_node( }); true } + NodeWsMessageReceived::RecordState { + hyperdeck_id, + status, + } => { + if let Some(hyperdeck) = state.hyperdecks.get_mut(&hyperdeck_id) { + hyperdeck.recording_status = if matches!(status, TransportStatus::Record) { + RecordingState::Recording + } else { + RecordingState::NotRecording + }; + true + } else { + false + } + } + NodeWsMessageReceived::RecordTimeRemaining { + hyperdeck_id, + slot_id, + remaining, + } => { + if let Some(hyperdeck) = state.hyperdecks.get_mut(&hyperdeck_id) { + hyperdeck + .slots + .entry(slot_id) + .or_insert(HyperdeckRecordBay { + recording_time_remaining: remaining, + }); + true + } else { + false + } + } } } @@ -139,6 +170,8 @@ async fn handle_message_from_client( ip: ip.clone(), port, connection_state: api::message::HyperdeckConnectionState::Disconnected, + recording_status: api::message::RecordingState::NotRecording, + slots: HashMap::new(), }, ); let _ = node_commands_tx.send(NodeWsCommand::AddHyperdeck(AddHyperdeckCommand { @@ -210,9 +243,6 @@ async fn run_node_process(cancel: CancellationToken) { } } -#[derive(Default)] -struct AppState {} - #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "type")] enum NodeWsCommand { @@ -223,12 +253,40 @@ enum NodeWsCommand { } #[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "snake_case")] #[serde(tag = "event")] enum NodeWsMessageReceived { - Log { message: String }, - HyperdeckConnected { id: String }, - HypderdeckDisconnected { id: String }, + Log { + message: String, + }, + HyperdeckConnected { + id: String, + }, + HypderdeckDisconnected { + id: String, + }, + RecordState { + hyperdeck_id: String, + status: TransportStatus, + }, + RecordTimeRemaining { + hyperdeck_id: String, + slot_id: usize, + remaining: u64, + }, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +enum TransportStatus { + Preview, + Stopped, + Play, + Forward, + Rewind, + Jog, + Shuttle, + Record, } #[derive(Debug, Serialize, Deserialize)] @@ -246,7 +304,6 @@ struct RemoveHyperdeckCommand { } async fn talk_to_node_ws( - state: AppState, ws_message_tx: tokio::sync::mpsc::UnboundedSender, commands_rx: tokio::sync::mpsc::UnboundedReceiver, cancel: CancellationToken,