Milestone 3
This commit is contained in:
parent
866f0419ad
commit
691eb327d0
6 changed files with 507 additions and 24 deletions
10
PLAN.md
10
PLAN.md
|
|
@ -25,11 +25,11 @@
|
||||||
- [x] Performance tests: instructions: create/destroy 100 virtual sinks and 100 virtual sources in a tight loop; measure wall time and ensure it stays within the target budget.
|
- [x] Performance tests: instructions: create/destroy 100 virtual sinks and 100 virtual sources in a tight loop; measure wall time and ensure it stays within the target budget.
|
||||||
|
|
||||||
- [ ] Milestone 3 - Link management API
|
- [ ] Milestone 3 - Link management API
|
||||||
- [ ] Implement link creation via link-factory (load libpipewire-module-link-factory and call pw_core_create_object with link.input.* and link.output.* props; see src/modules/module-link-factory.c, src/examples/internal.c, src/tools/pw-link.c).
|
- [x] Implement link creation via link-factory (load libpipewire-module-link-factory and call pw_core_create_object with link.input.* and link.output.* props; see src/modules/module-link-factory.c, src/examples/internal.c, src/tools/pw-link.c).
|
||||||
- [ ] Support linking by node+port names and by object IDs; add object.linger and link.passive options.
|
- [x] Support linking by node+port names and by object IDs; add object.linger and link.passive options.
|
||||||
- [ ] Add link deletion and link reconciliation (auto-remove stale links when endpoints vanish).
|
- [x] Add link deletion and link reconciliation (auto-remove stale links when endpoints vanish).
|
||||||
- [ ] Tests to add (non-happy path/edge cases): instructions: link to non-existent port; link output-to-output or input-to-input; remove node while link is initializing; create two links to same port and validate policy behavior.
|
- [x] Tests to add (non-happy path/edge cases): instructions: link to non-existent port; link output-to-output or input-to-input; remove node while link is initializing; create two links to same port and validate policy behavior.
|
||||||
- [ ] Performance tests: instructions: create 200 links between existing ports; measure create+destroy time and verify subsecond target where possible.
|
- [x] Performance tests: instructions: create 200 links between existing ports; measure create+destroy time and verify subsecond target where possible.
|
||||||
|
|
||||||
- [ ] Milestone 4 - Persistence and "ephemeral source" policy
|
- [ ] Milestone 4 - Persistence and "ephemeral source" policy
|
||||||
- [ ] Implement persistence (JSON or TOML) for: virtual nodes, links, and per-app routing rules. Persist on change; load on startup.
|
- [ ] Implement persistence (JSON or TOML) for: virtual nodes, links, and per-app routing rules. Persist on change; load on startup.
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,11 @@ class Client {
|
||||||
Status RemoveNode(NodeId node);
|
Status RemoveNode(NodeId node);
|
||||||
|
|
||||||
Result<Link> CreateLink(PortId output, PortId input, const LinkOptions& options);
|
Result<Link> CreateLink(PortId output, PortId input, const LinkOptions& options);
|
||||||
|
Result<Link> CreateLinkByName(std::string_view output_node,
|
||||||
|
std::string_view output_port,
|
||||||
|
std::string_view input_node,
|
||||||
|
std::string_view input_port,
|
||||||
|
const LinkOptions& options);
|
||||||
Status RemoveLink(LinkId link);
|
Status RemoveLink(LinkId link);
|
||||||
|
|
||||||
Result<RuleId> AddRouteRule(const RouteRule& rule);
|
Result<RuleId> AddRouteRule(const RouteRule& rule);
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,12 @@ Registry snapshot + add/remove events (milestone 1):
|
||||||
./build/warppipe_perf --mode registry --count 1000 --events 100
|
./build/warppipe_perf --mode registry --count 1000 --events 100
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Link creation + removal (milestone 3):
|
||||||
|
```
|
||||||
|
./build/warppipe_perf --mode links --links 200
|
||||||
|
./build/warppipe_perf --mode links --links 200 --batch 50
|
||||||
|
```
|
||||||
|
|
||||||
Optional format and loopback:
|
Optional format and loopback:
|
||||||
```
|
```
|
||||||
./build/warppipe_perf --mode create-destroy --count 200 --type sink --rate 48000 --channels 2
|
./build/warppipe_perf --mode create-destroy --count 200 --type sink --rate 48000 --channels 2
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <algorithm>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
|
@ -18,6 +20,8 @@ struct Options {
|
||||||
std::string target;
|
std::string target;
|
||||||
uint32_t count = 200;
|
uint32_t count = 200;
|
||||||
uint32_t events = 100;
|
uint32_t events = 100;
|
||||||
|
uint32_t links = 200;
|
||||||
|
uint32_t batch = 0;
|
||||||
uint32_t rate = 48000;
|
uint32_t rate = 48000;
|
||||||
uint32_t channels = 2;
|
uint32_t channels = 2;
|
||||||
};
|
};
|
||||||
|
|
@ -37,10 +41,12 @@ bool ParseUInt(const char* value, uint32_t* out) {
|
||||||
|
|
||||||
void PrintUsage() {
|
void PrintUsage() {
|
||||||
std::cout << "warppipe_perf usage:\n"
|
std::cout << "warppipe_perf usage:\n"
|
||||||
<< " --mode create-destroy|registry\n"
|
<< " --mode create-destroy|registry|links\n"
|
||||||
<< " --type sink|source|both\n"
|
<< " --type sink|source|both\n"
|
||||||
<< " --count N (default 200, per-type when --type both)\n"
|
<< " --count N (default 200, per-type when --type both)\n"
|
||||||
<< " --events N (registry mode, default 100)\n"
|
<< " --events N (registry mode, default 100)\n"
|
||||||
|
<< " --links N (links mode, default 200)\n"
|
||||||
|
<< " --batch N (links mode, batch size)\n"
|
||||||
<< " --rate N (default 48000)\n"
|
<< " --rate N (default 48000)\n"
|
||||||
<< " --channels N (default 2)\n"
|
<< " --channels N (default 2)\n"
|
||||||
<< " --target <node-name> (loopback target, optional)\n";
|
<< " --target <node-name> (loopback target, optional)\n";
|
||||||
|
|
@ -61,6 +67,14 @@ bool ParseArgs(int argc, char* argv[], Options* options) {
|
||||||
if (!ParseUInt(argv[++i], &options->events)) {
|
if (!ParseUInt(argv[++i], &options->events)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} else if (arg == "--links" && i + 1 < argc) {
|
||||||
|
if (!ParseUInt(argv[++i], &options->links)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (arg == "--batch" && i + 1 < argc) {
|
||||||
|
if (!ParseUInt(argv[++i], &options->batch)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} else if (arg == "--rate" && i + 1 < argc) {
|
} else if (arg == "--rate" && i + 1 < argc) {
|
||||||
if (!ParseUInt(argv[++i], &options->rate)) {
|
if (!ParseUInt(argv[++i], &options->rate)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -80,7 +94,7 @@ bool ParseArgs(int argc, char* argv[], Options* options) {
|
||||||
if (options->type != "sink" && options->type != "source" && options->type != "both") {
|
if (options->type != "sink" && options->type != "source" && options->type != "both") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (options->mode != "create-destroy" && options->mode != "registry") {
|
if (options->mode != "create-destroy" && options->mode != "registry" && options->mode != "links") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -94,6 +108,23 @@ double ToMillis(std::chrono::steady_clock::duration duration) {
|
||||||
return std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(duration).count();
|
return std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(duration).count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<warppipe::PortId> FindPort(warppipe::Client* client,
|
||||||
|
warppipe::NodeId node,
|
||||||
|
bool want_input) {
|
||||||
|
for (int attempt = 0; attempt < 50; ++attempt) {
|
||||||
|
auto ports = client->ListPorts(node);
|
||||||
|
if (ports.ok()) {
|
||||||
|
for (const auto& port : ports.value) {
|
||||||
|
if (port.is_input == want_input) {
|
||||||
|
return port.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usleep(5000);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
|
|
@ -226,6 +257,96 @@ int main(int argc, char* argv[]) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.mode == "links") {
|
||||||
|
const uint32_t pair_count = options.links;
|
||||||
|
const uint32_t batch = options.batch == 0 ? pair_count : options.batch;
|
||||||
|
double total_create_ms = 0.0;
|
||||||
|
double total_remove_ms = 0.0;
|
||||||
|
size_t total_links = 0;
|
||||||
|
|
||||||
|
for (uint32_t start_index = 0; start_index < pair_count; start_index += batch) {
|
||||||
|
const uint32_t batch_count = std::min(batch, pair_count - start_index);
|
||||||
|
std::vector<warppipe::NodeId> sinks;
|
||||||
|
std::vector<warppipe::NodeId> sources;
|
||||||
|
sinks.reserve(batch_count);
|
||||||
|
sources.reserve(batch_count);
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < batch_count; ++i) {
|
||||||
|
uint32_t index = start_index + i;
|
||||||
|
std::string sink_name = prefix + "-sink-" + std::to_string(index);
|
||||||
|
auto sink = client.value->CreateVirtualSink(sink_name, node_options);
|
||||||
|
if (!sink.ok()) {
|
||||||
|
std::cerr << "create sink failed at " << index << ": " << sink.status.message << "\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sinks.push_back(sink.value.node);
|
||||||
|
|
||||||
|
std::string source_name = prefix + "-source-" + std::to_string(index);
|
||||||
|
auto source = client.value->CreateVirtualSource(source_name, node_options);
|
||||||
|
if (!source.ok()) {
|
||||||
|
std::cerr << "create source failed at " << index << ": " << source.status.message << "\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sources.push_back(source.value.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t pair_limit = std::min(sinks.size(), sources.size());
|
||||||
|
std::vector<warppipe::LinkId> links;
|
||||||
|
links.reserve(pair_limit);
|
||||||
|
|
||||||
|
std::vector<warppipe::PortId> out_ports;
|
||||||
|
std::vector<warppipe::PortId> in_ports;
|
||||||
|
out_ports.reserve(pair_limit);
|
||||||
|
in_ports.reserve(pair_limit);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < pair_limit; ++i) {
|
||||||
|
auto out_port = FindPort(client.value.get(), sources[i], false);
|
||||||
|
auto in_port = FindPort(client.value.get(), sinks[i], true);
|
||||||
|
if (!out_port || !in_port) {
|
||||||
|
std::cerr << "port lookup failed at " << (start_index + i) << "\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out_ports.push_back(*out_port);
|
||||||
|
in_ports.push_back(*in_port);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto create_start = std::chrono::steady_clock::now();
|
||||||
|
for (size_t i = 0; i < out_ports.size() && i < in_ports.size(); ++i) {
|
||||||
|
auto link = client.value->CreateLink(out_ports[i], in_ports[i], warppipe::LinkOptions{});
|
||||||
|
if (!link.ok()) {
|
||||||
|
std::cerr << "link failed at " << (start_index + i) << ": " << link.status.message << "\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
links.push_back(link.value.id);
|
||||||
|
}
|
||||||
|
auto create_end = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
for (const auto& link_id : links) {
|
||||||
|
client.value->RemoveLink(link_id);
|
||||||
|
}
|
||||||
|
auto remove_end = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
for (const auto& node : sources) {
|
||||||
|
client.value->RemoveNode(node);
|
||||||
|
}
|
||||||
|
for (const auto& node : sinks) {
|
||||||
|
client.value->RemoveNode(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
total_links += links.size();
|
||||||
|
total_create_ms += ToMillis(create_end - create_start);
|
||||||
|
total_remove_ms += ToMillis(remove_end - create_end);
|
||||||
|
}
|
||||||
|
|
||||||
|
const double total_ms = total_create_ms + total_remove_ms;
|
||||||
|
std::cout << "link_count=" << total_links << "\n"
|
||||||
|
<< "link_create_ms=" << std::fixed << std::setprecision(2) << total_create_ms << "\n"
|
||||||
|
<< "link_remove_ms=" << total_remove_ms << "\n"
|
||||||
|
<< "link_total_ms=" << total_ms << "\n"
|
||||||
|
<< "link_batch=" << batch << "\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
PrintUsage();
|
PrintUsage();
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
291
src/warppipe.cpp
291
src/warppipe.cpp
|
|
@ -6,6 +6,7 @@
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include <pipewire/keys.h>
|
#include <pipewire/keys.h>
|
||||||
|
#include <pipewire/link.h>
|
||||||
#include <pipewire/pipewire.h>
|
#include <pipewire/pipewire.h>
|
||||||
|
|
||||||
#include <spa/param/audio/format-utils.h>
|
#include <spa/param/audio/format-utils.h>
|
||||||
|
|
@ -76,6 +77,58 @@ struct StreamData {
|
||||||
uint32_t rate = kDefaultRate;
|
uint32_t rate = kDefaultRate;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct LinkProxy {
|
||||||
|
pw_proxy* proxy = nullptr;
|
||||||
|
spa_hook listener{};
|
||||||
|
pw_thread_loop* loop = nullptr;
|
||||||
|
bool done = false;
|
||||||
|
bool failed = false;
|
||||||
|
std::string error;
|
||||||
|
uint32_t id = SPA_ID_INVALID;
|
||||||
|
};
|
||||||
|
|
||||||
|
void LinkProxyBound(void* data, uint32_t global_id) {
|
||||||
|
auto* link = static_cast<LinkProxy*>(data);
|
||||||
|
if (!link) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
link->id = global_id;
|
||||||
|
link->done = true;
|
||||||
|
if (link->loop) {
|
||||||
|
pw_thread_loop_signal(link->loop, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinkProxyRemoved(void* data) {
|
||||||
|
auto* link = static_cast<LinkProxy*>(data);
|
||||||
|
if (!link) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
link->done = true;
|
||||||
|
if (link->loop) {
|
||||||
|
pw_thread_loop_signal(link->loop, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinkProxyError(void* data, int, int res, const char* message) {
|
||||||
|
auto* link = static_cast<LinkProxy*>(data);
|
||||||
|
if (!link) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
link->failed = true;
|
||||||
|
link->error = message ? message : spa_strerror(res);
|
||||||
|
if (link->loop) {
|
||||||
|
pw_thread_loop_signal(link->loop, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const pw_proxy_events kLinkProxyEvents = {
|
||||||
|
PW_VERSION_PROXY_EVENTS,
|
||||||
|
.bound = LinkProxyBound,
|
||||||
|
.removed = LinkProxyRemoved,
|
||||||
|
.error = LinkProxyError,
|
||||||
|
};
|
||||||
|
|
||||||
void StreamProcess(void* data) {
|
void StreamProcess(void* data) {
|
||||||
auto* stream_data = static_cast<StreamData*>(data);
|
auto* stream_data = static_cast<StreamData*>(data);
|
||||||
if (!stream_data || !stream_data->stream) {
|
if (!stream_data || !stream_data->stream) {
|
||||||
|
|
@ -187,6 +240,7 @@ struct Client::Impl {
|
||||||
std::unordered_map<uint32_t, PortInfo> ports;
|
std::unordered_map<uint32_t, PortInfo> ports;
|
||||||
std::unordered_map<uint32_t, Link> links;
|
std::unordered_map<uint32_t, Link> links;
|
||||||
std::unordered_map<uint32_t, std::unique_ptr<StreamData>> virtual_streams;
|
std::unordered_map<uint32_t, std::unique_ptr<StreamData>> virtual_streams;
|
||||||
|
std::unordered_map<uint32_t, std::unique_ptr<LinkProxy>> link_proxies;
|
||||||
|
|
||||||
Status ConnectLocked();
|
Status ConnectLocked();
|
||||||
void DisconnectLocked();
|
void DisconnectLocked();
|
||||||
|
|
@ -270,6 +324,7 @@ void Client::Impl::RegistryGlobalRemove(void* data, uint32_t id) {
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(impl->cache_mutex);
|
std::lock_guard<std::mutex> lock(impl->cache_mutex);
|
||||||
impl->virtual_streams.erase(id);
|
impl->virtual_streams.erase(id);
|
||||||
|
impl->link_proxies.erase(id);
|
||||||
auto node_it = impl->nodes.find(id);
|
auto node_it = impl->nodes.find(id);
|
||||||
if (node_it != impl->nodes.end()) {
|
if (node_it != impl->nodes.end()) {
|
||||||
impl->nodes.erase(node_it);
|
impl->nodes.erase(node_it);
|
||||||
|
|
@ -519,7 +574,10 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
|
||||||
|
|
||||||
stream_data->node_id = node_id;
|
stream_data->node_id = node_id;
|
||||||
stream_data->ready = true;
|
stream_data->ready = true;
|
||||||
virtual_streams.emplace(node_id, std::move(stream_data));
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(cache_mutex);
|
||||||
|
virtual_streams.emplace(node_id, std::move(stream_data));
|
||||||
|
}
|
||||||
return {Status::Ok(), node_id};
|
return {Status::Ok(), node_id};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -575,7 +633,21 @@ Status Client::Impl::ConnectLocked() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Client::Impl::DisconnectLocked() {
|
void Client::Impl::DisconnectLocked() {
|
||||||
for (auto& entry : virtual_streams) {
|
std::unordered_map<uint32_t, std::unique_ptr<LinkProxy>> links;
|
||||||
|
std::unordered_map<uint32_t, std::unique_ptr<StreamData>> streams;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(cache_mutex);
|
||||||
|
links.swap(link_proxies);
|
||||||
|
streams.swap(virtual_streams);
|
||||||
|
}
|
||||||
|
for (auto& entry : links) {
|
||||||
|
LinkProxy* link = entry.second.get();
|
||||||
|
if (link && link->proxy) {
|
||||||
|
pw_proxy_destroy(link->proxy);
|
||||||
|
link->proxy = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 && stream_data->stream) {
|
||||||
pw_stream_disconnect(stream_data->stream);
|
pw_stream_disconnect(stream_data->stream);
|
||||||
|
|
@ -583,7 +655,6 @@ void Client::Impl::DisconnectLocked() {
|
||||||
stream_data->stream = nullptr;
|
stream_data->stream = nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
virtual_streams.clear();
|
|
||||||
if (registry_listener_attached) {
|
if (registry_listener_attached) {
|
||||||
spa_hook_remove(®istry_listener);
|
spa_hook_remove(®istry_listener);
|
||||||
registry_listener_attached = false;
|
registry_listener_attached = false;
|
||||||
|
|
@ -772,28 +843,214 @@ Status Client::RemoveNode(NodeId node) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pw_thread_loop_lock(impl_->thread_loop);
|
pw_thread_loop_lock(impl_->thread_loop);
|
||||||
auto it = impl_->virtual_streams.find(node.value);
|
std::unique_ptr<StreamData> owned_stream;
|
||||||
if (it == impl_->virtual_streams.end()) {
|
{
|
||||||
pw_thread_loop_unlock(impl_->thread_loop);
|
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||||
return Status::Error(StatusCode::kNotFound, "node not managed by warppipe");
|
auto it = impl_->virtual_streams.find(node.value);
|
||||||
|
if (it == impl_->virtual_streams.end()) {
|
||||||
|
pw_thread_loop_unlock(impl_->thread_loop);
|
||||||
|
return Status::Error(StatusCode::kNotFound, "node not managed by warppipe");
|
||||||
|
}
|
||||||
|
owned_stream = std::move(it->second);
|
||||||
|
impl_->virtual_streams.erase(it);
|
||||||
}
|
}
|
||||||
StreamData* stream_data = it->second.get();
|
if (owned_stream && owned_stream->stream) {
|
||||||
if (stream_data && stream_data->stream) {
|
pw_stream_disconnect(owned_stream->stream);
|
||||||
pw_stream_disconnect(stream_data->stream);
|
pw_stream_destroy(owned_stream->stream);
|
||||||
pw_stream_destroy(stream_data->stream);
|
owned_stream->stream = nullptr;
|
||||||
stream_data->stream = nullptr;
|
|
||||||
}
|
}
|
||||||
impl_->virtual_streams.erase(it);
|
|
||||||
pw_thread_loop_unlock(impl_->thread_loop);
|
pw_thread_loop_unlock(impl_->thread_loop);
|
||||||
return Status::Ok();
|
return Status::Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<Link> Client::CreateLink(PortId, PortId, const LinkOptions&) {
|
Result<Link> Client::CreateLink(PortId output, PortId input, const LinkOptions& options) {
|
||||||
return {Status::Error(StatusCode::kNotImplemented, "create link not implemented"), {}};
|
Status status = impl_->EnsureConnected();
|
||||||
|
if (!status.ok()) {
|
||||||
|
return {status, {}};
|
||||||
|
}
|
||||||
|
if (output.value == 0 || input.value == 0) {
|
||||||
|
return {Status::Error(StatusCode::kInvalidArgument, "invalid port id"), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
pw_thread_loop_lock(impl_->thread_loop);
|
||||||
|
|
||||||
|
PortInfo out_port;
|
||||||
|
PortInfo in_port;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||||
|
auto out_it = impl_->ports.find(output.value);
|
||||||
|
if (out_it == impl_->ports.end()) {
|
||||||
|
pw_thread_loop_unlock(impl_->thread_loop);
|
||||||
|
return {Status::Error(StatusCode::kNotFound, "output port not found"), {}};
|
||||||
|
}
|
||||||
|
auto in_it = impl_->ports.find(input.value);
|
||||||
|
if (in_it == impl_->ports.end()) {
|
||||||
|
pw_thread_loop_unlock(impl_->thread_loop);
|
||||||
|
return {Status::Error(StatusCode::kNotFound, "input port not found"), {}};
|
||||||
|
}
|
||||||
|
out_port = out_it->second;
|
||||||
|
in_port = in_it->second;
|
||||||
|
for (const auto& entry : impl_->links) {
|
||||||
|
const Link& link = entry.second;
|
||||||
|
if (link.output_port.value == output.value && link.input_port.value == input.value) {
|
||||||
|
pw_thread_loop_unlock(impl_->thread_loop);
|
||||||
|
return {Status::Error(StatusCode::kInvalidArgument, "link already exists"), {}};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out_port.is_input || !in_port.is_input) {
|
||||||
|
pw_thread_loop_unlock(impl_->thread_loop);
|
||||||
|
return {Status::Error(StatusCode::kInvalidArgument, "port directions do not match"), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
pw_properties* props = pw_properties_new(nullptr, nullptr);
|
||||||
|
if (!props) {
|
||||||
|
pw_thread_loop_unlock(impl_->thread_loop);
|
||||||
|
return {Status::Error(StatusCode::kInternal, "failed to allocate link properties"), {}};
|
||||||
|
}
|
||||||
|
pw_properties_setf(props, PW_KEY_LINK_OUTPUT_PORT, "%u", output.value);
|
||||||
|
pw_properties_setf(props, PW_KEY_LINK_INPUT_PORT, "%u", input.value);
|
||||||
|
if (options.passive) {
|
||||||
|
pw_properties_set(props, PW_KEY_LINK_PASSIVE, "true");
|
||||||
|
}
|
||||||
|
if (options.linger) {
|
||||||
|
pw_properties_set(props, PW_KEY_OBJECT_LINGER, "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
pw_proxy* proxy = reinterpret_cast<pw_proxy*>(
|
||||||
|
pw_core_create_object(impl_->core,
|
||||||
|
"link-factory",
|
||||||
|
PW_TYPE_INTERFACE_Link,
|
||||||
|
PW_VERSION_LINK,
|
||||||
|
&props->dict,
|
||||||
|
0));
|
||||||
|
pw_properties_free(props);
|
||||||
|
if (!proxy) {
|
||||||
|
pw_thread_loop_unlock(impl_->thread_loop);
|
||||||
|
return {Status::Error(StatusCode::kUnavailable, "failed to create link"), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto link_proxy = std::make_unique<LinkProxy>();
|
||||||
|
link_proxy->proxy = proxy;
|
||||||
|
link_proxy->loop = impl_->thread_loop;
|
||||||
|
pw_proxy_add_listener(proxy, &link_proxy->listener, &kLinkProxyEvents, link_proxy.get());
|
||||||
|
|
||||||
|
int wait_attempts = 0;
|
||||||
|
while (link_proxy->id == SPA_ID_INVALID && !link_proxy->failed && wait_attempts < 3) {
|
||||||
|
int wait_res = pw_thread_loop_timed_wait(impl_->thread_loop, kSyncWaitSeconds);
|
||||||
|
if (wait_res == -ETIMEDOUT) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++wait_attempts;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (link_proxy->failed) {
|
||||||
|
std::string error = link_proxy->error.empty() ? "link creation failed" : link_proxy->error;
|
||||||
|
pw_proxy_destroy(proxy);
|
||||||
|
pw_thread_loop_unlock(impl_->thread_loop);
|
||||||
|
return {Status::Error(StatusCode::kUnavailable, std::move(error)), {}};
|
||||||
|
}
|
||||||
|
if (link_proxy->id == SPA_ID_INVALID) {
|
||||||
|
pw_proxy_destroy(proxy);
|
||||||
|
pw_thread_loop_unlock(impl_->thread_loop);
|
||||||
|
return {Status::Error(StatusCode::kTimeout, "timed out waiting for link id"), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
Link link;
|
||||||
|
link.id = LinkId{link_proxy->id};
|
||||||
|
link.output_port = output;
|
||||||
|
link.input_port = input;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||||
|
impl_->link_proxies.emplace(link_proxy->id, std::move(link_proxy));
|
||||||
|
impl_->links[link.id.value] = link;
|
||||||
|
}
|
||||||
|
|
||||||
|
pw_thread_loop_unlock(impl_->thread_loop);
|
||||||
|
return {Status::Ok(), link};
|
||||||
}
|
}
|
||||||
|
|
||||||
Status Client::RemoveLink(LinkId) {
|
Result<Link> Client::CreateLinkByName(std::string_view output_node,
|
||||||
return Status::Error(StatusCode::kNotImplemented, "remove link not implemented");
|
std::string_view output_port,
|
||||||
|
std::string_view input_node,
|
||||||
|
std::string_view input_port,
|
||||||
|
const LinkOptions& options) {
|
||||||
|
Status status = impl_->EnsureConnected();
|
||||||
|
if (!status.ok()) {
|
||||||
|
return {status, {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output_node.empty() || output_port.empty() || input_node.empty() || input_port.empty()) {
|
||||||
|
return {Status::Error(StatusCode::kInvalidArgument, "node or port name missing"), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<PortId> out_id;
|
||||||
|
std::optional<PortId> in_id;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||||
|
for (const auto& entry : impl_->ports) {
|
||||||
|
const PortInfo& port = entry.second;
|
||||||
|
auto node_it = impl_->nodes.find(port.node.value);
|
||||||
|
if (node_it == impl_->nodes.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const NodeInfo& node = node_it->second;
|
||||||
|
if (port.is_input) {
|
||||||
|
if (node.name == input_node && port.name == input_port) {
|
||||||
|
in_id = port.id;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (node.name == output_node && port.name == output_port) {
|
||||||
|
out_id = port.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (out_id && in_id) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!out_id || !in_id) {
|
||||||
|
return {Status::Error(StatusCode::kNotFound, "matching ports not found"), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateLink(*out_id, *in_id, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
Status Client::RemoveLink(LinkId link) {
|
||||||
|
Status status = impl_->EnsureConnected();
|
||||||
|
if (!status.ok()) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
pw_thread_loop_lock(impl_->thread_loop);
|
||||||
|
|
||||||
|
bool removed = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||||
|
auto it = impl_->link_proxies.find(link.value);
|
||||||
|
if (it != impl_->link_proxies.end()) {
|
||||||
|
if (it->second && it->second->proxy) {
|
||||||
|
pw_proxy_destroy(it->second->proxy);
|
||||||
|
}
|
||||||
|
impl_->link_proxies.erase(it);
|
||||||
|
impl_->links.erase(link.value);
|
||||||
|
removed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!removed && impl_->registry) {
|
||||||
|
int res = pw_registry_destroy(impl_->registry, link.value);
|
||||||
|
if (res < 0) {
|
||||||
|
pw_thread_loop_unlock(impl_->thread_loop);
|
||||||
|
return Status::Error(StatusCode::kNotFound, "link not found");
|
||||||
|
}
|
||||||
|
removed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pw_thread_loop_unlock(impl_->thread_loop);
|
||||||
|
return removed ? Status::Ok() : Status::Error(StatusCode::kNotFound, "link not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<RuleId> Client::AddRouteRule(const RouteRule&) {
|
Result<RuleId> Client::AddRouteRule(const RouteRule&) {
|
||||||
|
|
|
||||||
|
|
@ -216,3 +216,97 @@ TEST_CASE("autoconnect reconnects after forced disconnect") {
|
||||||
auto nodes_after = result.value->ListNodes();
|
auto nodes_after = result.value->ListNodes();
|
||||||
REQUIRE(nodes_after.ok());
|
REQUIRE(nodes_after.ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("link creation validates ports and directions") {
|
||||||
|
auto result = warppipe::Client::Create(DefaultOptions());
|
||||||
|
if (!result.ok()) {
|
||||||
|
SUCCEED("PipeWire unavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
warppipe::NodeInfo node;
|
||||||
|
node.id = warppipe::NodeId{600001};
|
||||||
|
node.name = "warppipe-link-node";
|
||||||
|
node.media_class = "Audio/Sink";
|
||||||
|
REQUIRE(result.value->Test_InsertNode(node).ok());
|
||||||
|
|
||||||
|
warppipe::PortInfo out_port;
|
||||||
|
out_port.id = warppipe::PortId{600002};
|
||||||
|
out_port.node = node.id;
|
||||||
|
out_port.name = "out";
|
||||||
|
out_port.is_input = false;
|
||||||
|
REQUIRE(result.value->Test_InsertPort(out_port).ok());
|
||||||
|
|
||||||
|
warppipe::PortInfo in_port;
|
||||||
|
in_port.id = warppipe::PortId{600003};
|
||||||
|
in_port.node = node.id;
|
||||||
|
in_port.name = "in";
|
||||||
|
in_port.is_input = true;
|
||||||
|
REQUIRE(result.value->Test_InsertPort(in_port).ok());
|
||||||
|
|
||||||
|
auto invalid = result.value->CreateLink(warppipe::PortId{0}, in_port.id, warppipe::LinkOptions{});
|
||||||
|
REQUIRE_FALSE(invalid.ok());
|
||||||
|
REQUIRE(invalid.status.code == warppipe::StatusCode::kInvalidArgument);
|
||||||
|
|
||||||
|
auto missing_out = result.value->CreateLink(warppipe::PortId{123456}, in_port.id, warppipe::LinkOptions{});
|
||||||
|
REQUIRE_FALSE(missing_out.ok());
|
||||||
|
REQUIRE(missing_out.status.code == warppipe::StatusCode::kNotFound);
|
||||||
|
|
||||||
|
auto missing_in = result.value->CreateLink(out_port.id, warppipe::PortId{123457}, warppipe::LinkOptions{});
|
||||||
|
REQUIRE_FALSE(missing_in.ok());
|
||||||
|
REQUIRE(missing_in.status.code == warppipe::StatusCode::kNotFound);
|
||||||
|
|
||||||
|
auto mismatch = result.value->CreateLink(in_port.id, out_port.id, warppipe::LinkOptions{});
|
||||||
|
REQUIRE_FALSE(mismatch.ok());
|
||||||
|
REQUIRE(mismatch.status.code == warppipe::StatusCode::kInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("CreateLinkByName validates missing names") {
|
||||||
|
auto result = warppipe::Client::Create(DefaultOptions());
|
||||||
|
if (!result.ok()) {
|
||||||
|
SUCCEED("PipeWire unavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto invalid = result.value->CreateLinkByName("", "out", "node", "in", warppipe::LinkOptions{});
|
||||||
|
REQUIRE_FALSE(invalid.ok());
|
||||||
|
REQUIRE(invalid.status.code == warppipe::StatusCode::kInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("duplicate links are rejected") {
|
||||||
|
auto result = warppipe::Client::Create(DefaultOptions());
|
||||||
|
if (!result.ok()) {
|
||||||
|
SUCCEED("PipeWire unavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
warppipe::NodeInfo node;
|
||||||
|
node.id = warppipe::NodeId{600101};
|
||||||
|
node.name = "warppipe-link-dup";
|
||||||
|
node.media_class = "Audio/Sink";
|
||||||
|
REQUIRE(result.value->Test_InsertNode(node).ok());
|
||||||
|
|
||||||
|
warppipe::PortInfo out_port;
|
||||||
|
out_port.id = warppipe::PortId{600102};
|
||||||
|
out_port.node = node.id;
|
||||||
|
out_port.name = "out";
|
||||||
|
out_port.is_input = false;
|
||||||
|
REQUIRE(result.value->Test_InsertPort(out_port).ok());
|
||||||
|
|
||||||
|
warppipe::PortInfo in_port;
|
||||||
|
in_port.id = warppipe::PortId{600103};
|
||||||
|
in_port.node = node.id;
|
||||||
|
in_port.name = "in";
|
||||||
|
in_port.is_input = true;
|
||||||
|
REQUIRE(result.value->Test_InsertPort(in_port).ok());
|
||||||
|
|
||||||
|
auto first = result.value->CreateLink(out_port.id, in_port.id, warppipe::LinkOptions{});
|
||||||
|
if (!first.ok()) {
|
||||||
|
SUCCEED("Link factory unavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto second = result.value->CreateLink(out_port.id, in_port.id, warppipe::LinkOptions{});
|
||||||
|
REQUIRE_FALSE(second.ok());
|
||||||
|
REQUIRE(second.status.code == warppipe::StatusCode::kInvalidArgument);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue