Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 69 additions & 4 deletions include/libremidi/backends/linux/pipewire/context.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#pragma clang diagnostic ignored "-Wsign-compare"
#endif

#include <libremidi/backends/linux/pipewire/format.hpp>
#include <libremidi/backends/linux/pipewire/instance.hpp>
#include <libremidi/backends/linux/pipewire/loader.hpp>
#include <libremidi/backends/linux/pipewire/subscription.hpp>
Expand Down Expand Up @@ -547,10 +548,23 @@ class context : public std::enable_shared_from_this<context>
// FIXME lock-free?
mutable std::mutex m_graph_mtx;

//! What a node's listener is given. The node id is not on the `param` event,
//! and `m_bound_nodes` is a vector whose elements move, so this is held by
//! unique_ptr and its address stays put.
struct node_cb_data
{
context* self{};
std::uint32_t id{};
};

struct bound_node
{
pw_proxy* proxy{};
std::unique_ptr<spa_hook> hook;
std::unique_ptr<node_cb_data> cb;
//! The enumeration round whose answers count. A node that re-advertises
//! starts a new one, and the old round's replies are still in flight.
int format_seq{};
bool info_seen{};
bool emitted_added{};
node_info info;
Expand Down Expand Up @@ -1140,10 +1154,16 @@ class context : public std::enable_shared_from_this<context>
static constexpr pw_node_events events = {
.version = PW_VERSION_NODE_EVENTS,
.info = &on_node_info,
.param = nullptr,
.param = &on_node_param,
};
slot.cb = std::make_unique<node_cb_data>(node_cb_data{this, id});
auto* node_proxy = reinterpret_cast<pw_node*>(slot.proxy);
pw_node_add_listener(node_proxy, slot.hook.get(), &events, this);
pw_node_add_listener(node_proxy, slot.hook.get(), &events, slot.cb.get());

// Only video nodes are asked: every other kind would answer with formats
// nobody reads, one round trip each.
if (slot.info.kind == media_class::video)
enum_video_formats(node_proxy, slot);

if (!slot.info.name.empty() || !slot.info.media_class_str.empty())
{
Expand All @@ -1157,11 +1177,51 @@ class context : public std::enable_shared_from_this<context>
}
}

//! SPA_PARAM_EnumFormat arrives asynchronously, one `param` callback per
//! candidate. Drop what a previous round left so a node that changed its
//! mind does not read as the union of both.
void enum_video_formats(pw_node* node_proxy, bound_node& slot) noexcept
{
slot.info.video_formats.clear();
// The daemon assigns the sequence, not the caller: the request returns it
// and every reply carries it back, which is how a superseded round is told
// apart from the current one.
slot.format_seq
= pw_node_enum_params(node_proxy, 0, SPA_PARAM_EnumFormat, 0, UINT32_MAX, nullptr);
}

static void on_node_param(
void* data, int seq, std::uint32_t id, std::uint32_t /*index*/,
std::uint32_t /*next*/, const spa_pod* param) noexcept
{
auto* cb = static_cast<node_cb_data*>(data);
if (!cb || !cb->self || id != SPA_PARAM_EnumFormat || !param)
return;

video_format_caps caps;
if (!detail::parse_video_enum_format(param, caps))
return;

for (auto& [k, slot] : cb->self->m_bound_nodes)
{
if (k != cb->id)
continue;
// A reply from a round that has been superseded would otherwise land on
// top of the new one and read as the union of both.
if (seq != slot.format_seq)
break;
slot.info.video_formats.push_back(std::move(caps));
cb->self->mirror_node_to_snapshot(slot.info);
break;
}
}

static void on_node_info(void* data, const pw_node_info* info) noexcept
{
auto* self = static_cast<context*>(data);
if (!info)
auto* cb = static_cast<node_cb_data*>(data);
if (!cb || !cb->self || !info)
return;
auto* self = cb->self;
// info->props is only valid when PW_NODE_CHANGE_MASK_PROPS is set;
// otherwise re-reading clobbers cached values with empty placeholders.
const bool props_valid
Expand All @@ -1179,6 +1239,11 @@ class context : public std::enable_shared_from_this<context>
slot.info.kind = classify_media_class(slot.info.media_class_str);
}
slot.info_seen = true;
// A camera advertises differently once something opens it, so take the
// node at its word and ask again.
if ((info->change_mask & PW_NODE_CHANGE_MASK_PARAMS)
&& slot.info.kind == media_class::video && slot.proxy)
self->enum_video_formats(reinterpret_cast<pw_node*>(slot.proxy), slot);
self->mirror_node_to_snapshot(slot.info);
if (!slot.emitted_added)
{
Expand Down
108 changes: 108 additions & 0 deletions include/libremidi/backends/linux/pipewire/format.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <libremidi/backends/linux/pipewire/drm_modifiers.hpp>
#include <libremidi/backends/linux/pipewire/loader.hpp>
#include <libremidi/backends/linux/pipewire/types.hpp>
#include <libremidi/config.hpp>

#include <spa/param/video/raw.h>
Expand Down Expand Up @@ -206,6 +207,113 @@ inline bool extract_modifier_choice(
return false;
}

//! Read one SPA_PARAM_EnumFormat object into a capability record.
//!
//! Every property may be a plain value or a choice, and the two are not
//! interchangeable: a choice's values start past its body, not at the start of
//! the pod. Returns false for anything that is not a video format object.
inline bool parse_video_enum_format(const spa_pod* fmt_obj, video_format_caps& out)
{
out = {};
out.min_fps_denom = out.max_fps_denom = 1;
if (!fmt_obj || !spa_pod_is_object(fmt_obj))
return false;

const auto* obj = reinterpret_cast<const spa_pod_object*>(fmt_obj);
if (SPA_POD_OBJECT_TYPE(obj) != SPA_TYPE_OBJECT_Format)
return false;

if (const auto* p = spa_pod_object_find_prop(obj, nullptr, SPA_FORMAT_VIDEO_format))
{
const spa_pod* v = &p->value;
if (spa_pod_is_choice(v))
{
// The first value of an Enum choice is its default: that is the one a
// panel should show, and the rest are equally acceptable.
if (SPA_POD_CHOICE_N_VALUES(v) > 0
&& SPA_POD_CHOICE_VALUE_SIZE(v) == sizeof(std::uint32_t))
std::memcpy(&out.format, SPA_POD_CHOICE_VALUES(v), sizeof(out.format));
}
else if (spa_pod_is_id(v))
{
spa_pod_get_id(v, &out.format);
}
}

if (const auto* p = spa_pod_object_find_prop(obj, nullptr, SPA_FORMAT_VIDEO_size))
{
const spa_pod* v = &p->value;
if (spa_pod_is_choice(v) && SPA_POD_CHOICE_VALUE_SIZE(v) == sizeof(spa_rectangle))
{
const auto n = SPA_POD_CHOICE_N_VALUES(v);
const auto* r = static_cast<const spa_rectangle*>(SPA_POD_CHOICE_VALUES(v));
// Range carries default, min, max; Enum carries a list. Take the extent
// of whatever is there, which is the same answer for both.
for (std::uint32_t i = 0; i < n; ++i)
{
if (i == 0 || r[i].width < out.min_width)
out.min_width = r[i].width;
if (i == 0 || r[i].width > out.max_width)
out.max_width = r[i].width;
if (i == 0 || r[i].height < out.min_height)
out.min_height = r[i].height;
if (i == 0 || r[i].height > out.max_height)
out.max_height = r[i].height;
}
}
else if (spa_pod_is_rectangle(v))
{
spa_rectangle r{};
spa_pod_get_rectangle(v, &r);
out.min_width = out.max_width = r.width;
out.min_height = out.max_height = r.height;
}
}

if (const auto* p = spa_pod_object_find_prop(obj, nullptr, SPA_FORMAT_VIDEO_framerate))
{
const spa_pod* v = &p->value;
auto as_double = [](spa_fraction f) {
return f.denom ? double(f.num) / double(f.denom) : 0.;
};
if (spa_pod_is_choice(v) && SPA_POD_CHOICE_VALUE_SIZE(v) == sizeof(spa_fraction))
{
const auto n = SPA_POD_CHOICE_N_VALUES(v);
const auto* f = static_cast<const spa_fraction*>(SPA_POD_CHOICE_VALUES(v));
for (std::uint32_t i = 0; i < n; ++i)
{
if (i == 0 || as_double(f[i]) < as_double({out.min_fps_num, out.min_fps_denom}))
{
out.min_fps_num = f[i].num;
out.min_fps_denom = f[i].denom ? f[i].denom : 1;
}
if (i == 0 || as_double(f[i]) > as_double({out.max_fps_num, out.max_fps_denom}))
{
out.max_fps_num = f[i].num;
out.max_fps_denom = f[i].denom ? f[i].denom : 1;
}
}
}
else if (spa_pod_is_fraction(v))
{
spa_fraction f{};
spa_pod_get_fraction(v, &f);
out.min_fps_num = out.max_fps_num = f.num;
out.min_fps_denom = out.max_fps_denom = f.denom ? f.denom : 1;
}
}

std::uint64_t single{};
bool dont_fixate{};
if (detail::extract_modifier_choice(fmt_obj, single, out.modifiers, dont_fixate))
{
if (out.modifiers.empty() && single != 0)
out.modifiers.push_back(single);
}

return true;
}

} // namespace detail

inline std::vector<const spa_pod*>
Expand Down
25 changes: 25 additions & 0 deletions include/libremidi/backends/linux/pipewire/types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,26 @@ struct port_info
int direction{}; /// < `SPA_DIRECTION_INPUT=0, _OUTPUT=1`
};

//! One candidate out of a node's SPA_PARAM_EnumFormat: what it can actually
//! produce or accept. Each property of a candidate may be fixed or a choice,
//! so a range is stored and a fixed value is a range of one.
struct video_format_caps
{
//! SPA_VIDEO_FORMAT_*; 0 when the candidate leaves it open.
std::uint32_t format{};

std::uint32_t min_width{}, max_width{};
std::uint32_t min_height{}, max_height{};

//! Frames per second as the fraction SPA carries, num/denom.
std::uint32_t min_fps_num{}, min_fps_denom{1};
std::uint32_t max_fps_num{}, max_fps_denom{1};

//! DRM format modifiers the candidate offers, empty when it is shared-memory
//! only.
std::vector<std::uint64_t> modifiers;
};

struct node_info
{
std::uint32_t id{};
Expand All @@ -149,6 +169,11 @@ struct node_info

std::vector<port_info> inputs;
std::vector<port_info> outputs;

//! Parsed from SPA_PARAM_EnumFormat, and empty until the daemon answers --
//! or for a node that publishes none. Empty means "unknown", never "nothing
//! is supported": a reader must fall back rather than refuse.
std::vector<video_format_caps> video_formats;
};

struct graph_snapshot
Expand Down
Loading