lesavka: refresh staged devices and steady diagnostics
This commit is contained in:
parent
c24bef1bf2
commit
ba443371e6
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.11.24"
|
version = "0.11.25"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -13,7 +13,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
const CAMERA_PREVIEW_WIDTH: i32 = 360;
|
const CAMERA_PREVIEW_WIDTH: i32 = 360;
|
||||||
const CAMERA_PREVIEW_HEIGHT: i32 = 202;
|
const CAMERA_PREVIEW_HEIGHT: i32 = 202;
|
||||||
const CAMERA_PREVIEW_IDLE: &str = "Select a camera and click Start Preview.";
|
const CAMERA_PREVIEW_IDLE: &str = "Select a webcam and click Start Preview.";
|
||||||
const MIC_MONITOR_RATE: i32 = 16_000;
|
const MIC_MONITOR_RATE: i32 = 16_000;
|
||||||
const MIC_MONITOR_CHANNELS: i32 = 1;
|
const MIC_MONITOR_CHANNELS: i32 = 1;
|
||||||
const MIC_MONITOR_SAMPLE_BYTES: usize = 2;
|
const MIC_MONITOR_SAMPLE_BYTES: usize = 2;
|
||||||
@ -345,7 +345,7 @@ impl LocalCameraPreview {
|
|||||||
|
|
||||||
self.set_status(match self.selected_device.as_deref() {
|
self.set_status(match self.selected_device.as_deref() {
|
||||||
Some(camera) => format!(
|
Some(camera) => format!(
|
||||||
"Selected {camera}. Start Preview to confirm framing here before you launch the relay."
|
"Selected {camera}. Start Preview to confirm webcam framing here before you launch the relay."
|
||||||
),
|
),
|
||||||
None => CAMERA_PREVIEW_IDLE.to_string(),
|
None => CAMERA_PREVIEW_IDLE.to_string(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -13,7 +13,7 @@ use {
|
|||||||
BreakoutSizePreset, CapturePowerStatus, CaptureSizePreset, DisplaySurface,
|
BreakoutSizePreset, CapturePowerStatus, CaptureSizePreset, DisplaySurface,
|
||||||
FeedSourcePreset, InputRouting, LauncherState,
|
FeedSourcePreset, InputRouting, LauncherState,
|
||||||
},
|
},
|
||||||
super::ui_components::build_launcher_view,
|
super::ui_components::{build_launcher_view, sync_input_device_combo, sync_stage_device_combo},
|
||||||
super::ui_runtime::{
|
super::ui_runtime::{
|
||||||
RelayChild, append_session_log, apply_popout_window_size, attach_child_log_streams,
|
RelayChild, append_session_log, apply_popout_window_size, attach_child_log_streams,
|
||||||
capture_swap_key, copy_session_log, dock_all_displays_to_preview, dock_display_to_preview,
|
capture_swap_key, copy_session_log, dock_all_displays_to_preview, dock_display_to_preview,
|
||||||
@ -125,6 +125,21 @@ impl NetworkTelemetry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
fn retained_stage_selection(current: Option<&str>, values: &[String]) -> Option<String> {
|
||||||
|
current
|
||||||
|
.filter(|selected| values.iter().any(|value| value == *selected))
|
||||||
|
.map(str::to_string)
|
||||||
|
.or_else(|| values.first().cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
fn retained_input_selection(current: Option<&str>, values: &[String]) -> Option<String> {
|
||||||
|
current
|
||||||
|
.filter(|selected| values.iter().any(|value| value == *selected))
|
||||||
|
.map(str::to_string)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
fn network_spread_ms(samples: &VecDeque<(Instant, f32)>) -> f32 {
|
fn network_spread_ms(samples: &VecDeque<(Instant, f32)>) -> f32 {
|
||||||
if samples.len() < 2 {
|
if samples.len() < 2 {
|
||||||
@ -1040,6 +1055,104 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let state = Rc::clone(&state);
|
||||||
|
let widgets = widgets.clone();
|
||||||
|
let widgets_handle = widgets.clone();
|
||||||
|
let child_proc = Rc::clone(&child_proc);
|
||||||
|
let tests = Rc::clone(&tests);
|
||||||
|
let camera_combo = camera_combo.clone();
|
||||||
|
let microphone_combo = microphone_combo.clone();
|
||||||
|
let speaker_combo = speaker_combo.clone();
|
||||||
|
let keyboard_combo = keyboard_combo.clone();
|
||||||
|
let mouse_combo = mouse_combo.clone();
|
||||||
|
widgets.device_refresh_button.connect_clicked(move |_| {
|
||||||
|
let catalog = DeviceCatalog::discover();
|
||||||
|
let (
|
||||||
|
selected_camera,
|
||||||
|
selected_microphone,
|
||||||
|
selected_speaker,
|
||||||
|
selected_keyboard,
|
||||||
|
selected_mouse,
|
||||||
|
) = {
|
||||||
|
let state = state.borrow();
|
||||||
|
(
|
||||||
|
retained_stage_selection(
|
||||||
|
state.devices.camera.as_deref(),
|
||||||
|
&catalog.cameras,
|
||||||
|
),
|
||||||
|
retained_stage_selection(
|
||||||
|
state.devices.microphone.as_deref(),
|
||||||
|
&catalog.microphones,
|
||||||
|
),
|
||||||
|
retained_stage_selection(
|
||||||
|
state.devices.speaker.as_deref(),
|
||||||
|
&catalog.speakers,
|
||||||
|
),
|
||||||
|
retained_input_selection(
|
||||||
|
state.devices.keyboard.as_deref(),
|
||||||
|
&catalog.keyboards,
|
||||||
|
),
|
||||||
|
retained_input_selection(state.devices.mouse.as_deref(), &catalog.mice),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
{
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state.select_camera(selected_camera);
|
||||||
|
state.select_microphone(selected_microphone);
|
||||||
|
state.select_speaker(selected_speaker);
|
||||||
|
state.select_keyboard(selected_keyboard);
|
||||||
|
state.select_mouse(selected_mouse);
|
||||||
|
}
|
||||||
|
let state_snapshot = state.borrow().clone();
|
||||||
|
sync_stage_device_combo(
|
||||||
|
&camera_combo,
|
||||||
|
&catalog.cameras,
|
||||||
|
state_snapshot.devices.camera.as_deref(),
|
||||||
|
);
|
||||||
|
sync_stage_device_combo(
|
||||||
|
µphone_combo,
|
||||||
|
&catalog.microphones,
|
||||||
|
state_snapshot.devices.microphone.as_deref(),
|
||||||
|
);
|
||||||
|
sync_stage_device_combo(
|
||||||
|
&speaker_combo,
|
||||||
|
&catalog.speakers,
|
||||||
|
state_snapshot.devices.speaker.as_deref(),
|
||||||
|
);
|
||||||
|
sync_input_device_combo(
|
||||||
|
&keyboard_combo,
|
||||||
|
&catalog.keyboards,
|
||||||
|
state_snapshot.devices.keyboard.as_deref(),
|
||||||
|
"all keyboards",
|
||||||
|
);
|
||||||
|
sync_input_device_combo(
|
||||||
|
&mouse_combo,
|
||||||
|
&catalog.mice,
|
||||||
|
state_snapshot.devices.mouse.as_deref(),
|
||||||
|
"all mice",
|
||||||
|
);
|
||||||
|
if let Err(err) = tests
|
||||||
|
.borrow_mut()
|
||||||
|
.set_camera_selection(state_snapshot.devices.camera.as_deref())
|
||||||
|
{
|
||||||
|
widgets_handle
|
||||||
|
.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.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
refresh_launcher_ui(
|
||||||
|
&widgets_handle,
|
||||||
|
&state.borrow(),
|
||||||
|
child_proc.borrow().is_some(),
|
||||||
|
);
|
||||||
|
refresh_test_buttons(&widgets_handle, &mut tests.borrow_mut());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let state = Rc::clone(&state);
|
let state = Rc::clone(&state);
|
||||||
let child_proc = Rc::clone(&child_proc);
|
let child_proc = Rc::clone(&child_proc);
|
||||||
|
|||||||
@ -55,6 +55,7 @@ pub struct LauncherWidgets {
|
|||||||
pub diagnostics_buffer: gtk::TextBuffer,
|
pub diagnostics_buffer: gtk::TextBuffer,
|
||||||
pub diagnostics_scroll: gtk::ScrolledWindow,
|
pub diagnostics_scroll: gtk::ScrolledWindow,
|
||||||
pub diagnostics_popout_scroll: Rc<RefCell<Option<gtk::ScrolledWindow>>>,
|
pub diagnostics_popout_scroll: Rc<RefCell<Option<gtk::ScrolledWindow>>>,
|
||||||
|
pub diagnostics_rendered_text: Rc<RefCell<String>>,
|
||||||
pub session_log_buffer: gtk::TextBuffer,
|
pub session_log_buffer: gtk::TextBuffer,
|
||||||
pub session_log_view: gtk::TextView,
|
pub session_log_view: gtk::TextView,
|
||||||
pub summary: SummaryWidgets,
|
pub summary: SummaryWidgets,
|
||||||
@ -70,6 +71,7 @@ pub struct LauncherWidgets {
|
|||||||
pub clipboard_button: gtk::Button,
|
pub clipboard_button: gtk::Button,
|
||||||
pub probe_button: gtk::Button,
|
pub probe_button: gtk::Button,
|
||||||
pub usb_recover_button: gtk::Button,
|
pub usb_recover_button: gtk::Button,
|
||||||
|
pub device_refresh_button: gtk::Button,
|
||||||
pub swap_key_button: gtk::Button,
|
pub swap_key_button: gtk::Button,
|
||||||
pub camera_test_button: gtk::Button,
|
pub camera_test_button: gtk::Button,
|
||||||
pub microphone_test_button: gtk::Button,
|
pub microphone_test_button: gtk::Button,
|
||||||
@ -201,6 +203,15 @@ pub fn build_launcher_view(
|
|||||||
devices_panel.set_hexpand(true);
|
devices_panel.set_hexpand(true);
|
||||||
devices_panel.set_vexpand(false);
|
devices_panel.set_vexpand(false);
|
||||||
devices_body.set_spacing(8);
|
devices_body.set_spacing(8);
|
||||||
|
let devices_actions = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
|
devices_actions.set_halign(gtk::Align::End);
|
||||||
|
let device_refresh_button = gtk::Button::with_label("Refresh Devices");
|
||||||
|
stabilize_button(&device_refresh_button, 132);
|
||||||
|
device_refresh_button.set_tooltip_text(Some(
|
||||||
|
"Re-scan webcams, microphones, speakers, keyboards, and mice without restarting Lesavka.",
|
||||||
|
));
|
||||||
|
devices_actions.append(&device_refresh_button);
|
||||||
|
devices_body.append(&devices_actions);
|
||||||
|
|
||||||
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_row = gtk::Box::new(gtk::Orientation::Horizontal, 12);
|
||||||
@ -307,7 +318,7 @@ pub fn build_launcher_view(
|
|||||||
devices_body.append(&media_group);
|
devices_body.append(&media_group);
|
||||||
staging_row.append(&devices_panel);
|
staging_row.append(&devices_panel);
|
||||||
|
|
||||||
let (preview_panel, preview_body) = build_panel("Selected Camera Preview");
|
let (preview_panel, preview_body) = build_panel("Webcam Test");
|
||||||
preview_panel.set_hexpand(true);
|
preview_panel.set_hexpand(true);
|
||||||
preview_panel.set_vexpand(false);
|
preview_panel.set_vexpand(false);
|
||||||
preview_body.set_spacing(6);
|
preview_body.set_spacing(6);
|
||||||
@ -321,7 +332,7 @@ pub fn build_launcher_view(
|
|||||||
);
|
);
|
||||||
camera_preview.set_keep_aspect_ratio(true);
|
camera_preview.set_keep_aspect_ratio(true);
|
||||||
camera_preview.add_css_class("camera-preview-frame");
|
camera_preview.add_css_class("camera-preview-frame");
|
||||||
let camera_status = gtk::Label::new(Some("Select a camera and click Start Preview."));
|
let camera_status = gtk::Label::new(Some("Select a webcam and click Start Preview."));
|
||||||
camera_status.add_css_class("dim-label");
|
camera_status.add_css_class("dim-label");
|
||||||
camera_status.set_wrap(true);
|
camera_status.set_wrap(true);
|
||||||
camera_status.set_xalign(0.0);
|
camera_status.set_xalign(0.0);
|
||||||
@ -464,7 +475,7 @@ pub fn build_launcher_view(
|
|||||||
diagnostics_view.set_editable(false);
|
diagnostics_view.set_editable(false);
|
||||||
diagnostics_view.set_cursor_visible(false);
|
diagnostics_view.set_cursor_visible(false);
|
||||||
diagnostics_view.set_monospace(true);
|
diagnostics_view.set_monospace(true);
|
||||||
diagnostics_view.set_wrap_mode(gtk::WrapMode::WordChar);
|
diagnostics_view.set_wrap_mode(gtk::WrapMode::None);
|
||||||
let diagnostics_scroll = gtk::ScrolledWindow::builder()
|
let diagnostics_scroll = gtk::ScrolledWindow::builder()
|
||||||
.hexpand(true)
|
.hexpand(true)
|
||||||
.vexpand(false)
|
.vexpand(false)
|
||||||
@ -629,6 +640,7 @@ pub fn build_launcher_view(
|
|||||||
diagnostics_buffer: diagnostics_buffer.clone(),
|
diagnostics_buffer: diagnostics_buffer.clone(),
|
||||||
diagnostics_scroll: diagnostics_scroll.clone(),
|
diagnostics_scroll: diagnostics_scroll.clone(),
|
||||||
diagnostics_popout_scroll: diagnostics_popout_scroll.clone(),
|
diagnostics_popout_scroll: diagnostics_popout_scroll.clone(),
|
||||||
|
diagnostics_rendered_text: Rc::new(RefCell::new(String::new())),
|
||||||
session_log_buffer: session_log_buffer.clone(),
|
session_log_buffer: session_log_buffer.clone(),
|
||||||
session_log_view: session_log_view.clone(),
|
session_log_view: session_log_view.clone(),
|
||||||
summary: SummaryWidgets {
|
summary: SummaryWidgets {
|
||||||
@ -652,6 +664,7 @@ pub fn build_launcher_view(
|
|||||||
clipboard_button: clipboard_button.clone(),
|
clipboard_button: clipboard_button.clone(),
|
||||||
probe_button: probe_button.clone(),
|
probe_button: probe_button.clone(),
|
||||||
usb_recover_button: usb_recover_button.clone(),
|
usb_recover_button: usb_recover_button.clone(),
|
||||||
|
device_refresh_button: device_refresh_button.clone(),
|
||||||
swap_key_button: swap_key_button.clone(),
|
swap_key_button: swap_key_button.clone(),
|
||||||
camera_test_button: camera_test_button.clone(),
|
camera_test_button: camera_test_button.clone(),
|
||||||
microphone_test_button: microphone_test_button.clone(),
|
microphone_test_button: microphone_test_button.clone(),
|
||||||
@ -976,6 +989,33 @@ pub fn sync_breakout_size_combo(
|
|||||||
combo.set_active_id(Some(selected.as_id()));
|
combo.set_active_id(Some(selected.as_id()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sync_stage_device_combo(
|
||||||
|
combo: >k::ComboBoxText,
|
||||||
|
values: &[String],
|
||||||
|
selected: Option<&str>,
|
||||||
|
) {
|
||||||
|
combo.remove_all();
|
||||||
|
combo.append(Some("auto"), "auto");
|
||||||
|
for value in values {
|
||||||
|
append_stage_choice(combo, value);
|
||||||
|
}
|
||||||
|
super::ui_runtime::set_combo_active_text(combo, selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sync_input_device_combo(
|
||||||
|
combo: >k::ComboBoxText,
|
||||||
|
values: &[String],
|
||||||
|
selected: Option<&str>,
|
||||||
|
all_label: &str,
|
||||||
|
) {
|
||||||
|
combo.remove_all();
|
||||||
|
combo.append(Some("all"), all_label);
|
||||||
|
for value in values {
|
||||||
|
append_input_choice(combo, value);
|
||||||
|
}
|
||||||
|
super::ui_runtime::set_combo_active_text(combo, selected);
|
||||||
|
}
|
||||||
|
|
||||||
fn attach_device_row(
|
fn attach_device_row(
|
||||||
grid: >k::Grid,
|
grid: >k::Grid,
|
||||||
row: i32,
|
row: i32,
|
||||||
|
|||||||
@ -89,6 +89,7 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
|
|||||||
widgets
|
widgets
|
||||||
.usb_recover_button
|
.usb_recover_button
|
||||||
.set_sensitive(state.server_available);
|
.set_sensitive(state.server_available);
|
||||||
|
widgets.device_refresh_button.set_sensitive(true);
|
||||||
widgets.input_toggle_button.set_label("Change Routing");
|
widgets.input_toggle_button.set_label("Change Routing");
|
||||||
widgets
|
widgets
|
||||||
.input_toggle_button
|
.input_toggle_button
|
||||||
@ -947,6 +948,22 @@ pub fn refresh_diagnostics_report(
|
|||||||
state: &LauncherState,
|
state: &LauncherState,
|
||||||
child_running: bool,
|
child_running: bool,
|
||||||
) {
|
) {
|
||||||
|
let mut snapshot = SnapshotReport::from_state(
|
||||||
|
state,
|
||||||
|
&widgets.diagnostics_log.borrow(),
|
||||||
|
quality_probe_command().to_string(),
|
||||||
|
);
|
||||||
|
if child_running && !snapshot.remote_active {
|
||||||
|
snapshot.recommendations.insert(
|
||||||
|
0,
|
||||||
|
"The relay child is still alive while launcher state says inactive; give it a moment or reconnect before trusting throughput feel.".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let rendered = snapshot.to_pretty_text();
|
||||||
|
if *widgets.diagnostics_rendered_text.borrow() == rendered {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let diagnostics_adjustment = widgets.diagnostics_scroll.vadjustment();
|
let diagnostics_adjustment = widgets.diagnostics_scroll.vadjustment();
|
||||||
let previous_value = diagnostics_adjustment.value();
|
let previous_value = diagnostics_adjustment.value();
|
||||||
let previous_max =
|
let previous_max =
|
||||||
@ -963,36 +980,28 @@ pub fn refresh_diagnostics_report(
|
|||||||
let was_at_bottom = previous_max <= 0.0 || previous_value >= (previous_max - 4.0);
|
let was_at_bottom = previous_max <= 0.0 || previous_value >= (previous_max - 4.0);
|
||||||
(adjustment.clone(), previous_value, was_at_bottom)
|
(adjustment.clone(), previous_value, was_at_bottom)
|
||||||
});
|
});
|
||||||
let mut snapshot = SnapshotReport::from_state(
|
let restore_adjustment =
|
||||||
state,
|
|adjustment: >k::Adjustment, previous_value: f64, was_at_bottom: bool| {
|
||||||
&widgets.diagnostics_log.borrow(),
|
|
||||||
quality_probe_command().to_string(),
|
|
||||||
);
|
|
||||||
if child_running && !snapshot.remote_active {
|
|
||||||
snapshot.recommendations.insert(
|
|
||||||
0,
|
|
||||||
"The relay child is still alive while launcher state says inactive; give it a moment or reconnect before trusting throughput feel.".to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
widgets
|
|
||||||
.diagnostics_buffer
|
|
||||||
.set_text(&snapshot.to_pretty_text());
|
|
||||||
glib::idle_add_local_once(move || {
|
|
||||||
let max = (diagnostics_adjustment.upper() - diagnostics_adjustment.page_size()).max(0.0);
|
|
||||||
let target = if was_at_bottom {
|
|
||||||
max
|
|
||||||
} else {
|
|
||||||
previous_value.min(max)
|
|
||||||
};
|
|
||||||
diagnostics_adjustment.set_value(target);
|
|
||||||
if let Some((adjustment, previous_value, was_at_bottom)) = popout_state {
|
|
||||||
let max = (adjustment.upper() - adjustment.page_size()).max(0.0);
|
let max = (adjustment.upper() - adjustment.page_size()).max(0.0);
|
||||||
let target = if was_at_bottom {
|
let target = if was_at_bottom {
|
||||||
max
|
max
|
||||||
} else {
|
} else {
|
||||||
previous_value.min(max)
|
previous_value.min(max)
|
||||||
};
|
};
|
||||||
adjustment.set_value(target);
|
if (adjustment.value() - target).abs() > 1.0 {
|
||||||
|
adjustment.set_value(target);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
*widgets.diagnostics_rendered_text.borrow_mut() = rendered.clone();
|
||||||
|
widgets.diagnostics_buffer.set_text(&rendered);
|
||||||
|
restore_adjustment(&diagnostics_adjustment, previous_value, was_at_bottom);
|
||||||
|
if let Some((adjustment, previous_value, was_at_bottom)) = popout_state.as_ref() {
|
||||||
|
restore_adjustment(adjustment, *previous_value, *was_at_bottom);
|
||||||
|
}
|
||||||
|
glib::idle_add_local_once(move || {
|
||||||
|
restore_adjustment(&diagnostics_adjustment, previous_value, was_at_bottom);
|
||||||
|
if let Some((adjustment, previous_value, was_at_bottom)) = popout_state {
|
||||||
|
restore_adjustment(&adjustment, previous_value, was_at_bottom);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1002,7 +1011,15 @@ pub fn open_session_log_popout(
|
|||||||
handle: &Rc<RefCell<Option<gtk::ApplicationWindow>>>,
|
handle: &Rc<RefCell<Option<gtk::ApplicationWindow>>>,
|
||||||
buffer: >k::TextBuffer,
|
buffer: >k::TextBuffer,
|
||||||
) {
|
) {
|
||||||
open_text_buffer_popout(app, handle, None, buffer, "Lesavka Log", "Copy Log");
|
open_text_buffer_popout(
|
||||||
|
app,
|
||||||
|
handle,
|
||||||
|
None,
|
||||||
|
buffer,
|
||||||
|
"Lesavka Log",
|
||||||
|
"Copy Log",
|
||||||
|
gtk::WrapMode::WordChar,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_diagnostics_popout(
|
pub fn open_diagnostics_popout(
|
||||||
@ -1018,6 +1035,7 @@ pub fn open_diagnostics_popout(
|
|||||||
buffer,
|
buffer,
|
||||||
"Lesavka Diagnostics",
|
"Lesavka Diagnostics",
|
||||||
"Copy Report",
|
"Copy Report",
|
||||||
|
gtk::WrapMode::None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1028,6 +1046,7 @@ fn open_text_buffer_popout(
|
|||||||
buffer: >k::TextBuffer,
|
buffer: >k::TextBuffer,
|
||||||
title: &str,
|
title: &str,
|
||||||
copy_button_label: &str,
|
copy_button_label: &str,
|
||||||
|
wrap_mode: gtk::WrapMode,
|
||||||
) {
|
) {
|
||||||
if let Some(window) = handle.borrow().as_ref() {
|
if let Some(window) = handle.borrow().as_ref() {
|
||||||
window.present();
|
window.present();
|
||||||
@ -1059,7 +1078,7 @@ fn open_text_buffer_popout(
|
|||||||
view.set_editable(false);
|
view.set_editable(false);
|
||||||
view.set_cursor_visible(false);
|
view.set_cursor_visible(false);
|
||||||
view.set_monospace(true);
|
view.set_monospace(true);
|
||||||
view.set_wrap_mode(gtk::WrapMode::WordChar);
|
view.set_wrap_mode(wrap_mode);
|
||||||
let scroll = gtk::ScrolledWindow::builder()
|
let scroll = gtk::ScrolledWindow::builder()
|
||||||
.hexpand(true)
|
.hexpand(true)
|
||||||
.vexpand(true)
|
.vexpand(true)
|
||||||
|
|||||||
@ -26,15 +26,21 @@ impl AudioOut {
|
|||||||
let mut pipe = format!(
|
let mut pipe = format!(
|
||||||
"appsrc name=src is-live=true format=time do-timestamp=true \
|
"appsrc name=src is-live=true format=time do-timestamp=true \
|
||||||
block=false ! \
|
block=false ! \
|
||||||
queue leaky=downstream ! \
|
queue max-size-time=500000000 max-size-bytes=0 max-size-buffers=0 ! \
|
||||||
aacparse ! avdec_aac ! audioresample ! audioconvert ! {}",
|
aacparse ! avdec_aac ! \
|
||||||
|
audioconvert ! audioresample ! \
|
||||||
|
audio/x-raw,format=S16LE,channels=2,rate=48000 ! \
|
||||||
|
queue max-size-time=400000000 max-size-bytes=0 max-size-buffers=0 ! {}",
|
||||||
sink,
|
sink,
|
||||||
);
|
);
|
||||||
if tee_dump {
|
if tee_dump {
|
||||||
pipe = format!(
|
pipe = format!(
|
||||||
"appsrc name=src is-live=true format=time do-timestamp=true block=false ! \
|
"appsrc name=src is-live=true format=time do-timestamp=true block=false ! \
|
||||||
tee name=t ! \
|
tee name=t ! \
|
||||||
queue leaky=downstream ! aacparse ! avdec_aac ! audioresample ! audioconvert ! {} \
|
queue max-size-time=500000000 max-size-bytes=0 max-size-buffers=0 ! \
|
||||||
|
aacparse ! avdec_aac ! audioconvert ! audioresample ! \
|
||||||
|
audio/x-raw,format=S16LE,channels=2,rate=48000 ! \
|
||||||
|
queue max-size-time=400000000 max-size-bytes=0 max-size-buffers=0 ! {} \
|
||||||
t. ! queue ! filesink location=/tmp/lesavka-audio.aac",
|
t. ! queue ! filesink location=/tmp/lesavka-audio.aac",
|
||||||
sink,
|
sink,
|
||||||
);
|
);
|
||||||
@ -177,7 +183,14 @@ fn normalize_sink_override(raw: &str) -> String {
|
|||||||
|
|
||||||
fn pulsesink_device_element(device: &str) -> String {
|
fn pulsesink_device_element(device: &str) -> String {
|
||||||
let escaped = device.replace('\\', "\\\\").replace('"', "\\\"");
|
let escaped = device.replace('\\', "\\\\").replace('"', "\\\"");
|
||||||
format!("pulsesink device=\"{escaped}\"")
|
let (buffer_time, latency_time) = if device.starts_with("bluez_output.") {
|
||||||
|
(750_000, 250_000)
|
||||||
|
} else {
|
||||||
|
(350_000, 100_000)
|
||||||
|
};
|
||||||
|
format!(
|
||||||
|
"pulsesink device=\"{escaped}\" buffer-time={buffer_time} latency-time={latency_time} sync=true"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_pw_sinks() -> Vec<(String, String)> {
|
fn list_pw_sinks() -> Vec<(String, String)> {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.11.24"
|
version = "0.11.25"
|
||||||
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.24"), "lesavka-common CLI (v0.11.24)");
|
assert_eq!(banner("0.11.25"), "lesavka-common CLI (v0.11.25)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.11.24"
|
version = "0.11.25"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -56,7 +56,7 @@ mod audio_include_contract {
|
|||||||
let sink = pick_sink_element().expect("device sink");
|
let sink = pick_sink_element().expect("device sink");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sink,
|
sink,
|
||||||
"pulsesink device=\"alsa_output.pci-0000_00_1f.3.analog-stereo\""
|
"pulsesink device=\"alsa_output.pci-0000_00_1f.3.analog-stereo\" buffer-time=350000 latency-time=100000 sync=true"
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -86,12 +86,28 @@ exit 0
|
|||||||
let sink = pick_sink_element().expect("pick sink");
|
let sink = pick_sink_element().expect("pick sink");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sink,
|
sink,
|
||||||
"pulsesink device=\"alsa_output.usb-DAC_1234-00.analog-stereo\""
|
"pulsesink device=\"alsa_output.usb-DAC_1234-00.analog-stereo\" buffer-time=350000 latency-time=100000 sync=true"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn bluetooth_sink_override_uses_more_headroom() {
|
||||||
|
with_var(
|
||||||
|
"LESAVKA_AUDIO_SINK",
|
||||||
|
Some("bluez_output.80_C3_BA_76_26_AB.1"),
|
||||||
|
|| {
|
||||||
|
let sink = pick_sink_element().expect("bluetooth sink");
|
||||||
|
assert_eq!(
|
||||||
|
sink,
|
||||||
|
"pulsesink device=\"bluez_output.80_C3_BA_76_26_AB.1\" buffer-time=750000 latency-time=250000 sync=true"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn pick_sink_element_falls_back_to_autoaudiosink_without_pactl_default() {
|
fn pick_sink_element_falls_back_to_autoaudiosink_without_pactl_default() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user