Reduce API volume for volume
The HUD no longer polls the volume API, and volume update requests are debounced -- thus reducing the volume of API requests for volume changes (ba dum tss)
This commit is contained in:
parent
68cdc16508
commit
0f3bc8f690
2 changed files with 102 additions and 14 deletions
|
|
@ -11,9 +11,8 @@ use gtk4::prelude::*;
|
||||||
use gtk4_layer_shell::{Edge, Layer, LayerShell};
|
use gtk4_layer_shell::{Edge, Layer, LayerShell};
|
||||||
use shepherd_api::Command;
|
use shepherd_api::Command;
|
||||||
use shepherd_ipc::IpcClient;
|
use shepherd_ipc::IpcClient;
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::rc::Rc;
|
use std::sync::mpsc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
|
|
@ -216,7 +215,44 @@ fn build_hud_content(state: SharedState) -> gtk4::Box {
|
||||||
volume_slider.set_value(info.percent as f64);
|
volume_slider.set_value(info.percent as f64);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle slider value changes
|
// Handle slider value changes with debouncing
|
||||||
|
// Create a channel for volume requests - the worker will debounce them
|
||||||
|
let (volume_tx, volume_rx) = mpsc::channel::<u8>();
|
||||||
|
|
||||||
|
// Spawn a dedicated volume worker thread that debounces requests
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
const DEBOUNCE_MS: u64 = 50; // Wait 50ms for more changes before sending
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Wait for first volume request
|
||||||
|
let Ok(mut latest_percent) = volume_rx.recv() else {
|
||||||
|
break; // Channel closed
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drain any pending requests, keeping only the latest value
|
||||||
|
// Use a short timeout to debounce rapid changes
|
||||||
|
loop {
|
||||||
|
match volume_rx.recv_timeout(std::time::Duration::from_millis(DEBOUNCE_MS)) {
|
||||||
|
Ok(percent) => {
|
||||||
|
latest_percent = percent; // Update to latest value
|
||||||
|
}
|
||||||
|
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||||
|
// No more changes for DEBOUNCE_MS, send the request
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||||
|
return; // Channel closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send only the final value
|
||||||
|
if let Err(e) = crate::volume::set_volume(latest_percent) {
|
||||||
|
tracing::error!("Failed to set volume: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let slider_changing = std::rc::Rc::new(std::cell::Cell::new(false));
|
let slider_changing = std::rc::Rc::new(std::cell::Cell::new(false));
|
||||||
let slider_changing_clone = slider_changing.clone();
|
let slider_changing_clone = slider_changing.clone();
|
||||||
|
|
||||||
|
|
@ -224,14 +260,10 @@ fn build_hud_content(state: SharedState) -> gtk4::Box {
|
||||||
slider_changing_clone.set(true);
|
slider_changing_clone.set(true);
|
||||||
let percent = value.clamp(0.0, 100.0) as u8;
|
let percent = value.clamp(0.0, 100.0) as u8;
|
||||||
|
|
||||||
// Update in background thread to avoid blocking UI
|
// Send to debounce worker (non-blocking)
|
||||||
std::thread::spawn(move || {
|
let _ = volume_tx.send(percent);
|
||||||
if let Err(e) = crate::volume::set_volume(percent) {
|
|
||||||
tracing::error!("Failed to set volume: {}", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Allow the slider to update
|
// Allow the slider to update immediately in UI
|
||||||
slider.set_value(value);
|
slider.set_value(value);
|
||||||
glib::Propagation::Stop
|
glib::Propagation::Stop
|
||||||
});
|
});
|
||||||
|
|
@ -371,8 +403,8 @@ fn build_hud_content(state: SharedState) -> gtk4::Box {
|
||||||
battery_label_clone.set_text("--%");
|
battery_label_clone.set_text("--%");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update volume from daemon
|
// Update volume from cached state (updated via events, no polling needed)
|
||||||
if let Some(volume) = crate::volume::get_volume_status() {
|
if let Some(volume) = state.volume_info() {
|
||||||
volume_button_clone.set_icon_name(volume.icon_name());
|
volume_button_clone.set_icon_name(volume.icon_name());
|
||||||
volume_label_clone.set_text(&format!("{}%", volume.percent));
|
volume_label_clone.set_text(&format!("{}%", volume.percent));
|
||||||
|
|
||||||
|
|
@ -555,9 +587,25 @@ fn run_event_loop(socket_path: PathBuf, state: SharedState) -> anyhow::Result<()
|
||||||
tracing::info!("Connecting to shepherdd at {:?}", socket_path);
|
tracing::info!("Connecting to shepherdd at {:?}", socket_path);
|
||||||
|
|
||||||
match IpcClient::connect(&socket_path).await {
|
match IpcClient::connect(&socket_path).await {
|
||||||
Ok(client) => {
|
Ok(mut client) => {
|
||||||
tracing::info!("Connected to shepherdd");
|
tracing::info!("Connected to shepherdd");
|
||||||
|
|
||||||
|
// Get initial volume before subscribing (can't send commands after subscribe)
|
||||||
|
match client.send(Command::GetVolume).await {
|
||||||
|
Ok(response) => {
|
||||||
|
if let shepherd_api::ResponseResult::Ok(
|
||||||
|
shepherd_api::ResponsePayload::Volume(info),
|
||||||
|
) = response.result
|
||||||
|
{
|
||||||
|
tracing::debug!("Got initial volume: {}%", info.percent);
|
||||||
|
state.set_initial_volume(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to get initial volume: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut stream = match client.subscribe().await {
|
let mut stream = match client.subscribe().await {
|
||||||
Ok(stream) => stream,
|
Ok(stream) => stream,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
//! The HUD subscribes to events from shepherdd and tracks session state.
|
//! The HUD subscribes to events from shepherdd and tracks session state.
|
||||||
|
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use shepherd_api::{Event, EventPayload, SessionEndReason};
|
use shepherd_api::{Event, EventPayload, SessionEndReason, VolumeInfo, VolumeRestrictions};
|
||||||
use shepherd_util::{EntryId, SessionId};
|
use shepherd_util::{EntryId, SessionId};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::watch;
|
use tokio::sync::watch;
|
||||||
|
|
@ -84,18 +84,25 @@ pub struct SharedState {
|
||||||
metrics_tx: Arc<watch::Sender<SystemMetrics>>,
|
metrics_tx: Arc<watch::Sender<SystemMetrics>>,
|
||||||
/// System metrics receiver
|
/// System metrics receiver
|
||||||
metrics_rx: watch::Receiver<SystemMetrics>,
|
metrics_rx: watch::Receiver<SystemMetrics>,
|
||||||
|
/// Volume info sender (updated via events, not polling)
|
||||||
|
volume_tx: Arc<watch::Sender<Option<VolumeInfo>>>,
|
||||||
|
/// Volume info receiver
|
||||||
|
volume_rx: watch::Receiver<Option<VolumeInfo>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SharedState {
|
impl SharedState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let (session_tx, session_rx) = watch::channel(SessionState::NoSession);
|
let (session_tx, session_rx) = watch::channel(SessionState::NoSession);
|
||||||
let (metrics_tx, metrics_rx) = watch::channel(SystemMetrics::default());
|
let (metrics_tx, metrics_rx) = watch::channel(SystemMetrics::default());
|
||||||
|
let (volume_tx, volume_rx) = watch::channel(None);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
session_tx: Arc::new(session_tx),
|
session_tx: Arc::new(session_tx),
|
||||||
session_rx,
|
session_rx,
|
||||||
metrics_tx: Arc::new(metrics_tx),
|
metrics_tx: Arc::new(metrics_tx),
|
||||||
metrics_rx,
|
metrics_rx,
|
||||||
|
volume_tx: Arc::new(volume_tx),
|
||||||
|
volume_rx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,6 +131,35 @@ impl SharedState {
|
||||||
let _ = self.metrics_tx.send(metrics);
|
let _ = self.metrics_tx.send(metrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get current volume info (cached from events)
|
||||||
|
pub fn volume_info(&self) -> Option<VolumeInfo> {
|
||||||
|
self.volume_rx.borrow().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set initial volume info (called once on connect)
|
||||||
|
pub fn set_initial_volume(&self, info: VolumeInfo) {
|
||||||
|
let _ = self.volume_tx.send(Some(info));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update volume from VolumeChanged event (preserves restrictions from initial fetch)
|
||||||
|
fn update_volume(&self, percent: u8, muted: bool) {
|
||||||
|
self.volume_tx.send_modify(|vol| {
|
||||||
|
if let Some(v) = vol {
|
||||||
|
v.percent = percent;
|
||||||
|
v.muted = muted;
|
||||||
|
} else {
|
||||||
|
// If we don't have initial volume yet, create a basic one
|
||||||
|
*vol = Some(VolumeInfo {
|
||||||
|
percent,
|
||||||
|
muted,
|
||||||
|
available: true,
|
||||||
|
backend: None,
|
||||||
|
restrictions: VolumeRestrictions::unrestricted(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Update time remaining for current session
|
/// Update time remaining for current session
|
||||||
pub fn update_time_remaining(&self, remaining_secs: u64) {
|
pub fn update_time_remaining(&self, remaining_secs: u64) {
|
||||||
self.session_tx.send_modify(|state| {
|
self.session_tx.send_modify(|state| {
|
||||||
|
|
@ -230,6 +266,10 @@ impl SharedState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EventPayload::VolumeChanged { percent, muted } => {
|
||||||
|
self.update_volume(*percent, *muted);
|
||||||
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue