Skip to main content

pixelflux/
pipewire.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7//! Run-time binding to `libpipewire-0.3` and the SPA pod encoding shared by every PipeWire user
8//! in pixelflux: the webcam sink (a `Video/Source` node) and the portal screen capture (a
9//! `Video/Input` stream). The library is loaded with dlopen, so pixelflux keeps no build-time or
10//! load-time dependency on PipeWire and each user simply stays off where the library or the
11//! daemon is absent. Pods are built and read by hand against the SPA wire layout: 8-byte aligned
12//! `size, type, payload` cells, objects carrying `key, flags, cell` properties, and a choice
13//! cell whose first value is the default (and, once fixated, the only) value.
14
15use std::ffi::{c_char, c_int, c_void};
16use std::mem;
17use std::ptr;
18
19use libloading::Library;
20
21pub const SPA_TYPE_ID: u32 = 3;
22pub const SPA_TYPE_INT: u32 = 4;
23pub const SPA_TYPE_LONG: u32 = 5;
24pub const SPA_TYPE_RECTANGLE: u32 = 10;
25pub const SPA_TYPE_FRACTION: u32 = 11;
26pub const SPA_TYPE_OBJECT: u32 = 15;
27pub const SPA_TYPE_CHOICE: u32 = 19;
28pub const SPA_TYPE_OBJECT_FORMAT: u32 = 0x40003;
29pub const SPA_TYPE_OBJECT_PARAM_BUFFERS: u32 = 0x40004;
30pub const SPA_TYPE_OBJECT_PARAM_META: u32 = 0x40005;
31pub const SPA_PARAM_ENUM_FORMAT: u32 = 3;
32pub const SPA_PARAM_FORMAT: u32 = 4;
33pub const SPA_PARAM_BUFFERS: u32 = 5;
34pub const SPA_PARAM_META: u32 = 6;
35pub const SPA_FORMAT_MEDIA_TYPE: u32 = 1;
36pub const SPA_FORMAT_MEDIA_SUBTYPE: u32 = 2;
37pub const SPA_FORMAT_VIDEO_FORMAT: u32 = 0x20001;
38pub const SPA_FORMAT_VIDEO_MODIFIER: u32 = 0x20002;
39pub const SPA_FORMAT_VIDEO_SIZE: u32 = 0x20003;
40pub const SPA_FORMAT_VIDEO_FRAMERATE: u32 = 0x20004;
41pub const SPA_FORMAT_VIDEO_MAX_FRAMERATE: u32 = 0x20005;
42pub const SPA_MEDIA_TYPE_VIDEO: u32 = 2;
43pub const SPA_MEDIA_SUBTYPE_RAW: u32 = 1;
44pub const SPA_MEDIA_SUBTYPE_MJPG: u32 = 0x20002;
45pub const SPA_VIDEO_FORMAT_I420: u32 = 2;
46pub const SPA_VIDEO_FORMAT_YUY2: u32 = 4;
47pub const SPA_VIDEO_FORMAT_RGBX: u32 = 7;
48pub const SPA_VIDEO_FORMAT_BGRX: u32 = 8;
49pub const SPA_VIDEO_FORMAT_RGBA: u32 = 11;
50pub const SPA_VIDEO_FORMAT_BGRA: u32 = 12;
51pub const SPA_VIDEO_FORMAT_ARGB: u32 = 13;
52pub const SPA_VIDEO_FORMAT_ABGR: u32 = 14;
53pub const SPA_VIDEO_FORMAT_NV12: u32 = 23;
54pub const SPA_PARAM_BUFFERS_BUFFERS: u32 = 1;
55pub const SPA_PARAM_BUFFERS_BLOCKS: u32 = 2;
56pub const SPA_PARAM_BUFFERS_SIZE: u32 = 3;
57pub const SPA_PARAM_BUFFERS_STRIDE: u32 = 4;
58pub const SPA_PARAM_BUFFERS_ALIGN: u32 = 5;
59pub const SPA_PARAM_BUFFERS_DATATYPE: u32 = 6;
60pub const SPA_PARAM_META_TYPE: u32 = 1;
61pub const SPA_PARAM_META_SIZE: u32 = 2;
62pub const SPA_META_HEADER: u32 = 1;
63pub const SPA_META_VIDEO_CROP: u32 = 2;
64pub const SPA_META_VIDEO_DAMAGE: u32 = 3;
65pub const SPA_META_CURSOR: u32 = 5;
66pub const SPA_META_HEADER_SIZE: u32 = 32;
67pub const SPA_META_REGION_SIZE: u32 = 16;
68pub const SPA_DATA_MEMPTR: u32 = 1;
69pub const SPA_DATA_MEMFD: u32 = 2;
70pub const SPA_DATA_DMABUF: u32 = 3;
71pub const SPA_CHUNK_FLAG_CORRUPTED: i32 = 1;
72pub const SPA_CHOICE_NONE: u32 = 0;
73pub const SPA_CHOICE_RANGE: u32 = 1;
74pub const SPA_CHOICE_ENUM: u32 = 3;
75pub const SPA_CHOICE_FLAGS: u32 = 4;
76pub const SPA_POD_PROP_FLAG_MANDATORY: u32 = 1 << 3;
77pub const SPA_POD_PROP_FLAG_DONT_FIXATE: u32 = 1 << 4;
78pub const PW_DIRECTION_INPUT: c_int = 0;
79pub const PW_DIRECTION_OUTPUT: c_int = 1;
80pub const PW_ID_ANY: u32 = 0xFFFF_FFFF;
81pub const PW_STREAM_FLAG_AUTOCONNECT: u32 = 1;
82pub const PW_STREAM_FLAG_MAP_BUFFERS: u32 = 4;
83pub const PW_STREAM_FLAG_DRIVER: u32 = 8;
84pub const PW_STREAM_STATE_ERROR: c_int = -1;
85pub const PW_STREAM_STATE_UNCONNECTED: c_int = 0;
86pub const PW_STREAM_STATE_PAUSED: c_int = 2;
87pub const PW_STREAM_STATE_STREAMING: c_int = 3;
88pub const PW_VERSION_STREAM_EVENTS: u32 = 2;
89
90#[repr(C)]
91pub struct PwStreamEvents {
92    pub version: u32,
93    pub destroy: Option<unsafe extern "C" fn(*mut c_void)>,
94    pub state_changed: Option<unsafe extern "C" fn(*mut c_void, c_int, c_int, *const c_char)>,
95    pub control_info: Option<unsafe extern "C" fn(*mut c_void, u32, *const c_void)>,
96    pub io_changed: Option<unsafe extern "C" fn(*mut c_void, u32, *mut c_void, u32)>,
97    pub param_changed: Option<unsafe extern "C" fn(*mut c_void, u32, *const c_void)>,
98    pub add_buffer: Option<unsafe extern "C" fn(*mut c_void, *mut PwBuffer)>,
99    pub remove_buffer: Option<unsafe extern "C" fn(*mut c_void, *mut PwBuffer)>,
100    pub process: Option<unsafe extern "C" fn(*mut c_void)>,
101    pub drained: Option<unsafe extern "C" fn(*mut c_void)>,
102    pub command: Option<unsafe extern "C" fn(*mut c_void, *const c_void)>,
103    pub trigger_done: Option<unsafe extern "C" fn(*mut c_void)>,
104}
105
106impl PwStreamEvents {
107    /// An event table with every callback unset; users fill in the ones they serve.
108    pub const fn empty() -> Self {
109        Self {
110            version: PW_VERSION_STREAM_EVENTS,
111            destroy: None,
112            state_changed: None,
113            control_info: None,
114            io_changed: None,
115            param_changed: None,
116            add_buffer: None,
117            remove_buffer: None,
118            process: None,
119            drained: None,
120            command: None,
121            trigger_done: None,
122        }
123    }
124}
125
126#[repr(C)]
127pub struct PwBuffer {
128    pub buffer: *mut SpaBuffer,
129    pub user_data: *mut c_void,
130    pub size: u64,
131    pub requested: u64,
132    pub time: u64,
133}
134
135#[repr(C)]
136pub struct SpaBuffer {
137    pub n_metas: u32,
138    pub n_datas: u32,
139    pub metas: *mut SpaMeta,
140    pub datas: *mut SpaData,
141}
142
143#[repr(C)]
144pub struct SpaMeta {
145    pub type_: u32,
146    pub size: u32,
147    pub data: *mut c_void,
148}
149
150#[repr(C)]
151pub struct SpaData {
152    pub type_: u32,
153    pub flags: u32,
154    pub fd: i64,
155    pub mapoffset: u32,
156    pub maxsize: u32,
157    pub data: *mut c_void,
158    pub chunk: *mut SpaChunk,
159}
160
161#[repr(C)]
162pub struct SpaChunk {
163    pub offset: u32,
164    pub size: u32,
165    pub stride: i32,
166    pub flags: i32,
167}
168
169#[repr(C)]
170pub struct SpaMetaHeader {
171    pub flags: u32,
172    pub offset: u32,
173    pub pts: i64,
174    pub dts_offset: i64,
175    pub seq: u64,
176}
177
178/// `spa_meta_region`: one damage or crop rectangle; a zero-sized one ends a damage array.
179#[repr(C)]
180#[derive(Clone, Copy)]
181pub struct SpaMetaRegion {
182    pub x: i32,
183    pub y: i32,
184    pub width: u32,
185    pub height: u32,
186}
187
188#[repr(C)]
189pub struct SpaMetaCursor {
190    pub id: u32,
191    pub flags: u32,
192    pub x: i32,
193    pub y: i32,
194    pub hot_x: i32,
195    pub hot_y: i32,
196    pub bitmap_offset: u32,
197}
198
199#[repr(C)]
200pub struct SpaMetaBitmap {
201    pub format: u32,
202    pub width: u32,
203    pub height: u32,
204    pub stride: i32,
205    pub offset: u32,
206}
207
208/// Bytes a cursor meta needs to carry a `w`x`h` sprite (`CURSOR_META_SIZE`).
209pub const fn cursor_meta_size(w: u32, h: u32) -> u32 {
210    (mem::size_of::<SpaMetaCursor>() + mem::size_of::<SpaMetaBitmap>()) as u32 + w * h * 4
211}
212
213type PwInit = unsafe extern "C" fn(*mut c_int, *mut *mut *mut c_char);
214type PwThreadLoopNew = unsafe extern "C" fn(*const c_char, *const c_void) -> *mut c_void;
215type PwThreadLoopGetLoop = unsafe extern "C" fn(*mut c_void) -> *mut c_void;
216type PwThreadLoopInt = unsafe extern "C" fn(*mut c_void) -> c_int;
217type PwThreadLoopVoid = unsafe extern "C" fn(*mut c_void);
218type PwContextNew = unsafe extern "C" fn(*mut c_void, *mut c_void, usize) -> *mut c_void;
219type PwContextConnect = unsafe extern "C" fn(*mut c_void, *mut c_void, usize) -> *mut c_void;
220type PwContextConnectFd =
221    unsafe extern "C" fn(*mut c_void, c_int, *mut c_void, usize) -> *mut c_void;
222type PwContextDestroy = unsafe extern "C" fn(*mut c_void);
223type PwCoreDisconnect = unsafe extern "C" fn(*mut c_void) -> c_int;
224type PwPropertiesNew = unsafe extern "C" fn(*const c_char, ...) -> *mut c_void;
225type PwPropertiesSet = unsafe extern "C" fn(*mut c_void, *const c_char, *const c_char) -> c_int;
226type PwStreamNew = unsafe extern "C" fn(*mut c_void, *const c_char, *mut c_void) -> *mut c_void;
227type PwStreamAddListener =
228    unsafe extern "C" fn(*mut c_void, *mut c_void, *const PwStreamEvents, *mut c_void);
229type PwStreamConnect =
230    unsafe extern "C" fn(*mut c_void, c_int, u32, u32, *mut *const c_void, u32) -> c_int;
231type PwStreamUpdateParams = unsafe extern "C" fn(*mut c_void, *mut *const c_void, u32) -> c_int;
232type PwStreamDequeueBuffer = unsafe extern "C" fn(*mut c_void) -> *mut PwBuffer;
233type PwStreamQueueBuffer = unsafe extern "C" fn(*mut c_void, *mut PwBuffer) -> c_int;
234type PwStreamTriggerProcess = unsafe extern "C" fn(*mut c_void) -> c_int;
235type PwStreamSetActive = unsafe extern "C" fn(*mut c_void, bool) -> c_int;
236type PwStreamVoid = unsafe extern "C" fn(*mut c_void);
237type PwStreamInt = unsafe extern "C" fn(*mut c_void) -> c_int;
238
239/// Entry points resolved from `libpipewire-0.3.so.0`.
240#[derive(Clone, Copy)]
241pub struct Api {
242    pub thread_loop_new: PwThreadLoopNew,
243    pub thread_loop_get_loop: PwThreadLoopGetLoop,
244    pub thread_loop_start: PwThreadLoopInt,
245    pub thread_loop_stop: PwThreadLoopVoid,
246    pub thread_loop_lock: PwThreadLoopVoid,
247    pub thread_loop_unlock: PwThreadLoopVoid,
248    pub thread_loop_destroy: PwThreadLoopVoid,
249    pub context_new: PwContextNew,
250    pub context_connect: PwContextConnect,
251    pub context_connect_fd: PwContextConnectFd,
252    pub context_destroy: PwContextDestroy,
253    pub core_disconnect: PwCoreDisconnect,
254    pub properties_new: PwPropertiesNew,
255    pub properties_set: PwPropertiesSet,
256    pub stream_new: PwStreamNew,
257    pub stream_add_listener: PwStreamAddListener,
258    pub stream_connect: PwStreamConnect,
259    pub stream_update_params: PwStreamUpdateParams,
260    pub stream_dequeue_buffer: PwStreamDequeueBuffer,
261    pub stream_queue_buffer: PwStreamQueueBuffer,
262    pub stream_trigger_process: PwStreamTriggerProcess,
263    pub stream_set_active: PwStreamSetActive,
264    pub stream_disconnect: PwStreamInt,
265    pub stream_destroy: PwStreamVoid,
266}
267
268/// The library handle is kept for the life of the process: PipeWire's own globals (pw_init) make
269/// unloading it unsafe, and every later user reuses it.
270pub fn api() -> Result<&'static Api, String> {
271    static API: std::sync::OnceLock<Result<Api, String>> = std::sync::OnceLock::new();
272    API.get_or_init(|| unsafe {
273        let lib = Library::new("libpipewire-0.3.so.0")
274            .map_err(|e| format!("libpipewire-0.3 not available: {}", e))?;
275        macro_rules! sym {
276            ($name:literal, $t:ty) => {
277                *lib.get::<$t>(concat!($name, "\0").as_bytes())
278                    .map_err(|e| format!("{}: {}", $name, e))?
279            };
280        }
281        let init: PwInit = sym!("pw_init", PwInit);
282        let api = Api {
283            thread_loop_new: sym!("pw_thread_loop_new", PwThreadLoopNew),
284            thread_loop_get_loop: sym!("pw_thread_loop_get_loop", PwThreadLoopGetLoop),
285            thread_loop_start: sym!("pw_thread_loop_start", PwThreadLoopInt),
286            thread_loop_stop: sym!("pw_thread_loop_stop", PwThreadLoopVoid),
287            thread_loop_lock: sym!("pw_thread_loop_lock", PwThreadLoopVoid),
288            thread_loop_unlock: sym!("pw_thread_loop_unlock", PwThreadLoopVoid),
289            thread_loop_destroy: sym!("pw_thread_loop_destroy", PwThreadLoopVoid),
290            context_new: sym!("pw_context_new", PwContextNew),
291            context_connect: sym!("pw_context_connect", PwContextConnect),
292            context_connect_fd: sym!("pw_context_connect_fd", PwContextConnectFd),
293            context_destroy: sym!("pw_context_destroy", PwContextDestroy),
294            core_disconnect: sym!("pw_core_disconnect", PwCoreDisconnect),
295            properties_new: sym!("pw_properties_new", PwPropertiesNew),
296            properties_set: sym!("pw_properties_set", PwPropertiesSet),
297            stream_new: sym!("pw_stream_new", PwStreamNew),
298            stream_add_listener: sym!("pw_stream_add_listener", PwStreamAddListener),
299            stream_connect: sym!("pw_stream_connect", PwStreamConnect),
300            stream_update_params: sym!("pw_stream_update_params", PwStreamUpdateParams),
301            stream_dequeue_buffer: sym!("pw_stream_dequeue_buffer", PwStreamDequeueBuffer),
302            stream_queue_buffer: sym!("pw_stream_queue_buffer", PwStreamQueueBuffer),
303            stream_trigger_process: sym!("pw_stream_trigger_process", PwStreamTriggerProcess),
304            stream_set_active: sym!("pw_stream_set_active", PwStreamSetActive),
305            stream_disconnect: sym!("pw_stream_disconnect", PwStreamInt),
306            stream_destroy: sym!("pw_stream_destroy", PwStreamVoid),
307        };
308        init(ptr::null_mut(), ptr::null_mut());
309        mem::forget(lib);
310        Ok(api)
311    })
312    .as_ref()
313    .map_err(|e| e.clone())
314}
315
316// --- SPA pod construction ---------------------------------------------------------------------
317
318pub fn push_u32(v: &mut Vec<u8>, x: u32) {
319    v.extend_from_slice(&x.to_ne_bytes());
320}
321
322fn pad8(v: &mut Vec<u8>) {
323    while !v.len().is_multiple_of(8) {
324        v.push(0);
325    }
326}
327
328/// One `size, type, payload` cell, padded to 8 bytes.
329pub fn pod_prim(v: &mut Vec<u8>, ty: u32, payload: &[u8]) {
330    push_u32(v, payload.len() as u32);
331    push_u32(v, ty);
332    v.extend_from_slice(payload);
333    pad8(v);
334}
335
336pub fn pod_id(v: &mut Vec<u8>, x: u32) {
337    pod_prim(v, SPA_TYPE_ID, &x.to_ne_bytes());
338}
339
340pub fn pod_int(v: &mut Vec<u8>, x: i32) {
341    pod_prim(v, SPA_TYPE_INT, &x.to_ne_bytes());
342}
343
344pub fn pod_rect(v: &mut Vec<u8>, w: u32, h: u32) {
345    let mut p = Vec::new();
346    push_u32(&mut p, w);
347    push_u32(&mut p, h);
348    pod_prim(v, SPA_TYPE_RECTANGLE, &p);
349}
350
351pub fn pod_frac(v: &mut Vec<u8>, num: u32, den: u32) {
352    let mut p = Vec::new();
353    push_u32(&mut p, num);
354    push_u32(&mut p, den);
355    pod_prim(v, SPA_TYPE_FRACTION, &p);
356}
357
358/// A choice cell over fixed-size values of `child_ty`: `values` holds the default first, then
359/// the alternatives (Enum), the bounds (Range), or the flag words (Flags), each `child_size` bytes.
360fn pod_choice(v: &mut Vec<u8>, choice: u32, child_ty: u32, child_size: u32, values: &[u8]) {
361    let mut body = Vec::new();
362    push_u32(&mut body, choice);
363    push_u32(&mut body, 0);
364    push_u32(&mut body, child_size);
365    push_u32(&mut body, child_ty);
366    body.extend_from_slice(values);
367    pod_prim(v, SPA_TYPE_CHOICE, &body);
368}
369
370pub fn pod_choice_int(v: &mut Vec<u8>, choice: u32, values: &[i32]) {
371    let bytes: Vec<u8> = values.iter().flat_map(|x| x.to_ne_bytes()).collect();
372    pod_choice(v, choice, SPA_TYPE_INT, 4, &bytes);
373}
374
375pub fn pod_choice_id(v: &mut Vec<u8>, choice: u32, values: &[u32]) {
376    let bytes: Vec<u8> = values.iter().flat_map(|x| x.to_ne_bytes()).collect();
377    pod_choice(v, choice, SPA_TYPE_ID, 4, &bytes);
378}
379
380pub fn pod_choice_long(v: &mut Vec<u8>, choice: u32, values: &[i64]) {
381    let bytes: Vec<u8> = values.iter().flat_map(|x| x.to_ne_bytes()).collect();
382    pod_choice(v, choice, SPA_TYPE_LONG, 8, &bytes);
383}
384
385/// A rectangle choice; `values` are `(w, h)` pairs, default first.
386pub fn pod_choice_rect(v: &mut Vec<u8>, choice: u32, values: &[(u32, u32)]) {
387    let bytes: Vec<u8> = values
388        .iter()
389        .flat_map(|(w, h)| [w.to_ne_bytes(), h.to_ne_bytes()].concat())
390        .collect();
391    pod_choice(v, choice, SPA_TYPE_RECTANGLE, 8, &bytes);
392}
393
394/// A fraction choice; `values` are `(num, den)` pairs, default first.
395pub fn pod_choice_frac(v: &mut Vec<u8>, choice: u32, values: &[(u32, u32)]) {
396    let bytes: Vec<u8> = values
397        .iter()
398        .flat_map(|(n, d)| [n.to_ne_bytes(), d.to_ne_bytes()].concat())
399        .collect();
400    pod_choice(v, choice, SPA_TYPE_FRACTION, 8, &bytes);
401}
402
403/// One object property: key, flags, then the value cell `value` appends.
404pub fn prop_flags(v: &mut Vec<u8>, key: u32, flags: u32, value: impl FnOnce(&mut Vec<u8>)) {
405    push_u32(v, key);
406    push_u32(v, flags);
407    value(v);
408}
409
410pub fn prop(v: &mut Vec<u8>, key: u32, value: impl FnOnce(&mut Vec<u8>)) {
411    prop_flags(v, key, 0, value);
412}
413
414/// An object pod of type `ty` carrying param `id` and the properties `props` appends.
415pub fn object(ty: u32, id: u32, props: impl FnOnce(&mut Vec<u8>)) -> Vec<u8> {
416    let mut body = Vec::new();
417    push_u32(&mut body, ty);
418    push_u32(&mut body, id);
419    props(&mut body);
420    let mut v = Vec::new();
421    pod_prim(&mut v, SPA_TYPE_OBJECT, &body);
422    v
423}
424
425// --- SPA pod reading --------------------------------------------------------------------------
426
427/// One property of an object pod: its key, flags, and value cell.
428#[derive(Clone, Copy, Debug)]
429pub struct PodProp<'a> {
430    pub key: u32,
431    pub flags: u32,
432    pub ty: u32,
433    pub payload: &'a [u8],
434}
435
436fn u32_at(v: &[u8], off: usize) -> Option<u32> {
437    v.get(off..off + 4)
438        .map(|b| u32::from_ne_bytes(b.try_into().unwrap()))
439}
440
441/// The bytes of the pod a PipeWire callback points at: its header names the payload size.
442///
443/// # Safety
444/// `pod` must point at a complete SPA pod that outlives the returned slice.
445pub unsafe fn pod_bytes<'a>(pod: *const c_void) -> &'a [u8] {
446    let size = unsafe { *(pod as *const u32) } as usize;
447    unsafe { std::slice::from_raw_parts(pod as *const u8, size + 8) }
448}
449
450/// Walk an object pod: `(object type, param id, properties)`; `None` for anything else.
451pub fn object_props(pod: &[u8]) -> Option<(u32, u32, Vec<PodProp<'_>>)> {
452    let size = u32_at(pod, 0)? as usize;
453    if u32_at(pod, 4)? != SPA_TYPE_OBJECT || pod.len() < size + 8 {
454        return None;
455    }
456    let body = &pod[8..8 + size];
457    let ty = u32_at(body, 0)?;
458    let id = u32_at(body, 4)?;
459    let mut props = Vec::new();
460    let mut off = 8;
461    while off + 16 <= body.len() {
462        let key = u32_at(body, off)?;
463        let flags = u32_at(body, off + 4)?;
464        let vsize = u32_at(body, off + 8)? as usize;
465        let vty = u32_at(body, off + 12)?;
466        let payload = body.get(off + 16..off + 16 + vsize)?;
467        props.push(PodProp {
468            key,
469            flags,
470            ty: vty,
471            payload,
472        });
473        off += 16 + vsize.div_ceil(8) * 8;
474    }
475    Some((ty, id, props))
476}
477
478/// A cell's first value: a plain cell as it is, a choice cell's default (its only value once
479/// fixated). Returns the value type and bytes.
480pub fn pod_value(ty: u32, payload: &[u8]) -> Option<(u32, &[u8])> {
481    if ty != SPA_TYPE_CHOICE {
482        return Some((ty, payload));
483    }
484    let child_size = u32_at(payload, 8)? as usize;
485    let child_ty = u32_at(payload, 12)?;
486    Some((child_ty, payload.get(16..16 + child_size)?))
487}
488
489impl PodProp<'_> {
490    pub fn as_u32(&self) -> Option<u32> {
491        let (ty, b) = pod_value(self.ty, self.payload)?;
492        (ty == SPA_TYPE_ID || ty == SPA_TYPE_INT)
493            .then(|| u32_at(b, 0))
494            .flatten()
495    }
496
497    pub fn as_i64(&self) -> Option<i64> {
498        let (ty, b) = pod_value(self.ty, self.payload)?;
499        (ty == SPA_TYPE_LONG)
500            .then(|| {
501                b.get(..8)
502                    .map(|x| i64::from_ne_bytes(x.try_into().unwrap()))
503            })
504            .flatten()
505    }
506
507    /// A rectangle or fraction pair.
508    pub fn as_pair(&self) -> Option<(u32, u32)> {
509        let (ty, b) = pod_value(self.ty, self.payload)?;
510        (ty == SPA_TYPE_RECTANGLE || ty == SPA_TYPE_FRACTION)
511            .then(|| Some((u32_at(b, 0)?, u32_at(b, 4)?)))
512            .flatten()
513    }
514}
515
516/// The property `key` of an object pod's property list.
517pub fn find_prop<'a>(props: &'a [PodProp<'a>], key: u32) -> Option<&'a PodProp<'a>> {
518    props.iter().find(|p| p.key == key)
519}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524
525    #[test]
526    fn struct_layouts_match_libpipewire() {
527        assert_eq!(mem::size_of::<PwStreamEvents>(), 96);
528        assert_eq!(mem::size_of::<PwBuffer>(), 40);
529        assert_eq!(mem::size_of::<SpaBuffer>(), 24);
530        assert_eq!(mem::size_of::<SpaData>(), 40);
531        assert_eq!(mem::size_of::<SpaChunk>(), 16);
532        assert_eq!(mem::size_of::<SpaMetaHeader>(), 32);
533        assert_eq!(
534            mem::size_of::<SpaMetaRegion>(),
535            SPA_META_REGION_SIZE as usize
536        );
537        assert_eq!(mem::size_of::<SpaMetaCursor>(), 28);
538        assert_eq!(mem::size_of::<SpaMetaBitmap>(), 20);
539        assert_eq!(cursor_meta_size(64, 64), 28 + 20 + 64 * 64 * 4);
540        assert_eq!(mem::offset_of!(PwStreamEvents, process), 64);
541        assert_eq!(mem::offset_of!(SpaData, data), 24);
542        assert_eq!(mem::offset_of!(SpaData, chunk), 32);
543    }
544
545    /// Every value cell the builders emit reads back through the reader, plain or as a choice
546    /// whose default is the first value, with property flags preserved.
547    #[test]
548    fn built_pods_read_back() {
549        let pod = object(SPA_TYPE_OBJECT_FORMAT, SPA_PARAM_ENUM_FORMAT, |p| {
550            prop(p, SPA_FORMAT_MEDIA_TYPE, |v| {
551                pod_id(v, SPA_MEDIA_TYPE_VIDEO)
552            });
553            prop(p, SPA_FORMAT_VIDEO_FORMAT, |v| {
554                pod_choice_id(
555                    v,
556                    SPA_CHOICE_ENUM,
557                    &[
558                        SPA_VIDEO_FORMAT_BGRX,
559                        SPA_VIDEO_FORMAT_BGRX,
560                        SPA_VIDEO_FORMAT_BGRA,
561                    ],
562                )
563            });
564            prop_flags(
565                p,
566                SPA_FORMAT_VIDEO_MODIFIER,
567                SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE,
568                |v| {
569                    pod_choice_long(
570                        v,
571                        SPA_CHOICE_ENUM,
572                        &[0x0300_0000_0000_0001, 0x0300_0000_0000_0001, 0],
573                    )
574                },
575            );
576            prop(p, SPA_FORMAT_VIDEO_SIZE, |v| {
577                pod_choice_rect(v, SPA_CHOICE_RANGE, &[(1280, 720), (1, 1), (16384, 16384)])
578            });
579            prop(p, SPA_FORMAT_VIDEO_FRAMERATE, |v| pod_frac(v, 0, 1));
580            prop(p, SPA_FORMAT_VIDEO_MAX_FRAMERATE, |v| {
581                pod_choice_frac(v, SPA_CHOICE_RANGE, &[(60, 1), (1, 1), (60, 1)])
582            });
583            prop(p, SPA_FORMAT_VIDEO_MAX_FRAMERATE + 1, |v| pod_int(v, 4));
584        });
585        assert_eq!(pod.len() % 8, 0);
586        let (ty, id, props) = object_props(&pod).unwrap();
587        assert_eq!((ty, id), (SPA_TYPE_OBJECT_FORMAT, SPA_PARAM_ENUM_FORMAT));
588        assert_eq!(
589            find_prop(&props, SPA_FORMAT_MEDIA_TYPE).unwrap().as_u32(),
590            Some(SPA_MEDIA_TYPE_VIDEO)
591        );
592        assert_eq!(
593            find_prop(&props, SPA_FORMAT_VIDEO_FORMAT).unwrap().as_u32(),
594            Some(SPA_VIDEO_FORMAT_BGRX)
595        );
596        let modifier = find_prop(&props, SPA_FORMAT_VIDEO_MODIFIER).unwrap();
597        assert_eq!(
598            modifier.flags,
599            SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE
600        );
601        assert_eq!(modifier.as_i64(), Some(0x0300_0000_0000_0001));
602        assert_eq!(
603            find_prop(&props, SPA_FORMAT_VIDEO_SIZE).unwrap().as_pair(),
604            Some((1280, 720))
605        );
606        assert_eq!(
607            find_prop(&props, SPA_FORMAT_VIDEO_FRAMERATE)
608                .unwrap()
609                .as_pair(),
610            Some((0, 1))
611        );
612        assert_eq!(
613            find_prop(&props, SPA_FORMAT_VIDEO_MAX_FRAMERATE)
614                .unwrap()
615                .as_pair(),
616            Some((60, 1))
617        );
618        assert_eq!(
619            find_prop(&props, SPA_FORMAT_VIDEO_MAX_FRAMERATE + 1)
620                .unwrap()
621                .as_u32(),
622            Some(4)
623        );
624        assert!(find_prop(&props, SPA_FORMAT_MEDIA_SUBTYPE).is_none());
625        let bytes = unsafe { pod_bytes(pod.as_ptr() as *const c_void) };
626        assert_eq!(bytes.len(), pod.len());
627    }
628}