use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;
use pipewire::context::ContextRc;
use pipewire::main_loop::MainLoopRc;
use pipewire::node::{Node, NodeInfoRef};
use pipewire::proxy::Listener;
use pipewire::spa::pod::{Pod, Value};
use pipewire::spa::sys::{SPA_PROP_channelVolumes, SPA_PROP_mute};
use pipewire::types::ObjectType;
use tablero::volume::{average_to_user_volume, device_kind_from_props};
fn parse_props(pod: &Pod) -> Option<(f32, bool)> {
let obj = pod.as_object().ok()?;
let channel_volumes_pod = obj.find_prop(pipewire::spa::utils::Id(SPA_PROP_channelVolumes))?;
let mute_pod = obj.find_prop(pipewire::spa::utils::Id(SPA_PROP_mute))?;
let value: Value = pipewire::spa::pod::deserialize::PodDeserializer::deserialize_any_from(
channel_volumes_pod.value().as_bytes(),
)
.ok()
.map(|(_, v)| v)?;
let user_volume = match &value {
Value::ValueArray(pipewire::spa::pod::ValueArray::Float(v)) => average_to_user_volume(v)?,
Value::Float(v) => average_to_user_volume(&[*v])?,
_ => return None,
};
let mute = mute_pod.value().get_bool().ok()?;
Some((user_volume, mute))
}
fn main() {
pipewire::init();
let mainloop = MainLoopRc::new(None).expect("main loop");
let context = ContextRc::new(&mainloop, None).expect("context");
let core = context.connect_rc(None).expect("core");
let registry = core.get_registry_rc().expect("registry");
let registry_weak = registry.downgrade();
let node_proxies: Rc<RefCell<Vec<Node>>> = Rc::new(RefCell::new(Vec::new()));
let node_proxies_for_cb = node_proxies.clone();
let node_listeners: Rc<RefCell<Vec<Box<dyn Listener>>>> = Rc::new(RefCell::new(Vec::new()));
let node_listeners_for_cb = node_listeners.clone();
let sink_count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let sink_count_for_cb = sink_count.clone();
let _registry_listener = registry
.add_listener_local()
.global(move |obj| {
if obj.type_ != ObjectType::Node {
return;
}
let Some(props) = obj.props else { return };
if props.get("media.class") != Some("Audio/Sink") {
return;
}
*sink_count_for_cb.borrow_mut() += 1;
let id = obj.id;
let name = props.get("node.name").unwrap_or("?").to_string();
let desc = props.get("node.description").unwrap_or("?").to_string();
println!(
"sink id={id} name={name:?} desc={desc:?} (registry-global props: icon={:?} form={:?})",
props.get("device.icon-name"),
props.get("device.form-factor"),
);
let Some(registry) = registry_weak.upgrade() else { return };
let node: Node = match registry.bind(obj) {
Ok(n) => n,
Err(e) => {
eprintln!(" bind failed: {e}");
return;
}
};
println!(" bound node {id}");
let node_listener = node
.add_listener_local()
.info(move |info: &NodeInfoRef| {
let info_props = info.props();
let kind = info_props
.map(device_kind_from_props)
.unwrap_or(tablero::widget::DeviceKind::Other);
let icon = info_props.and_then(|p| p.get("device.icon-name"));
let form = info_props.and_then(|p| p.get("device.form-factor"));
println!(
" info: state={:?} kind={kind:?} icon={icon:?} form={form:?}",
info.state()
);
})
.param(move |_seq, _id, _index, _next, param: Option<&Pod>| {
if let Some(p) = param {
if let Some((vol, mute)) = parse_props(p) {
println!(" param: volume={vol} mute={mute}");
} else {
println!(" param: (unparseable Props POD)");
}
}
})
.register();
node.subscribe_params(&[pipewire::spa::param::ParamType::Props]);
node.enum_params(
0,
Some(pipewire::spa::param::ParamType::Props),
0,
1,
);
node_listeners_for_cb
.borrow_mut()
.push(Box::new(node_listener));
node_proxies_for_cb.borrow_mut().push(node);
})
.register();
let ml_weak = mainloop.downgrade();
let timer = mainloop.loop_().add_timer(move |_| {
if let Some(ml) = ml_weak.upgrade() {
ml.quit();
}
});
timer.update_timer(Some(Duration::from_secs(2)), Some(Duration::from_secs(2)));
mainloop.run();
if *sink_count.borrow() == 0 {
eprintln!("No audio sinks found.");
}
}