lesavka: cover inline preview capture requests

This commit is contained in:
Brad Stein 2026-04-18 03:34:54 -03:00
parent bec9885537
commit e3cb555c90

View File

@ -348,6 +348,26 @@ impl LauncherPreview {
))
}
#[cfg(test)]
pub(crate) fn activate_surface_for_test(&self, monitor_id: usize, surface: PreviewSurface) {
let feed = match surface {
PreviewSurface::Inline => self
.inline_feeds
.lock()
.ok()
.and_then(|feeds| feeds.get(monitor_id).cloned()),
PreviewSurface::Window => self
.window_feeds
.lock()
.ok()
.and_then(|feeds| feeds.get(monitor_id).cloned()),
};
if let Some(feed) = feed {
feed.session_active.store(true, Ordering::Relaxed);
feed.active_bindings.fetch_add(1, Ordering::AcqRel);
}
}
fn rebuild_feed(
&self,
feeds: &Arc<Mutex<[PreviewFeed; 2]>>,
@ -1518,10 +1538,140 @@ mod tests {
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,
PREVIEW_HEIGHT, PREVIEW_WIDTH, PreviewSurface, PreviewTelemetry,
LauncherPreview, PREVIEW_HEIGHT, PREVIEW_WIDTH, PreviewSurface, PreviewTelemetry,
adapt_inline_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(),
}))
}
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() {
@ -1609,4 +1759,105 @@ mod tests {
assert_eq!(snapshot.server_encoder_label, "x264enc");
assert_eq!(snapshot.decoder_label, "nvh264dec");
}
#[test]
#[serial]
fn inline_preview_requests_selected_reencode_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,
capture.width,
capture.height,
capture.fps,
capture.max_bitrate_kbit,
capture.preset != CaptureSizePreset::Source,
);
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.requested_width, 1920);
assert_eq!(request.requested_height, 1080);
assert_eq!(request.requested_fps, 24);
assert_eq!(request.max_bitrate, 4_000);
assert!(request.prefer_reencode);
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::Source);
let capture = state.capture_size_choice(1);
preview.set_capture_profile(
1,
capture.width,
capture.height,
capture.fps,
capture.max_bitrate_kbit,
capture.preset != CaptureSizePreset::Source,
);
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.requested_width, 1920);
assert_eq!(request.requested_height, 1080);
assert_eq!(request.requested_fps, 30);
assert_eq!(request.max_bitrate, 12_000);
assert!(!request.prefer_reencode);
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");
}
}