Reverts: - fix: move VU meter above volume controls to stop blocking mute button - Merge feature/graph-ux-meters (region fix, toggle, segmented VU meters) - Merge feature/level-meters (pw_stream peak metering, /api/peaks) VU meters / level meters don't belong in a patchbay. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
647 lines
24 KiB
C++
647 lines
24 KiB
C++
#include "web_server.h"
|
|
#include <sstream>
|
|
#include <iomanip>
|
|
#include <cstdio>
|
|
#include <algorithm>
|
|
#include <condition_variable>
|
|
#include <fstream>
|
|
#include <sys/stat.h>
|
|
#include <pwd.h>
|
|
|
|
using namespace pwgraph;
|
|
|
|
// ============================================================================
|
|
// File I/O helpers
|
|
// ============================================================================
|
|
|
|
std::string WebServer::dataDir() const {
|
|
const char *home = getenv("HOME");
|
|
if (!home) {
|
|
auto *pw = getpwuid(getuid());
|
|
if (pw) home = pw->pw_dir;
|
|
}
|
|
std::string dir = (home ? home : "/tmp");
|
|
dir += "/.config/pwweb";
|
|
mkdir(dir.c_str(), 0755);
|
|
return dir;
|
|
}
|
|
|
|
std::string WebServer::readFile(const std::string &path) const {
|
|
std::ifstream f(path);
|
|
if (!f.is_open()) return "{}";
|
|
return std::string((std::istreambuf_iterator<char>(f)),
|
|
std::istreambuf_iterator<char>());
|
|
}
|
|
|
|
void WebServer::writeFile(const std::string &path, const std::string &data) const {
|
|
std::ofstream f(path);
|
|
if (f.is_open()) f << data;
|
|
}
|
|
|
|
// ============================================================================
|
|
// JSON serialization helpers
|
|
// ============================================================================
|
|
|
|
static std::string escapeJson(const std::string &s) {
|
|
std::ostringstream o;
|
|
for (char c : s) {
|
|
switch (c) {
|
|
case '"': o << "\\\""; break;
|
|
case '\\': o << "\\\\"; break;
|
|
case '\b': o << "\\b"; break;
|
|
case '\f': o << "\\f"; break;
|
|
case '\n': o << "\\n"; break;
|
|
case '\r': o << "\\r"; break;
|
|
case '\t': o << "\\t"; break;
|
|
default:
|
|
if (static_cast<unsigned char>(c) < 0x20) {
|
|
o << "\\u" << std::hex << std::setw(4) << std::setfill('0')
|
|
<< (int)(unsigned char)c;
|
|
} else {
|
|
o << c;
|
|
}
|
|
}
|
|
}
|
|
return o.str();
|
|
}
|
|
|
|
static const char *portModeStr(PortMode m) {
|
|
switch (m) {
|
|
case PortMode::Input: return "input";
|
|
case PortMode::Output: return "output";
|
|
case PortMode::Duplex: return "duplex";
|
|
default: return "none";
|
|
}
|
|
}
|
|
|
|
static std::string nodeTypeStr(NodeType t) {
|
|
std::string result;
|
|
if ((t & NodeType::Audio) != NodeType::None) {
|
|
if (!result.empty()) result += '+';
|
|
result += "audio";
|
|
}
|
|
if ((t & NodeType::Video) != NodeType::None) {
|
|
if (!result.empty()) result += '+';
|
|
result += "video";
|
|
}
|
|
if ((t & NodeType::Midi) != NodeType::None) {
|
|
if (!result.empty()) result += '+';
|
|
result += "midi";
|
|
}
|
|
return result.empty() ? "other" : result;
|
|
}
|
|
|
|
std::string WebServer::buildGraphJson() const {
|
|
auto snap = m_engine.snapshot();
|
|
|
|
std::ostringstream json;
|
|
json << "{\"type\":\"graph\",\"nodes\":[";
|
|
|
|
bool first_node = true;
|
|
for (auto &n : snap.nodes) {
|
|
if (!n.ready) continue;
|
|
if (!first_node) json << ",";
|
|
first_node = false;
|
|
|
|
json << "{\"id\":" << n.id
|
|
<< ",\"name\":\"" << escapeJson(n.name) << "\""
|
|
<< ",\"nick\":\"" << escapeJson(n.nick) << "\""
|
|
<< ",\"media_name\":\"" << escapeJson(n.media_name) << "\""
|
|
<< ",\"mode\":\"" << portModeStr(n.mode) << "\""
|
|
<< ",\"node_type\":\"" << nodeTypeStr(n.node_type) << "\""
|
|
<< ",\"volume\":" << n.volume
|
|
<< ",\"mute\":" << (n.mute ? "true" : "false")
|
|
<< ",\"sample_rate\":" << n.sample_rate
|
|
<< ",\"channels\":" << n.channels
|
|
<< ",\"quantum\":" << n.quantum
|
|
<< ",\"rate\":" << n.rate
|
|
<< ",\"format\":\"" << escapeJson(n.format) << "\""
|
|
<< ",\"device_name\":\"" << escapeJson(n.device_name) << "\""
|
|
<< ",\"device_bus\":\"" << escapeJson(n.device_bus) << "\""
|
|
<< ",\"api\":\"" << escapeJson(n.api) << "\""
|
|
<< ",\"priority\":" << n.priority
|
|
<< ",\"port_ids\":[";
|
|
bool first_p = true;
|
|
for (uint32_t pid : n.port_ids) {
|
|
if (!first_p) json << ",";
|
|
first_p = false;
|
|
json << pid;
|
|
}
|
|
json << "]}";
|
|
}
|
|
|
|
json << "],\"ports\":[";
|
|
|
|
bool first_port = true;
|
|
for (auto &p : snap.ports) {
|
|
if (!first_port) json << ",";
|
|
first_port = false;
|
|
|
|
std::string port_type_name;
|
|
uint32_t pt = p.port_type;
|
|
if (pt == m_engine.audioPortType()) port_type_name = "audio";
|
|
else if (pt == m_engine.midiPortType()) port_type_name = "midi";
|
|
else if (pt == m_engine.videoPortType()) port_type_name = "video";
|
|
else port_type_name = "other";
|
|
|
|
json << "{\"id\":" << p.id
|
|
<< ",\"node_id\":" << p.node_id
|
|
<< ",\"name\":\"" << escapeJson(p.name) << "\""
|
|
<< ",\"mode\":\"" << portModeStr(p.mode) << "\""
|
|
<< ",\"port_type\":\"" << port_type_name << "\""
|
|
<< ",\"flags\":" << (int)(uint8_t)p.flags
|
|
<< "}";
|
|
}
|
|
|
|
json << "],\"links\":[";
|
|
|
|
bool first_link = true;
|
|
for (auto &l : snap.links) {
|
|
if (!first_link) json << ",";
|
|
first_link = false;
|
|
|
|
json << "{\"id\":" << l.id
|
|
<< ",\"output_port_id\":" << l.port1_id
|
|
<< ",\"input_port_id\":" << l.port2_id << "}";
|
|
}
|
|
|
|
json << "]}";
|
|
return json.str();
|
|
}
|
|
|
|
// ============================================================================
|
|
// WebServer
|
|
// ============================================================================
|
|
|
|
WebServer::WebServer(GraphEngine &engine, int port)
|
|
: m_engine(engine), m_port(port), m_running(false)
|
|
{
|
|
}
|
|
|
|
WebServer::~WebServer() {
|
|
stop();
|
|
}
|
|
|
|
bool WebServer::start() {
|
|
setupRoutes();
|
|
|
|
m_running = true;
|
|
m_thread = std::thread([this]() {
|
|
fprintf(stderr, "pwweb: starting web server on port %d\n", m_port);
|
|
if (!m_http.listen("0.0.0.0", m_port)) {
|
|
fprintf(stderr, "pwweb: failed to bind to port %d\n", m_port);
|
|
m_running = false;
|
|
}
|
|
});
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
return m_running;
|
|
}
|
|
|
|
void WebServer::stop() {
|
|
if (m_running) {
|
|
m_running = false;
|
|
// Close all SSE clients so their loops exit
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_sse_mutex);
|
|
for (auto *sink : m_sse_clients)
|
|
sink->done();
|
|
m_sse_clients.clear();
|
|
}
|
|
m_http.stop();
|
|
if (m_thread.joinable())
|
|
m_thread.join();
|
|
}
|
|
}
|
|
|
|
void WebServer::broadcastGraph() {
|
|
if (!m_running) return;
|
|
std::string json = buildGraphJson();
|
|
|
|
std::lock_guard<std::mutex> lock(m_sse_mutex);
|
|
for (auto it = m_sse_clients.begin(); it != m_sse_clients.end(); ) {
|
|
auto *sink = *it;
|
|
std::string msg = "data: " + json + "\n\n";
|
|
if (sink->write(msg.c_str(), msg.size())) {
|
|
++it;
|
|
} else {
|
|
it = m_sse_clients.erase(it);
|
|
}
|
|
}
|
|
}
|
|
|
|
void WebServer::setupRoutes() {
|
|
// Serve frontend static files from ./frontend/dist
|
|
m_http.set_mount_point("/", "./frontend/dist");
|
|
|
|
// SSE endpoint: long-lived event stream
|
|
m_http.Get("/events", [this](const httplib::Request &req, httplib::Response &res) {
|
|
res.set_header("Content-Type", "text/event-stream");
|
|
res.set_header("Cache-Control", "no-cache");
|
|
res.set_header("Connection", "keep-alive");
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
|
|
res.set_content_provider(
|
|
"text/event-stream",
|
|
[this](size_t /*offset*/, httplib::DataSink &sink) {
|
|
// Register this SSE client
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_sse_mutex);
|
|
m_sse_clients.insert(&sink);
|
|
}
|
|
fprintf(stderr, "pwweb: SSE client connected (total: %zu)\n",
|
|
m_sse_clients.size());
|
|
|
|
// Send initial graph state
|
|
{
|
|
std::string json = buildGraphJson();
|
|
std::string msg = "data: " + json + "\n\n";
|
|
sink.write(msg.c_str(), msg.size());
|
|
}
|
|
|
|
// Block until connection closes or server stops
|
|
std::mutex mtx;
|
|
std::unique_lock<std::mutex> lock(mtx);
|
|
std::condition_variable cv;
|
|
auto sink_ptr = &sink;
|
|
|
|
// Poll: check if sink is still open, broadcast triggers via broadcastGraph()
|
|
while (m_running && sink.write("", 0)) {
|
|
// write with 0 bytes keeps connection alive; sleep between checks
|
|
// Actually, we need to block. Let's use a simpler approach:
|
|
// just block on a condition variable that gets notified
|
|
cv.wait_for(lock, std::chrono::seconds(30), [&] {
|
|
return !m_running;
|
|
});
|
|
// Send a SSE comment as keepalive
|
|
if (m_running) {
|
|
std::string keepalive = ": keepalive\n\n";
|
|
if (!sink.write(keepalive.c_str(), keepalive.size()))
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Unregister
|
|
{
|
|
std::lock_guard<std::mutex> l(m_sse_mutex);
|
|
m_sse_clients.erase(sink_ptr);
|
|
}
|
|
fprintf(stderr, "pwweb: SSE client disconnected (remaining: %zu)\n",
|
|
m_sse_clients.size());
|
|
|
|
sink.done();
|
|
return false; // stop calling this callback
|
|
}
|
|
);
|
|
});
|
|
|
|
// REST API: GET /api/graph
|
|
m_http.Get("/api/graph", [this](const httplib::Request &, httplib::Response &res) {
|
|
std::string json = buildGraphJson();
|
|
res.set_content(json, "application/json");
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
});
|
|
|
|
// REST API: POST /api/connect
|
|
m_http.Post("/api/connect", [this](const httplib::Request &req, httplib::Response &res) {
|
|
uint32_t out_id = 0, in_id = 0;
|
|
if (sscanf(req.body.c_str(),
|
|
"{\"output_port_id\":%u,\"input_port_id\":%u}", &out_id, &in_id) == 2 ||
|
|
sscanf(req.body.c_str(),
|
|
"{\"output_port_id\":%u, \"input_port_id\":%u}", &out_id, &in_id) == 2)
|
|
{
|
|
bool ok = m_engine.connectPorts(out_id, in_id);
|
|
if (ok) broadcastGraph();
|
|
res.set_content(
|
|
ok ? "{\"ok\":true}" : "{\"ok\":false,\"error\":\"connect failed\"}",
|
|
"application/json");
|
|
} else {
|
|
res.status = 400;
|
|
res.set_content("{\"error\":\"invalid json\"}", "application/json");
|
|
}
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
});
|
|
|
|
// REST API: POST /api/disconnect
|
|
m_http.Post("/api/disconnect", [this](const httplib::Request &req, httplib::Response &res) {
|
|
uint32_t out_id = 0, in_id = 0;
|
|
if (sscanf(req.body.c_str(),
|
|
"{\"output_port_id\":%u,\"input_port_id\":%u}", &out_id, &in_id) == 2 ||
|
|
sscanf(req.body.c_str(),
|
|
"{\"output_port_id\":%u, \"input_port_id\":%u}", &out_id, &in_id) == 2)
|
|
{
|
|
bool ok = m_engine.disconnectPorts(out_id, in_id);
|
|
if (ok) broadcastGraph();
|
|
res.set_content(
|
|
ok ? "{\"ok\":true}" : "{\"ok\":false,\"error\":\"disconnect failed\"}",
|
|
"application/json");
|
|
} else {
|
|
res.status = 400;
|
|
res.set_content("{\"error\":\"invalid json\"}", "application/json");
|
|
}
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
});
|
|
|
|
// CORS preflight
|
|
auto cors_handler = [](const httplib::Request &, httplib::Response &res) {
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
res.set_header("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
res.set_header("Access-Control-Allow-Headers", "Content-Type");
|
|
res.status = 204;
|
|
};
|
|
m_http.Options("/api/connect", cors_handler);
|
|
m_http.Options("/api/disconnect", cors_handler);
|
|
m_http.Options("/api/positions", cors_handler);
|
|
m_http.Options("/api/patchbay", cors_handler);
|
|
|
|
// Positions persistence: GET /api/positions
|
|
m_http.Get("/api/positions", [this](const httplib::Request &, httplib::Response &res) {
|
|
std::string path = dataDir() + "/positions.json";
|
|
res.set_content(readFile(path), "application/json");
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
});
|
|
|
|
// Positions persistence: PUT /api/positions
|
|
m_http.Put("/api/positions", [this](const httplib::Request &req, httplib::Response &res) {
|
|
std::string path = dataDir() + "/positions.json";
|
|
writeFile(path, req.body);
|
|
res.set_content("{\"ok\":true}", "application/json");
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
});
|
|
|
|
// Patchbay: GET /api/patchbay
|
|
m_http.Get("/api/patchbay", [this](const httplib::Request &, httplib::Response &res) {
|
|
std::string path = dataDir() + "/patchbay.json";
|
|
res.set_content(readFile(path), "application/json");
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
});
|
|
|
|
// Patchbay: PUT /api/patchbay (save current connections as a named snapshot)
|
|
m_http.Put("/api/patchbay", [this](const httplib::Request &req, httplib::Response &res) {
|
|
std::string path = dataDir() + "/patchbay.json";
|
|
writeFile(path, req.body);
|
|
res.set_content("{\"ok\":true}", "application/json");
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
});
|
|
|
|
// Patchbay: POST /api/patchbay/apply (apply saved connections to current graph)
|
|
m_http.Post("/api/patchbay/apply", [this](const httplib::Request &req, httplib::Response &res) {
|
|
// req.body is a JSON array of {output_port_id, input_port_id} pairs
|
|
// We just parse it and connect each pair
|
|
// Simple parsing: find all pairs
|
|
std::string body = req.body;
|
|
size_t pos = 0;
|
|
int connected = 0;
|
|
while (pos < body.size()) {
|
|
uint32_t out_id = 0, in_id = 0;
|
|
int chars = 0;
|
|
if (sscanf(body.c_str() + pos,
|
|
"{\"output_port_id\":%u,\"input_port_id\":%u}%n",
|
|
&out_id, &in_id, &chars) == 2 ||
|
|
sscanf(body.c_str() + pos,
|
|
"{\"output_port_id\":%u, \"input_port_id\":%u}%n",
|
|
&out_id, &in_id, &chars) == 2)
|
|
{
|
|
if (m_engine.connectPorts(out_id, in_id))
|
|
connected++;
|
|
}
|
|
pos = body.find('{', pos + 1);
|
|
if (pos == std::string::npos) break;
|
|
}
|
|
if (connected > 0) broadcastGraph();
|
|
char buf[64];
|
|
snprintf(buf, sizeof(buf), "{\"ok\":true,\"connected\":%d}", connected);
|
|
res.set_content(buf, "application/json");
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
});
|
|
|
|
// Volume: POST /api/volume {"node_id": N, "volume": 0.0-1.0}
|
|
m_http.Post("/api/volume", [this](const httplib::Request &req, httplib::Response &res) {
|
|
uint32_t node_id = 0;
|
|
float volume = 0;
|
|
if (sscanf(req.body.c_str(),
|
|
"{\"node_id\":%u,\"volume\":%f}", &node_id, &volume) == 2 ||
|
|
sscanf(req.body.c_str(),
|
|
"{\"node_id\":%u, \"volume\":%f}", &node_id, &volume) == 2)
|
|
{
|
|
if (volume < 0) volume = 0;
|
|
if (volume > 1.5) volume = 1.5;
|
|
bool ok = m_engine.setNodeVolume(node_id, volume);
|
|
if (ok) broadcastGraph();
|
|
res.set_content(ok ? "{\"ok\":true}" : "{\"ok\":false}", "application/json");
|
|
} else {
|
|
res.status = 400;
|
|
res.set_content("{\"error\":\"invalid json\"}", "application/json");
|
|
}
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
});
|
|
|
|
// Mute: POST /api/mute {"node_id": N, "mute": true/false}
|
|
m_http.Post("/api/mute", [this](const httplib::Request &req, httplib::Response &res) {
|
|
uint32_t node_id = 0;
|
|
if (sscanf(req.body.c_str(), "{\"node_id\":%u", &node_id) == 1 ||
|
|
sscanf(req.body.c_str(), "{\"node_id\": %u", &node_id) == 1)
|
|
{
|
|
// Check for "mute":true precisely — the old `find("true")` was imprecise
|
|
bool mute = req.body.find("\"mute\":true") != std::string::npos ||
|
|
req.body.find("\"mute\": true") != std::string::npos;
|
|
bool ok = m_engine.setNodeMute(node_id, mute);
|
|
if (ok) broadcastGraph();
|
|
res.set_content(ok ? "{\"ok\":true}" : "{\"ok\":false}", "application/json");
|
|
} else {
|
|
res.status = 400;
|
|
res.set_content("{\"error\":\"invalid json\"}", "application/json");
|
|
}
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
});
|
|
|
|
m_http.Options("/api/volume", cors_handler);
|
|
m_http.Options("/api/mute", cors_handler);
|
|
m_http.Options("/api/create-null-sink", cors_handler);
|
|
m_http.Options("/api/create-loopback", cors_handler);
|
|
m_http.Options("/api/unload-module", cors_handler);
|
|
m_http.Options("/api/load-module", cors_handler);
|
|
m_http.Options("/api/destroy-node", cors_handler);
|
|
|
|
// Destroy node: POST /api/destroy-node {"node_id":N}
|
|
m_http.Post("/api/destroy-node", [this](const httplib::Request &req, httplib::Response &res) {
|
|
uint32_t node_id = 0;
|
|
if (sscanf(req.body.c_str(), "{\"node_id\":%u}", &node_id) == 1) {
|
|
// Find the module that owns this node and unload it
|
|
// First try to destroy via registry (works for pw-cli created nodes)
|
|
auto snap = m_engine.snapshot();
|
|
bool found = false;
|
|
for (auto &n : snap.nodes) {
|
|
if (n.id == node_id) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (found) {
|
|
// Use pw-cli to destroy
|
|
std::string cmd = "pw-cli destroy " + std::to_string(node_id) + " 2>/dev/null";
|
|
int ret = system(cmd.c_str());
|
|
(void)ret;
|
|
sleep(1);
|
|
broadcastGraph();
|
|
}
|
|
res.set_content(found ? "{\"ok\":true}" : "{\"ok\":false,\"error\":\"node not found\"}", "application/json");
|
|
} else {
|
|
res.status = 400;
|
|
res.set_content("{\"error\":\"invalid json\"}", "application/json");
|
|
}
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
});
|
|
|
|
// Helper: extract a string value from simple JSON
|
|
auto extractStr = [](const std::string &body, const std::string &key) -> std::string {
|
|
size_t pos = body.find("\"" + key + "\"");
|
|
if (pos == std::string::npos) return "";
|
|
size_t start = body.find('"', pos + key.size() + 2);
|
|
if (start == std::string::npos) return "";
|
|
start++;
|
|
size_t end = body.find('"', start);
|
|
if (end == std::string::npos) return "";
|
|
return body.substr(start, end - start);
|
|
};
|
|
|
|
// Helper: run pactl load-module and return module ID
|
|
auto loadModuleViaPactl = [](const std::string &cmd) -> int {
|
|
FILE *fp = popen(cmd.c_str(), "r");
|
|
if (!fp) return -1;
|
|
char buf[32] = {};
|
|
fgets(buf, sizeof(buf), fp);
|
|
int status = pclose(fp);
|
|
if (status != 0) return -1;
|
|
return atoi(buf);
|
|
};
|
|
|
|
// Create null sink: POST /api/create-null-sink {"name":"My Sink"}
|
|
m_http.Post("/api/create-null-sink", [this, extractStr, loadModuleViaPactl](const httplib::Request &req, httplib::Response &res) {
|
|
std::string name = extractStr(req.body, "name");
|
|
if (name.empty()) name = "pwweb-null";
|
|
|
|
std::string cmd = "pactl load-module module-null-sink sink_name=\"" + name +
|
|
"\" sink_properties=node.name=\"" + name + "\" 2>/dev/null";
|
|
int id = loadModuleViaPactl(cmd);
|
|
|
|
if (id > 0) {
|
|
sleep(1);
|
|
broadcastGraph();
|
|
}
|
|
|
|
char buf[128];
|
|
snprintf(buf, sizeof(buf), "{\"ok\":%s,\"module_id\":%d}",
|
|
id > 0 ? "true" : "false", id);
|
|
res.set_content(buf, "application/json");
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
});
|
|
|
|
// Create loopback: POST /api/create-loopback {"name":"My Loopback"}
|
|
m_http.Post("/api/create-loopback", [this, extractStr, loadModuleViaPactl](const httplib::Request &req, httplib::Response &res) {
|
|
std::string name = extractStr(req.body, "name");
|
|
if (name.empty()) name = "pwweb-loopback";
|
|
|
|
std::string cmd = "pactl load-module module-loopback source=\"" + name +
|
|
".monitor\" sink=\"" + name + "\" 2>/dev/null";
|
|
int id = loadModuleViaPactl(cmd);
|
|
|
|
// If that fails, try without specific source/sink (generic loopback)
|
|
if (id <= 0) {
|
|
cmd = "pactl load-module module-loopback 2>/dev/null";
|
|
id = loadModuleViaPactl(cmd);
|
|
}
|
|
|
|
if (id > 0) {
|
|
sleep(1);
|
|
broadcastGraph();
|
|
}
|
|
|
|
char buf[128];
|
|
snprintf(buf, sizeof(buf), "{\"ok\":%s,\"module_id\":%d}",
|
|
id > 0 ? "true" : "false", id);
|
|
res.set_content(buf, "application/json");
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
});
|
|
|
|
// Generic module loading: POST /api/load-module {"module":"module-null-sink","args":"key=val ..."}
|
|
m_http.Post("/api/load-module", [this, extractStr, loadModuleViaPactl](const httplib::Request &req, httplib::Response &res) {
|
|
std::string module = extractStr(req.body, "module");
|
|
std::string args = extractStr(req.body, "args");
|
|
if (module.empty()) {
|
|
res.status = 400;
|
|
res.set_content("{\"error\":\"module name required\"}", "application/json");
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
return;
|
|
}
|
|
|
|
std::string cmd = "pactl load-module " + module;
|
|
if (!args.empty()) cmd += " " + args;
|
|
cmd += " 2>/dev/null";
|
|
|
|
int id = loadModuleViaPactl(cmd);
|
|
if (id > 0) {
|
|
sleep(1);
|
|
broadcastGraph();
|
|
}
|
|
|
|
char buf[128];
|
|
snprintf(buf, sizeof(buf), "{\"ok\":%s,\"module_id\":%d}",
|
|
id > 0 ? "true" : "false", id);
|
|
res.set_content(buf, "application/json");
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
});
|
|
|
|
// Unload module: POST /api/unload-module {"module_id":N}
|
|
m_http.Post("/api/unload-module", [this](const httplib::Request &req, httplib::Response &res) {
|
|
uint32_t module_id = 0;
|
|
if (sscanf(req.body.c_str(), "{\"module_id\":%u}", &module_id) == 1) {
|
|
bool ok = m_engine.unloadModule(module_id);
|
|
if (ok) broadcastGraph();
|
|
res.set_content(ok ? "{\"ok\":true}" : "{\"ok\":false}", "application/json");
|
|
} else {
|
|
res.status = 400;
|
|
res.set_content("{\"error\":\"invalid json\"}", "application/json");
|
|
}
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
});
|
|
|
|
m_http.Options("/api/quantum", cors_handler);
|
|
|
|
// Get current quantum: GET /api/quantum
|
|
m_http.Get("/api/quantum", [](const httplib::Request &, httplib::Response &res) {
|
|
int quantum = 0;
|
|
FILE *fp = popen("pw-metadata 0 default.clock.quantum 2>/dev/null | grep -oP \"value:'\\K[0-9]+\"", "r");
|
|
if (fp) {
|
|
char buf[32] = {};
|
|
if (fgets(buf, sizeof(buf), fp))
|
|
quantum = atoi(buf);
|
|
pclose(fp);
|
|
}
|
|
char out[64];
|
|
snprintf(out, sizeof(out), "{\"quantum\":%d}", quantum);
|
|
res.set_content(out, "application/json");
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
});
|
|
|
|
// Set quantum: POST /api/quantum {"quantum":256}
|
|
m_http.Post("/api/quantum", [](const httplib::Request &req, httplib::Response &res) {
|
|
int quantum = 0;
|
|
if (sscanf(req.body.c_str(), "{\"quantum\":%d}", &quantum) == 1 && quantum > 0) {
|
|
char cmd[128];
|
|
snprintf(cmd, sizeof(cmd), "pw-metadata 0 default.clock.quantum %d 2>/dev/null", quantum);
|
|
int ret = system(cmd);
|
|
(void)ret;
|
|
char out[64];
|
|
snprintf(out, sizeof(out), "{\"ok\":true,\"quantum\":%d}", quantum);
|
|
res.set_content(out, "application/json");
|
|
} else {
|
|
res.status = 400;
|
|
res.set_content("{\"error\":\"invalid json\"}", "application/json");
|
|
}
|
|
res.set_header("Access-Control-Allow-Origin", "*");
|
|
});
|
|
}
|
|
|
|
// end of web_server.cpp
|