use blit_remote::{AUDIO_FRAME_CODEC_OPUS, S2C_AUDIO_FRAME};
use opus::{Application, Channels, Encoder as OpusEncoder};
use std::collections::{HashMap, VecDeque};
use std::io::BufRead;
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
fn pdeathsig_hook() -> impl FnMut() -> std::io::Result<()> {
|| unsafe {
if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
}
const FRAME_SAMPLES: usize = 960;
const CHANNELS: usize = 2;
const FRAME_FLOATS: usize = FRAME_SAMPLES * CHANNELS;
const MAX_OPUS_PACKET: usize = 4000;
pub const DEFAULT_BITRATE: i32 = 64_000;
const RING_CAPACITY: usize = 10;
const HEAL_COOLDOWN: Duration = Duration::from_secs(1);
const MAX_HEALS: u32 = 5;
const HEAL_WINDOW: Duration = Duration::from_secs(30);
#[derive(Clone)]
pub struct OpusFrame {
pub timestamp: u32,
pub data: Vec<u8>,
}
pub struct AudioBroadcast {
subscribers: std::sync::Mutex<HashMap<u64, mpsc::UnboundedSender<Vec<u8>>>>,
ring: std::sync::Mutex<VecDeque<OpusFrame>>,
has_listener: Arc<AtomicBool>,
}
impl AudioBroadcast {
pub fn new() -> Arc<Self> {
Arc::new(Self {
subscribers: std::sync::Mutex::new(HashMap::new()),
ring: std::sync::Mutex::new(VecDeque::with_capacity(RING_CAPACITY)),
has_listener: Arc::new(AtomicBool::new(false)),
})
}
pub fn subscribe(&self, id: u64, tx: mpsc::UnboundedSender<Vec<u8>>) {
let ring_guard = self.ring.lock().unwrap();
for frame in ring_guard.iter() {
let _ = tx.send(msg_audio_frame(frame));
}
let mut subs = self.subscribers.lock().unwrap();
subs.insert(id, tx);
self.has_listener.store(true, Ordering::Release);
}
pub fn unsubscribe(&self, id: u64) {
let mut subs = self.subscribers.lock().unwrap();
subs.remove(&id);
if subs.is_empty() {
self.has_listener.store(false, Ordering::Release);
}
}
fn has_listener_flag(&self) -> Arc<AtomicBool> {
self.has_listener.clone()
}
}
async fn fanout_task(mut opus_rx: mpsc::Receiver<OpusFrame>, broadcast: Arc<AudioBroadcast>) {
while let Some(frame) = opus_rx.recv().await {
let msg = msg_audio_frame(&frame);
{
let mut ring = broadcast.ring.lock().unwrap();
if ring.len() >= RING_CAPACITY {
ring.pop_front();
}
ring.push_back(frame);
}
let subs = broadcast.subscribers.lock().unwrap();
for tx in subs.values() {
let _ = tx.send(msg.clone());
}
}
}
pub struct AudioPipeline {
dbus_child: Child,
pipewire_child: Child,
wireplumber_child: Option<Child>,
pipewire_pulse_child: Child,
capture: Option<crate::audio_pw::Capture>,
pub runtime_dir: PathBuf,
alive: bool,
bitrate_tx: tokio::sync::watch::Sender<i32>,
encoder_alive: Arc<AtomicBool>,
dbus_address: String,
verbose: bool,
last_heal: Option<Instant>,
first_heal_at: Option<Instant>,
heals: u32,
}
const PIPEWIRE_CONF_TEMPLATE: &str = r#"
context.properties = {
core.daemon = true
core.name = pipewire-0
default.clock.rate = 48000
}
context.spa-libs = {
audio.convert.* = audioconvert/libspa-audioconvert
support.* = support/libspa-support
}
context.modules = [
{ name = libpipewire-module-protocol-native }
{ name = libpipewire-module-access }
{ name = libpipewire-module-client-node }
{ name = libpipewire-module-adapter }
{ name = libpipewire-module-link-factory }
{ name = libpipewire-module-metadata }
{ name = libpipewire-module-spa-node-factory }
]
context.objects = [
{ factory = adapter
args = {
factory.name = support.null-audio-sink
node.name = blit-sink
media.class = Audio/Sink
object.linger = true
audio.position = [ FL FR ]
audio.rate = 48000
monitor.channel-volumes = true
monitor.passthrough = true
}
}
]
"#;
const WIREPLUMBER_CONF_TEMPLATE: &str = r#"
wireplumber.profiles = {
main = {
support.dbus = disabled
support.portal-permissionstore = disabled
support.reserve-device = disabled
# hardware.audio stays enabled — its policy.node links streams to sinks.
hardware.bluetooth = disabled
hardware.video-capture = disabled
monitor.alsa = disabled
monitor.alsa.reserve-device = disabled
monitor.bluez = disabled
monitor.bluez.midi = disabled
monitor.bluez.seat-monitoring = disabled
monitor.libcamera = disabled
monitor.v4l2 = disabled
}
}
"#;
fn find_program(name: &str) -> Option<PathBuf> {
let path_var = std::env::var("PATH").unwrap_or_default();
for dir in path_var.split(':') {
let candidate = Path::new(dir).join(name);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
pub fn pipewire_available() -> bool {
missing_pipewire_binaries().is_empty() && crate::audio_pw::available()
}
pub fn missing_pipewire_binaries() -> Vec<&'static str> {
["pipewire", "pipewire-pulse", "dbus-daemon"]
.into_iter()
.filter(|name| find_program(name).is_none())
.collect()
}
fn wait_for_socket(path: &Path, timeout: std::time::Duration) -> bool {
let start = Instant::now();
while start.elapsed() < timeout {
if path.exists() {
return true;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
false
}
impl AudioPipeline {
pub fn spawn(
runtime_dir: &Path,
instance_id: u16,
bitrate: i32,
verbose: bool,
epoch: Instant,
broadcast: Arc<AudioBroadcast>,
) -> Result<Self, String> {
let audio_dir = runtime_dir.join(format!("blit-audio-{instance_id}"));
if audio_dir.exists() {
let _ = std::fs::remove_dir_all(&audio_dir);
}
std::fs::create_dir_all(&audio_dir)
.map_err(|e| format!("failed to create audio runtime dir: {e}"))?;
let conf_dir = audio_dir.join("pipewire");
std::fs::create_dir_all(&conf_dir)
.map_err(|e| format!("failed to create PipeWire config dir: {e}"))?;
let conf_path = conf_dir.join("pipewire.conf");
std::fs::write(&conf_path, PIPEWIRE_CONF_TEMPLATE)
.map_err(|e| format!("failed to write PipeWire config: {e}"))?;
let mut dbus_child = unsafe {
Command::new("dbus-daemon")
.args(["--session", "--print-address=1", "--nofork"])
.env("XDG_RUNTIME_DIR", &audio_dir)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(if verbose {
Stdio::inherit()
} else {
Stdio::null()
})
.pre_exec(pdeathsig_hook())
.spawn()
.map_err(|e| format!("failed to start dbus-daemon: {e}"))?
};
let dbus_stdout = dbus_child
.stdout
.take()
.ok_or("dbus-daemon stdout missing")?;
let mut dbus_reader = std::io::BufReader::new(dbus_stdout);
let mut dbus_address = String::new();
dbus_reader
.read_line(&mut dbus_address)
.map_err(|e| format!("failed to read dbus-daemon address: {e}"))?;
let dbus_address = dbus_address.trim();
if dbus_address.is_empty() {
let _ = dbus_child.kill();
return Err("dbus-daemon exited without printing an address".into());
}
let mut pipewire_child = match unsafe {
Command::new("pipewire")
.env("XDG_CONFIG_HOME", &audio_dir)
.env("DBUS_SESSION_BUS_ADDRESS", dbus_address)
.env("XDG_RUNTIME_DIR", &audio_dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(if verbose {
Stdio::inherit()
} else {
Stdio::null()
})
.pre_exec(pdeathsig_hook())
.spawn()
} {
Ok(c) => c,
Err(e) => {
let _ = dbus_child.kill();
let _ = dbus_child.wait();
return Err(format!("failed to start pipewire: {e}"));
}
};
let pw_socket = audio_dir.join("pipewire-0");
if !wait_for_socket(&pw_socket, std::time::Duration::from_secs(2)) {
if matches!(pipewire_child.try_wait(), Ok(Some(_))) {
let _ = dbus_child.kill();
let _ = dbus_child.wait();
return Err("pipewire exited before creating its socket".into());
}
}
let mut wireplumber_child = if find_program("wireplumber").is_some() {
let wp_conf_dir = audio_dir.join("wireplumber").join("wireplumber.conf.d");
let _ = std::fs::create_dir_all(&wp_conf_dir);
let _ = std::fs::write(wp_conf_dir.join("99-blit.conf"), WIREPLUMBER_CONF_TEMPLATE);
let child = unsafe {
Command::new("wireplumber")
.env("PIPEWIRE_REMOTE", audio_dir.join("pipewire-0"))
.env("XDG_CONFIG_HOME", &audio_dir)
.env("DBUS_SESSION_BUS_ADDRESS", dbus_address)
.env("XDG_RUNTIME_DIR", &audio_dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(if verbose {
Stdio::inherit()
} else {
Stdio::null()
})
.pre_exec(pdeathsig_hook())
.spawn()
};
match child {
Ok(c) => {
std::thread::sleep(std::time::Duration::from_millis(250));
Some(c)
}
Err(e) => {
if verbose {
eprintln!("[audio] failed to start wireplumber: {e}");
}
None
}
}
} else {
None
};
let mut pipewire_pulse_child = match unsafe {
Command::new("pipewire-pulse")
.env("PIPEWIRE_REMOTE", audio_dir.join("pipewire-0"))
.env("DBUS_SESSION_BUS_ADDRESS", dbus_address)
.env("XDG_RUNTIME_DIR", &audio_dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(if verbose {
Stdio::inherit()
} else {
Stdio::null()
})
.pre_exec(pdeathsig_hook())
.spawn()
} {
Ok(c) => c,
Err(e) => {
if let Some(ref mut wp) = wireplumber_child {
let _ = wp.kill();
}
let _ = pipewire_child.kill();
let _ = dbus_child.kill();
if let Some(ref mut wp) = wireplumber_child {
let _ = wp.wait();
}
let _ = pipewire_child.wait();
let _ = dbus_child.wait();
return Err(format!("failed to start pipewire-pulse: {e}"));
}
};
let pulse_socket = audio_dir.join("pulse").join("native");
if !wait_for_socket(&pulse_socket, std::time::Duration::from_secs(2))
&& matches!(pipewire_pulse_child.try_wait(), Ok(Some(_)))
{
if let Some(ref mut wp) = wireplumber_child {
let _ = wp.kill();
}
let _ = pipewire_child.kill();
let _ = dbus_child.kill();
if let Some(ref mut wp) = wireplumber_child {
let _ = wp.wait();
}
let _ = pipewire_child.wait();
let _ = dbus_child.wait();
return Err("pipewire-pulse exited before creating its socket".into());
}
let (capture, capture_rx) = match crate::audio_pw::Capture::start(&audio_dir, "blit-sink") {
Ok(pair) => pair,
Err(e) => {
let _ = pipewire_pulse_child.kill();
if let Some(ref mut wp) = wireplumber_child {
let _ = wp.kill();
}
let _ = pipewire_child.kill();
let _ = dbus_child.kill();
let _ = pipewire_pulse_child.wait();
if let Some(ref mut wp) = wireplumber_child {
let _ = wp.wait();
}
let _ = pipewire_child.wait();
let _ = dbus_child.wait();
return Err(format!("failed to start PipeWire capture: {e}"));
}
};
if verbose {
eprintln!(
"[audio] spawned dbus={} pipewire={} pipewire-pulse={} capture=in-process dir={}",
dbus_child.id(),
pipewire_child.id(),
pipewire_pulse_child.id(),
audio_dir.display(),
);
}
let (opus_tx, opus_rx) = mpsc::channel::<OpusFrame>(RING_CAPACITY * 2);
let bitrate = if bitrate > 0 {
bitrate
} else {
DEFAULT_BITRATE
};
let (bitrate_tx, bitrate_rx) = tokio::sync::watch::channel(bitrate);
let encoder_alive = Arc::new(AtomicBool::new(true));
let encoder_alive_clone = encoder_alive.clone();
let has_listener = broadcast.has_listener_flag();
let has_listener_clone = has_listener.clone();
let verbose_copy = verbose;
tokio::spawn(async move {
let result = encoder_task(
capture_rx,
opus_tx,
bitrate,
verbose_copy,
epoch,
bitrate_rx,
has_listener_clone,
)
.await;
encoder_alive_clone.store(false, Ordering::Release);
if let Err(e) = result
&& verbose_copy
{
eprintln!("[audio] encoder task exited: {e}");
}
});
let broadcast_for_fanout = broadcast.clone();
tokio::spawn(async move {
fanout_task(opus_rx, broadcast_for_fanout).await;
});
Ok(Self {
dbus_child,
pipewire_child,
wireplumber_child,
pipewire_pulse_child,
capture: Some(capture),
runtime_dir: audio_dir,
alive: true,
bitrate_tx,
encoder_alive,
dbus_address: dbus_address.to_string(),
verbose,
last_heal: None,
first_heal_at: None,
heals: 0,
})
}
pub fn is_alive(&mut self) -> bool {
if !self.alive {
return false;
}
if matches!(self.pipewire_child.try_wait(), Ok(Some(_)))
|| matches!(self.dbus_child.try_wait(), Ok(Some(_)))
{
self.alive = false;
return false;
}
let wp_dead = self
.wireplumber_child
.as_mut()
.is_some_and(|wp| matches!(wp.try_wait(), Ok(Some(_))));
let pulse_dead = matches!(self.pipewire_pulse_child.try_wait(), Ok(Some(_)));
let encoder_dead = !self.encoder_alive.load(Ordering::Acquire);
let needs_heal = wp_dead || pulse_dead || encoder_dead;
if !needs_heal {
return true;
}
let now = Instant::now();
let can_heal = self
.last_heal
.is_none_or(|t| now.duration_since(t) >= HEAL_COOLDOWN);
if !can_heal {
return true;
}
if self
.first_heal_at
.is_none_or(|t| now.duration_since(t) > HEAL_WINDOW)
{
self.first_heal_at = Some(now);
self.heals = 0;
}
self.heals += 1;
if self.heals > MAX_HEALS {
eprintln!(
"[audio] too many sub-process restarts ({}), giving up",
self.heals
);
self.alive = false;
return false;
}
self.last_heal = Some(now);
if wp_dead {
eprintln!("[audio] wireplumber died, restarting");
self.restart_wireplumber();
}
if pulse_dead {
eprintln!("[audio] pipewire-pulse died, restarting");
self.restart_pipewire_pulse();
}
if encoder_dead {
eprintln!("[audio] encoder died, triggering full pipeline restart");
self.alive = false;
return false;
}
true
}
pub fn shutdown(&mut self) {
self.alive = false;
self.capture.take();
let _ = self.pipewire_pulse_child.kill();
if let Some(ref mut wp) = self.wireplumber_child {
let _ = wp.kill();
}
let _ = self.pipewire_child.kill();
let _ = self.dbus_child.kill();
let _ = self.pipewire_pulse_child.wait();
if let Some(ref mut wp) = self.wireplumber_child {
let _ = wp.wait();
}
let _ = self.pipewire_child.wait();
let _ = self.dbus_child.wait();
let _ = std::fs::remove_dir_all(&self.runtime_dir);
}
fn restart_wireplumber(&mut self) {
if let Some(ref mut wp) = self.wireplumber_child {
let _ = wp.kill();
let _ = wp.wait();
}
let child = unsafe {
Command::new("wireplumber")
.env("PIPEWIRE_REMOTE", self.runtime_dir.join("pipewire-0"))
.env("XDG_CONFIG_HOME", &self.runtime_dir)
.env("DBUS_SESSION_BUS_ADDRESS", &self.dbus_address)
.env("XDG_RUNTIME_DIR", &self.runtime_dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(if self.verbose {
Stdio::inherit()
} else {
Stdio::null()
})
.pre_exec(pdeathsig_hook())
.spawn()
};
match child {
Ok(c) => {
self.wireplumber_child = Some(c);
}
Err(e) => {
eprintln!("[audio] failed to restart wireplumber: {e}");
self.wireplumber_child = None;
}
}
}
fn restart_pipewire_pulse(&mut self) {
let _ = self.pipewire_pulse_child.kill();
let _ = self.pipewire_pulse_child.wait();
let _ = std::fs::remove_dir_all(self.runtime_dir.join("pulse"));
match unsafe {
Command::new("pipewire-pulse")
.env("PIPEWIRE_REMOTE", self.runtime_dir.join("pipewire-0"))
.env("DBUS_SESSION_BUS_ADDRESS", &self.dbus_address)
.env("XDG_RUNTIME_DIR", &self.runtime_dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(if self.verbose {
Stdio::inherit()
} else {
Stdio::null()
})
.pre_exec(pdeathsig_hook())
.spawn()
} {
Ok(c) => {
self.pipewire_pulse_child = c;
}
Err(e) => {
eprintln!("[audio] failed to restart pipewire-pulse: {e}");
}
}
}
pub fn set_bitrate(&self, bitrate: i32) {
let _ = self.bitrate_tx.send(bitrate);
}
pub fn pulse_server_path(&self) -> String {
let pulse_dir = self.runtime_dir.join("pulse");
format!("unix:{}", pulse_dir.join("native").display())
}
pub fn pipewire_remote_path(&self) -> String {
self.runtime_dir
.join("pipewire-0")
.to_string_lossy()
.into_owned()
}
}
impl Drop for AudioPipeline {
fn drop(&mut self) {
self.shutdown();
}
}
pub fn msg_audio_frame(frame: &OpusFrame) -> Vec<u8> {
let mut msg = Vec::with_capacity(1 + 4 + 1 + frame.data.len());
msg.push(S2C_AUDIO_FRAME);
msg.extend_from_slice(&frame.timestamp.to_le_bytes());
msg.push(AUDIO_FRAME_CODEC_OPUS);
msg.extend_from_slice(&frame.data);
msg
}
async fn encoder_task(
mut pcm_rx: mpsc::UnboundedReceiver<Vec<u8>>,
tx: mpsc::Sender<OpusFrame>,
bitrate: i32,
verbose: bool,
epoch: Instant,
mut bitrate_rx: tokio::sync::watch::Receiver<i32>,
has_listener: Arc<AtomicBool>,
) -> Result<(), String> {
let mut encoder = OpusEncoder::new(48000, Channels::Stereo, Application::Audio)
.map_err(|e| format!("failed to create Opus encoder: {e}"))?;
encoder
.set_bitrate(opus::Bitrate::Bits(bitrate))
.map_err(|e| format!("failed to set Opus bitrate: {e}"))?;
if let Err(e) = encoder.set_dtx(true)
&& verbose
{
eprintln!("[audio] failed to enable Opus DTX: {e}");
}
let mut current_bitrate = bitrate;
if verbose {
eprintln!("[audio] encoder ready, bitrate={bitrate} bps");
}
let mut pcm_buf = vec![0f32; FRAME_FLOATS];
let mut byte_buf: Vec<u8> = Vec::with_capacity(FRAME_FLOATS * 4 * 2);
let mut opus_out = vec![0u8; MAX_OPUS_PACKET];
loop {
if bitrate_rx.has_changed().unwrap_or(false) {
let new_bitrate = *bitrate_rx.borrow_and_update();
if new_bitrate != current_bitrate {
if let Err(e) = encoder.set_bitrate(opus::Bitrate::Bits(new_bitrate)) {
if verbose {
eprintln!("[audio] failed to update bitrate to {new_bitrate}: {e}");
}
} else {
if verbose {
eprintln!(
"[audio] bitrate updated: {current_bitrate} -> {new_bitrate} bps"
);
}
current_bitrate = new_bitrate;
}
}
}
let chunk = match pcm_rx.recv().await {
Some(c) => c,
None => return Ok(()), };
byte_buf.extend_from_slice(&chunk);
while byte_buf.len() >= FRAME_FLOATS * 4 {
let consumed = FRAME_FLOATS * 4;
if !has_listener.load(Ordering::Acquire) {
byte_buf.drain(..consumed);
continue;
}
for (i, sample) in pcm_buf.iter_mut().enumerate() {
let off = i * 4;
*sample = f32::from_le_bytes([
byte_buf[off],
byte_buf[off + 1],
byte_buf[off + 2],
byte_buf[off + 3],
]);
}
let encoded_len = match encoder.encode_float(&pcm_buf, &mut opus_out) {
Ok(len) => len,
Err(e) => {
if verbose {
eprintln!("[audio] Opus encode error, skipping frame: {e}");
}
byte_buf.drain(..consumed);
continue;
}
};
let frame = OpusFrame {
timestamp: epoch.elapsed().as_millis() as u32,
data: opus_out[..encoded_len].to_vec(),
};
match tx.try_send(frame) {
Ok(()) => {}
Err(mpsc::error::TrySendError::Full(_)) => {
}
Err(mpsc::error::TrySendError::Closed(_)) => {
return Ok(());
}
}
byte_buf.drain(..consumed);
}
}
}