Skip to main content

pixelflux/webcam/
pipewire.rs

1//! PipeWire sink: the camera as a `Video/Source` node.
2//!
3//! Where a PipeWire daemon is reachable, the same frames that feed the interposer ring are also
4//! offered as a PipeWire camera node, which PipeWire-native consumers (GStreamer `pipewiresrc`,
5//! portal-aware applications) link to directly and which `pipewire-v4l2` exposes as a V4L2 device
6//! to plain libc consumers. `libpipewire-0.3` is loaded at run time and the SPA pods are built by
7//! hand, so pixelflux keeps no build-time or load-time dependency on PipeWire and the sink simply
8//! stays off where the library or the daemon is absent.
9//!
10//! The stream is the graph driver: every published frame is copied into the node's latest-frame
11//! slot and one graph cycle is triggered; the process callback then fills whatever buffer the
12//! consumers negotiated (memfd-backed, mapped for us) from that slot.
13
14use std::ffi::{c_char, c_int, c_void, CStr, CString};
15use std::mem;
16use std::ptr;
17use std::sync::atomic::{AtomicI32, AtomicPtr, AtomicU64, Ordering};
18use std::sync::{Arc, Mutex};
19use std::time::{Duration, Instant};
20
21use libloading::Library;
22
23use super::ring::{RingFormat, V4L2_PIX_FMT_NV12, V4L2_PIX_FMT_YUV420, V4L2_PIX_FMT_YUYV, V4L2_PIX_FMT_MJPEG};
24
25const SPA_TYPE_ID: u32 = 3;
26const SPA_TYPE_INT: u32 = 4;
27const SPA_TYPE_RECTANGLE: u32 = 10;
28const SPA_TYPE_FRACTION: u32 = 11;
29const SPA_TYPE_OBJECT: u32 = 15;
30const SPA_TYPE_CHOICE: u32 = 19;
31const SPA_TYPE_OBJECT_FORMAT: u32 = 0x40003;
32const SPA_TYPE_OBJECT_PARAM_BUFFERS: u32 = 0x40004;
33const SPA_TYPE_OBJECT_PARAM_META: u32 = 0x40005;
34const SPA_PARAM_ENUM_FORMAT: u32 = 3;
35const SPA_PARAM_FORMAT: u32 = 4;
36const SPA_PARAM_BUFFERS: u32 = 5;
37const SPA_PARAM_META: u32 = 6;
38const SPA_FORMAT_MEDIA_TYPE: u32 = 1;
39const SPA_FORMAT_MEDIA_SUBTYPE: u32 = 2;
40const SPA_FORMAT_VIDEO_FORMAT: u32 = 0x20001;
41const SPA_FORMAT_VIDEO_SIZE: u32 = 0x20003;
42const SPA_FORMAT_VIDEO_FRAMERATE: u32 = 0x20004;
43const SPA_MEDIA_TYPE_VIDEO: u32 = 2;
44const SPA_MEDIA_SUBTYPE_RAW: u32 = 1;
45const SPA_MEDIA_SUBTYPE_MJPG: u32 = 0x20002;
46const SPA_VIDEO_FORMAT_I420: u32 = 2;
47const SPA_VIDEO_FORMAT_YUY2: u32 = 4;
48const SPA_VIDEO_FORMAT_NV12: u32 = 23;
49const SPA_PARAM_BUFFERS_BUFFERS: u32 = 1;
50const SPA_PARAM_BUFFERS_BLOCKS: u32 = 2;
51const SPA_PARAM_BUFFERS_SIZE: u32 = 3;
52const SPA_PARAM_BUFFERS_STRIDE: u32 = 4;
53const SPA_PARAM_BUFFERS_ALIGN: u32 = 5;
54const SPA_PARAM_BUFFERS_DATATYPE: u32 = 6;
55const SPA_PARAM_META_TYPE: u32 = 1;
56const SPA_PARAM_META_SIZE: u32 = 2;
57const SPA_META_HEADER: u32 = 1;
58const SPA_META_HEADER_SIZE: u32 = 32;
59const SPA_DATA_MEMPTR: u32 = 1;
60const SPA_DATA_MEMFD: u32 = 2;
61const SPA_CHOICE_RANGE: u32 = 1;
62const SPA_CHOICE_FLAGS: u32 = 4;
63const PW_DIRECTION_OUTPUT: c_int = 1;
64const PW_ID_ANY: u32 = 0xFFFF_FFFF;
65const PW_STREAM_FLAG_MAP_BUFFERS: u32 = 4;
66const PW_STREAM_FLAG_DRIVER: u32 = 8;
67const PW_STREAM_STATE_ERROR: c_int = -1;
68const PW_STREAM_STATE_PAUSED: c_int = 2;
69const PW_STREAM_STATE_STREAMING: c_int = 3;
70const PW_VERSION_STREAM_EVENTS: u32 = 2;
71
72#[repr(C)]
73struct PwStreamEvents {
74    version: u32,
75    destroy: Option<unsafe extern "C" fn(*mut c_void)>,
76    state_changed: Option<unsafe extern "C" fn(*mut c_void, c_int, c_int, *const c_char)>,
77    control_info: Option<unsafe extern "C" fn(*mut c_void, u32, *const c_void)>,
78    io_changed: Option<unsafe extern "C" fn(*mut c_void, u32, *mut c_void, u32)>,
79    param_changed: Option<unsafe extern "C" fn(*mut c_void, u32, *const c_void)>,
80    add_buffer: Option<unsafe extern "C" fn(*mut c_void, *mut PwBuffer)>,
81    remove_buffer: Option<unsafe extern "C" fn(*mut c_void, *mut PwBuffer)>,
82    process: Option<unsafe extern "C" fn(*mut c_void)>,
83    drained: Option<unsafe extern "C" fn(*mut c_void)>,
84    command: Option<unsafe extern "C" fn(*mut c_void, *const c_void)>,
85    trigger_done: Option<unsafe extern "C" fn(*mut c_void)>,
86}
87
88#[repr(C)]
89struct PwBuffer {
90    buffer: *mut SpaBuffer,
91    user_data: *mut c_void,
92    size: u64,
93    requested: u64,
94    time: u64,
95}
96
97#[repr(C)]
98struct SpaBuffer {
99    n_metas: u32,
100    n_datas: u32,
101    metas: *mut SpaMeta,
102    datas: *mut SpaData,
103}
104
105#[repr(C)]
106struct SpaMeta {
107    type_: u32,
108    size: u32,
109    data: *mut c_void,
110}
111
112#[repr(C)]
113struct SpaData {
114    type_: u32,
115    flags: u32,
116    fd: i64,
117    mapoffset: u32,
118    maxsize: u32,
119    data: *mut c_void,
120    chunk: *mut SpaChunk,
121}
122
123#[repr(C)]
124struct SpaChunk {
125    offset: u32,
126    size: u32,
127    stride: i32,
128    flags: i32,
129}
130
131#[repr(C)]
132struct SpaMetaHeader {
133    flags: u32,
134    offset: u32,
135    pts: i64,
136    dts_offset: i64,
137    seq: u64,
138}
139
140type PwInit = unsafe extern "C" fn(*mut c_int, *mut *mut *mut c_char);
141type PwThreadLoopNew = unsafe extern "C" fn(*const c_char, *const c_void) -> *mut c_void;
142type PwThreadLoopGetLoop = unsafe extern "C" fn(*mut c_void) -> *mut c_void;
143type PwThreadLoopInt = unsafe extern "C" fn(*mut c_void) -> c_int;
144type PwThreadLoopVoid = unsafe extern "C" fn(*mut c_void);
145type PwContextNew = unsafe extern "C" fn(*mut c_void, *mut c_void, usize) -> *mut c_void;
146type PwContextConnect = unsafe extern "C" fn(*mut c_void, *mut c_void, usize) -> *mut c_void;
147type PwContextDestroy = unsafe extern "C" fn(*mut c_void);
148type PwCoreDisconnect = unsafe extern "C" fn(*mut c_void) -> c_int;
149type PwPropertiesNew = unsafe extern "C" fn(*const c_char, ...) -> *mut c_void;
150type PwPropertiesSet = unsafe extern "C" fn(*mut c_void, *const c_char, *const c_char) -> c_int;
151type PwStreamNew = unsafe extern "C" fn(*mut c_void, *const c_char, *mut c_void) -> *mut c_void;
152type PwStreamAddListener = unsafe extern "C" fn(*mut c_void, *mut c_void, *const PwStreamEvents, *mut c_void);
153type PwStreamConnect = unsafe extern "C" fn(*mut c_void, c_int, u32, u32, *mut *const c_void, u32) -> c_int;
154type PwStreamUpdateParams = unsafe extern "C" fn(*mut c_void, *mut *const c_void, u32) -> c_int;
155type PwStreamDequeueBuffer = unsafe extern "C" fn(*mut c_void) -> *mut PwBuffer;
156type PwStreamQueueBuffer = unsafe extern "C" fn(*mut c_void, *mut PwBuffer) -> c_int;
157type PwStreamTriggerProcess = unsafe extern "C" fn(*mut c_void) -> c_int;
158type PwStreamVoid = unsafe extern "C" fn(*mut c_void);
159type PwStreamInt = unsafe extern "C" fn(*mut c_void) -> c_int;
160
161/// Entry points resolved from `libpipewire-0.3.so.0`.
162#[derive(Clone, Copy)]
163struct Api {
164    thread_loop_new: PwThreadLoopNew,
165    thread_loop_get_loop: PwThreadLoopGetLoop,
166    thread_loop_start: PwThreadLoopInt,
167    thread_loop_stop: PwThreadLoopVoid,
168    thread_loop_lock: PwThreadLoopVoid,
169    thread_loop_unlock: PwThreadLoopVoid,
170    thread_loop_destroy: PwThreadLoopVoid,
171    context_new: PwContextNew,
172    context_connect: PwContextConnect,
173    context_destroy: PwContextDestroy,
174    core_disconnect: PwCoreDisconnect,
175    properties_new: PwPropertiesNew,
176    properties_set: PwPropertiesSet,
177    stream_new: PwStreamNew,
178    stream_add_listener: PwStreamAddListener,
179    stream_connect: PwStreamConnect,
180    stream_update_params: PwStreamUpdateParams,
181    stream_dequeue_buffer: PwStreamDequeueBuffer,
182    stream_queue_buffer: PwStreamQueueBuffer,
183    stream_trigger_process: PwStreamTriggerProcess,
184    stream_disconnect: PwStreamInt,
185    stream_destroy: PwStreamVoid,
186}
187
188/// The library handle is kept for the life of the process: PipeWire's own globals (pw_init) make
189/// unloading it unsafe, and every later camera reuses it.
190fn api() -> Result<&'static Api, String> {
191    static API: std::sync::OnceLock<Result<Api, String>> = std::sync::OnceLock::new();
192    API.get_or_init(|| unsafe {
193        let lib = Library::new("libpipewire-0.3.so.0").map_err(|e| format!("libpipewire-0.3 not available: {}", e))?;
194        macro_rules! sym {
195            ($name:literal, $t:ty) => {
196                *lib.get::<$t>(concat!($name, "\0").as_bytes()).map_err(|e| format!("{}: {}", $name, e))?
197            };
198        }
199        let init: PwInit = sym!("pw_init", PwInit);
200        let api = Api {
201            thread_loop_new: sym!("pw_thread_loop_new", PwThreadLoopNew),
202            thread_loop_get_loop: sym!("pw_thread_loop_get_loop", PwThreadLoopGetLoop),
203            thread_loop_start: sym!("pw_thread_loop_start", PwThreadLoopInt),
204            thread_loop_stop: sym!("pw_thread_loop_stop", PwThreadLoopVoid),
205            thread_loop_lock: sym!("pw_thread_loop_lock", PwThreadLoopVoid),
206            thread_loop_unlock: sym!("pw_thread_loop_unlock", PwThreadLoopVoid),
207            thread_loop_destroy: sym!("pw_thread_loop_destroy", PwThreadLoopVoid),
208            context_new: sym!("pw_context_new", PwContextNew),
209            context_connect: sym!("pw_context_connect", PwContextConnect),
210            context_destroy: sym!("pw_context_destroy", PwContextDestroy),
211            core_disconnect: sym!("pw_core_disconnect", PwCoreDisconnect),
212            properties_new: sym!("pw_properties_new", PwPropertiesNew),
213            properties_set: sym!("pw_properties_set", PwPropertiesSet),
214            stream_new: sym!("pw_stream_new", PwStreamNew),
215            stream_add_listener: sym!("pw_stream_add_listener", PwStreamAddListener),
216            stream_connect: sym!("pw_stream_connect", PwStreamConnect),
217            stream_update_params: sym!("pw_stream_update_params", PwStreamUpdateParams),
218            stream_dequeue_buffer: sym!("pw_stream_dequeue_buffer", PwStreamDequeueBuffer),
219            stream_queue_buffer: sym!("pw_stream_queue_buffer", PwStreamQueueBuffer),
220            stream_trigger_process: sym!("pw_stream_trigger_process", PwStreamTriggerProcess),
221            stream_disconnect: sym!("pw_stream_disconnect", PwStreamInt),
222            stream_destroy: sym!("pw_stream_destroy", PwStreamVoid),
223        };
224        init(ptr::null_mut(), ptr::null_mut());
225        mem::forget(lib);
226        Ok(api)
227    })
228    .as_ref()
229    .map_err(|e| e.clone())
230}
231
232// --- SPA pod construction ---------------------------------------------------------------------
233
234fn push_u32(v: &mut Vec<u8>, x: u32) {
235    v.extend_from_slice(&x.to_ne_bytes());
236}
237
238fn pad8(v: &mut Vec<u8>) {
239    while v.len() % 8 != 0 {
240        v.push(0);
241    }
242}
243
244fn pod_prim(v: &mut Vec<u8>, ty: u32, payload: &[u8]) {
245    push_u32(v, payload.len() as u32);
246    push_u32(v, ty);
247    v.extend_from_slice(payload);
248    pad8(v);
249}
250
251fn pod_id(v: &mut Vec<u8>, x: u32) {
252    pod_prim(v, SPA_TYPE_ID, &x.to_ne_bytes());
253}
254
255fn pod_int(v: &mut Vec<u8>, x: i32) {
256    pod_prim(v, SPA_TYPE_INT, &x.to_ne_bytes());
257}
258
259fn pod_rect(v: &mut Vec<u8>, w: u32, h: u32) {
260    let mut p = Vec::new();
261    push_u32(&mut p, w);
262    push_u32(&mut p, h);
263    pod_prim(v, SPA_TYPE_RECTANGLE, &p);
264}
265
266fn pod_frac(v: &mut Vec<u8>, num: u32, den: u32) {
267    let mut p = Vec::new();
268    push_u32(&mut p, num);
269    push_u32(&mut p, den);
270    pod_prim(v, SPA_TYPE_FRACTION, &p);
271}
272
273fn pod_choice_int(v: &mut Vec<u8>, choice: u32, values: &[i32]) {
274    let mut body = Vec::new();
275    push_u32(&mut body, choice);
276    push_u32(&mut body, 0);
277    push_u32(&mut body, 4);
278    push_u32(&mut body, SPA_TYPE_INT);
279    for x in values {
280        body.extend_from_slice(&x.to_ne_bytes());
281    }
282    pod_prim(v, SPA_TYPE_CHOICE, &body);
283}
284
285fn prop(v: &mut Vec<u8>, key: u32, value: impl FnOnce(&mut Vec<u8>)) {
286    push_u32(v, key);
287    push_u32(v, 0);
288    value(v);
289}
290
291fn object(ty: u32, id: u32, props: impl FnOnce(&mut Vec<u8>)) -> Vec<u8> {
292    let mut body = Vec::new();
293    push_u32(&mut body, ty);
294    push_u32(&mut body, id);
295    props(&mut body);
296    let mut v = Vec::new();
297    pod_prim(&mut v, SPA_TYPE_OBJECT, &body);
298    v
299}
300
301fn spa_video_format(fourcc: u32) -> Option<u32> {
302    match fourcc {
303        V4L2_PIX_FMT_YUV420 => Some(SPA_VIDEO_FORMAT_I420),
304        V4L2_PIX_FMT_NV12 => Some(SPA_VIDEO_FORMAT_NV12),
305        V4L2_PIX_FMT_YUYV => Some(SPA_VIDEO_FORMAT_YUY2),
306        _ => None,
307    }
308}
309
310/// `SPA_PARAM_EnumFormat`: one video format at the device size and rate — raw with the given
311/// SPA pixel format, or `video/mjpg` for the MJPEG device (`spa_format` None).
312fn format_pod(fmt: &RingFormat, spa_format: Option<u32>) -> Vec<u8> {
313    object(SPA_TYPE_OBJECT_FORMAT, SPA_PARAM_ENUM_FORMAT, |p| {
314        prop(p, SPA_FORMAT_MEDIA_TYPE, |v| pod_id(v, SPA_MEDIA_TYPE_VIDEO));
315        match spa_format {
316            Some(raw) => {
317                prop(p, SPA_FORMAT_MEDIA_SUBTYPE, |v| pod_id(v, SPA_MEDIA_SUBTYPE_RAW));
318                prop(p, SPA_FORMAT_VIDEO_FORMAT, |v| pod_id(v, raw));
319            }
320            None => prop(p, SPA_FORMAT_MEDIA_SUBTYPE, |v| pod_id(v, SPA_MEDIA_SUBTYPE_MJPG)),
321        }
322        prop(p, SPA_FORMAT_VIDEO_SIZE, |v| pod_rect(v, fmt.width, fmt.height));
323        prop(p, SPA_FORMAT_VIDEO_FRAMERATE, |v| pod_frac(v, fmt.fps_num, fmt.fps_den));
324    })
325}
326
327/// `SPA_PARAM_Buffers` answered once the format is chosen: memfd (or plain memory) buffers of
328/// exactly one frame.
329fn buffers_pod(fmt: &RingFormat) -> Vec<u8> {
330    object(SPA_TYPE_OBJECT_PARAM_BUFFERS, SPA_PARAM_BUFFERS, |p| {
331        prop(p, SPA_PARAM_BUFFERS_BUFFERS, |v| pod_choice_int(v, SPA_CHOICE_RANGE, &[4, 2, 8]));
332        prop(p, SPA_PARAM_BUFFERS_BLOCKS, |v| pod_int(v, 1));
333        prop(p, SPA_PARAM_BUFFERS_SIZE, |v| pod_int(v, fmt.sizeimage as i32));
334        prop(p, SPA_PARAM_BUFFERS_STRIDE, |v| pod_int(v, fmt.bytesperline as i32));
335        prop(p, SPA_PARAM_BUFFERS_ALIGN, |v| pod_int(v, 16));
336        prop(p, SPA_PARAM_BUFFERS_DATATYPE, |v| {
337            pod_choice_int(v, SPA_CHOICE_FLAGS, &[((1 << SPA_DATA_MEMFD) | (1 << SPA_DATA_MEMPTR)) as i32])
338        });
339    })
340}
341
342fn meta_pod() -> Vec<u8> {
343    object(SPA_TYPE_OBJECT_PARAM_META, SPA_PARAM_META, |p| {
344        prop(p, SPA_PARAM_META_TYPE, |v| pod_id(v, SPA_META_HEADER));
345        prop(p, SPA_PARAM_META_SIZE, |v| pod_int(v, SPA_META_HEADER_SIZE as i32));
346    })
347}
348
349// --- the sink ---------------------------------------------------------------------------------
350
351/// State shared with the stream callbacks (handed to PipeWire as the listener's user data).
352struct Shared {
353    api: Api,
354    stream: AtomicPtr<c_void>,
355    frame: Mutex<Vec<u8>>,
356    frame_seq: AtomicU64,
357    frame_ts_ns: AtomicU64,
358    state: AtomicI32,
359    stride: i32,
360    buffers_pod: Vec<u8>,
361    meta_pod: Vec<u8>,
362}
363
364unsafe extern "C" fn on_state_changed(data: *mut c_void, _old: c_int, new: c_int, error: *const c_char) {
365    unsafe {
366        let shared = &*(data as *const Shared);
367        shared.state.store(new, Ordering::Release);
368        if new == PW_STREAM_STATE_ERROR {
369            let msg = if error.is_null() { String::new() } else { CStr::from_ptr(error).to_string_lossy().into_owned() };
370            eprintln!("[webcam] PipeWire stream error: {}", msg);
371        }
372    }
373}
374
375unsafe extern "C" fn on_param_changed(data: *mut c_void, id: u32, param: *const c_void) {
376    if id != SPA_PARAM_FORMAT || param.is_null() {
377        return;
378    }
379    unsafe {
380        let shared = &*(data as *const Shared);
381        let stream = shared.stream.load(Ordering::Acquire);
382        if stream.is_null() {
383            return;
384        }
385        let mut params: [*const c_void; 2] = [shared.buffers_pod.as_ptr() as *const c_void, shared.meta_pod.as_ptr() as *const c_void];
386        (shared.api.stream_update_params)(stream, params.as_mut_ptr(), 2);
387    }
388}
389
390unsafe extern "C" fn on_process(data: *mut c_void) {
391    unsafe {
392        let shared = &*(data as *const Shared);
393        let stream = shared.stream.load(Ordering::Acquire);
394        if stream.is_null() {
395            return;
396        }
397        let frame = shared.frame.lock().unwrap_or_else(|e| e.into_inner());
398        if frame.is_empty() {
399            return;
400        }
401        let b = (shared.api.stream_dequeue_buffer)(stream);
402        if b.is_null() {
403            return;
404        }
405        let spa_buf = (*b).buffer;
406        if !spa_buf.is_null() && (*spa_buf).n_datas >= 1 {
407            let d = &mut *(*spa_buf).datas;
408            let mut written = 0usize;
409            if !d.data.is_null() && !d.chunk.is_null() {
410                written = frame.len().min(d.maxsize as usize);
411                ptr::copy_nonoverlapping(frame.as_ptr(), d.data as *mut u8, written);
412                (*d.chunk).offset = 0;
413                (*d.chunk).size = written as u32;
414                (*d.chunk).stride = shared.stride;
415                (*d.chunk).flags = 0;
416            }
417            for i in 0..(*spa_buf).n_metas as usize {
418                let m = &*(*spa_buf).metas.add(i);
419                if m.type_ == SPA_META_HEADER && m.size >= SPA_META_HEADER_SIZE && !m.data.is_null() {
420                    let h = &mut *(m.data as *mut SpaMetaHeader);
421                    h.flags = 0;
422                    h.offset = 0;
423                    h.pts = shared.frame_ts_ns.load(Ordering::Relaxed) as i64;
424                    h.dts_offset = 0;
425                    h.seq = shared.frame_seq.load(Ordering::Relaxed);
426                }
427            }
428            (*b).size = written as u64;
429        }
430        (shared.api.stream_queue_buffer)(stream, b);
431    }
432}
433
434pub struct PipeWireSink {
435    api: Api,
436    thread_loop: *mut c_void,
437    context: *mut c_void,
438    core: *mut c_void,
439    stream: *mut c_void,
440    _hook: Box<[u64; 8]>,
441    _events: Box<PwStreamEvents>,
442    _format_pod: Vec<u8>,
443    shared: Arc<Shared>,
444    failed: bool,
445}
446
447unsafe impl Send for PipeWireSink {}
448
449impl PipeWireSink {
450    /// Connect to the PipeWire daemon and publish the camera node; fails (and the sink stays off)
451    /// when the library or the daemon is missing or the format has no PipeWire equivalent.
452    pub fn connect(node_name: &str, description: &str, fmt: &RingFormat) -> Result<Self, String> {
453        let api = *api()?;
454        // The node name reaches here from Python, and a C string is what PipeWire takes.
455        let node_name = CString::new(node_name).map_err(|_| "node.name must not contain NUL bytes".to_string())?;
456        let description =
457            CString::new(description).map_err(|_| "node.description must not contain NUL bytes".to_string())?;
458        let spa_format = if fmt.fourcc == V4L2_PIX_FMT_MJPEG {
459            None
460        } else {
461            Some(spa_video_format(fmt.fourcc).ok_or_else(|| "pixel format has no PipeWire equivalent".to_string())?)
462        };
463        unsafe {
464            let thread_loop = (api.thread_loop_new)(c"pixelflux-webcam-pw".as_ptr(), ptr::null());
465            if thread_loop.is_null() {
466                return Err("pw_thread_loop_new failed".into());
467            }
468            let mut sink = PipeWireSink {
469                api,
470                thread_loop,
471                context: ptr::null_mut(),
472                core: ptr::null_mut(),
473                stream: ptr::null_mut(),
474                _hook: Box::new([0u64; 8]),
475                _events: Box::new(PwStreamEvents {
476                    version: PW_VERSION_STREAM_EVENTS,
477                    destroy: None,
478                    state_changed: Some(on_state_changed),
479                    control_info: None,
480                    io_changed: None,
481                    param_changed: Some(on_param_changed),
482                    add_buffer: None,
483                    remove_buffer: None,
484                    process: Some(on_process),
485                    drained: None,
486                    command: None,
487                    trigger_done: None,
488                }),
489                _format_pod: format_pod(fmt, spa_format),
490                shared: Arc::new(Shared {
491                    api,
492                    stream: AtomicPtr::new(ptr::null_mut()),
493                    frame: Mutex::new(Vec::new()),
494                    frame_seq: AtomicU64::new(0),
495                    frame_ts_ns: AtomicU64::new(0),
496                    state: AtomicI32::new(0),
497                    stride: fmt.bytesperline as i32,
498                    buffers_pod: buffers_pod(fmt),
499                    meta_pod: meta_pod(),
500                }),
501                failed: false,
502            };
503            let pw_loop = (api.thread_loop_get_loop)(thread_loop);
504            sink.context = (api.context_new)(pw_loop, ptr::null_mut(), 0);
505            if sink.context.is_null() {
506                return Err("pw_context_new failed".into());
507            }
508            if (api.thread_loop_start)(thread_loop) < 0 {
509                return Err("pw_thread_loop_start failed".into());
510            }
511            (api.thread_loop_lock)(thread_loop);
512            let result = (|| -> Result<(), String> {
513                sink.core = (api.context_connect)(sink.context, ptr::null_mut(), 0);
514                if sink.core.is_null() {
515                    return Err("no PipeWire daemon reachable".into());
516                }
517                let props = (api.properties_new)(ptr::null::<c_char>());
518                if props.is_null() {
519                    return Err("pw_properties_new failed".into());
520                }
521                for (k, v) in [
522                    (c"media.class", c"Video/Source" as &CStr),
523                    (c"media.role", c"Camera"),
524                    (c"node.name", node_name.as_c_str()),
525                    (c"node.description", description.as_c_str()),
526                    (c"node.virtual", c"true"),
527                ] {
528                    (api.properties_set)(props, k.as_ptr(), v.as_ptr());
529                }
530                sink.stream = (api.stream_new)(sink.core, description.as_ptr(), props);
531                if sink.stream.is_null() {
532                    return Err("pw_stream_new failed".into());
533                }
534                sink.shared.stream.store(sink.stream, Ordering::Release);
535                let user_data = Arc::as_ptr(&sink.shared) as *mut c_void;
536                (api.stream_add_listener)(sink.stream, sink._hook.as_mut_ptr() as *mut c_void, &*sink._events, user_data);
537                let mut params: [*const c_void; 1] = [sink._format_pod.as_ptr() as *const c_void];
538                let rc = (api.stream_connect)(
539                    sink.stream,
540                    PW_DIRECTION_OUTPUT,
541                    PW_ID_ANY,
542                    PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_DRIVER,
543                    params.as_mut_ptr(),
544                    1,
545                );
546                if rc < 0 {
547                    return Err(format!("pw_stream_connect failed ({})", rc));
548                }
549                Ok(())
550            })();
551            (api.thread_loop_unlock)(thread_loop);
552            result?;
553            let deadline = Instant::now() + Duration::from_secs(3);
554            loop {
555                let st = sink.shared.state.load(Ordering::Acquire);
556                if st == PW_STREAM_STATE_ERROR {
557                    return Err("PipeWire stream entered the error state".into());
558                }
559                if st == PW_STREAM_STATE_PAUSED || st == PW_STREAM_STATE_STREAMING || Instant::now() >= deadline {
560                    break;
561                }
562                std::thread::sleep(Duration::from_millis(10));
563            }
564            Ok(sink)
565        }
566    }
567
568    pub fn is_failed(&self) -> bool {
569        self.failed || self.shared.state.load(Ordering::Acquire) == PW_STREAM_STATE_ERROR
570    }
571
572    /// Whether a consumer is linked (the node is running); the latest frame is always kept so a
573    /// consumer that links later starts from it.
574    pub fn is_streaming(&self) -> bool {
575        self.shared.state.load(Ordering::Acquire) == PW_STREAM_STATE_STREAMING
576    }
577
578    /// Store one device frame and schedule a graph cycle so linked consumers receive it. With no
579    /// consumer linked nothing is copied; the first frame after a link fills the slot.
580    pub fn publish(&mut self, frame: &[u8], ts_ns: u64) {
581        if self.is_failed() || !self.is_streaming() {
582            return;
583        }
584        {
585            let mut slot = self.shared.frame.lock().unwrap_or_else(|e| e.into_inner());
586            slot.clear();
587            slot.extend_from_slice(frame);
588        }
589        self.shared.frame_seq.fetch_add(1, Ordering::Relaxed);
590        self.shared.frame_ts_ns.store(ts_ns, Ordering::Relaxed);
591        unsafe {
592            (self.api.thread_loop_lock)(self.thread_loop);
593            (self.api.stream_trigger_process)(self.stream);
594            (self.api.thread_loop_unlock)(self.thread_loop);
595        }
596    }
597}
598
599impl Drop for PipeWireSink {
600    fn drop(&mut self) {
601        unsafe {
602            (self.api.thread_loop_lock)(self.thread_loop);
603            self.shared.stream.store(ptr::null_mut(), Ordering::Release);
604            if !self.stream.is_null() {
605                (self.api.stream_disconnect)(self.stream);
606                (self.api.stream_destroy)(self.stream);
607            }
608            if !self.core.is_null() {
609                (self.api.core_disconnect)(self.core);
610            }
611            (self.api.thread_loop_unlock)(self.thread_loop);
612            (self.api.thread_loop_stop)(self.thread_loop);
613            if !self.context.is_null() {
614                (self.api.context_destroy)(self.context);
615            }
616            (self.api.thread_loop_destroy)(self.thread_loop);
617        }
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624
625    fn u32_at(v: &[u8], off: usize) -> u32 {
626        u32::from_ne_bytes(v[off..off + 4].try_into().unwrap())
627    }
628
629    /// Walk an object pod: (object type, param id, [(key, value type, value payload)]).
630    fn parse_object(pod: &[u8]) -> (u32, u32, Vec<(u32, u32, Vec<u8>)>) {
631        assert_eq!(pod.len() % 8, 0);
632        assert_eq!(u32_at(pod, 0) as usize, pod.len() - 8);
633        assert_eq!(u32_at(pod, 4), SPA_TYPE_OBJECT);
634        let mut props = Vec::new();
635        let mut off = 16;
636        while off < pod.len() {
637            let key = u32_at(pod, off);
638            let size = u32_at(pod, off + 8) as usize;
639            let ty = u32_at(pod, off + 12);
640            props.push((key, ty, pod[off + 16..off + 16 + size].to_vec()));
641            off += 16 + size.div_ceil(8) * 8;
642        }
643        (u32_at(pod, 8), u32_at(pod, 12), props)
644    }
645
646    #[test]
647    fn format_pod_is_a_well_formed_object() {
648        let fmt = RingFormat::raw(V4L2_PIX_FMT_YUV420, 1280, 720, 30, 1).unwrap();
649        let (ty, id, props) = parse_object(&format_pod(&fmt, Some(SPA_VIDEO_FORMAT_I420)));
650        assert_eq!((ty, id), (SPA_TYPE_OBJECT_FORMAT, SPA_PARAM_ENUM_FORMAT));
651        let get = |k: u32| props.iter().find(|p| p.0 == k).cloned().unwrap();
652        assert_eq!(get(SPA_FORMAT_MEDIA_TYPE).1, SPA_TYPE_ID);
653        assert_eq!(u32_at(&get(SPA_FORMAT_MEDIA_TYPE).2, 0), SPA_MEDIA_TYPE_VIDEO);
654        assert_eq!(u32_at(&get(SPA_FORMAT_MEDIA_SUBTYPE).2, 0), SPA_MEDIA_SUBTYPE_RAW);
655        assert_eq!(u32_at(&get(SPA_FORMAT_VIDEO_FORMAT).2, 0), SPA_VIDEO_FORMAT_I420);
656        let size = get(SPA_FORMAT_VIDEO_SIZE);
657        assert_eq!(size.1, SPA_TYPE_RECTANGLE);
658        assert_eq!((u32_at(&size.2, 0), u32_at(&size.2, 4)), (1280, 720));
659        let rate = get(SPA_FORMAT_VIDEO_FRAMERATE);
660        assert_eq!(rate.1, SPA_TYPE_FRACTION);
661        assert_eq!((u32_at(&rate.2, 0), u32_at(&rate.2, 4)), (30, 1));
662    }
663
664    #[test]
665    fn mjpg_format_pod_names_no_pixel_format() {
666        let fmt = RingFormat::compressed(V4L2_PIX_FMT_MJPEG, 640, 480, 30, 1).unwrap();
667        let (ty, id, props) = parse_object(&format_pod(&fmt, None));
668        assert_eq!((ty, id), (SPA_TYPE_OBJECT_FORMAT, SPA_PARAM_ENUM_FORMAT));
669        let get = |k: u32| props.iter().find(|p| p.0 == k).cloned();
670        assert_eq!(u32_at(&get(SPA_FORMAT_MEDIA_SUBTYPE).unwrap().2, 0), SPA_MEDIA_SUBTYPE_MJPG);
671        assert!(get(SPA_FORMAT_VIDEO_FORMAT).is_none());
672        let size = get(SPA_FORMAT_VIDEO_SIZE).unwrap();
673        assert_eq!((u32_at(&size.2, 0), u32_at(&size.2, 4)), (640, 480));
674        let (_, _, bprops) = parse_object(&buffers_pod(&fmt));
675        let bget = |k: u32| bprops.iter().find(|p| p.0 == k).cloned().unwrap();
676        assert_eq!(u32_at(&bget(SPA_PARAM_BUFFERS_SIZE).2, 0), 640 * 480 * 2);
677        assert_eq!(u32_at(&bget(SPA_PARAM_BUFFERS_STRIDE).2, 0), 0);
678    }
679
680    #[test]
681    fn buffers_pod_carries_frame_geometry() {
682        let fmt = RingFormat::raw(V4L2_PIX_FMT_YUYV, 640, 480, 30, 1).unwrap();
683        let (ty, id, props) = parse_object(&buffers_pod(&fmt));
684        assert_eq!((ty, id), (SPA_TYPE_OBJECT_PARAM_BUFFERS, SPA_PARAM_BUFFERS));
685        let get = |k: u32| props.iter().find(|p| p.0 == k).cloned().unwrap();
686        assert_eq!((get(SPA_PARAM_BUFFERS_SIZE).1, u32_at(&get(SPA_PARAM_BUFFERS_SIZE).2, 0)), (SPA_TYPE_INT, 640 * 480 * 2));
687        assert_eq!(u32_at(&get(SPA_PARAM_BUFFERS_STRIDE).2, 0), 1280);
688        assert_eq!(u32_at(&get(SPA_PARAM_BUFFERS_BLOCKS).2, 0), 1);
689        let buffers = get(SPA_PARAM_BUFFERS_BUFFERS);
690        assert_eq!(buffers.1, SPA_TYPE_CHOICE);
691        assert_eq!(u32_at(&buffers.2, 0), SPA_CHOICE_RANGE);
692        assert_eq!((u32_at(&buffers.2, 8), u32_at(&buffers.2, 12)), (4, SPA_TYPE_INT));
693        assert_eq!(&buffers.2[16..28], &[4u8, 0, 0, 0, 2, 0, 0, 0, 8, 0, 0, 0]);
694        let dt = get(SPA_PARAM_BUFFERS_DATATYPE);
695        assert_eq!(u32_at(&dt.2, 0), SPA_CHOICE_FLAGS);
696        assert_eq!(u32_at(&dt.2, 16), (1 << SPA_DATA_MEMFD) | (1 << SPA_DATA_MEMPTR));
697        let (mty, mid, mprops) = parse_object(&meta_pod());
698        assert_eq!((mty, mid), (SPA_TYPE_OBJECT_PARAM_META, SPA_PARAM_META));
699        assert_eq!(u32_at(&mprops[0].2, 0), SPA_META_HEADER);
700        assert_eq!(u32_at(&mprops[1].2, 0), SPA_META_HEADER_SIZE);
701    }
702
703    #[test]
704    fn struct_layouts_match_libpipewire() {
705        assert_eq!(mem::size_of::<PwStreamEvents>(), 96);
706        assert_eq!(mem::size_of::<PwBuffer>(), 40);
707        assert_eq!(mem::size_of::<SpaBuffer>(), 24);
708        assert_eq!(mem::size_of::<SpaData>(), 40);
709        assert_eq!(mem::size_of::<SpaChunk>(), 16);
710        assert_eq!(mem::size_of::<SpaMetaHeader>(), 32);
711        assert_eq!(mem::offset_of!(PwStreamEvents, process), 64);
712        assert_eq!(mem::offset_of!(SpaData, data), 24);
713        assert_eq!(mem::offset_of!(SpaData, chunk), 32);
714    }
715}