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:
Albert Armea 2025-12-28 15:58:05 -05:00
parent 68cdc16508
commit 0f3bc8f690
2 changed files with 102 additions and 14 deletions

View file

@ -11,9 +11,8 @@ use gtk4::prelude::*;
use gtk4_layer_shell::{Edge, Layer, LayerShell};
use shepherd_api::Command;
use shepherd_ipc::IpcClient;
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::mpsc;
use std::time::Duration;
use tokio::runtime::Runtime;
@ -216,7 +215,44 @@ fn build_hud_content(state: SharedState) -> gtk4::Box {
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_clone = slider_changing.clone();
@ -224,14 +260,10 @@ fn build_hud_content(state: SharedState) -> gtk4::Box {
slider_changing_clone.set(true);
let percent = value.clamp(0.0, 100.0) as u8;
// Update in background thread to avoid blocking UI
std::thread::spawn(move || {
if let Err(e) = crate::volume::set_volume(percent) {
tracing::error!("Failed to set volume: {}", e);
}
});
// Send to debounce worker (non-blocking)
let _ = volume_tx.send(percent);
// Allow the slider to update
// Allow the slider to update immediately in UI
slider.set_value(value);
glib::Propagation::Stop
});
@ -371,8 +403,8 @@ fn build_hud_content(state: SharedState) -> gtk4::Box {
battery_label_clone.set_text("--%");
}
// Update volume from daemon
if let Some(volume) = crate::volume::get_volume_status() {
// Update volume from cached state (updated via events, no polling needed)
if let Some(volume) = state.volume_info() {
volume_button_clone.set_icon_name(volume.icon_name());
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);
match IpcClient::connect(&socket_path).await {
Ok(client) => {
Ok(mut client) => {
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 {
Ok(stream) => stream,
Err(e) => {

View file

@ -3,7 +3,7 @@
//! The HUD subscribes to events from shepherdd and tracks session state.
use chrono::Local;
use shepherd_api::{Event, EventPayload, SessionEndReason};
use shepherd_api::{Event, EventPayload, SessionEndReason, VolumeInfo, VolumeRestrictions};
use shepherd_util::{EntryId, SessionId};
use std::sync::Arc;
use tokio::sync::watch;
@ -84,18 +84,25 @@ pub struct SharedState {
metrics_tx: Arc<watch::Sender<SystemMetrics>>,
/// System metrics receiver
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 {
pub fn new() -> Self {
let (session_tx, session_rx) = watch::channel(SessionState::NoSession);
let (metrics_tx, metrics_rx) = watch::channel(SystemMetrics::default());
let (volume_tx, volume_rx) = watch::channel(None);
Self {
session_tx: Arc::new(session_tx),
session_rx,
metrics_tx: Arc::new(metrics_tx),
metrics_rx,
volume_tx: Arc::new(volume_tx),
volume_rx,
}
}
@ -124,6 +131,35 @@ impl SharedState {
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
pub fn update_time_remaining(&self, remaining_secs: u64) {
self.session_tx.send_modify(|state| {
@ -230,6 +266,10 @@ impl SharedState {
}
}
EventPayload::VolumeChanged { percent, muted } => {
self.update_volume(*percent, *muted);
}
_ => {}
}
}