diff --git a/include/libremidi/backends/linux/pipewire/context.hpp b/include/libremidi/backends/linux/pipewire/context.hpp index 59994277..9a12db20 100644 --- a/include/libremidi/backends/linux/pipewire/context.hpp +++ b/include/libremidi/backends/linux/pipewire/context.hpp @@ -7,6 +7,7 @@ #pragma clang diagnostic ignored "-Wsign-compare" #endif +#include #include #include #include @@ -547,10 +548,23 @@ class context : public std::enable_shared_from_this // 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 hook; + std::unique_ptr 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; @@ -1140,10 +1154,16 @@ class context : public std::enable_shared_from_this 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{this, id}); auto* node_proxy = reinterpret_cast(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()) { @@ -1157,11 +1177,51 @@ class context : public std::enable_shared_from_this } } + //! 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(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(data); - if (!info) + auto* cb = static_cast(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 @@ -1179,6 +1239,11 @@ class context : public std::enable_shared_from_this 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(slot.proxy), slot); self->mirror_node_to_snapshot(slot.info); if (!slot.emitted_added) { diff --git a/include/libremidi/backends/linux/pipewire/format.hpp b/include/libremidi/backends/linux/pipewire/format.hpp index f7190a0b..9bc153b5 100644 --- a/include/libremidi/backends/linux/pipewire/format.hpp +++ b/include/libremidi/backends/linux/pipewire/format.hpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -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(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(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(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 diff --git a/include/libremidi/backends/linux/pipewire/types.hpp b/include/libremidi/backends/linux/pipewire/types.hpp index 8eef2aa9..e5e621be 100644 --- a/include/libremidi/backends/linux/pipewire/types.hpp +++ b/include/libremidi/backends/linux/pipewire/types.hpp @@ -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 modifiers; +}; + struct node_info { std::uint32_t id{}; @@ -149,6 +169,11 @@ struct node_info std::vector inputs; std::vector 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_formats; }; struct graph_snapshot