lesavka: cover inline preview capture requests
This commit is contained in:
parent
bec9885537
commit
e3cb555c90
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user