lesavka: honor staged audio devices
This commit is contained in:
parent
74893c97ae
commit
11aa2b5ec4
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.11.26"
|
version = "0.11.27"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -221,6 +221,17 @@ mod tests {
|
|||||||
assert!(!envs.contains_key("LESAVKA_DISABLE_VIDEO_RENDER"));
|
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]
|
#[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()];
|
||||||
|
|||||||
@ -650,12 +650,6 @@ impl LauncherState {
|
|||||||
if self.devices.camera.is_none() {
|
if self.devices.camera.is_none() {
|
||||||
self.devices.camera = catalog.cameras.first().cloned();
|
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>) {
|
pub fn set_swap_key(&mut self, swap_key: impl Into<String>) {
|
||||||
@ -1078,8 +1072,8 @@ mod tests {
|
|||||||
state.apply_catalog_defaults(&catalog);
|
state.apply_catalog_defaults(&catalog);
|
||||||
|
|
||||||
assert_eq!(state.devices.camera.as_deref(), Some("/dev/video-special"));
|
assert_eq!(state.devices.camera.as_deref(), Some("/dev/video-special"));
|
||||||
assert_eq!(state.devices.microphone.as_deref(), Some("alsa_input.usb"));
|
assert!(state.devices.microphone.is_none());
|
||||||
assert_eq!(state.devices.speaker.as_deref(), Some("alsa_output.usb"));
|
assert!(state.devices.speaker.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -61,6 +61,15 @@ enum ClipboardMessage {
|
|||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
const NETWORK_TELEMETRY_WINDOW: Duration = Duration::from_secs(8);
|
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))]
|
#[cfg(not(coverage))]
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct NetworkTelemetry {
|
struct NetworkTelemetry {
|
||||||
@ -1140,9 +1149,12 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
|
|||||||
.status_label
|
.status_label
|
||||||
.set_text(&format!("Device refresh succeeded, but the webcam test could not switch cleanly: {err}"));
|
.set_text(&format!("Device refresh succeeded, but the webcam test could not switch cleanly: {err}"));
|
||||||
} else {
|
} else {
|
||||||
widgets_handle.status_label.set_text(
|
let message = if usb_audio_kernel_support_missing() {
|
||||||
"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.",
|
"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(
|
refresh_launcher_ui(
|
||||||
&widgets_handle,
|
&widgets_handle,
|
||||||
|
|||||||
@ -212,9 +212,8 @@ pub fn build_launcher_view(
|
|||||||
devices_body.set_spacing(8);
|
devices_body.set_spacing(8);
|
||||||
|
|
||||||
let control_group = build_subgroup("Control Inputs");
|
let control_group = build_subgroup("Control Inputs");
|
||||||
let control_row = gtk::Box::new(gtk::Orientation::Horizontal, 12);
|
let control_stack = gtk::Box::new(gtk::Orientation::Vertical, 10);
|
||||||
control_row.set_homogeneous(true);
|
control_group.append(&control_stack);
|
||||||
control_group.append(&control_row);
|
|
||||||
|
|
||||||
let camera_combo = gtk::ComboBoxText::new();
|
let camera_combo = gtk::ComboBoxText::new();
|
||||||
camera_combo.append(Some("auto"), "auto");
|
camera_combo.append(Some("auto"), "auto");
|
||||||
@ -249,8 +248,8 @@ pub fn build_launcher_view(
|
|||||||
keyboard_combo.set_tooltip_text(Some(
|
keyboard_combo.set_tooltip_text(Some(
|
||||||
"Leave this on all keyboards to relay every keyboard, or pick one specific device.",
|
"Leave this on all keyboards to relay every keyboard, or pick one specific device.",
|
||||||
));
|
));
|
||||||
let keyboard_block = build_selector_block("Keyboard", &keyboard_combo);
|
let keyboard_row = build_inline_selector_row("Keyboard", &keyboard_combo);
|
||||||
control_row.append(&keyboard_block);
|
control_stack.append(&keyboard_row);
|
||||||
|
|
||||||
let mouse_combo = gtk::ComboBoxText::new();
|
let mouse_combo = gtk::ComboBoxText::new();
|
||||||
mouse_combo.append(Some("all"), "all mice");
|
mouse_combo.append(Some("all"), "all mice");
|
||||||
@ -261,8 +260,8 @@ pub fn build_launcher_view(
|
|||||||
mouse_combo.set_tooltip_text(Some(
|
mouse_combo.set_tooltip_text(Some(
|
||||||
"Leave this on all mice to relay every pointer, or pick one specific device.",
|
"Leave this on all mice to relay every pointer, or pick one specific device.",
|
||||||
));
|
));
|
||||||
let mouse_block = build_selector_block("Mouse", &mouse_combo);
|
let mouse_row = build_inline_selector_row("Mouse", &mouse_combo);
|
||||||
control_row.append(&mouse_block);
|
control_stack.append(&mouse_row);
|
||||||
devices_body.append(&control_group);
|
devices_body.append(&control_group);
|
||||||
|
|
||||||
let media_group = build_subgroup("Media Controls");
|
let media_group = build_subgroup("Media Controls");
|
||||||
@ -1050,10 +1049,12 @@ fn attach_device_row(
|
|||||||
grid.attach(test_button, 2, row, 1, 1);
|
grid.attach(test_button, 2, row, 1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_selector_block(label: &str, combo: >k::ComboBoxText) -> gtk::Box {
|
fn build_inline_selector_row(label: &str, combo: >k::ComboBoxText) -> gtk::Box {
|
||||||
let block = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
let block = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
let label_widget = gtk::Label::new(Some(label));
|
let label_widget = gtk::Label::new(Some(label));
|
||||||
label_widget.set_halign(gtk::Align::Start);
|
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_hexpand(true);
|
||||||
combo.set_size_request(0, -1);
|
combo.set_size_request(0, -1);
|
||||||
block.append(&label_widget);
|
block.append(&label_widget);
|
||||||
@ -1061,6 +1062,22 @@ fn build_selector_block(label: &str, combo: >k::ComboBoxText) -> gtk::Box {
|
|||||||
block
|
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) {
|
fn append_input_choice(combo: >k::ComboBoxText, value: &str) {
|
||||||
let short = value.rsplit('/').next().unwrap_or(value);
|
let short = value.rsplit('/').next().unwrap_or(value);
|
||||||
let label = Device::open(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(
|
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.",
|
"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();
|
let capture_resolution_combo = gtk::ComboBoxText::new();
|
||||||
capture_resolution_combo.set_tooltip_text(Some(
|
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.",
|
"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_column_spacing(8);
|
||||||
controls_grid.set_row_spacing(8);
|
controls_grid.set_row_spacing(8);
|
||||||
controls_grid.set_hexpand(true);
|
controls_grid.set_hexpand(true);
|
||||||
controls_grid.attach(&feed_source_combo, 0, 0, 1, 1);
|
let feed_row = build_inline_combo_row("Feed", &feed_source_combo, 4);
|
||||||
controls_grid.attach(&capture_resolution_combo, 1, 0, 1, 1);
|
let capture_row = build_inline_combo_row("Capture", &capture_resolution_combo, 7);
|
||||||
controls_grid.attach(&breakout_combo, 0, 1, 1, 1);
|
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);
|
controls_grid.attach(&action_button, 1, 1, 1, 1);
|
||||||
footer_shell.append(&controls_grid);
|
footer_shell.append(&controls_grid);
|
||||||
root.append(&footer_shell);
|
root.append(&footer_shell);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.11.26"
|
version = "0.11.27"
|
||||||
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.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
|
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() {
|
run_as_user() {
|
||||||
sudo -u "$ORIG_USER" env HOME="$USER_HOME" SSH_AUTH_SOCK="${SSH_AUTH_SOCK:-}" "$@"
|
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}
|
REPO_URL=${REPO_URL:-$DEFAULT_REPO_URL}
|
||||||
|
|
||||||
log "1. Installing base packages"
|
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 \
|
sudo pacman -Sq --needed --noconfirm \
|
||||||
git rustup protobuf abseil-cpp gcc clang llvm-libs compiler-rt evtest base-devel libpulse \
|
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 \
|
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
|
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"
|
log "1d. Verifying runtime tools"
|
||||||
require_command pactl "libpulse"
|
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 wmctrl "wmctrl"
|
||||||
require_command qdbus6 "qt6-tools"
|
require_command qdbus6 "qt6-tools"
|
||||||
require_command protoc "protobuf"
|
require_command protoc "protobuf"
|
||||||
@ -98,6 +121,15 @@ require_linkable "$(command -v protoc)" "protoc"
|
|||||||
if [[ -e /usr/lib/libclang.so ]]; then
|
if [[ -e /usr/lib/libclang.so ]]; then
|
||||||
require_linkable /usr/lib/libclang.so "libclang"
|
require_linkable /usr/lib/libclang.so "libclang"
|
||||||
fi
|
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
|
protoc --version >/dev/null
|
||||||
if ! run_as_user pactl info >/dev/null 2>&1; then
|
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."
|
echo "⚠️ pactl is installed, but no PulseAudio/PipeWire Pulse server is reachable right now."
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.11.26"
|
version = "0.11.27"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user