Fix node details
This commit is contained in:
parent
21cd3bd3f9
commit
9ac56d0d0b
2 changed files with 146 additions and 20 deletions
|
|
@ -881,6 +881,9 @@ void GraphEditorWidget::showNodeContextMenu(const QPoint &screenPos,
|
||||||
createRuleAction = menu.addAction(QStringLiteral("Create Rule..."));
|
createRuleAction = menu.addAction(QStringLiteral("Create Rule..."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
QAction *detailsAction = menu.addAction(QStringLiteral("Node Details"));
|
||||||
|
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
QAction *pasteAction = menu.addAction(QStringLiteral("Paste"));
|
QAction *pasteAction = menu.addAction(QStringLiteral("Paste"));
|
||||||
pasteAction->setShortcut(QKeySequence::Paste);
|
pasteAction->setShortcut(QKeySequence::Paste);
|
||||||
|
|
@ -900,6 +903,10 @@ void GraphEditorWidget::showNodeContextMenu(const QPoint &screenPos,
|
||||||
duplicateSelection();
|
duplicateSelection();
|
||||||
} else if (chosen == deleteAction && m_client) {
|
} else if (chosen == deleteAction && m_client) {
|
||||||
deleteSelection();
|
deleteSelection();
|
||||||
|
} else if (chosen == detailsAction) {
|
||||||
|
m_selectedNodeId = qtNodeId;
|
||||||
|
updateNodeDetailsPanel(qtNodeId);
|
||||||
|
m_sidebar->setCurrentWidget(m_nodeDetailsScroll);
|
||||||
} else if (chosen == pasteAction) {
|
} else if (chosen == pasteAction) {
|
||||||
pasteSelection(QPointF(0, 0));
|
pasteSelection(QPointF(0, 0));
|
||||||
} else if (chosen == createRuleAction) {
|
} else if (chosen == createRuleAction) {
|
||||||
|
|
@ -1936,12 +1943,10 @@ void GraphEditorWidget::onSelectionChanged() {
|
||||||
if (selected == m_selectedNodeId)
|
if (selected == m_selectedNodeId)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (selected != 0) {
|
m_selectedNodeId = selected;
|
||||||
m_selectedNodeId = selected;
|
if (selected != 0 && m_sidebar->currentWidget() == m_nodeDetailsScroll) {
|
||||||
updateNodeDetailsPanel(selected);
|
updateNodeDetailsPanel(selected);
|
||||||
m_sidebar->setCurrentWidget(m_nodeDetailsScroll);
|
} else if (selected == 0 && m_sidebar->currentWidget() == m_nodeDetailsScroll) {
|
||||||
} else {
|
|
||||||
m_selectedNodeId = 0;
|
|
||||||
clearNodeDetailsPanel();
|
clearNodeDetailsPanel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2099,6 +2104,9 @@ void GraphEditorWidget::updateNodeDetailsPanel(QtNodes::NodeId nodeId) {
|
||||||
}
|
}
|
||||||
layout->addWidget(targetCombo);
|
layout->addWidget(targetCombo);
|
||||||
|
|
||||||
|
connect(loopbackCheck, &QCheckBox::toggled, targetCombo,
|
||||||
|
&QWidget::setEnabled);
|
||||||
|
|
||||||
if (vnResult.ok() && vnResult.value.loopback) {
|
if (vnResult.ok() && vnResult.value.loopback) {
|
||||||
loopbackCheck->setChecked(true);
|
loopbackCheck->setChecked(true);
|
||||||
int idx = targetCombo->findData(
|
int idx = targetCombo->findData(
|
||||||
|
|
@ -2106,9 +2114,6 @@ void GraphEditorWidget::updateNodeDetailsPanel(QtNodes::NodeId nodeId) {
|
||||||
if (idx >= 0)
|
if (idx >= 0)
|
||||||
targetCombo->setCurrentIndex(idx);
|
targetCombo->setCurrentIndex(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(loopbackCheck, &QCheckBox::toggled, targetCombo,
|
|
||||||
&QWidget::setEnabled);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
layout->addSpacing(12);
|
layout->addSpacing(12);
|
||||||
|
|
|
||||||
145
src/warppipe.cpp
145
src/warppipe.cpp
|
|
@ -10,6 +10,7 @@
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include <pipewire/impl-module.h>
|
||||||
#include <pipewire/keys.h>
|
#include <pipewire/keys.h>
|
||||||
#include <pipewire/link.h>
|
#include <pipewire/link.h>
|
||||||
#include <pipewire/pipewire.h>
|
#include <pipewire/pipewire.h>
|
||||||
|
|
@ -104,6 +105,7 @@ bool MatchesRule(const NodeInfo& node, const RuleMatch& match) {
|
||||||
|
|
||||||
struct StreamData {
|
struct StreamData {
|
||||||
pw_stream* stream = nullptr;
|
pw_stream* stream = nullptr;
|
||||||
|
pw_impl_module* module = nullptr;
|
||||||
spa_hook listener{};
|
spa_hook listener{};
|
||||||
pw_thread_loop* loop = nullptr;
|
pw_thread_loop* loop = nullptr;
|
||||||
bool is_source = false;
|
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, std::unique_ptr<NodeProxyData>> node_proxies;
|
||||||
std::unordered_map<uint32_t, uint32_t> node_channel_counts;
|
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_map<uint32_t, MeterState> meter_states;
|
||||||
std::unordered_set<uint32_t> metered_nodes;
|
std::unordered_set<uint32_t> metered_nodes;
|
||||||
MeterState master_meter;
|
MeterState master_meter;
|
||||||
|
|
@ -789,6 +793,7 @@ void Client::Impl::ClearCache() {
|
||||||
nodes.clear();
|
nodes.clear();
|
||||||
ports.clear();
|
ports.clear();
|
||||||
links.clear();
|
links.clear();
|
||||||
|
loopback_internal_nodes.clear();
|
||||||
pending_auto_links.clear();
|
pending_auto_links.clear();
|
||||||
auto_link_claimed_pairs.clear();
|
auto_link_claimed_pairs.clear();
|
||||||
policy_sync_pending = false;
|
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_properties* props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio",
|
||||||
PW_KEY_MEDIA_CATEGORY, media_category,
|
PW_KEY_MEDIA_CATEGORY, media_category,
|
||||||
PW_KEY_MEDIA_ROLE, "Music",
|
PW_KEY_MEDIA_ROLE, "Music",
|
||||||
|
|
@ -888,9 +990,6 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
|
||||||
if (node_group) {
|
if (node_group) {
|
||||||
pw_properties_set(props, PW_KEY_NODE_GROUP, 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);
|
pw_stream* stream = pw_stream_new(core, stream_name.c_str(), props);
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
|
|
@ -901,7 +1000,7 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
|
||||||
stream_data->stream = stream;
|
stream_data->stream = stream;
|
||||||
stream_data->loop = thread_loop;
|
stream_data->loop = thread_loop;
|
||||||
stream_data->is_source = is_source;
|
stream_data->is_source = is_source;
|
||||||
stream_data->loopback = options.behavior == VirtualBehavior::kLoopback;
|
stream_data->loopback = false;
|
||||||
if (options.target_node) {
|
if (options.target_node) {
|
||||||
stream_data->target_node = *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_direction direction = is_source ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT;
|
||||||
enum pw_stream_flags flags = PW_STREAM_FLAG_MAP_BUFFERS;
|
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);
|
int res = pw_stream_connect(stream, direction, PW_ID_ANY, flags, params, 1);
|
||||||
if (res < 0) {
|
if (res < 0) {
|
||||||
pw_stream_destroy(stream);
|
pw_stream_destroy(stream);
|
||||||
|
|
@ -1042,7 +1138,11 @@ void Client::Impl::DisconnectLocked() {
|
||||||
}
|
}
|
||||||
for (auto& entry : streams) {
|
for (auto& entry : streams) {
|
||||||
StreamData* stream_data = entry.second.get();
|
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_disconnect(stream_data->stream);
|
||||||
pw_stream_destroy(stream_data->stream);
|
pw_stream_destroy(stream_data->stream);
|
||||||
stream_data->stream = nullptr;
|
stream_data->stream = nullptr;
|
||||||
|
|
@ -1827,6 +1927,9 @@ Result<std::vector<NodeInfo>> Client::ListNodes() {
|
||||||
std::vector<NodeInfo> items;
|
std::vector<NodeInfo> items;
|
||||||
items.reserve(impl_->nodes.size());
|
items.reserve(impl_->nodes.size());
|
||||||
for (const auto& entry : impl_->nodes) {
|
for (const auto& entry : impl_->nodes) {
|
||||||
|
if (impl_->loopback_internal_nodes.count(entry.first)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
NodeInfo info = entry.second;
|
NodeInfo info = entry.second;
|
||||||
if (!info.is_virtual &&
|
if (!info.is_virtual &&
|
||||||
impl_->virtual_streams.find(entry.first) !=
|
impl_->virtual_streams.find(entry.first) !=
|
||||||
|
|
@ -1933,10 +2036,28 @@ Status Client::RemoveNode(NodeId node) {
|
||||||
owned_stream = std::move(it->second);
|
owned_stream = std::move(it->second);
|
||||||
impl_->virtual_streams.erase(it);
|
impl_->virtual_streams.erase(it);
|
||||||
}
|
}
|
||||||
if (owned_stream && owned_stream->stream) {
|
if (owned_stream) {
|
||||||
pw_stream_disconnect(owned_stream->stream);
|
if (owned_stream->module) {
|
||||||
pw_stream_destroy(owned_stream->stream);
|
std::string capture_name = "input." + owned_stream->name;
|
||||||
owned_stream->stream = nullptr;
|
{
|
||||||
|
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);
|
pw_thread_loop_unlock(impl_->thread_loop);
|
||||||
impl_->AutoSave();
|
impl_->AutoSave();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue