481 lines
17 KiB
Rust

use super::{
DEFAULT_EYE_SOURCE_HEIGHT, DEFAULT_EYE_SOURCE_WIDTH, INLINE_PREVIEW_MAX_KBIT,
INLINE_PREVIEW_REQUEST_FPS, INLINE_PREVIEW_REQUEST_HEIGHT, INLINE_PREVIEW_REQUEST_WIDTH,
LauncherPreview, PREVIEW_HEIGHT, PREVIEW_WIDTH, PreviewSurface, PreviewTelemetry,
preview_render_size, sanitize_preview_request,
};
use crate::launcher::state::{CaptureSizePreset, LauncherState};
use futures::stream;
use lesavka_common::lesavka::relay_server::{Relay, RelayServer};
use lesavka_common::lesavka::{MonitorRequest, VideoPacket};
use serial_test::serial;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use tonic::{Request, Response, Status};
#[derive(Clone, Default)]
struct ProbeRelay {
requests: Arc<Mutex<Vec<MonitorRequest>>>,
}
#[tonic::async_trait]
impl Relay for ProbeRelay {
type StreamKeyboardStream = Pin<
Box<
dyn futures::Stream<Item = Result<lesavka_common::lesavka::KeyboardReport, Status>>
+ Send,
>,
>;
type StreamMouseStream = Pin<
Box<
dyn futures::Stream<Item = Result<lesavka_common::lesavka::MouseReport, Status>> + Send,
>,
>;
type CaptureVideoStream =
Pin<Box<dyn futures::Stream<Item = Result<VideoPacket, Status>> + Send>>;
type CaptureAudioStream = Pin<
Box<
dyn futures::Stream<Item = Result<lesavka_common::lesavka::AudioPacket, Status>> + Send,
>,
>;
type StreamMicrophoneStream =
Pin<Box<dyn futures::Stream<Item = Result<lesavka_common::lesavka::Empty, Status>> + Send>>;
type StreamCameraStream =
Pin<Box<dyn futures::Stream<Item = Result<lesavka_common::lesavka::Empty, Status>> + Send>>;
async fn stream_keyboard(
&self,
_request: Request<tonic::Streaming<lesavka_common::lesavka::KeyboardReport>>,
) -> Result<Response<Self::StreamKeyboardStream>, Status> {
Ok(Response::new(Box::pin(stream::empty())))
}
async fn stream_mouse(
&self,
_request: Request<tonic::Streaming<lesavka_common::lesavka::MouseReport>>,
) -> Result<Response<Self::StreamMouseStream>, Status> {
Ok(Response::new(Box::pin(stream::empty())))
}
async fn capture_video(
&self,
request: Request<MonitorRequest>,
) -> Result<Response<Self::CaptureVideoStream>, Status> {
self.requests.lock().unwrap().push(request.into_inner());
let (_tx, rx) = mpsc::channel(1);
Ok(Response::new(Box::pin(ReceiverStream::new(rx))))
}
async fn capture_audio(
&self,
_request: Request<MonitorRequest>,
) -> Result<Response<Self::CaptureAudioStream>, Status> {
Ok(Response::new(Box::pin(stream::empty())))
}
async fn stream_microphone(
&self,
_request: Request<tonic::Streaming<lesavka_common::lesavka::AudioPacket>>,
) -> Result<Response<Self::StreamMicrophoneStream>, Status> {
Ok(Response::new(Box::pin(stream::empty())))
}
async fn stream_camera(
&self,
_request: Request<tonic::Streaming<VideoPacket>>,
) -> Result<Response<Self::StreamCameraStream>, Status> {
Ok(Response::new(Box::pin(stream::empty())))
}
async fn paste_text(
&self,
_request: Request<lesavka_common::lesavka::PasteRequest>,
) -> Result<Response<lesavka_common::lesavka::PasteReply>, Status> {
Ok(Response::new(lesavka_common::lesavka::PasteReply {
ok: true,
error: String::new(),
}))
}
async fn reset_usb(
&self,
_request: Request<lesavka_common::lesavka::Empty>,
) -> Result<Response<lesavka_common::lesavka::ResetUsbReply>, Status> {
Ok(Response::new(lesavka_common::lesavka::ResetUsbReply {
ok: true,
}))
}
async fn get_capture_power(
&self,
_request: Request<lesavka_common::lesavka::Empty>,
) -> Result<Response<lesavka_common::lesavka::CapturePowerState>, Status> {
Ok(Response::new(lesavka_common::lesavka::CapturePowerState {
available: true,
enabled: true,
unit: "relay.service".to_string(),
detail: "active/running".to_string(),
active_leases: 1,
mode: "auto".to_string(),
detected_devices: 2,
}))
}
async fn set_capture_power(
&self,
_request: Request<lesavka_common::lesavka::SetCapturePowerRequest>,
) -> Result<Response<lesavka_common::lesavka::CapturePowerState>, Status> {
self.get_capture_power(Request::new(lesavka_common::lesavka::Empty {}))
.await
}
}
#[test]
fn inline_preview_profile_uses_lightweight_defaults() {
let profile = PreviewSurface::Inline.profile();
assert_eq!(profile.display_width, PREVIEW_WIDTH);
assert_eq!(profile.display_height, PREVIEW_HEIGHT);
assert_eq!(profile.requested_width, INLINE_PREVIEW_REQUEST_WIDTH);
assert_eq!(profile.requested_height, INLINE_PREVIEW_REQUEST_HEIGHT);
assert_eq!(profile.requested_fps, INLINE_PREVIEW_REQUEST_FPS);
assert_eq!(profile.max_bitrate_kbit, INLINE_PREVIEW_MAX_KBIT);
}
#[test]
fn breakout_preview_profile_defaults_to_higher_quality() {
let profile = PreviewSurface::Window.profile();
assert_eq!(profile.display_width, 1280);
assert_eq!(profile.display_height, 720);
assert_eq!(profile.requested_width, DEFAULT_EYE_SOURCE_WIDTH);
assert_eq!(profile.requested_height, DEFAULT_EYE_SOURCE_HEIGHT);
assert_eq!(profile.requested_fps, 60);
assert_eq!(profile.max_bitrate_kbit, 18_000);
}
#[test]
fn preview_render_size_fits_source_into_display_budget() {
let profile = PreviewSurface::Inline.profile();
assert_eq!(preview_render_size(profile, 1920, 1080), (960, 540));
}
#[test]
fn preview_render_size_never_upscales_beyond_source_geometry() {
let profile = PreviewSurface::Window.profile();
assert_eq!(preview_render_size(profile, 1280, 720), (1280, 720));
}
#[test]
fn preview_request_sanitizer_keeps_requested_source_geometry() {
let adapted = sanitize_preview_request(1920, 1080, 60, 18_000);
assert_eq!(adapted, (1920, 1080, 60, 18_000));
}
#[test]
fn preview_request_sanitizer_clamps_invalid_values() {
let adapted = sanitize_preview_request(0, 0, 0, 0);
assert_eq!(adapted, (2, 2, 1, 800));
}
#[test]
fn preview_telemetry_reports_fps_jitter_loss_and_drop_metrics() {
let mut telemetry = PreviewTelemetry::default();
let start = Instant::now();
telemetry.note_decoder("nvh264dec");
telemetry.record_packet_at(start, 1, 30, 0, 1, 41, 38, 2, "x264enc", 215);
telemetry.record_presented_frame_at(start + Duration::from_millis(5));
telemetry.record_packet_at(
start + Duration::from_millis(33),
2,
30,
0,
1,
41,
38,
2,
"x264enc",
215,
);
telemetry.record_presented_frame_at(start + Duration::from_millis(37));
telemetry.record_packet_at(
start + Duration::from_millis(80),
4,
27,
2,
3,
77,
88,
4,
"x264enc",
382,
);
telemetry.record_presented_frame_at(start + Duration::from_millis(90));
let snapshot = telemetry.snapshot_at(start + Duration::from_millis(120));
assert!(snapshot.receive_fps >= 12.0);
assert!(snapshot.present_fps >= 12.0);
assert_eq!(snapshot.server_fps, 27.0);
assert!(snapshot.stream_spread_ms > 0.0);
assert!(snapshot.packet_loss_pct > 0.0);
assert_eq!(snapshot.dropped_frames, 2);
assert_eq!(snapshot.queue_depth, 3);
assert_eq!(snapshot.queue_depth_peak, 3);
assert!(snapshot.packet_gap_peak_ms >= 47.0);
assert!(snapshot.present_gap_peak_ms >= 53.0);
assert_eq!(snapshot.server_source_gap_peak_ms, 77.0);
assert_eq!(snapshot.server_send_gap_peak_ms, 88.0);
assert_eq!(snapshot.server_queue_peak, 4);
assert_eq!(snapshot.server_process_cpu_pct, 38.2);
assert_eq!(snapshot.server_encoder_label, "x264enc");
assert_eq!(snapshot.decoder_label, "nvh264dec");
}
#[test]
#[serial]
fn inline_preview_requests_selected_source_profile_on_wire() {
let relay = ProbeRelay::default();
let requests = relay.requests.clone();
let rt = tokio::runtime::Runtime::new().expect("runtime");
let addr = rt.block_on(async move {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
drop(listener);
tokio::spawn(async move {
let _ = tonic::transport::Server::builder()
.add_service(RelayServer::new(relay))
.serve(addr)
.await;
});
addr
});
let preview = LauncherPreview::new(format!("http://{addr}")).expect("preview");
let state = LauncherState::default();
let capture = state.capture_size_choice(1);
preview.set_capture_profile(
1,
1,
capture.width,
capture.height,
capture.fps,
capture.max_bitrate_kbit,
);
preview.activate_surface_for_test(1, PreviewSurface::Inline);
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if let Some(request) = requests.lock().unwrap().last().cloned() {
assert_eq!(request.id, 1);
assert_eq!(request.source_id, Some(1));
assert_eq!(request.requested_width, 1920);
assert_eq!(request.requested_height, 1080);
assert_eq!(request.requested_fps, 60);
assert_eq!(request.max_bitrate, 18_000);
preview.shutdown_all();
return;
}
std::thread::sleep(Duration::from_millis(50));
}
preview.shutdown_all();
panic!("preview did not issue a capture request within timeout");
}
#[test]
#[serial]
fn inline_preview_requests_honest_source_profile_on_wire() {
let relay = ProbeRelay::default();
let requests = relay.requests.clone();
let rt = tokio::runtime::Runtime::new().expect("runtime");
let addr = rt.block_on(async move {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
drop(listener);
tokio::spawn(async move {
let _ = tonic::transport::Server::builder()
.add_service(RelayServer::new(relay))
.serve(addr)
.await;
});
addr
});
let preview = LauncherPreview::new(format!("http://{addr}")).expect("preview");
let mut state = LauncherState::default();
state.set_capture_size_preset(1, CaptureSizePreset::P1080);
let capture = state.capture_size_choice(1);
preview.set_capture_profile(
1,
1,
capture.width,
capture.height,
capture.fps,
capture.max_bitrate_kbit,
);
preview.activate_surface_for_test(1, PreviewSurface::Inline);
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if let Some(request) = requests.lock().unwrap().last().cloned() {
assert_eq!(request.id, 1);
assert_eq!(request.source_id, Some(1));
assert_eq!(request.requested_width, 1920);
assert_eq!(request.requested_height, 1080);
assert_eq!(request.requested_fps, 60);
assert_eq!(request.max_bitrate, 18_000);
preview.shutdown_all();
return;
}
std::thread::sleep(Duration::from_millis(50));
}
preview.shutdown_all();
panic!("preview did not issue a source capture request within timeout");
}
#[test]
#[serial]
fn inline_preview_requests_native_720p_source_mode_on_wire() {
let relay = ProbeRelay::default();
let requests = relay.requests.clone();
let rt = tokio::runtime::Runtime::new().expect("runtime");
let addr = rt.block_on(async move {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
drop(listener);
tokio::spawn(async move {
let _ = tonic::transport::Server::builder()
.add_service(RelayServer::new(relay))
.serve(addr)
.await;
});
addr
});
let preview = LauncherPreview::new(format!("http://{addr}")).expect("preview");
let mut state = LauncherState::default();
state.set_capture_size_preset(1, CaptureSizePreset::P720);
let capture = state.capture_size_choice(1);
preview.set_capture_profile(
1,
1,
capture.width,
capture.height,
capture.fps,
capture.max_bitrate_kbit,
);
preview.activate_surface_for_test(1, PreviewSurface::Inline);
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if let Some(request) = requests.lock().unwrap().last().cloned() {
assert_eq!(request.id, 1);
assert_eq!(request.source_id, Some(1));
assert_eq!(request.requested_width, 1280);
assert_eq!(request.requested_height, 720);
assert_eq!(request.requested_fps, 60);
assert_eq!(request.max_bitrate, 12_000);
preview.shutdown_all();
return;
}
std::thread::sleep(Duration::from_millis(50));
}
preview.shutdown_all();
panic!("preview did not issue a 720p source capture request within timeout");
}
#[test]
#[serial]
fn inline_preview_legacy_low_modes_fall_forward_to_720p_on_wire() {
let relay = ProbeRelay::default();
let requests = relay.requests.clone();
let rt = tokio::runtime::Runtime::new().expect("runtime");
let addr = rt.block_on(async move {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
drop(listener);
tokio::spawn(async move {
let _ = tonic::transport::Server::builder()
.add_service(RelayServer::new(relay))
.serve(addr)
.await;
});
addr
});
let preview = LauncherPreview::new(format!("http://{addr}")).expect("preview");
let mut state = LauncherState::default();
state.set_capture_size_preset(1, CaptureSizePreset::P480);
let capture = state.capture_size_choice(1);
preview.set_capture_profile(
1,
1,
capture.width,
capture.height,
capture.fps,
capture.max_bitrate_kbit,
);
preview.activate_surface_for_test(1, PreviewSurface::Inline);
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if let Some(request) = requests.lock().unwrap().last().cloned() {
assert_eq!(request.id, 1);
assert_eq!(request.source_id, Some(1));
assert_eq!(request.requested_width, 1280);
assert_eq!(request.requested_height, 720);
assert_eq!(request.requested_fps, 60);
assert_eq!(request.max_bitrate, 12_000);
preview.shutdown_all();
return;
}
std::thread::sleep(Duration::from_millis(50));
}
preview.shutdown_all();
panic!("preview did not issue a 720p fallback source capture request within timeout");
}
#[test]
#[serial]
fn preview_can_request_other_eye_as_a_distinct_stream() {
let relay = ProbeRelay::default();
let requests = relay.requests.clone();
let rt = tokio::runtime::Runtime::new().expect("runtime");
let addr = rt.block_on(async move {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
drop(listener);
tokio::spawn(async move {
let _ = tonic::transport::Server::builder()
.add_service(RelayServer::new(relay))
.serve(addr)
.await;
});
addr
});
let preview = LauncherPreview::new(format!("http://{addr}")).expect("preview");
preview.set_capture_profile(0, 1, 1920, 1080, 30, 12_000);
preview.activate_surface_for_test(0, PreviewSurface::Inline);
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if let Some(request) = requests.lock().unwrap().last().cloned() {
assert_eq!(request.id, 0);
assert_eq!(request.source_id, Some(1));
assert_eq!(request.requested_width, 1920);
assert_eq!(request.requested_height, 1080);
preview.shutdown_all();
return;
}
std::thread::sleep(Duration::from_millis(50));
}
preview.shutdown_all();
panic!("preview did not issue a mirrored capture request within timeout");
}