use pipewire as pw;
use pipewire::spa::{
param::{
format::{FormatProperties, MediaSubtype, MediaType},
ParamType,
},
pod::{serialize::PodSerializer, Object, Pod, Property, PropertyFlags, Value},
utils::{Choice, ChoiceEnum, Fraction, Rectangle, SpaTypes},
};
use crate::{Config, Format};
pub const MAX_BUFFERS: i32 = 16;
pub fn enumformat_pod(format: Format, width: u32, height: u32, fps: u32) -> Vec<u8> {
let obj = pw::spa::pod::object!(
SpaTypes::ObjectParamFormat,
ParamType::EnumFormat,
pw::spa::pod::property!(FormatProperties::MediaType, Id, MediaType::Video),
pw::spa::pod::property!(FormatProperties::MediaSubtype, Id, MediaSubtype::Raw),
pw::spa::pod::property!(FormatProperties::VideoFormat, Id, format.video_format()),
pw::spa::pod::property!(
FormatProperties::VideoSize,
Rectangle,
Rectangle { width, height }
),
pw::spa::pod::property!(
FormatProperties::VideoFramerate,
Fraction,
Fraction { num: fps, denom: 1 }
),
);
serialize(obj)
}
pub fn buffers_pod(stride: u32, height: u32, num_planes: u32, max_buffers: u32) -> Vec<u8> {
let min = 2u32.min(max_buffers);
let max = max_buffers.clamp(2, MAX_BUFFERS as u32).max(min);
let default = 4.min(max).max(min);
let obj = Object {
type_: SpaTypes::ObjectParamBuffers.as_raw(),
id: ParamType::Buffers.as_raw(),
properties: vec![
Property {
key: pw::spa::sys::SPA_PARAM_BUFFERS_buffers,
flags: PropertyFlags::empty(),
value: Value::Choice(pw::spa::pod::ChoiceValue::Int(Choice(
pw::spa::utils::ChoiceFlags::empty(),
ChoiceEnum::Range {
default: default as i32,
min: min as i32,
max: max as i32,
},
))),
},
Property {
key: pw::spa::sys::SPA_PARAM_BUFFERS_blocks,
flags: PropertyFlags::empty(),
value: Value::Int(num_planes as i32),
},
Property {
key: pw::spa::sys::SPA_PARAM_BUFFERS_size,
flags: PropertyFlags::empty(),
value: Value::Int(stride as i32 * height as i32),
},
Property {
key: pw::spa::sys::SPA_PARAM_BUFFERS_stride,
flags: PropertyFlags::empty(),
value: Value::Int(stride as i32),
},
],
};
serialize(obj)
}
pub fn meta_pod() -> Vec<u8> {
let obj = Object {
type_: SpaTypes::ObjectParamMeta.as_raw(),
id: ParamType::Meta.as_raw(),
properties: vec![
Property {
key: pw::spa::sys::SPA_PARAM_META_type,
flags: PropertyFlags::empty(),
value: Value::Int(pw::spa::sys::SPA_META_Header as i32),
},
Property {
key: pw::spa::sys::SPA_PARAM_META_size,
flags: PropertyFlags::empty(),
value: Value::Int(std::mem::size_of::<pw::spa::sys::spa_meta_header>() as i32),
},
],
};
serialize(obj)
}
pub fn advertised_param_blobs(config: &Config) -> Vec<Vec<u8>> {
let mut advertised: Vec<(Format, u32, u32, u32)> = Vec::new();
for mode in &config.modes {
for &format in &mode.formats {
for &fps in &mode.fps {
let entry = (format, mode.width, mode.height, fps);
if !advertised.contains(&entry) {
advertised.push(entry);
}
}
}
}
let mut blobs: Vec<Vec<u8>> = Vec::with_capacity(advertised.len() + 1);
for &(format, width, height, fps) in &advertised {
blobs.push(enumformat_pod(format, width, height, fps));
}
blobs.push(meta_pod());
blobs
}
pub fn serialize(obj: Object) -> Vec<u8> {
let (cursor, _size) =
PodSerializer::serialize(std::io::Cursor::new(Vec::new()), &Value::Object(obj))
.expect("failed to serialize POD");
let bytes = cursor.into_inner();
Pod::from_bytes(&bytes).expect("failed to parse serialized POD");
bytes
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Mode;
use libspa::param::video::VideoInfoRaw;
const W: u32 = 384;
const H: u32 = 272;
const FPS: u32 = 47;
#[test]
fn enumformat_pod_roundtrip() {
for f in Format::all() {
let blob = enumformat_pod(*f, W, H, FPS);
let pod = Pod::from_bytes(&blob).expect("serialize should round-trip");
let mut info = VideoInfoRaw::default();
info.parse(pod)
.expect("EnumFormat must parse as a video format");
assert_eq!(
info.format(),
f.video_format(),
"format id mismatch for {f:?}"
);
assert_eq!(info.size().width, W);
assert_eq!(info.size().height, H);
assert_eq!(info.framerate().num, FPS);
assert_eq!(info.framerate().denom, 1);
}
}
#[test]
fn enumformat_pod_multi_fps() {
for fps in [15u32, 30, 60] {
let blob = enumformat_pod(Format::Rgba, W, H, fps);
let pod = Pod::from_bytes(&blob).expect("serialize should round-trip");
let mut info = VideoInfoRaw::default();
info.parse(pod)
.expect("EnumFormat must parse as a video format");
assert_eq!(info.framerate().num, fps);
assert_eq!(info.framerate().denom, 1);
assert_eq!(info.size().width, W);
assert_eq!(info.size().height, H);
}
}
#[test]
fn advertised_param_blobs_dedup() {
let config = Config {
name: "cam".into(),
media_name: "cam".into(),
modes: vec![
Mode {
width: W,
height: H,
fps: vec![FPS, FPS],
formats: vec![Format::Rgba, Format::Rgba],
},
Mode {
width: W,
height: H,
fps: vec![FPS + 1],
formats: vec![Format::Rgba],
},
],
max_buffers: 4,
};
let blobs = advertised_param_blobs(&config);
assert_eq!(blobs.len(), 3);
let meta = Pod::from_bytes(&blobs[2]).expect("meta pod must parse");
let obj = meta.as_object().expect("meta pod must be an object");
assert_eq!(obj.id().0, ParamType::Meta.as_raw());
}
#[test]
fn advertised_param_blobs_grouped_by_format_then_size() {
let config = Config {
name: "cam".into(),
media_name: "cam".into(),
modes: vec![
Mode {
width: 1920,
height: 1080,
fps: vec![24, 30],
formats: vec![Format::Rgba, Format::Nv12],
},
Mode {
width: 1280,
height: 720,
fps: vec![24, 30],
formats: vec![Format::Rgba, Format::Nv12],
},
],
max_buffers: 4,
};
let blobs = advertised_param_blobs(&config);
let mut seen: Vec<(u32, u32, u32, u32)> = Vec::new();
for blob in &blobs {
let pod = Pod::from_bytes(blob).expect("blob must parse");
let obj = pod.as_object().expect("must be an object");
if obj.id().0 != ParamType::EnumFormat.as_raw() {
continue;
}
let mut info = VideoInfoRaw::default();
info.parse(pod).expect("must parse as video format");
seen.push((
info.format().0,
info.size().width,
info.size().height,
info.framerate().num,
));
}
let mut groups_in_order: Vec<(u32, u32, u32)> = Vec::new();
for (fmt, w, h, _) in &seen {
let group = (*fmt, *w, *h);
match groups_in_order.iter().position(|g| *g == group) {
Some(pos) => {
if pos != groups_in_order.len() - 1 {
panic!(
"group ({fmt}, {w}x{h}) is not contiguous — fps
for a (format, size) must be advertised
consecutively"
);
}
}
None => groups_in_order.push(group),
}
}
}
#[test]
fn buffers_and_meta_pods_roundtrip() {
for blob in [
buffers_pod(7680, 1080, 1, 4),
buffers_pod(1920, 1080, 3, 16), buffers_pod(1920, 1080, 3, 2), meta_pod(),
] {
let pod = Pod::from_bytes(&blob).expect("serialize should round-trip");
assert!(
pod.size() > 0 && !pod.as_bytes().is_empty(),
"pod must be non-trivial"
);
}
}
}