fix(relay): restore stable startup and media defaults
This commit is contained in:
parent
d4fab3f958
commit
8dcdbb7770
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.11.30"
|
version = "0.11.31"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -263,33 +263,52 @@ impl LesavkaClientApp {
|
|||||||
"📸 using camera settings from server"
|
"📸 using camera settings from server"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
match CameraCapture::new(
|
let ep = vid_ep.clone();
|
||||||
std::env::var("LESAVKA_CAM_SOURCE").ok().as_deref(),
|
let cam_source = std::env::var("LESAVKA_CAM_SOURCE").ok();
|
||||||
camera_cfg,
|
tokio::spawn(async move {
|
||||||
) {
|
let result = tokio::task::spawn_blocking(move || {
|
||||||
Ok(cam) => {
|
CameraCapture::new(cam_source.as_deref(), camera_cfg)
|
||||||
let cam = Arc::new(cam);
|
})
|
||||||
tokio::spawn(Self::cam_loop(vid_ep.clone(), cam));
|
.await;
|
||||||
|
match result {
|
||||||
|
Ok(Ok(cam)) => {
|
||||||
|
let cam = Arc::new(cam);
|
||||||
|
tokio::spawn(Self::cam_loop(ep, cam));
|
||||||
|
}
|
||||||
|
Ok(Err(err)) => {
|
||||||
|
warn!(
|
||||||
|
"📸 webcam uplink is unavailable for this relay session; continuing without StreamCamera: {err:#}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"📸 webcam uplink setup task failed before StreamCamera could start: {err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
});
|
||||||
warn!(
|
|
||||||
"📸 webcam uplink is unavailable for this relay session; continuing without StreamCamera: {err:#}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if caps.microphone && std::env::var("LESAVKA_MIC_DISABLE").is_err() {
|
if caps.microphone && std::env::var("LESAVKA_MIC_DISABLE").is_err() {
|
||||||
match MicrophoneCapture::new() {
|
let ep = vid_ep.clone();
|
||||||
Ok(mic) => {
|
tokio::spawn(async move {
|
||||||
let mic = Arc::new(mic);
|
let result = tokio::task::spawn_blocking(MicrophoneCapture::new).await;
|
||||||
tokio::spawn(Self::voice_loop(vid_ep.clone(), mic)); // renamed
|
match result {
|
||||||
|
Ok(Ok(mic)) => {
|
||||||
|
let mic = Arc::new(mic);
|
||||||
|
tokio::spawn(Self::voice_loop(ep, mic));
|
||||||
|
}
|
||||||
|
Ok(Err(err)) => {
|
||||||
|
warn!(
|
||||||
|
"🎤 microphone uplink is unavailable for this relay session; continuing without StreamMicrophone: {err:#}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"🎤 microphone uplink setup task failed before StreamMicrophone could start: {err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
});
|
||||||
warn!(
|
|
||||||
"🎤 microphone uplink is unavailable for this relay session; continuing without StreamMicrophone: {err:#}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*────────── central reactor ───────────────────*/
|
/*────────── central reactor ───────────────────*/
|
||||||
|
|||||||
@ -343,7 +343,7 @@ impl CameraCapture {
|
|||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
fn choose_encoder() -> (&'static str, Option<&'static str>) {
|
fn choose_encoder() -> (&'static str, Option<&'static str>) {
|
||||||
if gst::ElementFactory::find("nvh264enc").is_some() {
|
if buildable_encoder("nvh264enc") {
|
||||||
return (
|
return (
|
||||||
"nvh264enc",
|
"nvh264enc",
|
||||||
supported_encoder_property(
|
supported_encoder_property(
|
||||||
@ -352,13 +352,13 @@ impl CameraCapture {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if gst::ElementFactory::find("vaapih264enc").is_some() {
|
if buildable_encoder("vaapih264enc") {
|
||||||
return (
|
return (
|
||||||
"vaapih264enc",
|
"vaapih264enc",
|
||||||
supported_encoder_property("vaapih264enc", &["keyframe-period"]),
|
supported_encoder_property("vaapih264enc", &["keyframe-period"]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if gst::ElementFactory::find("v4l2h264enc").is_some() {
|
if buildable_encoder("v4l2h264enc") {
|
||||||
return (
|
return (
|
||||||
"v4l2h264enc",
|
"v4l2h264enc",
|
||||||
supported_encoder_property("v4l2h264enc", &["idrcount"]),
|
supported_encoder_property("v4l2h264enc", &["idrcount"]),
|
||||||
@ -382,6 +382,12 @@ impl CameraCapture {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
fn buildable_encoder(encoder: &'static str) -> bool {
|
||||||
|
gst::ElementFactory::find(encoder).is_some()
|
||||||
|
&& gst::ElementFactory::make(encoder).build().is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
fn supported_encoder_property(
|
fn supported_encoder_property(
|
||||||
encoder: &'static str,
|
encoder: &'static str,
|
||||||
|
|||||||
@ -58,9 +58,13 @@ pub fn runtime_env_vars(state: &LauncherState) -> BTreeMap<String, String> {
|
|||||||
}
|
}
|
||||||
if let Some(camera) = state.devices.camera.as_ref() {
|
if let Some(camera) = state.devices.camera.as_ref() {
|
||||||
envs.insert("LESAVKA_CAM_SOURCE".to_string(), camera.clone());
|
envs.insert("LESAVKA_CAM_SOURCE".to_string(), camera.clone());
|
||||||
|
} else {
|
||||||
|
envs.insert("LESAVKA_CAM_DISABLE".to_string(), "1".to_string());
|
||||||
}
|
}
|
||||||
if let Some(microphone) = state.devices.microphone.as_ref() {
|
if let Some(microphone) = state.devices.microphone.as_ref() {
|
||||||
envs.insert("LESAVKA_MIC_SOURCE".to_string(), microphone.clone());
|
envs.insert("LESAVKA_MIC_SOURCE".to_string(), microphone.clone());
|
||||||
|
} else {
|
||||||
|
envs.insert("LESAVKA_MIC_DISABLE".to_string(), "1".to_string());
|
||||||
}
|
}
|
||||||
if let Some(speaker) = state.devices.speaker.as_ref() {
|
if let Some(speaker) = state.devices.speaker.as_ref() {
|
||||||
envs.insert("LESAVKA_AUDIO_SINK".to_string(), speaker.clone());
|
envs.insert("LESAVKA_AUDIO_SINK".to_string(), speaker.clone());
|
||||||
@ -232,6 +236,17 @@ mod tests {
|
|||||||
assert!(!envs.contains_key("LESAVKA_AUDIO_SINK"));
|
assert!(!envs.contains_key("LESAVKA_AUDIO_SINK"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_env_vars_disable_uplink_media_when_unstaged() {
|
||||||
|
let state = LauncherState::new();
|
||||||
|
|
||||||
|
let envs = runtime_env_vars(&state);
|
||||||
|
assert_eq!(envs.get("LESAVKA_CAM_DISABLE"), Some(&"1".to_string()));
|
||||||
|
assert_eq!(envs.get("LESAVKA_MIC_DISABLE"), Some(&"1".to_string()));
|
||||||
|
assert!(!envs.contains_key("LESAVKA_CAM_SOURCE"));
|
||||||
|
assert!(!envs.contains_key("LESAVKA_MIC_SOURCE"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn maybe_run_launcher_returns_false_with_explicit_opt_out() {
|
fn maybe_run_launcher_returns_false_with_explicit_opt_out() {
|
||||||
let args = vec!["--no-launcher".to_string()];
|
let args = vec!["--no-launcher".to_string()];
|
||||||
|
|||||||
@ -647,9 +647,7 @@ impl LauncherState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_catalog_defaults(&mut self, catalog: &DeviceCatalog) {
|
pub fn apply_catalog_defaults(&mut self, catalog: &DeviceCatalog) {
|
||||||
if self.devices.camera.is_none() {
|
let _ = catalog;
|
||||||
self.devices.camera = catalog.cameras.first().cloned();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_swap_key(&mut self, swap_key: impl Into<String>) {
|
pub fn set_swap_key(&mut self, swap_key: impl Into<String>) {
|
||||||
@ -1057,7 +1055,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn catalog_defaults_fill_only_missing_values() {
|
fn catalog_defaults_do_not_auto_stage_media_devices() {
|
||||||
let mut state = LauncherState::new();
|
let mut state = LauncherState::new();
|
||||||
state.select_camera(Some("/dev/video-special".to_string()));
|
state.select_camera(Some("/dev/video-special".to_string()));
|
||||||
|
|
||||||
@ -1074,6 +1072,12 @@ mod tests {
|
|||||||
assert_eq!(state.devices.camera.as_deref(), Some("/dev/video-special"));
|
assert_eq!(state.devices.camera.as_deref(), Some("/dev/video-special"));
|
||||||
assert!(state.devices.microphone.is_none());
|
assert!(state.devices.microphone.is_none());
|
||||||
assert!(state.devices.speaker.is_none());
|
assert!(state.devices.speaker.is_none());
|
||||||
|
|
||||||
|
let mut fresh = LauncherState::new();
|
||||||
|
fresh.apply_catalog_defaults(&catalog);
|
||||||
|
assert!(fresh.devices.camera.is_none());
|
||||||
|
assert!(fresh.devices.microphone.is_none());
|
||||||
|
assert!(fresh.devices.speaker.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -198,6 +198,7 @@ pub fn build_launcher_view(
|
|||||||
let staging_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let staging_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
staging_row.set_hexpand(true);
|
staging_row.set_hexpand(true);
|
||||||
staging_row.set_vexpand(false);
|
staging_row.set_vexpand(false);
|
||||||
|
staging_row.set_valign(gtk::Align::Start);
|
||||||
staging_row.set_homogeneous(true);
|
staging_row.set_homogeneous(true);
|
||||||
workspace.append(&staging_row);
|
workspace.append(&staging_row);
|
||||||
|
|
||||||
@ -210,6 +211,7 @@ pub fn build_launcher_view(
|
|||||||
build_panel_with_action("Device Staging", Some(device_refresh_button.upcast_ref()));
|
build_panel_with_action("Device Staging", Some(device_refresh_button.upcast_ref()));
|
||||||
devices_panel.set_hexpand(true);
|
devices_panel.set_hexpand(true);
|
||||||
devices_panel.set_vexpand(false);
|
devices_panel.set_vexpand(false);
|
||||||
|
devices_panel.set_valign(gtk::Align::Start);
|
||||||
devices_body.set_spacing(8);
|
devices_body.set_spacing(8);
|
||||||
|
|
||||||
let control_group = build_subgroup("Control Inputs");
|
let control_group = build_subgroup("Control Inputs");
|
||||||
@ -319,6 +321,7 @@ pub fn build_launcher_view(
|
|||||||
let (preview_panel, preview_body) = build_panel("Device Testing");
|
let (preview_panel, preview_body) = build_panel("Device Testing");
|
||||||
preview_panel.set_hexpand(true);
|
preview_panel.set_hexpand(true);
|
||||||
preview_panel.set_vexpand(false);
|
preview_panel.set_vexpand(false);
|
||||||
|
preview_panel.set_valign(gtk::Align::Start);
|
||||||
preview_body.set_spacing(8);
|
preview_body.set_spacing(8);
|
||||||
let camera_preview = gtk::Picture::new();
|
let camera_preview = gtk::Picture::new();
|
||||||
camera_preview.set_can_shrink(false);
|
camera_preview.set_can_shrink(false);
|
||||||
@ -337,11 +340,11 @@ pub fn build_launcher_view(
|
|||||||
camera_status.set_visible(false);
|
camera_status.set_visible(false);
|
||||||
let camera_preview_shell = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
let camera_preview_shell = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||||
camera_preview_shell.set_hexpand(true);
|
camera_preview_shell.set_hexpand(true);
|
||||||
camera_preview_shell.set_vexpand(true);
|
camera_preview_shell.set_vexpand(false);
|
||||||
camera_preview_shell.set_size_request(-1, CAMERA_PREVIEW_VIEWPORT_HEIGHT);
|
camera_preview_shell.set_size_request(-1, CAMERA_PREVIEW_VIEWPORT_HEIGHT);
|
||||||
let camera_preview_frame = gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false);
|
let camera_preview_frame = gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false);
|
||||||
camera_preview_frame.set_hexpand(true);
|
camera_preview_frame.set_hexpand(true);
|
||||||
camera_preview_frame.set_vexpand(true);
|
camera_preview_frame.set_vexpand(false);
|
||||||
camera_preview_frame.set_size_request(-1, CAMERA_PREVIEW_VIEWPORT_HEIGHT);
|
camera_preview_frame.set_size_request(-1, CAMERA_PREVIEW_VIEWPORT_HEIGHT);
|
||||||
camera_preview_frame.set_child(Some(&camera_preview));
|
camera_preview_frame.set_child(Some(&camera_preview));
|
||||||
camera_preview_shell.append(&camera_preview_frame);
|
camera_preview_shell.append(&camera_preview_frame);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.11.30"
|
version = "0.11.31"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn banner_includes_version() {
|
fn banner_includes_version() {
|
||||||
assert_eq!(banner("0.11.30"), "lesavka-common CLI (v0.11.30)");
|
assert_eq!(banner("0.11.31"), "lesavka-common CLI (v0.11.31)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.11.30"
|
version = "0.11.31"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -347,12 +347,12 @@ fn preferred_uac_device_candidates(preferred: &str) -> Vec<String> {
|
|||||||
let allow_aliases = auto_family.contains(&preferred);
|
let allow_aliases = auto_family.contains(&preferred);
|
||||||
push_audio_candidate_family(&mut out, &mut seen, preferred);
|
push_audio_candidate_family(&mut out, &mut seen, preferred);
|
||||||
if allow_aliases {
|
if allow_aliases {
|
||||||
for alias in auto_family {
|
|
||||||
push_audio_candidate_family(&mut out, &mut seen, alias);
|
|
||||||
}
|
|
||||||
for detected in detect_uac_card_candidates() {
|
for detected in detect_uac_card_candidates() {
|
||||||
push_audio_candidate_family(&mut out, &mut seen, &detected);
|
push_audio_candidate_family(&mut out, &mut seen, &detected);
|
||||||
}
|
}
|
||||||
|
for alias in auto_family {
|
||||||
|
push_audio_candidate_family(&mut out, &mut seen, alias);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|||||||
@ -422,7 +422,7 @@ pub async fn eye_ball_with_request(
|
|||||||
let server_encoder_label = if use_test_src {
|
let server_encoder_label = if use_test_src {
|
||||||
"x264enc(testsrc)".to_string()
|
"x264enc(testsrc)".to_string()
|
||||||
} else {
|
} else {
|
||||||
"source-pass-through(auto-caps)".to_string()
|
"source-pass-through".to_string()
|
||||||
};
|
};
|
||||||
let server_process_cpu_tenths = server_process_cpu_metric();
|
let server_process_cpu_tenths = server_process_cpu_metric();
|
||||||
if !use_test_src {
|
if !use_test_src {
|
||||||
@ -444,11 +444,12 @@ pub async fn eye_ball_with_request(
|
|||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
"v4l2src name=cam_{eye} device=\"{dev}\" io-mode=mmap do-timestamp=true ! \
|
"v4l2src name=cam_{eye} device=\"{dev}\" io-mode=mmap do-timestamp=true ! \
|
||||||
video/x-h264 ! \
|
video/x-h264,width={},height={},framerate={}/1 ! \
|
||||||
queue max-size-buffers={queue_buffers} max-size-time=0 max-size-bytes=0 leaky=downstream ! \
|
queue max-size-buffers={queue_buffers} max-size-time=0 max-size-bytes=0 leaky=downstream ! \
|
||||||
h264parse disable-passthrough=true config-interval=-1 ! \
|
h264parse disable-passthrough=true config-interval=-1 ! \
|
||||||
video/x-h264,stream-format=byte-stream,alignment=au ! \
|
video/x-h264,stream-format=byte-stream,alignment=au ! \
|
||||||
appsink name=sink emit-signals=true max-buffers={appsink_buffers} drop=true",
|
appsink name=sink emit-signals=true max-buffers={appsink_buffers} drop=true",
|
||||||
|
request.width, request.height, request.fps,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -137,9 +137,17 @@ mod inputs_contract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_keyboard_pair(name: &str) -> Option<(VirtualDevice, evdev::Device)> {
|
fn build_keyboard_pair(name: &str) -> Option<(VirtualDevice, evdev::Device)> {
|
||||||
|
build_keyboard_pair_with_keys(name, &[evdev::KeyCode::KEY_A, evdev::KeyCode::KEY_ENTER])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_keyboard_pair_with_keys(
|
||||||
|
name: &str,
|
||||||
|
supported_keys: &[evdev::KeyCode],
|
||||||
|
) -> Option<(VirtualDevice, evdev::Device)> {
|
||||||
let mut keys = AttributeSet::<evdev::KeyCode>::new();
|
let mut keys = AttributeSet::<evdev::KeyCode>::new();
|
||||||
keys.insert(evdev::KeyCode::KEY_A);
|
for key in supported_keys {
|
||||||
keys.insert(evdev::KeyCode::KEY_ENTER);
|
keys.insert(*key);
|
||||||
|
}
|
||||||
|
|
||||||
let mut vdev = VirtualDevice::builder()
|
let mut vdev = VirtualDevice::builder()
|
||||||
.ok()?
|
.ok()?
|
||||||
@ -518,6 +526,49 @@ mod inputs_contract {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn quick_toggle_tap_flips_routing_when_processed_through_input_aggregator() {
|
||||||
|
let Some((mut vdev, dev)) = build_keyboard_pair_with_keys(
|
||||||
|
"lesavka-input-toggle-pause",
|
||||||
|
&[
|
||||||
|
evdev::KeyCode::KEY_A,
|
||||||
|
evdev::KeyCode::KEY_ENTER,
|
||||||
|
evdev::KeyCode::KEY_PAUSE,
|
||||||
|
],
|
||||||
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let (kbd_tx, _) = tokio::sync::broadcast::channel(16);
|
||||||
|
let (mou_tx, _) = tokio::sync::broadcast::channel(16);
|
||||||
|
let keyboard = KeyboardAggregator::new(dev, false, kbd_tx.clone(), None);
|
||||||
|
let mut agg = InputAggregator::new(false, kbd_tx, mou_tx, None);
|
||||||
|
agg.quick_toggle_key = Some(evdev::KeyCode::KEY_PAUSE);
|
||||||
|
agg.quick_toggle_debounce = Duration::from_millis(0);
|
||||||
|
agg.keyboards.push(keyboard);
|
||||||
|
|
||||||
|
vdev.emit(&[
|
||||||
|
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::KEY_PAUSE.0, 1),
|
||||||
|
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::KEY_PAUSE.0, 0),
|
||||||
|
])
|
||||||
|
.expect("emit pause tap");
|
||||||
|
thread::sleep(std::time::Duration::from_millis(20));
|
||||||
|
|
||||||
|
agg.process_keyboard_updates();
|
||||||
|
let quick_toggle_now = agg.quick_toggle_active();
|
||||||
|
agg.observe_quick_toggle(quick_toggle_now);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
agg.pending_release,
|
||||||
|
"a quick swap-key tap should start the local handoff path"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!agg.released,
|
||||||
|
"the relay should still be in pending-release until the local handoff completes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn observe_quick_toggle_uses_rising_edge_to_avoid_repeat_toggling() {
|
fn observe_quick_toggle_uses_rising_edge_to_avoid_repeat_toggling() {
|
||||||
let mut agg = new_aggregator();
|
let mut agg = new_aggregator();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user