Add capture routing rules (source → app) to complement playback rules
This commit is contained in:
parent
621d67ebab
commit
242d0ec09f
6 changed files with 418 additions and 78 deletions
|
|
@ -1864,9 +1864,17 @@ void GraphEditorWidget::rebuildRulesList() {
|
|||
matchLabel->setStyleSheet(valueStyle);
|
||||
infoLayout->addWidget(matchLabel);
|
||||
|
||||
auto *targetLabel = new QLabel(
|
||||
QString(QChar(0x2192)) + QStringLiteral(" ") +
|
||||
QString::fromStdString(rule.target_node));
|
||||
QString dirPrefix;
|
||||
QString nodeTarget;
|
||||
if (rule.direction == warppipe::RuleDirection::kCapture) {
|
||||
dirPrefix = QString(QChar(0x2190)) + QStringLiteral(" ");
|
||||
nodeTarget = QString::fromStdString(rule.source_node);
|
||||
} else {
|
||||
dirPrefix = QString(QChar(0x2192)) + QStringLiteral(" ");
|
||||
nodeTarget = QString::fromStdString(rule.target_node);
|
||||
}
|
||||
|
||||
auto *targetLabel = new QLabel(dirPrefix + nodeTarget);
|
||||
targetLabel->setStyleSheet(labelStyle);
|
||||
infoLayout->addWidget(targetLabel);
|
||||
|
||||
|
|
@ -1880,9 +1888,11 @@ void GraphEditorWidget::rebuildRulesList() {
|
|||
std::string ruleBin = rule.match.process_binary;
|
||||
std::string ruleRole = rule.match.media_role;
|
||||
std::string ruleTarget = rule.target_node;
|
||||
warppipe::RuleDirection ruleDir = rule.direction;
|
||||
std::string ruleSource = rule.source_node;
|
||||
connect(editBtn, &QPushButton::clicked, this,
|
||||
[this, ruleApp, ruleBin, ruleRole, ruleTarget, ruleId]() {
|
||||
showAddRuleDialog(ruleApp, ruleBin, ruleRole, ruleTarget, ruleId);
|
||||
[this, ruleApp, ruleBin, ruleRole, ruleTarget, ruleId, ruleDir, ruleSource]() {
|
||||
showAddRuleDialog(ruleApp, ruleBin, ruleRole, ruleTarget, ruleId, ruleDir, ruleSource);
|
||||
});
|
||||
cardLayout->addWidget(editBtn);
|
||||
|
||||
|
|
@ -1931,7 +1941,9 @@ void GraphEditorWidget::showAddRuleDialog(const std::string &prefillApp,
|
|||
const std::string &prefillBin,
|
||||
const std::string &prefillRole,
|
||||
const std::string &prefillTarget,
|
||||
warppipe::RuleId editRuleId) {
|
||||
warppipe::RuleId editRuleId,
|
||||
warppipe::RuleDirection prefillDirection,
|
||||
const std::string &prefillSource) {
|
||||
if (!m_client)
|
||||
return;
|
||||
|
||||
|
|
@ -1955,6 +1967,15 @@ void GraphEditorWidget::showAddRuleDialog(const std::string &prefillApp,
|
|||
form->setContentsMargins(16, 16, 16, 16);
|
||||
form->setSpacing(8);
|
||||
|
||||
auto *directionCombo = new QComboBox();
|
||||
directionCombo->addItem(QStringLiteral("Playback (App ") + QString(QChar(0x2192)) + QStringLiteral(" Sink)"),
|
||||
static_cast<int>(warppipe::RuleDirection::kPlayback));
|
||||
directionCombo->addItem(QStringLiteral("Capture (Source ") + QString(QChar(0x2192)) + QStringLiteral(" App)"),
|
||||
static_cast<int>(warppipe::RuleDirection::kCapture));
|
||||
if (prefillDirection == warppipe::RuleDirection::kCapture)
|
||||
directionCombo->setCurrentIndex(1);
|
||||
form->addRow(QStringLiteral("Direction:"), directionCombo);
|
||||
|
||||
auto *appNameEdit = new QLineEdit();
|
||||
appNameEdit->setPlaceholderText(QStringLiteral("e.g. Firefox"));
|
||||
if (!prefillApp.empty())
|
||||
|
|
@ -1974,22 +1995,47 @@ void GraphEditorWidget::showAddRuleDialog(const std::string &prefillApp,
|
|||
form->addRow(QStringLiteral("Media Role:"), mediaRoleEdit);
|
||||
|
||||
auto *targetCombo = new QComboBox();
|
||||
auto *sourceCombo = new QComboBox();
|
||||
auto *targetLabel = new QLabel(QStringLiteral("Target Sink:"));
|
||||
auto *sourceLabel = new QLabel(QStringLiteral("Source Node:"));
|
||||
|
||||
auto nodesResult = m_client->ListNodes();
|
||||
if (nodesResult.ok()) {
|
||||
for (const auto &node : nodesResult.value) {
|
||||
if (node.media_class.find("Sink") != std::string::npos) {
|
||||
QString label = QString::fromStdString(
|
||||
node.description.empty() ? node.name : node.description);
|
||||
targetCombo->addItem(label, QString::fromStdString(node.name));
|
||||
QString data = QString::fromStdString(node.name);
|
||||
if (node.media_class.find("Sink") != std::string::npos) {
|
||||
targetCombo->addItem(label, data);
|
||||
}
|
||||
if (node.media_class.find("Source") != std::string::npos) {
|
||||
sourceCombo->addItem(label, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!prefillTarget.empty()) {
|
||||
int idx = targetCombo->findData(QString::fromStdString(prefillTarget));
|
||||
if (idx >= 0)
|
||||
targetCombo->setCurrentIndex(idx);
|
||||
if (idx >= 0) targetCombo->setCurrentIndex(idx);
|
||||
}
|
||||
form->addRow(QStringLiteral("Target Node:"), targetCombo);
|
||||
if (!prefillSource.empty()) {
|
||||
int idx = sourceCombo->findData(QString::fromStdString(prefillSource));
|
||||
if (idx >= 0) sourceCombo->setCurrentIndex(idx);
|
||||
}
|
||||
|
||||
form->addRow(targetLabel, targetCombo);
|
||||
form->addRow(sourceLabel, sourceCombo);
|
||||
|
||||
auto updateVisibility = [=](int index) {
|
||||
bool isCapture = directionCombo->itemData(index).toInt() ==
|
||||
static_cast<int>(warppipe::RuleDirection::kCapture);
|
||||
targetLabel->setVisible(!isCapture);
|
||||
targetCombo->setVisible(!isCapture);
|
||||
sourceLabel->setVisible(isCapture);
|
||||
sourceCombo->setVisible(isCapture);
|
||||
};
|
||||
connect(directionCombo, &QComboBox::currentIndexChanged, &dlg, updateVisibility);
|
||||
updateVisibility(directionCombo->currentIndex());
|
||||
|
||||
auto *buttons = new QDialogButtonBox(
|
||||
QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
|
|
@ -2007,16 +2053,26 @@ void GraphEditorWidget::showAddRuleDialog(const std::string &prefillApp,
|
|||
std::string appName = appNameEdit->text().trimmed().toStdString();
|
||||
std::string procBin = processBinEdit->text().trimmed().toStdString();
|
||||
std::string role = mediaRoleEdit->text().trimmed().toStdString();
|
||||
std::string target = targetCombo->currentData().toString().toStdString();
|
||||
auto dir = static_cast<warppipe::RuleDirection>(
|
||||
directionCombo->currentData().toInt());
|
||||
|
||||
if (appName.empty() && procBin.empty() && role.empty()) {
|
||||
QMessageBox::warning(this, QStringLiteral("Invalid Rule"),
|
||||
QStringLiteral("At least one match field must be filled."));
|
||||
return;
|
||||
}
|
||||
if (target.empty()) {
|
||||
|
||||
std::string target = targetCombo->currentData().toString().toStdString();
|
||||
std::string source = sourceCombo->currentData().toString().toStdString();
|
||||
|
||||
if (dir == warppipe::RuleDirection::kPlayback && target.empty()) {
|
||||
QMessageBox::warning(this, QStringLiteral("Invalid Rule"),
|
||||
QStringLiteral("A target node must be selected."));
|
||||
QStringLiteral("A target sink must be selected."));
|
||||
return;
|
||||
}
|
||||
if (dir == warppipe::RuleDirection::kCapture && source.empty()) {
|
||||
QMessageBox::warning(this, QStringLiteral("Invalid Rule"),
|
||||
QStringLiteral("A source node must be selected."));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -2028,7 +2084,12 @@ void GraphEditorWidget::showAddRuleDialog(const std::string &prefillApp,
|
|||
rule.match.application_name = appName;
|
||||
rule.match.process_binary = procBin;
|
||||
rule.match.media_role = role;
|
||||
rule.direction = dir;
|
||||
if (dir == warppipe::RuleDirection::kPlayback) {
|
||||
rule.target_node = target;
|
||||
} else {
|
||||
rule.source_node = source;
|
||||
}
|
||||
m_client->AddRouteRule(rule);
|
||||
rebuildRulesList();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,9 @@ private:
|
|||
const std::string &prefillBin = {},
|
||||
const std::string &prefillRole = {},
|
||||
const std::string &prefillTarget = {},
|
||||
warppipe::RuleId editRuleId = {});
|
||||
warppipe::RuleId editRuleId = {},
|
||||
warppipe::RuleDirection prefillDirection = warppipe::RuleDirection::kPlayback,
|
||||
const std::string &prefillSource = {});
|
||||
void setConnectionStyle(ConnectionStyleType style);
|
||||
void onSelectionChanged();
|
||||
void updateNodeDetailsPanel(QtNodes::NodeId nodeId);
|
||||
|
|
|
|||
|
|
@ -132,10 +132,17 @@ struct RuleMatch {
|
|||
std::string media_role;
|
||||
};
|
||||
|
||||
enum class RuleDirection : uint8_t {
|
||||
kPlayback = 0,
|
||||
kCapture,
|
||||
};
|
||||
|
||||
struct RouteRule {
|
||||
RuleId id;
|
||||
RuleMatch match;
|
||||
std::string target_node;
|
||||
RuleDirection direction = RuleDirection::kPlayback;
|
||||
std::string source_node;
|
||||
};
|
||||
|
||||
struct VirtualNodeInfo {
|
||||
|
|
|
|||
152
src/warppipe.cpp
152
src/warppipe.cpp
|
|
@ -78,6 +78,7 @@ struct PendingAutoLink {
|
|||
uint32_t source_node_id = 0;
|
||||
std::string target_node_name;
|
||||
uint32_t rule_id = 0;
|
||||
RuleDirection direction = RuleDirection::kPlayback;
|
||||
};
|
||||
|
||||
bool MatchesRule(const NodeInfo& node, const RuleMatch& match) {
|
||||
|
|
@ -1200,28 +1201,48 @@ void Client::Impl::DisconnectLocked() {
|
|||
|
||||
void Client::Impl::CheckRulesForNode(const NodeInfo& node) {
|
||||
for (const auto& entry : route_rules) {
|
||||
if (MatchesRule(node, entry.second.match)) {
|
||||
if (!MatchesRule(node, entry.second.match)) continue;
|
||||
const RouteRule& rule = entry.second;
|
||||
|
||||
PendingAutoLink pending;
|
||||
pending.source_node_id = node.id.value;
|
||||
pending.target_node_name = entry.second.target_node;
|
||||
pending.rule_id = entry.first;
|
||||
pending.direction = rule.direction;
|
||||
|
||||
if (rule.direction == RuleDirection::kPlayback) {
|
||||
pending.source_node_id = node.id.value;
|
||||
pending.target_node_name = rule.target_node;
|
||||
} else if (!rule.source_node.empty()) {
|
||||
// source_node_id = app (input side); target_node_name = hw source (output side)
|
||||
pending.source_node_id = node.id.value;
|
||||
pending.target_node_name = rule.source_node;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
pending_auto_links.push_back(std::move(pending));
|
||||
SchedulePolicySync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Client::Impl::EnforceRulesForLink(uint32_t link_id, uint32_t out_port,
|
||||
uint32_t in_port) {
|
||||
auto port_it = ports.find(out_port);
|
||||
if (port_it == ports.end()) return;
|
||||
uint32_t src_node_id = port_it->second.node.value;
|
||||
auto node_it = nodes.find(src_node_id);
|
||||
if (node_it == nodes.end()) return;
|
||||
auto out_port_it = ports.find(out_port);
|
||||
auto in_port_it = ports.find(in_port);
|
||||
if (out_port_it == ports.end() || in_port_it == ports.end()) return;
|
||||
uint32_t src_node_id = out_port_it->second.node.value;
|
||||
uint32_t dest_node_id = in_port_it->second.node.value;
|
||||
|
||||
auto src_node_it = nodes.find(src_node_id);
|
||||
auto dest_node_it = nodes.find(dest_node_id);
|
||||
|
||||
bool should_destroy = false;
|
||||
|
||||
// Playback enforcement: if source app matches a playback rule, link must go to rule target
|
||||
if (src_node_it != nodes.end()) {
|
||||
std::vector<uint32_t> rule_target_ids;
|
||||
for (const auto& rule_entry : route_rules) {
|
||||
if (!MatchesRule(node_it->second, rule_entry.second.match)) continue;
|
||||
if (rule_entry.second.direction != RuleDirection::kPlayback) continue;
|
||||
if (!MatchesRule(src_node_it->second, rule_entry.second.match)) continue;
|
||||
for (const auto& n : nodes) {
|
||||
if (n.second.name == rule_entry.second.target_node) {
|
||||
rule_target_ids.push_back(n.first);
|
||||
|
|
@ -1229,14 +1250,38 @@ void Client::Impl::EnforceRulesForLink(uint32_t link_id, uint32_t out_port,
|
|||
}
|
||||
}
|
||||
}
|
||||
if (rule_target_ids.empty()) return;
|
||||
|
||||
auto in_port_it = ports.find(in_port);
|
||||
if (in_port_it == ports.end()) return;
|
||||
uint32_t dest_node_id = in_port_it->second.node.value;
|
||||
if (!rule_target_ids.empty()) {
|
||||
bool links_to_target = false;
|
||||
for (uint32_t tid : rule_target_ids) {
|
||||
if (dest_node_id == tid) return;
|
||||
if (dest_node_id == tid) { links_to_target = true; break; }
|
||||
}
|
||||
if (!links_to_target) should_destroy = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Capture enforcement: if dest app matches a capture rule, link must come from rule source
|
||||
if (!should_destroy && dest_node_it != nodes.end()) {
|
||||
std::vector<uint32_t> rule_source_ids;
|
||||
for (const auto& rule_entry : route_rules) {
|
||||
if (rule_entry.second.direction != RuleDirection::kCapture) continue;
|
||||
if (!MatchesRule(dest_node_it->second, rule_entry.second.match)) continue;
|
||||
for (const auto& n : nodes) {
|
||||
if (n.second.name == rule_entry.second.source_node) {
|
||||
rule_source_ids.push_back(n.first);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!rule_source_ids.empty()) {
|
||||
bool links_from_source = false;
|
||||
for (uint32_t sid : rule_source_ids) {
|
||||
if (src_node_id == sid) { links_from_source = true; break; }
|
||||
}
|
||||
if (!links_from_source) should_destroy = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!should_destroy) return;
|
||||
|
||||
if (link_proxies.count(link_id)) return;
|
||||
for (const auto& proxy : auto_link_proxies) {
|
||||
|
|
@ -1305,6 +1350,18 @@ void Client::Impl::ProcessPendingAutoLinks() {
|
|||
std::vector<PortEntry> src_ports;
|
||||
std::vector<PortEntry> tgt_ports;
|
||||
|
||||
if (it->direction == RuleDirection::kCapture) {
|
||||
// Capture: target_node_name is the hw source (outputs), source_node_id is the app (inputs)
|
||||
for (const auto& port_entry : ports) {
|
||||
const PortInfo& port = port_entry.second;
|
||||
if (port.node.value == target_node_id && !port.is_input) {
|
||||
src_ports.push_back({port_entry.first, port.name});
|
||||
}
|
||||
if (port.node.value == it->source_node_id && port.is_input) {
|
||||
tgt_ports.push_back({port_entry.first, port.name});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const auto& port_entry : ports) {
|
||||
const PortInfo& port = port_entry.second;
|
||||
if (port.node.value == it->source_node_id && !port.is_input) {
|
||||
|
|
@ -1314,6 +1371,7 @@ void Client::Impl::ProcessPendingAutoLinks() {
|
|||
tgt_ports.push_back({port_entry.first, port.name});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (src_ports.empty() || tgt_ports.empty()) {
|
||||
++it;
|
||||
|
|
@ -1643,6 +1701,8 @@ void Client::Impl::AutoSave() {
|
|||
rule_obj["match"]["process_binary"] = entry.second.match.process_binary;
|
||||
rule_obj["match"]["media_role"] = entry.second.match.media_role;
|
||||
rule_obj["target_node"] = entry.second.target_node;
|
||||
rule_obj["direction"] = entry.second.direction == RuleDirection::kCapture ? "capture" : "playback";
|
||||
rule_obj["source_node"] = entry.second.source_node;
|
||||
rules_array.push_back(std::move(rule_obj));
|
||||
}
|
||||
}
|
||||
|
|
@ -2595,9 +2655,12 @@ Result<RuleId> Client::AddRouteRule(const RouteRule& rule) {
|
|||
rule.match.media_role.empty()) {
|
||||
return {Status::Error(StatusCode::kInvalidArgument, "rule match has no criteria"), {}};
|
||||
}
|
||||
if (rule.target_node.empty()) {
|
||||
if (rule.direction == RuleDirection::kPlayback && rule.target_node.empty()) {
|
||||
return {Status::Error(StatusCode::kInvalidArgument, "rule target node is empty"), {}};
|
||||
}
|
||||
if (rule.direction == RuleDirection::kCapture && rule.source_node.empty()) {
|
||||
return {Status::Error(StatusCode::kInvalidArgument, "rule source node is empty"), {}};
|
||||
}
|
||||
|
||||
uint32_t id = 0;
|
||||
{
|
||||
|
|
@ -2610,9 +2673,16 @@ Result<RuleId> Client::AddRouteRule(const RouteRule& rule) {
|
|||
for (const auto& node_entry : impl_->nodes) {
|
||||
if (MatchesRule(node_entry.second, rule.match)) {
|
||||
PendingAutoLink pending;
|
||||
pending.rule_id = id;
|
||||
pending.direction = rule.direction;
|
||||
|
||||
if (rule.direction == RuleDirection::kPlayback) {
|
||||
pending.source_node_id = node_entry.first;
|
||||
pending.target_node_name = rule.target_node;
|
||||
pending.rule_id = id;
|
||||
} else {
|
||||
pending.source_node_id = node_entry.first;
|
||||
pending.target_node_name = rule.source_node;
|
||||
}
|
||||
impl_->pending_auto_links.push_back(std::move(pending));
|
||||
}
|
||||
}
|
||||
|
|
@ -2631,6 +2701,8 @@ Result<RuleId> Client::AddRouteRule(const RouteRule& rule) {
|
|||
Status Client::RemoveRouteRule(RuleId id) {
|
||||
RuleMatch match;
|
||||
std::string target_node;
|
||||
std::string source_node;
|
||||
RuleDirection direction = RuleDirection::kPlayback;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||
|
|
@ -2640,6 +2712,8 @@ Status Client::RemoveRouteRule(RuleId id) {
|
|||
}
|
||||
match = it->second.match;
|
||||
target_node = it->second.target_node;
|
||||
source_node = it->second.source_node;
|
||||
direction = it->second.direction;
|
||||
impl_->route_rules.erase(it);
|
||||
|
||||
auto pending_it = impl_->pending_auto_links.begin();
|
||||
|
|
@ -2657,6 +2731,8 @@ Status Client::RemoveRouteRule(RuleId id) {
|
|||
std::vector<std::pair<uint32_t, uint32_t>> pairs_to_remove;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||
|
||||
if (direction == RuleDirection::kPlayback) {
|
||||
uint32_t target_id = 0;
|
||||
for (const auto& n : impl_->nodes) {
|
||||
if (n.second.name == target_node) {
|
||||
|
|
@ -2681,6 +2757,32 @@ Status Client::RemoveRouteRule(RuleId id) {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uint32_t source_id = 0;
|
||||
for (const auto& n : impl_->nodes) {
|
||||
if (n.second.name == source_node) {
|
||||
source_id = n.first;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (source_id) {
|
||||
for (const auto& link_entry : impl_->links) {
|
||||
uint32_t out_port = link_entry.second.output_port.value;
|
||||
uint32_t in_port = link_entry.second.input_port.value;
|
||||
auto out_port_it = impl_->ports.find(out_port);
|
||||
if (out_port_it == impl_->ports.end() ||
|
||||
out_port_it->second.node.value != source_id) continue;
|
||||
auto in_port_it = impl_->ports.find(in_port);
|
||||
if (in_port_it == impl_->ports.end()) continue;
|
||||
auto dest_node_it = impl_->nodes.find(in_port_it->second.node.value);
|
||||
if (dest_node_it == impl_->nodes.end()) continue;
|
||||
if (MatchesRule(dest_node_it->second, match)) {
|
||||
links_to_destroy.push_back(link_entry.first);
|
||||
pairs_to_remove.emplace_back(out_port, in_port);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!links_to_destroy.empty() && impl_->thread_loop) {
|
||||
|
|
@ -2802,6 +2904,8 @@ Status Client::SaveConfig(std::string_view path) {
|
|||
rule_obj["match"]["process_binary"] = entry.second.match.process_binary;
|
||||
rule_obj["match"]["media_role"] = entry.second.match.media_role;
|
||||
rule_obj["target_node"] = entry.second.target_node;
|
||||
rule_obj["direction"] = entry.second.direction == RuleDirection::kCapture ? "capture" : "playback";
|
||||
rule_obj["source_node"] = entry.second.source_node;
|
||||
rules_array.push_back(std::move(rule_obj));
|
||||
}
|
||||
}
|
||||
|
|
@ -2943,10 +3047,16 @@ Status Client::LoadConfig(std::string_view path) {
|
|||
rule.match.media_role = m.value("media_role", "");
|
||||
}
|
||||
rule.target_node = rule_obj.value("target_node", "");
|
||||
if (!rule.target_node.empty() &&
|
||||
(!rule.match.application_name.empty() ||
|
||||
rule.source_node = rule_obj.value("source_node", "");
|
||||
std::string dir_str = rule_obj.value("direction", "playback");
|
||||
rule.direction = (dir_str == "capture") ? RuleDirection::kCapture
|
||||
: RuleDirection::kPlayback;
|
||||
bool has_match = !rule.match.application_name.empty() ||
|
||||
!rule.match.process_binary.empty() ||
|
||||
!rule.match.media_role.empty())) {
|
||||
!rule.match.media_role.empty();
|
||||
bool has_target = (rule.direction == RuleDirection::kPlayback && !rule.target_node.empty()) ||
|
||||
(rule.direction == RuleDirection::kCapture && !rule.source_node.empty());
|
||||
if (has_match && has_target) {
|
||||
AddRouteRule(rule);
|
||||
}
|
||||
} catch (...) {
|
||||
|
|
|
|||
|
|
@ -317,10 +317,10 @@ bool acceptRuleDialog(const QString &appName, const QString &targetNodeName) {
|
|||
}
|
||||
|
||||
auto combos = dialog->findChildren<QComboBox *>();
|
||||
if (!combos.isEmpty()) {
|
||||
int idx = combos[0]->findData(targetNodeName);
|
||||
if (combos.size() >= 2) {
|
||||
int idx = combos[1]->findData(targetNodeName);
|
||||
if (idx >= 0) {
|
||||
combos[0]->setCurrentIndex(idx);
|
||||
combos[1]->setCurrentIndex(idx);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1013,3 +1013,163 @@ TEST_CASE("EnsureNodeMeter fails for nonexistent node") {
|
|||
REQUIRE_FALSE(status.ok());
|
||||
REQUIRE(status.code == warppipe::StatusCode::kNotFound);
|
||||
}
|
||||
|
||||
TEST_CASE("capture rule validation rejects empty source_node") {
|
||||
auto result = warppipe::Client::Create(DefaultOptions());
|
||||
if (!result.ok()) {
|
||||
SUCCEED("PipeWire unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
warppipe::RouteRule rule;
|
||||
rule.match.application_name = "discord";
|
||||
rule.direction = warppipe::RuleDirection::kCapture;
|
||||
auto r = result.value->AddRouteRule(rule);
|
||||
REQUIRE_FALSE(r.ok());
|
||||
REQUIRE(r.status.code == warppipe::StatusCode::kInvalidArgument);
|
||||
}
|
||||
|
||||
TEST_CASE("capture rule validation accepts valid source_node") {
|
||||
auto result = warppipe::Client::Create(DefaultOptions());
|
||||
if (!result.ok()) {
|
||||
SUCCEED("PipeWire unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
warppipe::RouteRule rule;
|
||||
rule.match.application_name = "discord";
|
||||
rule.direction = warppipe::RuleDirection::kCapture;
|
||||
rule.source_node = "alsa_input.usb-mic";
|
||||
auto r = result.value->AddRouteRule(rule);
|
||||
REQUIRE(r.ok());
|
||||
REQUIRE(r.value.value != 0);
|
||||
|
||||
auto list = result.value->ListRouteRules();
|
||||
REQUIRE(list.ok());
|
||||
REQUIRE(list.value.size() == 1);
|
||||
REQUIRE(list.value[0].direction == warppipe::RuleDirection::kCapture);
|
||||
REQUIRE(list.value[0].source_node == "alsa_input.usb-mic");
|
||||
}
|
||||
|
||||
TEST_CASE("capture rule creates pending auto-link for matching app") {
|
||||
auto result = warppipe::Client::Create(DefaultOptions());
|
||||
if (!result.ok()) {
|
||||
SUCCEED("PipeWire unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
warppipe::RouteRule rule;
|
||||
rule.match.application_name = "teams";
|
||||
rule.direction = warppipe::RuleDirection::kCapture;
|
||||
rule.source_node = "hw-mic";
|
||||
REQUIRE(result.value->AddRouteRule(rule).ok());
|
||||
|
||||
REQUIRE(result.value->Test_GetPendingAutoLinkCount() == 0);
|
||||
|
||||
warppipe::NodeInfo app_node;
|
||||
app_node.id = warppipe::NodeId{800001};
|
||||
app_node.name = "teams-capture";
|
||||
app_node.media_class = "Stream/Input/Audio";
|
||||
app_node.application_name = "teams";
|
||||
REQUIRE(result.value->Test_InsertNode(app_node).ok());
|
||||
|
||||
REQUIRE(result.value->Test_GetPendingAutoLinkCount() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("capture rule ignores non-matching app") {
|
||||
auto result = warppipe::Client::Create(DefaultOptions());
|
||||
if (!result.ok()) {
|
||||
SUCCEED("PipeWire unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
warppipe::RouteRule rule;
|
||||
rule.match.application_name = "discord";
|
||||
rule.direction = warppipe::RuleDirection::kCapture;
|
||||
rule.source_node = "hw-mic";
|
||||
REQUIRE(result.value->AddRouteRule(rule).ok());
|
||||
|
||||
warppipe::NodeInfo node;
|
||||
node.id = warppipe::NodeId{800002};
|
||||
node.name = "zoom-capture";
|
||||
node.media_class = "Stream/Input/Audio";
|
||||
node.application_name = "zoom";
|
||||
REQUIRE(result.value->Test_InsertNode(node).ok());
|
||||
|
||||
REQUIRE(result.value->Test_GetPendingAutoLinkCount() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("capture rule added after app creates pending link") {
|
||||
auto result = warppipe::Client::Create(DefaultOptions());
|
||||
if (!result.ok()) {
|
||||
SUCCEED("PipeWire unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
warppipe::NodeInfo node;
|
||||
node.id = warppipe::NodeId{800003};
|
||||
node.name = "discord-capture";
|
||||
node.media_class = "Stream/Input/Audio";
|
||||
node.application_name = "discord";
|
||||
REQUIRE(result.value->Test_InsertNode(node).ok());
|
||||
|
||||
warppipe::RouteRule rule;
|
||||
rule.match.application_name = "discord";
|
||||
rule.direction = warppipe::RuleDirection::kCapture;
|
||||
rule.source_node = "hw-mic";
|
||||
REQUIRE(result.value->AddRouteRule(rule).ok());
|
||||
|
||||
REQUIRE(result.value->Test_GetPendingAutoLinkCount() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("playback rule still rejects empty target_node") {
|
||||
auto result = warppipe::Client::Create(DefaultOptions());
|
||||
if (!result.ok()) {
|
||||
SUCCEED("PipeWire unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
warppipe::RouteRule rule;
|
||||
rule.match.application_name = "firefox";
|
||||
rule.direction = warppipe::RuleDirection::kPlayback;
|
||||
auto r = result.value->AddRouteRule(rule);
|
||||
REQUIRE_FALSE(r.ok());
|
||||
REQUIRE(r.status.code == warppipe::StatusCode::kInvalidArgument);
|
||||
}
|
||||
|
||||
TEST_CASE("save and load capture rule round trip") {
|
||||
auto result = warppipe::Client::Create(DefaultOptions());
|
||||
if (!result.ok()) {
|
||||
SUCCEED("PipeWire unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
warppipe::RouteRule rule;
|
||||
rule.match.application_name = "discord";
|
||||
rule.match.process_binary = "Discord";
|
||||
rule.direction = warppipe::RuleDirection::kCapture;
|
||||
rule.source_node = "alsa_input.usb-mic";
|
||||
REQUIRE(result.value->AddRouteRule(rule).ok());
|
||||
|
||||
const char* path = "/tmp/warppipe_test_capture_config.json";
|
||||
REQUIRE(result.value->SaveConfig(path).ok());
|
||||
|
||||
auto result2 = warppipe::Client::Create(DefaultOptions());
|
||||
if (!result2.ok()) {
|
||||
SUCCEED("PipeWire unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
REQUIRE(result2.value->LoadConfig(path).ok());
|
||||
|
||||
auto rules = result2.value->ListRouteRules();
|
||||
REQUIRE(rules.ok());
|
||||
REQUIRE(rules.value.size() == 1);
|
||||
REQUIRE(rules.value[0].match.application_name == "discord");
|
||||
REQUIRE(rules.value[0].match.process_binary == "Discord");
|
||||
REQUIRE(rules.value[0].direction == warppipe::RuleDirection::kCapture);
|
||||
REQUIRE(rules.value[0].source_node == "alsa_input.usb-mic");
|
||||
REQUIRE(rules.value[0].target_node.empty());
|
||||
|
||||
std::remove(path);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue