- Rename display param to target_display to avoid tracing conflict - Refactor session loop to avoid holding borrow across handle_message
207 lines
6.8 KiB
Rust
207 lines
6.8 KiB
Rust
//! Session management for the agent
|
|
//!
|
|
//! Handles the lifecycle of a remote session including:
|
|
//! - Connection to server
|
|
//! - Authentication
|
|
//! - Frame capture and encoding loop
|
|
//! - Input event handling
|
|
|
|
use crate::capture::{self, Capturer, Display};
|
|
use crate::config::Config;
|
|
use crate::encoder::{self, Encoder};
|
|
use crate::input::InputController;
|
|
use crate::proto::{Message, message};
|
|
use crate::transport::WebSocketTransport;
|
|
use anyhow::Result;
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, Instant};
|
|
use tokio::sync::mpsc;
|
|
|
|
/// Session manager handles the remote control session
|
|
pub struct SessionManager {
|
|
config: Config,
|
|
transport: Option<WebSocketTransport>,
|
|
state: SessionState,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
enum SessionState {
|
|
Disconnected,
|
|
Connecting,
|
|
Connected,
|
|
Active,
|
|
}
|
|
|
|
impl SessionManager {
|
|
/// Create a new session manager
|
|
pub fn new(config: Config) -> Self {
|
|
Self {
|
|
config,
|
|
transport: None,
|
|
state: SessionState::Disconnected,
|
|
}
|
|
}
|
|
|
|
/// Connect to the server
|
|
pub async fn connect(&mut self) -> Result<()> {
|
|
self.state = SessionState::Connecting;
|
|
|
|
let transport = WebSocketTransport::connect(
|
|
&self.config.server_url,
|
|
&self.config.api_key,
|
|
).await?;
|
|
|
|
self.transport = Some(transport);
|
|
self.state = SessionState::Connected;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Run the session main loop
|
|
pub async fn run(&mut self) -> Result<()> {
|
|
if self.transport.is_none() {
|
|
anyhow::bail!("Not connected");
|
|
}
|
|
|
|
self.state = SessionState::Active;
|
|
|
|
// Get primary display
|
|
let primary_display = capture::primary_display()?;
|
|
tracing::info!("Using display: {} ({}x{})", primary_display.name, primary_display.width, primary_display.height);
|
|
|
|
// Create capturer
|
|
let mut capturer = capture::create_capturer(
|
|
primary_display.clone(),
|
|
self.config.capture.use_dxgi,
|
|
self.config.capture.gdi_fallback,
|
|
)?;
|
|
|
|
// Create encoder
|
|
let mut encoder = encoder::create_encoder(
|
|
&self.config.encoding.codec,
|
|
self.config.encoding.quality,
|
|
)?;
|
|
|
|
// Create input controller
|
|
let mut input = InputController::new()?;
|
|
|
|
// Calculate frame interval
|
|
let frame_interval = Duration::from_millis(1000 / self.config.capture.fps as u64);
|
|
let mut last_frame_time = Instant::now();
|
|
|
|
// Main loop
|
|
loop {
|
|
// Check for incoming messages (non-blocking)
|
|
// Collect messages first, then release borrow before handling
|
|
let messages: Vec<Message> = {
|
|
let transport = self.transport.as_mut().unwrap();
|
|
let mut msgs = Vec::new();
|
|
while let Some(msg) = transport.try_recv()? {
|
|
msgs.push(msg);
|
|
}
|
|
msgs
|
|
};
|
|
|
|
for msg in messages {
|
|
self.handle_message(&mut input, msg)?;
|
|
}
|
|
|
|
// Capture and send frame if interval elapsed
|
|
if last_frame_time.elapsed() >= frame_interval {
|
|
last_frame_time = Instant::now();
|
|
|
|
if let Some(frame) = capturer.capture()? {
|
|
let encoded = encoder.encode(&frame)?;
|
|
|
|
// Skip empty frames (no changes)
|
|
if encoded.size > 0 {
|
|
let msg = Message {
|
|
payload: Some(message::Payload::VideoFrame(encoded.frame)),
|
|
};
|
|
let transport = self.transport.as_mut().unwrap();
|
|
transport.send(msg).await?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Small sleep to prevent busy loop
|
|
tokio::time::sleep(Duration::from_millis(1)).await;
|
|
|
|
// Check if still connected
|
|
if !transport.is_connected() {
|
|
tracing::warn!("Connection lost");
|
|
break;
|
|
}
|
|
}
|
|
|
|
self.state = SessionState::Disconnected;
|
|
Ok(())
|
|
}
|
|
|
|
/// Handle incoming message from server
|
|
fn handle_message(&mut self, input: &mut InputController, msg: Message) -> Result<()> {
|
|
match msg.payload {
|
|
Some(message::Payload::MouseEvent(mouse)) => {
|
|
// Handle mouse event
|
|
use crate::proto::MouseEventType;
|
|
use crate::input::MouseButton;
|
|
|
|
match MouseEventType::try_from(mouse.event_type).unwrap_or(MouseEventType::MouseMove) {
|
|
MouseEventType::MouseMove => {
|
|
input.mouse_move(mouse.x, mouse.y)?;
|
|
}
|
|
MouseEventType::MouseDown => {
|
|
input.mouse_move(mouse.x, mouse.y)?;
|
|
if let Some(ref buttons) = mouse.buttons {
|
|
if buttons.left { input.mouse_click(MouseButton::Left, true)?; }
|
|
if buttons.right { input.mouse_click(MouseButton::Right, true)?; }
|
|
if buttons.middle { input.mouse_click(MouseButton::Middle, true)?; }
|
|
}
|
|
}
|
|
MouseEventType::MouseUp => {
|
|
if let Some(ref buttons) = mouse.buttons {
|
|
if buttons.left { input.mouse_click(MouseButton::Left, false)?; }
|
|
if buttons.right { input.mouse_click(MouseButton::Right, false)?; }
|
|
if buttons.middle { input.mouse_click(MouseButton::Middle, false)?; }
|
|
}
|
|
}
|
|
MouseEventType::MouseWheel => {
|
|
input.mouse_scroll(mouse.wheel_delta_x, mouse.wheel_delta_y)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Some(message::Payload::KeyEvent(key)) => {
|
|
// Handle keyboard event
|
|
input.key_event(key.vk_code as u16, key.down)?;
|
|
}
|
|
|
|
Some(message::Payload::SpecialKey(special)) => {
|
|
use crate::proto::SpecialKey;
|
|
match SpecialKey::try_from(special.key).ok() {
|
|
Some(SpecialKey::CtrlAltDel) => {
|
|
input.send_ctrl_alt_del()?;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
Some(message::Payload::Heartbeat(_)) => {
|
|
// Respond to heartbeat
|
|
// TODO: Send heartbeat ack
|
|
}
|
|
|
|
Some(message::Payload::Disconnect(disc)) => {
|
|
tracing::info!("Disconnect requested: {}", disc.reason);
|
|
return Err(anyhow::anyhow!("Disconnect: {}", disc.reason));
|
|
}
|
|
|
|
_ => {
|
|
// Ignore unknown messages
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|