Skip to content
Draft

test #56

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,21 @@ jobs:
"libayatana-appindicator3-dev:${package_arch}" # tray icon
"libglib2.0-dev:${package_arch}"
"libgtk-3-dev:${package_arch}"
"libgstreamer1.0-dev:${package_arch}"
"libgstreamer-plugins-base1.0-dev:${package_arch}"
"gstreamer1.0-plugins-base:${package_arch}"
"gstreamer1.0-plugins-good:${package_arch}"
"gstreamer1.0-plugins-bad:${package_arch}"
"gstreamer1.0-plugins-ugly:${package_arch}"
"gstreamer1.0-libav:${package_arch}"
"libgstrtspserver-1.0-dev:${package_arch}"
"libges-1.0-dev:${package_arch}"
"libxdo-dev:${package_arch}"
"libx11-dev:${package_arch}" # X11 for remote desktop
"libxrandr-dev:${package_arch}" # RandR for monitor detection
"libxi-dev:${package_arch}" # X11 input
"libxcb1-dev:${package_arch}" # XCB for x11rb
"libxcb-randr0-dev:${package_arch}" # XCB RandR
)
echo "::endgroup::"

Expand Down
9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"crates/server",
"crates/client-desktop",
# "crates/common",
# "crates/clients-*",
]
Expand All @@ -17,14 +18,14 @@ repository = "https://github.com/LizardByte/Koko.git"
license = ""
version = "0.0.0"
categories = [
"multimedia",
"multimedia::audio",
"multimedia::images",
"network-programming",
"multimedia::video",
]
keywords = [
"koko",
"media-server",
"remote-desktop",
"streaming",
"low-latency",
"self-hosted",
]
publish = false # disable publishing to crates.io
Expand Down
Empty file added config.example.toml
Empty file.
53 changes: 53 additions & 0 deletions crates/client-desktop/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
[package]
name = "koko-client"
description = "Ultra-low latency remote desktop client."
license.workspace = true
version.workspace = true
authors.workspace = true
edition.workspace = true
readme.workspace = true
documentation.workspace = true
homepage.workspace = true
repository.workspace = true
publish.workspace = true

[[bin]]
name = "koko-client"
path = "src/main.rs"

[dependencies]
# UI Framework
eframe = "0.29"
egui = "0.29"
egui_extras = "0.29"

# Networking
tokio = { version = "1.0", features = ["full"] }
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
futures = "0.3"

# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# Video decoding
gstreamer = "0.23"
gstreamer-app = "0.23"
gstreamer-video = "0.23"

# Input handling
rdev = "0.5"

# Clipboard
arboard = "3.4"

# Authentication
base64 = "0.22"

# Logging
log = "0.4"
env_logger = "0.11"

# Utilities
anyhow = "1.0"

199 changes: 199 additions & 0 deletions crates/client-desktop/src/connection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
//! Connection management for WebSocket communication with server.

use anyhow::Result;
use futures::{SinkExt, StreamExt};
use log::{debug, error, info, warn};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::{RwLock, mpsc};
use tokio_tungstenite::{connect_async, tungstenite::Message};

/// Message types from server
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ServerMessage {
Frame {
data: Vec<u8>,
width: u32,
height: u32,
timestamp: u64,
monitor_index: usize,
},
Clipboard {
text: String,
timestamp: u64,
},
Monitors {
monitors: Vec<MonitorInfo>,
},
Error {
message: String,
},
}

/// Monitor information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonitorInfo {
pub index: usize,
pub name: String,
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
pub primary: bool,
}

/// Message types to server
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ClientMessage {
Input { event: InputEvent },
Clipboard { text: String },
GetMonitors,
}

/// Input event types
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum InputEvent {
Mouse(MouseEvent),
Keyboard(KeyEvent),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MouseEvent {
Move { x: i32, y: i32 },
ButtonDown { button: MouseButton },
ButtonUp { button: MouseButton },
Scroll { delta_x: i32, delta_y: i32 },
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum MouseButton {
Left,
Middle,
Right,
Button4,
Button5,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum KeyEvent {
KeyDown { code: u32, key: String },
KeyUp { code: u32, key: String },
}

/// Connection state
pub struct Connection {
sender: mpsc::UnboundedSender<ClientMessage>,
pub frame_rx: Arc<RwLock<mpsc::UnboundedReceiver<Vec<u8>>>>,
pub monitors: Arc<RwLock<Vec<MonitorInfo>>>,
pub clipboard_rx: Arc<RwLock<mpsc::UnboundedReceiver<String>>>,
}

impl Connection {
/// Connect to the server
pub async fn connect(
url: &str,
token: &str,
) -> Result<Self> {
let url_with_auth = format!("{}?token={}", url, token);
info!("Connecting to server: {}", url);

let (ws_stream, _) = connect_async(&url_with_auth).await?;
info!("WebSocket connection established");

let (write, mut read) = ws_stream.split();

// Channels for communication
let (client_tx, mut client_rx) = mpsc::unbounded_channel::<ClientMessage>();
let (frame_tx, frame_rx) = mpsc::unbounded_channel::<Vec<u8>>();
let (clipboard_tx, clipboard_rx) = mpsc::unbounded_channel::<String>();

let monitors = Arc::new(RwLock::new(Vec::new()));
let monitors_clone = monitors.clone();

// Spawn task to send messages to server
tokio::spawn(async move {
let mut write = write;
while let Some(msg) = client_rx.recv().await {
if let Ok(json) = serde_json::to_string(&msg) {
if write.send(Message::Text(json)).await.is_err() {
error!("Failed to send message to server");
break;
}
}
}
});

// Spawn task to receive messages from server
tokio::spawn(async move {
while let Some(msg) = read.next().await {
match msg {
Ok(Message::Text(text)) => {
if let Ok(server_msg) = serde_json::from_str::<ServerMessage>(&text) {
match server_msg {
ServerMessage::Frame { data, .. } => {
let _ = frame_tx.send(data);
}
ServerMessage::Clipboard { text, .. } => {
let _ = clipboard_tx.send(text);
}
ServerMessage::Monitors { monitors } => {
*monitors_clone.write().await = monitors;
info!(
"Received monitor list: {} monitors",
monitors_clone.read().await.len()
);
}
ServerMessage::Error { message } => {
error!("Server error: {}", message);
}
}
}
}
Ok(Message::Close(_)) => {
info!("Server closed connection");
break;
}
Err(e) => {
error!("WebSocket error: {}", e);
break;
}
_ => {}
}
}
});

Ok(Self {
sender: client_tx,
frame_rx: Arc::new(RwLock::new(frame_rx)),
monitors,
clipboard_rx: Arc::new(RwLock::new(clipboard_rx)),
})
}

/// Send input event to server
pub fn send_input(
&self,
event: InputEvent,
) -> Result<()> {
self.sender.send(ClientMessage::Input { event })?;
Ok(())
}

/// Send clipboard content to server
pub fn send_clipboard(
&self,
text: String,
) -> Result<()> {
self.sender.send(ClientMessage::Clipboard { text })?;
Ok(())
}

/// Request monitor list from server
pub fn request_monitors(&self) -> Result<()> {
self.sender.send(ClientMessage::GetMonitors)?;
Ok(())
}
}
1 change: 1 addition & 0 deletions crates/client-desktop/src/decoder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions crates/client-desktop/src/input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

30 changes: 30 additions & 0 deletions crates/client-desktop/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
mod connection;
mod decoder;
mod input;
mod ui;

use anyhow::Result;
use log::info;

#[tokio::main]
async fn main() -> Result<()> {
// Initialize logging
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();

info!("Starting Koko Remote Desktop Client");

// Run the UI
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([1280.0, 720.0])
.with_title("Koko Remote Desktop Client"),
..Default::default()
};

eframe::run_native(
"Koko Client",
native_options,
Box::new(|cc| Ok(Box::new(ui::ClientApp::new(cc)))),
)
.map_err(|e| anyhow::anyhow!("Failed to run client: {}", e))
}
1 change: 1 addition & 0 deletions crates/client-desktop/src/ui.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

22 changes: 17 additions & 5 deletions crates/server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "koko"
description = "Self-hosted media server."
description = "Ultra-low latency remote desktop server."
license.workspace = true
version.workspace = true
authors.workspace = true
Expand All @@ -14,14 +14,14 @@ build = "../../build.rs"

categories = [
"multimedia",
"multimedia::audio",
"multimedia::images",
"multimedia::video",
"network-programming",
]
keywords = [
"koko",
"media-server",
"self-hosted",
"remote-desktop",
"streaming",
"low-latency",
]

include = [
Expand Down Expand Up @@ -74,6 +74,18 @@ tray-icon = "0.21.1"
webbrowser = "1.0.3"
# common = { path = "../common" }

[target.'cfg(target_os = "linux")'.dependencies]
# Remote Desktop Dependencies (Linux only)
gstreamer = "0.23"
gstreamer-app = "0.23"
gstreamer-video = "0.23"
x11rb = { version = "0.13", features = ["all-extensions"] }
xcb = "1.4"
rdev = "0.5"
arboard = "3.4"
futures = "0.3"
rocket_ws = "0.1.1"

[target.'cfg(target_os = "macos")'.dependencies]
objc2-core-foundation = "0.3.0"

Expand Down
Loading
Loading