lesavka: honor staged audio devices
This commit is contained in:
parent
74893c97ae
commit
11aa2b5ec4
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.11.26"
|
||||
version = "0.11.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -221,6 +221,17 @@ mod tests {
|
||||
assert!(!envs.contains_key("LESAVKA_DISABLE_VIDEO_RENDER"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_env_vars_leave_auto_audio_devices_unset() {
|
||||
let mut state = LauncherState::new();
|
||||
state.select_microphone(Some("auto".to_string()));
|
||||
state.select_speaker(Some("auto".to_string()));
|
||||
|
||||
let envs = runtime_env_vars(&state);
|
||||
assert!(!envs.contains_key("LESAVKA_MIC_SOURCE"));
|
||||
assert!(!envs.contains_key("LESAVKA_AUDIO_SINK"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_run_launcher_returns_false_with_explicit_opt_out() {
|
||||
let args = vec!["--no-launcher".to_string()];
|
||||
|
||||
@ -650,12 +650,6 @@ impl LauncherState {
|
||||
if self.devices.camera.is_none() {
|
||||
self.devices.camera = catalog.cameras.first().cloned();
|
||||
}
|
||||
if self.devices.microphone.is_none() {
|
||||
self.devices.microphone = catalog.microphones.first().cloned();
|
||||
}
|
||||
if self.devices.speaker.is_none() {
|
||||
self.devices.speaker = catalog.speakers.first().cloned();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_swap_key(&mut self, swap_key: impl Into<String>) {
|
||||
@ -1078,8 +1072,8 @@ mod tests {
|
||||
state.apply_catalog_defaults(&catalog);
|
||||
|
||||
assert_eq!(state.devices.camera.as_deref(), Some("/dev/video-special"));
|
||||
assert_eq!(state.devices.microphone.as_deref(), Some("alsa_input.usb"));
|
||||
assert_eq!(state.devices.speaker.as_deref(), Some("alsa_output.usb"));
|
||||
assert!(state.devices.microphone.is_none());
|
||||
assert!(state.devices.speaker.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -61,6 +61,15 @@ enum ClipboardMessage {
|
||||
#[cfg(not(coverage))]
|
||||
const NETWORK_TELEMETRY_WINDOW: Duration = Duration::from_secs(8);
|
||||
|
||||
#[cfg(not(coverage))]
|
||||
fn usb_audio_kernel_support_missing() -> bool {
|
||||
Command::new("modinfo")
|
||||
.arg("snd_usb_audio")
|
||||
.status()
|
||||
.map(|status| !status.success())
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
#[cfg(not(coverage))]
|
||||
#[derive(Default)]
|
||||
struct NetworkTelemetry {
|
||||
@ -1140,9 +1149,12 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
|
||||
.status_label
|
||||
.set_text(&format!("Device refresh succeeded, but the webcam test could not switch cleanly: {err}"));
|
||||
} else {
|
||||
widgets_handle.status_label.set_text(
|
||||
"Device staging refreshed. Newly attached devices are ready for local tests; reconnect the relay if you want the live session to use a new webcam, mic, or speaker.",
|
||||
);
|
||||
let message = if usb_audio_kernel_support_missing() {
|
||||
"Device staging refreshed. USB audio devices may still stay invisible until the host boots a kernel with snd_usb_audio available; reconnect the relay if you want the live session to use a new webcam, mic, or speaker."
|
||||
} else {
|
||||
"Device staging refreshed. Newly attached devices are ready for local tests; reconnect the relay if you want the live session to use a new webcam, mic, or speaker."
|
||||
};
|
||||
widgets_handle.status_label.set_text(message);
|
||||
}
|
||||
refresh_launcher_ui(
|
||||
&widgets_handle,
|
||||
|
||||
@ -212,9 +212,8 @@ pub fn build_launcher_view(
|
||||
devices_body.set_spacing(8);
|
||||
|
||||
let control_group = build_subgroup("Control Inputs");
|
||||
let control_row = gtk::Box::new(gtk::Orientation::Horizontal, 12);
|
||||
control_row.set_homogeneous(true);
|
||||
control_group.append(&control_row);
|
||||
let control_stack = gtk::Box::new(gtk::Orientation::Vertical, 10);
|
||||
control_group.append(&control_stack);
|
||||
|
||||
let camera_combo = gtk::ComboBoxText::new();
|
||||
camera_combo.append(Some("auto"), "auto");
|
||||
@ -249,8 +248,8 @@ pub fn build_launcher_view(
|
||||
keyboard_combo.set_tooltip_text(Some(
|
||||
"Leave this on all keyboards to relay every keyboard, or pick one specific device.",
|
||||
));
|
||||
let keyboard_block = build_selector_block("Keyboard", &keyboard_combo);
|
||||
control_row.append(&keyboard_block);
|
||||
let keyboard_row = build_inline_selector_row("Keyboard", &keyboard_combo);
|
||||
control_stack.append(&keyboard_row);
|
||||
|
||||
let mouse_combo = gtk::ComboBoxText::new();
|
||||
mouse_combo.append(Some("all"), "all mice");
|
||||
@ -261,8 +260,8 @@ pub fn build_launcher_view(
|
||||
mouse_combo.set_tooltip_text(Some(
|
||||
"Leave this on all mice to relay every pointer, or pick one specific device.",
|
||||
));
|
||||
let mouse_block = build_selector_block("Mouse", &mouse_combo);
|
||||
control_row.append(&mouse_block);
|
||||
let mouse_row = build_inline_selector_row("Mouse", &mouse_combo);
|
||||
control_stack.append(&mouse_row);
|
||||
devices_body.append(&control_group);
|
||||
|
||||
let media_group = build_subgroup("Media Controls");
|
||||
@ -1050,10 +1049,12 @@ fn attach_device_row(
|
||||
grid.attach(test_button, 2, row, 1, 1);
|
||||
}
|
||||
|
||||
fn build_selector_block(label: &str, combo: >k::ComboBoxText) -> gtk::Box {
|
||||
let block = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
||||
fn build_inline_selector_row(label: &str, combo: >k::ComboBoxText) -> gtk::Box {
|
||||
let block = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
let label_widget = gtk::Label::new(Some(label));
|
||||
label_widget.set_halign(gtk::Align::Start);
|
||||
label_widget.set_width_chars(9);
|
||||
label_widget.set_xalign(0.0);
|
||||
combo.set_hexpand(true);
|
||||
combo.set_size_request(0, -1);
|
||||
block.append(&label_widget);
|
||||
@ -1061,6 +1062,22 @@ fn build_selector_block(label: &str, combo: >k::ComboBoxText) -> gtk::Box {
|
||||
block
|
||||
}
|
||||
|
||||
fn build_inline_combo_row(
|
||||
label: &str,
|
||||
combo: &impl IsA<gtk::Widget>,
|
||||
min_label_chars: i32,
|
||||
) -> gtk::Box {
|
||||
let row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
let label_widget = gtk::Label::new(Some(label));
|
||||
label_widget.add_css_class("dim-label");
|
||||
label_widget.set_width_chars(min_label_chars);
|
||||
label_widget.set_xalign(0.0);
|
||||
label_widget.set_halign(gtk::Align::Start);
|
||||
row.append(&label_widget);
|
||||
row.append(combo);
|
||||
row
|
||||
}
|
||||
|
||||
fn append_input_choice(combo: >k::ComboBoxText, value: &str) {
|
||||
let short = value.rsplit('/').next().unwrap_or(value);
|
||||
let label = Device::open(value)
|
||||
@ -1178,7 +1195,8 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
|
||||
feed_source_combo.set_tooltip_text(Some(
|
||||
"Choose which physical eye feed appears in this pane. Off disables the pane; the opposite-eye option mirrors the other physical feed while preserving a separate stream load for realistic validation.",
|
||||
));
|
||||
feed_source_combo.set_size_request(118, -1);
|
||||
feed_source_combo.set_hexpand(true);
|
||||
feed_source_combo.set_size_request(0, -1);
|
||||
let capture_resolution_combo = gtk::ComboBoxText::new();
|
||||
capture_resolution_combo.set_tooltip_text(Some(
|
||||
"Choose the eye-stream source mode for this feed. Source keeps the HDMI device's own H.264 stream; cheaper source-device modes will appear here once the hardware proves it supports them.",
|
||||
@ -1199,9 +1217,15 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
|
||||
controls_grid.set_column_spacing(8);
|
||||
controls_grid.set_row_spacing(8);
|
||||
controls_grid.set_hexpand(true);
|
||||
controls_grid.attach(&feed_source_combo, 0, 0, 1, 1);
|
||||
controls_grid.attach(&capture_resolution_combo, 1, 0, 1, 1);
|
||||
controls_grid.attach(&breakout_combo, 0, 1, 1, 1);
|
||||
let feed_row = build_inline_combo_row("Feed", &feed_source_combo, 4);
|
||||
let capture_row = build_inline_combo_row("Capture", &capture_resolution_combo, 7);
|
||||
let breakout_row = build_inline_combo_row("Display", &breakout_combo, 7);
|
||||
feed_row.set_hexpand(true);
|
||||
capture_row.set_hexpand(true);
|
||||
breakout_row.set_hexpand(true);
|
||||
controls_grid.attach(&feed_row, 0, 0, 1, 1);
|
||||
controls_grid.attach(&capture_row, 1, 0, 1, 1);
|
||||
controls_grid.attach(&breakout_row, 0, 1, 1, 1);
|
||||
controls_grid.attach(&action_button, 1, 1, 1, 1);
|
||||
footer_shell.append(&controls_grid);
|
||||
root.append(&footer_shell);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.11.26"
|
||||
version = "0.11.27"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -17,6 +17,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn banner_includes_version() {
|
||||
assert_eq!(banner("0.11.26"), "lesavka-common CLI (v0.11.26)");
|
||||
assert_eq!(banner("0.11.27"), "lesavka-common CLI (v0.11.27)");
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,6 +36,28 @@ require_linkable() {
|
||||
fi
|
||||
}
|
||||
|
||||
require_gst_element() {
|
||||
local element=$1
|
||||
if gst-inspect-1.0 "$element" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
echo "❌ required GStreamer element '$element' is unavailable after install." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_kernel_module() {
|
||||
local module=$1
|
||||
local why=$2
|
||||
if modinfo "$module" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
echo "❌ required kernel module '$module' is unavailable for the running kernel $(uname -r)." >&2
|
||||
echo " Lesavka needs it for $why." >&2
|
||||
echo " This usually means the machine booted an older kernel than the modules that are currently installed." >&2
|
||||
echo " Reboot into the freshly installed kernel, then rerun the installer." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
run_as_user() {
|
||||
sudo -u "$ORIG_USER" env HOME="$USER_HOME" SSH_AUTH_SOCK="${SSH_AUTH_SOCK:-}" "$@"
|
||||
}
|
||||
@ -48,11 +70,9 @@ fi
|
||||
REPO_URL=${REPO_URL:-$DEFAULT_REPO_URL}
|
||||
|
||||
log "1. Installing base packages"
|
||||
# Intentionally leave the host audio stack alone. Workstations often carry
|
||||
# tightly versioned PipeWire packages, and Lesavka should not force an audio
|
||||
# stack upgrade just to install the client.
|
||||
sudo pacman -Sq --needed --noconfirm \
|
||||
git rustup protobuf abseil-cpp gcc clang llvm-libs compiler-rt evtest base-devel libpulse \
|
||||
pipewire pipewire-pulse wireplumber alsa-utils gst-plugin-pipewire \
|
||||
gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav \
|
||||
wmctrl qt6-tools wl-clipboard xclip xsel desktop-file-utils
|
||||
|
||||
@ -85,6 +105,9 @@ sudo usermod -aG input "$ORIG_USER"
|
||||
|
||||
log "1d. Verifying runtime tools"
|
||||
require_command pactl "libpulse"
|
||||
require_command gst-inspect-1.0 "gstreamer"
|
||||
require_command arecord "alsa-utils"
|
||||
require_command speaker-test "alsa-utils"
|
||||
require_command wmctrl "wmctrl"
|
||||
require_command qdbus6 "qt6-tools"
|
||||
require_command protoc "protobuf"
|
||||
@ -98,6 +121,15 @@ require_linkable "$(command -v protoc)" "protoc"
|
||||
if [[ -e /usr/lib/libclang.so ]]; then
|
||||
require_linkable /usr/lib/libclang.so "libclang"
|
||||
fi
|
||||
if [[ ! -d /lib/modules/$(uname -r) ]]; then
|
||||
echo "❌ no kernel module tree exists for the running kernel $(uname -r)." >&2
|
||||
echo " Reboot into the freshly installed kernel, then rerun the installer." >&2
|
||||
exit 1
|
||||
fi
|
||||
require_kernel_module snd_usb_audio "USB microphones and USB headsets"
|
||||
require_gst_element pulsesrc
|
||||
require_gst_element pulsesink
|
||||
require_gst_element pipewiresrc
|
||||
protoc --version >/dev/null
|
||||
if ! run_as_user pactl info >/dev/null 2>&1; then
|
||||
echo "⚠️ pactl is installed, but no PulseAudio/PipeWire Pulse server is reachable right now."
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.11.26"
|
||||
version = "0.11.27"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user