#![cfg(target_os = "linux")]
use libc::{RTLD_LAZY, RTLD_LOCAL, dlopen, dlsym};
use std::ffi::{CStr, CString, c_char, c_int, c_void};
use std::path::Path;
use std::ptr;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::mpsc;
#[repr(C)]
struct PwThreadLoop {
_priv: [u8; 0],
}
#[repr(C)]
struct PwLoop {
_priv: [u8; 0],
}
#[repr(C)]
struct PwStream {
_priv: [u8; 0],
}
#[repr(C)]
struct PwProperties {
_priv: [u8; 0],
}
#[repr(C)]
struct PwBuffer {
buffer: *mut SpaBuffer,
user_data: *mut c_void,
size: u64,
requested: u64,
time: u64,
}
#[repr(C)]
struct SpaBuffer {
n_metas: u32,
n_datas: u32,
metas: *mut c_void,
datas: *mut SpaData,
}
#[repr(C)]
struct SpaData {
type_: u32,
flags: u32,
fd: i64,
mapoffset: u32,
maxsize: u32,
data: *mut c_void,
chunk: *mut SpaChunk,
}
#[repr(C)]
struct SpaChunk {
offset: u32,
size: u32,
stride: i32,
flags: i32,
}
#[repr(C)]
struct PwStreamEvents {
version: u32,
destroy: Option<unsafe extern "C" fn(*mut c_void)>,
state_changed: Option<unsafe extern "C" fn(*mut c_void, i32, i32, *const c_char)>,
control_info: Option<unsafe extern "C" fn(*mut c_void, u32, *const c_void)>,
io_changed: Option<unsafe extern "C" fn(*mut c_void, u32, *mut c_void, u32)>,
param_changed: Option<unsafe extern "C" fn(*mut c_void, u32, *const c_void)>,
add_buffer: Option<unsafe extern "C" fn(*mut c_void, *mut PwBuffer)>,
remove_buffer: Option<unsafe extern "C" fn(*mut c_void, *mut PwBuffer)>,
process: Option<unsafe extern "C" fn(*mut c_void)>,
drained: Option<unsafe extern "C" fn(*mut c_void)>,
command: Option<unsafe extern "C" fn(*mut c_void, *const c_void)>,
trigger_done: Option<unsafe extern "C" fn(*mut c_void)>,
}
const PW_VERSION_STREAM_EVENTS: u32 = 2;
const PW_DIRECTION_INPUT: i32 = 0;
const PW_ID_ANY: u32 = u32::MAX;
const PW_STREAM_FLAG_AUTOCONNECT: u32 = 1 << 0;
const PW_STREAM_FLAG_MAP_BUFFERS: u32 = 1 << 2;
const PW_STREAM_FLAG_RT_PROCESS: u32 = 1 << 4;
const SPA_TYPE_ID: u32 = 3;
const SPA_TYPE_INT: u32 = 4;
const SPA_TYPE_OBJECT: u32 = 15;
const SPA_TYPE_OBJECT_FORMAT: u32 = 0x40003;
const SPA_PARAM_ENUM_FORMAT: u32 = 3;
const SPA_FORMAT_MEDIA_TYPE: u32 = 1;
const SPA_FORMAT_MEDIA_SUBTYPE: u32 = 2;
const SPA_FORMAT_AUDIO_FORMAT: u32 = 0x10001;
const SPA_FORMAT_AUDIO_RATE: u32 = 0x10003;
const SPA_FORMAT_AUDIO_CHANNELS: u32 = 0x10004;
const SPA_MEDIA_TYPE_AUDIO: u32 = 1;
const SPA_MEDIA_SUBTYPE_RAW: u32 = 1;
const SPA_AUDIO_FORMAT_F32_LE: u32 = 283;
type FnPwInit = unsafe extern "C" fn(*mut c_int, *mut *mut *mut c_char);
type FnPwDeinit = unsafe extern "C" fn();
type FnPwThreadLoopNew = unsafe extern "C" fn(*const c_char, *const c_void) -> *mut PwThreadLoop;
type FnPwThreadLoopDestroy = unsafe extern "C" fn(*mut PwThreadLoop);
type FnPwThreadLoopStart = unsafe extern "C" fn(*mut PwThreadLoop) -> c_int;
type FnPwThreadLoopStop = unsafe extern "C" fn(*mut PwThreadLoop);
type FnPwThreadLoopGetLoop = unsafe extern "C" fn(*mut PwThreadLoop) -> *mut PwLoop;
type FnPwStreamNewSimple = unsafe extern "C" fn(
*mut PwLoop,
*const c_char,
*mut PwProperties,
*const PwStreamEvents,
*mut c_void,
) -> *mut PwStream;
type FnPwStreamDestroy = unsafe extern "C" fn(*mut PwStream);
type FnPwStreamConnect =
unsafe extern "C" fn(*mut PwStream, i32, u32, u32, *mut *const c_void, u32) -> c_int;
type FnPwStreamDisconnect = unsafe extern "C" fn(*mut PwStream) -> c_int;
type FnPwStreamDequeueBuffer = unsafe extern "C" fn(*mut PwStream) -> *mut PwBuffer;
type FnPwStreamQueueBuffer = unsafe extern "C" fn(*mut PwStream, *mut PwBuffer) -> c_int;
type FnPwPropertiesNew = unsafe extern "C" fn(*const c_char) -> *mut PwProperties;
type FnPwPropertiesSet =
unsafe extern "C" fn(*mut PwProperties, *const c_char, *const c_char) -> c_int;
struct Syms {
pw_init: FnPwInit,
pw_thread_loop_new: FnPwThreadLoopNew,
pw_thread_loop_destroy: FnPwThreadLoopDestroy,
pw_thread_loop_start: FnPwThreadLoopStart,
pw_thread_loop_stop: FnPwThreadLoopStop,
pw_thread_loop_get_loop: FnPwThreadLoopGetLoop,
pw_stream_new_simple: FnPwStreamNewSimple,
pw_stream_destroy: FnPwStreamDestroy,
pw_stream_connect: FnPwStreamConnect,
pw_stream_disconnect: FnPwStreamDisconnect,
pw_stream_dequeue_buffer: FnPwStreamDequeueBuffer,
pw_stream_queue_buffer: FnPwStreamQueueBuffer,
pw_properties_new: FnPwPropertiesNew,
pw_properties_set: FnPwPropertiesSet,
#[allow(dead_code)]
pw_deinit: FnPwDeinit,
}
unsafe impl Send for Syms {}
unsafe impl Sync for Syms {}
static LOAD_ERROR: OnceLock<String> = OnceLock::new();
pub fn load_error() -> &'static str {
LOAD_ERROR.get().map(String::as_str).unwrap_or("")
}
fn record_dlerror(context: &str) {
unsafe {
let e = libc::dlerror();
let detail = if e.is_null() {
String::from("(no dlerror)")
} else {
CStr::from_ptr(e).to_string_lossy().into_owned()
};
let _ = LOAD_ERROR.set(format!("{context}: {detail}"));
}
}
fn syms() -> Option<&'static Syms> {
static CACHE: OnceLock<Option<Syms>> = OnceLock::new();
CACHE
.get_or_init(|| unsafe {
let candidates = [c"libpipewire-0.3.so.0", c"libpipewire-0.3.so"];
let mut handle = ptr::null_mut();
for name in candidates {
handle = dlopen(name.as_ptr(), RTLD_LAZY | RTLD_LOCAL);
if !handle.is_null() {
break;
}
}
if handle.is_null() {
record_dlerror("dlopen libpipewire-0.3.so.0 failed (check LD_LIBRARY_PATH)");
return None;
}
macro_rules! sym {
($name:literal, $ty:ty) => {{
let cname = CString::new($name).ok()?;
let ptr = dlsym(handle, cname.as_ptr());
if ptr.is_null() {
record_dlerror(&format!("dlsym {} failed", $name));
return None;
}
std::mem::transmute::<*mut c_void, $ty>(ptr)
}};
}
let syms = Syms {
pw_init: sym!("pw_init", FnPwInit),
pw_deinit: sym!("pw_deinit", FnPwDeinit),
pw_thread_loop_new: sym!("pw_thread_loop_new", FnPwThreadLoopNew),
pw_thread_loop_destroy: sym!("pw_thread_loop_destroy", FnPwThreadLoopDestroy),
pw_thread_loop_start: sym!("pw_thread_loop_start", FnPwThreadLoopStart),
pw_thread_loop_stop: sym!("pw_thread_loop_stop", FnPwThreadLoopStop),
pw_thread_loop_get_loop: sym!("pw_thread_loop_get_loop", FnPwThreadLoopGetLoop),
pw_stream_new_simple: sym!("pw_stream_new_simple", FnPwStreamNewSimple),
pw_stream_destroy: sym!("pw_stream_destroy", FnPwStreamDestroy),
pw_stream_connect: sym!("pw_stream_connect", FnPwStreamConnect),
pw_stream_disconnect: sym!("pw_stream_disconnect", FnPwStreamDisconnect),
pw_stream_dequeue_buffer: sym!("pw_stream_dequeue_buffer", FnPwStreamDequeueBuffer),
pw_stream_queue_buffer: sym!("pw_stream_queue_buffer", FnPwStreamQueueBuffer),
pw_properties_new: sym!("pw_properties_new", FnPwPropertiesNew),
pw_properties_set: sym!("pw_properties_set", FnPwPropertiesSet),
};
(syms.pw_init)(ptr::null_mut(), ptr::null_mut());
Some(syms)
})
.as_ref()
}
pub fn available() -> bool {
syms().is_some()
}
fn build_audio_format_pod() -> Vec<u64> {
let mut body: Vec<u8> = Vec::with_capacity(128);
body.extend_from_slice(&SPA_TYPE_OBJECT_FORMAT.to_le_bytes());
body.extend_from_slice(&SPA_PARAM_ENUM_FORMAT.to_le_bytes());
fn prop(out: &mut Vec<u8>, key: u32, pod_type: u32, value: &[u8]) {
out.extend_from_slice(&key.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&(value.len() as u32).to_le_bytes()); out.extend_from_slice(&pod_type.to_le_bytes());
out.extend_from_slice(value);
while !out.len().is_multiple_of(8) {
out.push(0);
}
}
prop(
&mut body,
SPA_FORMAT_MEDIA_TYPE,
SPA_TYPE_ID,
&SPA_MEDIA_TYPE_AUDIO.to_le_bytes(),
);
prop(
&mut body,
SPA_FORMAT_MEDIA_SUBTYPE,
SPA_TYPE_ID,
&SPA_MEDIA_SUBTYPE_RAW.to_le_bytes(),
);
prop(
&mut body,
SPA_FORMAT_AUDIO_FORMAT,
SPA_TYPE_ID,
&SPA_AUDIO_FORMAT_F32_LE.to_le_bytes(),
);
prop(
&mut body,
SPA_FORMAT_AUDIO_RATE,
SPA_TYPE_INT,
&48000i32.to_le_bytes(),
);
prop(
&mut body,
SPA_FORMAT_AUDIO_CHANNELS,
SPA_TYPE_INT,
&2i32.to_le_bytes(),
);
let mut bytes = Vec::with_capacity(8 + body.len());
bytes.extend_from_slice(&(body.len() as u32).to_le_bytes());
bytes.extend_from_slice(&SPA_TYPE_OBJECT.to_le_bytes());
bytes.extend_from_slice(&body);
assert!(
bytes.len().is_multiple_of(8),
"POD bytes must be 8-byte multiple"
);
let mut aligned: Vec<u64> = vec![0u64; bytes.len() / 8];
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), aligned.as_mut_ptr() as *mut u8, bytes.len());
}
aligned
}
struct CaptureState {
stream: *mut PwStream,
tx: mpsc::UnboundedSender<Vec<u8>>,
active: AtomicBool,
}
unsafe impl Send for CaptureState {}
unsafe impl Sync for CaptureState {}
unsafe extern "C" fn on_process(data: *mut c_void) {
unsafe {
let state = &*(data as *const CaptureState);
if !state.active.load(Ordering::Acquire) {
return;
}
let Some(s) = syms() else {
return;
};
let buf = (s.pw_stream_dequeue_buffer)(state.stream);
if buf.is_null() {
return;
}
let pw_buf = &*buf;
let spa_buf = pw_buf.buffer;
if !spa_buf.is_null() {
let sb = &*spa_buf;
if sb.n_datas >= 1 && !sb.datas.is_null() {
let d = &*sb.datas;
if !d.chunk.is_null() && !d.data.is_null() {
let c = &*d.chunk;
let size = c.size as usize;
let offset = c.offset as usize % d.maxsize.max(1) as usize;
if size > 0 {
let src = (d.data as *const u8).add(offset);
let slice = std::slice::from_raw_parts(src, size);
let _ = state.tx.send(slice.to_vec());
}
}
}
}
(s.pw_stream_queue_buffer)(state.stream, buf);
}
}
const STREAM_EVENTS: PwStreamEvents = PwStreamEvents {
version: PW_VERSION_STREAM_EVENTS,
destroy: None,
state_changed: None,
control_info: None,
io_changed: None,
param_changed: None,
add_buffer: None,
remove_buffer: None,
process: Some(on_process),
drained: None,
command: None,
trigger_done: None,
};
pub struct Capture {
thread_loop: *mut PwThreadLoop,
stream: *mut PwStream,
state: *mut CaptureState,
}
unsafe impl Send for Capture {}
impl Capture {
pub fn start(
runtime_dir: &Path,
target_node: &str,
) -> Result<(Self, mpsc::UnboundedReceiver<Vec<u8>>), String> {
let s = syms().ok_or_else(|| "libpipewire-0.3.so.0 not available".to_string())?;
unsafe {
std::env::set_var(
"PIPEWIRE_REMOTE",
runtime_dir.join("pipewire-0").as_os_str(),
);
std::env::set_var("XDG_RUNTIME_DIR", runtime_dir.as_os_str());
}
let (tx, rx) = mpsc::unbounded_channel::<Vec<u8>>();
unsafe {
let name = CString::new("blit-capture").unwrap();
let thread_loop = (s.pw_thread_loop_new)(name.as_ptr(), ptr::null());
if thread_loop.is_null() {
return Err("pw_thread_loop_new failed".to_string());
}
let loop_ = (s.pw_thread_loop_get_loop)(thread_loop);
let props = (s.pw_properties_new)(ptr::null());
if props.is_null() {
(s.pw_thread_loop_destroy)(thread_loop);
return Err("pw_properties_new failed".to_string());
}
let set = |k: &str, v: &str| {
let ck = CString::new(k).unwrap();
let cv = CString::new(v).unwrap();
(s.pw_properties_set)(props, ck.as_ptr(), cv.as_ptr());
};
set("media.type", "Audio");
set("media.category", "Capture");
set("media.role", "DSP");
set("stream.capture.sink", "true");
set("target.object", target_node);
set("node.name", "blit-capture");
set("node.latency", "256/48000");
let state = Box::into_raw(Box::new(CaptureState {
stream: ptr::null_mut(),
tx,
active: AtomicBool::new(true),
}));
let stream = (s.pw_stream_new_simple)(
loop_,
CString::new("blit-capture").unwrap().as_ptr(),
props, &STREAM_EVENTS,
state as *mut c_void,
);
if stream.is_null() {
drop(Box::from_raw(state));
(s.pw_thread_loop_destroy)(thread_loop);
return Err("pw_stream_new_simple failed".to_string());
}
(*state).stream = stream;
let pod = build_audio_format_pod();
let mut params: [*const c_void; 1] = [pod.as_ptr() as *const c_void];
let flags =
PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS;
let rc = (s.pw_stream_connect)(
stream,
PW_DIRECTION_INPUT,
PW_ID_ANY,
flags,
params.as_mut_ptr(),
params.len() as u32,
);
if rc < 0 {
(s.pw_stream_destroy)(stream);
drop(Box::from_raw(state));
(s.pw_thread_loop_destroy)(thread_loop);
return Err(format!("pw_stream_connect failed: {rc}"));
}
drop(pod);
if (s.pw_thread_loop_start)(thread_loop) < 0 {
(s.pw_stream_disconnect)(stream);
(s.pw_stream_destroy)(stream);
drop(Box::from_raw(state));
(s.pw_thread_loop_destroy)(thread_loop);
return Err("pw_thread_loop_start failed".to_string());
}
Ok((
Self {
thread_loop,
stream,
state,
},
rx,
))
}
}
}
impl Drop for Capture {
fn drop(&mut self) {
let Some(s) = syms() else {
return;
};
unsafe {
if !self.state.is_null() {
(*self.state).active.store(false, Ordering::Release);
}
if !self.thread_loop.is_null() {
(s.pw_thread_loop_stop)(self.thread_loop);
}
if !self.stream.is_null() {
(s.pw_stream_disconnect)(self.stream);
(s.pw_stream_destroy)(self.stream);
self.stream = ptr::null_mut();
}
if !self.thread_loop.is_null() {
(s.pw_thread_loop_destroy)(self.thread_loop);
self.thread_loop = ptr::null_mut();
}
if !self.state.is_null() {
drop(Box::from_raw(self.state));
self.state = ptr::null_mut();
}
}
}
}
#[allow(dead_code)]
unsafe fn cstr_to_string(p: *const c_char) -> String {
if p.is_null() {
return String::new();
}
unsafe { CStr::from_ptr(p).to_string_lossy().into_owned() }
}