Fix node details

This commit is contained in:
Joey Yakimowich-Payne 2026-02-06 08:41:55 -07:00
commit 9ac56d0d0b
2 changed files with 146 additions and 20 deletions

View file

@ -10,6 +10,7 @@
#include <unordered_set>
#include <utility>
#include <pipewire/impl-module.h>
#include <pipewire/keys.h>
#include <pipewire/link.h>
#include <pipewire/pipewire.h>
@ -104,6 +105,7 @@ bool MatchesRule(const NodeInfo& node, const RuleMatch& match) {
struct StreamData {
pw_stream* stream = nullptr;
pw_impl_module* module = nullptr;
spa_hook listener{};
pw_thread_loop* loop = nullptr;
bool is_source = false;
@ -349,6 +351,8 @@ struct Client::Impl {
std::unordered_map<uint32_t, std::unique_ptr<NodeProxyData>> node_proxies;
std::unordered_map<uint32_t, uint32_t> node_channel_counts;
std::unordered_set<uint32_t> loopback_internal_nodes;
std::unordered_map<uint32_t, MeterState> meter_states;
std::unordered_set<uint32_t> metered_nodes;
MeterState master_meter;
@ -789,6 +793,7 @@ void Client::Impl::ClearCache() {
nodes.clear();
ports.clear();
links.clear();
loopback_internal_nodes.clear();
pending_auto_links.clear();
auto_link_claimed_pairs.clear();
policy_sync_pending = false;
@ -873,6 +878,103 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
}
}
if (options.behavior == VirtualBehavior::kLoopback && options.target_node) {
std::string args;
args += "{ node.description = \"" + display_name + "\"";
args += " node.name = \"" + stream_name + "\"";
args += " audio.channels = " + std::to_string(options.format.channels);
args += " audio.rate = " + std::to_string(options.format.rate);
args += " capture.props = {";
args += " target.object = \"" + *options.target_node + "\"";
args += " stream.capture.sink = true";
args += " node.passive = true";
args += " node.dont-reconnect = true";
args += " }";
args += " playback.props = {";
args += " node.name = \"" + stream_name + "\"";
args += " node.description = \"" + display_name + "\"";
args += " media.class = \"" + media_class_value + "\"";
args += " node.virtual = true";
args += " }";
args += " }";
pw_impl_module* mod = pw_context_load_module(context,
"libpipewire-module-loopback", args.c_str(), nullptr);
if (!mod) {
return {Status::Error(StatusCode::kUnavailable, "failed to load loopback module"), 0};
}
auto sync_status = SyncLocked();
if (!sync_status.ok()) {
pw_impl_module_destroy(mod);
return {sync_status, 0};
}
uint32_t node_id = SPA_ID_INVALID;
{
std::lock_guard<std::mutex> lock(cache_mutex);
for (const auto& entry : nodes) {
if (entry.second.name == stream_name) {
node_id = entry.first;
break;
}
}
}
if (node_id == SPA_ID_INVALID) {
int wait_attempts = 0;
while (node_id == SPA_ID_INVALID && wait_attempts < 3) {
int wait_res = pw_thread_loop_timed_wait(thread_loop, kSyncWaitSeconds);
if (wait_res == -ETIMEDOUT) {
break;
}
auto sync2 = SyncLocked();
(void)sync2;
std::lock_guard<std::mutex> lock(cache_mutex);
for (const auto& entry : nodes) {
if (entry.second.name == stream_name) {
node_id = entry.first;
break;
}
}
++wait_attempts;
}
}
if (node_id == SPA_ID_INVALID) {
pw_impl_module_destroy(mod);
return {Status::Error(StatusCode::kTimeout, "loopback node did not appear in registry"), 0};
}
std::string capture_name = "input." + stream_name;
{
std::lock_guard<std::mutex> lock(cache_mutex);
for (const auto& entry : nodes) {
if (entry.second.name == capture_name) {
loopback_internal_nodes.insert(entry.first);
break;
}
}
}
auto stream_data = std::make_unique<StreamData>();
stream_data->module = mod;
stream_data->loop = thread_loop;
stream_data->is_source = is_source;
stream_data->loopback = true;
stream_data->target_node = *options.target_node;
stream_data->name = stream_name;
stream_data->rate = options.format.rate;
stream_data->channels = options.format.channels;
stream_data->node_id = node_id;
stream_data->ready = true;
{
std::lock_guard<std::mutex> lock(cache_mutex);
virtual_streams.emplace(node_id, std::move(stream_data));
}
return {Status::Ok(), node_id};
}
pw_properties* props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, media_category,
PW_KEY_MEDIA_ROLE, "Music",
@ -888,9 +990,6 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
if (node_group) {
pw_properties_set(props, PW_KEY_NODE_GROUP, node_group);
}
if (options.behavior == VirtualBehavior::kLoopback && options.target_node) {
pw_properties_set(props, PW_KEY_TARGET_OBJECT, options.target_node->c_str());
}
pw_stream* stream = pw_stream_new(core, stream_name.c_str(), props);
if (!stream) {
@ -901,7 +1000,7 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
stream_data->stream = stream;
stream_data->loop = thread_loop;
stream_data->is_source = is_source;
stream_data->loopback = options.behavior == VirtualBehavior::kLoopback;
stream_data->loopback = false;
if (options.target_node) {
stream_data->target_node = *options.target_node;
}
@ -926,9 +1025,6 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
enum pw_direction direction = is_source ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT;
enum pw_stream_flags flags = PW_STREAM_FLAG_MAP_BUFFERS;
if (options.behavior == VirtualBehavior::kLoopback && options.target_node) {
flags = static_cast<pw_stream_flags>(flags | PW_STREAM_FLAG_AUTOCONNECT);
}
int res = pw_stream_connect(stream, direction, PW_ID_ANY, flags, params, 1);
if (res < 0) {
pw_stream_destroy(stream);
@ -1042,7 +1138,11 @@ void Client::Impl::DisconnectLocked() {
}
for (auto& entry : streams) {
StreamData* stream_data = entry.second.get();
if (stream_data && stream_data->stream) {
if (!stream_data) continue;
if (stream_data->module) {
pw_impl_module_destroy(stream_data->module);
stream_data->module = nullptr;
} else if (stream_data->stream) {
pw_stream_disconnect(stream_data->stream);
pw_stream_destroy(stream_data->stream);
stream_data->stream = nullptr;
@ -1827,6 +1927,9 @@ Result<std::vector<NodeInfo>> Client::ListNodes() {
std::vector<NodeInfo> items;
items.reserve(impl_->nodes.size());
for (const auto& entry : impl_->nodes) {
if (impl_->loopback_internal_nodes.count(entry.first)) {
continue;
}
NodeInfo info = entry.second;
if (!info.is_virtual &&
impl_->virtual_streams.find(entry.first) !=
@ -1933,10 +2036,28 @@ Status Client::RemoveNode(NodeId node) {
owned_stream = std::move(it->second);
impl_->virtual_streams.erase(it);
}
if (owned_stream && owned_stream->stream) {
pw_stream_disconnect(owned_stream->stream);
pw_stream_destroy(owned_stream->stream);
owned_stream->stream = nullptr;
if (owned_stream) {
if (owned_stream->module) {
std::string capture_name = "input." + owned_stream->name;
{
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
for (auto it = impl_->loopback_internal_nodes.begin();
it != impl_->loopback_internal_nodes.end(); ++it) {
auto node_it = impl_->nodes.find(*it);
if (node_it != impl_->nodes.end() &&
node_it->second.name == capture_name) {
impl_->loopback_internal_nodes.erase(it);
break;
}
}
}
pw_impl_module_destroy(owned_stream->module);
owned_stream->module = nullptr;
} else if (owned_stream->stream) {
pw_stream_disconnect(owned_stream->stream);
pw_stream_destroy(owned_stream->stream);
owned_stream->stream = nullptr;
}
}
pw_thread_loop_unlock(impl_->thread_loop);
impl_->AutoSave();