#include #include #include #include #include #include #include #include #include #include #include namespace dmf_engine { struct ApiServerImpl { uint16_t port = 0; Graph* graph = nullptr; FlowManager* flow_manager = nullptr; ProcessManager* process_manager = nullptr; NodeControlClient* control_client = nullptr; std::string mxl_domain = "/dev/shm/mxl"; struct lws_context* context = nullptr; }; struct ApiServer::Impl { std::unique_ptr data; }; struct HttpRequest { std::string method; std::string path; std::string body; }; static ApiServerImpl* g_impl = nullptr; static int send_json_response(struct lws* wsi, const std::string& status_str, const std::string& json_body) { auto headers = "HTTP/1.1 " + status_str + "\r\n" "Content-Type: application/json\r\n" "Access-Control-Allow-Origin: *\r\n" "Access-Control-Allow-Methods: GET, POST, DELETE, PUT, OPTIONS\r\n" "Access-Control-Allow-Headers: Content-Type\r\n" "Content-Length: " + std::to_string(json_body.size()) + "\r\n" "\r\n"; std::vector buf(LWS_PRE + headers.size() + json_body.size()); std::memcpy(buf.data() + LWS_PRE, headers.data(), headers.size()); std::memcpy(buf.data() + LWS_PRE + headers.size(), json_body.data(), json_body.size()); lws_write(wsi, buf.data() + LWS_PRE, headers.size() + json_body.size(), LWS_WRITE_HTTP); if (lws_http_transaction_completed(wsi)) { return -1; } return 0; } static void handle_request(const std::string& method, const std::string& path, const std::string& body, std::string& status_str, std::string& response_body) { if (!g_impl || !g_impl->graph) { status_str = "500 Internal Server Error"; response_body = nlohmann::json({{"error", "Server not initialized"}}).dump(); return; } auto ok = [&](const nlohmann::json& j) { status_str = "200 OK"; response_body = j.dump(); }; auto created = [&](const nlohmann::json& j) { status_str = "201 Created"; response_body = j.dump(); }; auto error_resp = [&](int code, const std::string& msg) { status_str = std::to_string(code) + " Error"; response_body = nlohmann::json({{"error", msg}}).dump(); }; try { nlohmann::json req_body = body.empty() ? nlohmann::json::object() : nlohmann::json::parse(body); auto& graph = *g_impl->graph; auto& fm = *g_impl->flow_manager; auto& pm = *g_impl->process_manager; auto& cc = *g_impl->control_client; if (path == "/api/graph" && method == "GET") { ok(graph.serialize()); } else if (path == "/api/graph/nodes" && method == "POST") { if (!req_body.contains("type")) { error_resp(400, "Missing 'type' field"); } else { auto type = req_body["type"].get(); auto config = req_body.value("config", nlohmann::json::object()); if (req_body.contains("id") && req_body["id"].is_string()) { config["id"] = req_body["id"]; } auto id = graph.add_node(type, config); created({{"id", id}}); } } else if (path.find("/api/graph/nodes/") == 0 && method == "DELETE") { auto node_id = path.substr(std::string("/api/graph/nodes/").length()); cc.unregister_node(node_id); if (graph.remove_node(node_id)) { ok({{"deleted", node_id}}); } else { error_resp(404, "Node not found: " + node_id); } } else if (path == "/api/graph/edges" && method == "POST") { if (!req_body.contains("from_node") || !req_body.contains("to_node")) { error_resp(400, "Missing from_node/to_node"); } else { auto from_node = req_body["from_node"].get(); auto from_port = req_body.value("from_port", "video_out"); auto to_node = req_body["to_node"].get(); auto to_port = req_body.value("to_port", "video_in"); auto flow_id = fm.create_flow_id(); auto flow_def = fm.create_v210_flow_def(flow_id, 1920, 1080, 50, 1); auto edge_id = graph.add_edge(from_node, from_port, to_node, to_port, flow_id, flow_def); auto from_port_num = cc.get_port(from_node); auto to_port_num = cc.get_port(to_node); if (from_port_num > 0) { nlohmann::json cmd; cmd["cmd"] = "add_writer"; cmd["port_id"] = from_port; cmd["flow_id"] = flow_id; cmd["flow_def"] = flow_def; cc.send_command(from_port_num, cmd.dump()); } if (to_port_num > 0) { nlohmann::json cmd; cmd["cmd"] = "add_reader"; cmd["port_id"] = to_port; cmd["flow_id"] = flow_id; cc.send_command(to_port_num, cmd.dump()); } created({{"id", edge_id}, {"flow_id", flow_id}}); } } else if (path.find("/api/graph/edges/") == 0 && method == "DELETE") { auto edge_id = path.substr(std::string("/api/graph/edges/").length()); auto edges = graph.get_edges(); const GraphEdge* edge = nullptr; for (auto& e : edges) { if (e.id == edge_id) { edge = &e; break; } } if (graph.remove_edge(edge_id)) { if (edge) { auto from_port_num = cc.get_port(edge->from_node); auto to_port_num = cc.get_port(edge->to_node); if (from_port_num > 0) { nlohmann::json cmd; cmd["cmd"] = "remove_writer"; cmd["port_id"] = edge->from_port; cc.send_command(from_port_num, cmd.dump()); } if (to_port_num > 0) { nlohmann::json cmd; cmd["cmd"] = "remove_reader"; cmd["port_id"] = edge->to_port; cc.send_command(to_port_num, cmd.dump()); } } ok({{"deleted", edge_id}}); } else { error_resp(404, "Edge not found: " + edge_id); } } else if (path == "/api/graph/start" && method == "POST") { auto nodes = graph.get_nodes(); uint16_t port = g_impl->port + 100; for (auto& node : nodes) { pm.start_node(const_cast(node), g_impl->mxl_domain, port); cc.register_node(node.id, port); port++; } for (auto& edge : graph.get_edges()) { auto from_port_num = cc.get_port(edge.from_node); auto to_port_num = cc.get_port(edge.to_node); if (from_port_num > 0) { nlohmann::json cmd; cmd["cmd"] = "add_writer"; cmd["port_id"] = edge.from_port; cmd["flow_id"] = edge.flow_id; cmd["flow_def"] = edge.flow_def; cc.send_command(from_port_num, cmd.dump()); } if (to_port_num > 0) { nlohmann::json cmd; cmd["cmd"] = "add_reader"; cmd["port_id"] = edge.to_port; cmd["flow_id"] = edge.flow_id; cc.send_command(to_port_num, cmd.dump()); } } ok({{"status", "started"}}); } else if (path == "/api/graph/stop" && method == "POST") { auto nodes = graph.get_nodes(); for (auto& node : nodes) { cc.unregister_node(node.id); pm.stop_node(const_cast(node)); } ok({{"status", "stopped"}}); } else if (path.find("/api/graph/nodes/") == 0 && path.find("/connect-input") != std::string::npos && method == "POST") { auto prefix = std::string("/api/graph/nodes/"); auto suffix_start = path.find("/connect-input"); auto node_id = path.substr(prefix.length(), suffix_start - prefix.length()); if (!req_body.contains("flow_id") || !req_body.contains("port_id")) { error_resp(400, "Missing flow_id/port_id"); return; } auto port_num = cc.get_port(node_id); if (port_num == 0) { error_resp(404, "Node not running or not found: " + node_id); return; } nlohmann::json cmd; cmd["cmd"] = "add_reader"; cmd["port_id"] = req_body["port_id"].get(); cmd["flow_id"] = req_body["flow_id"].get(); if (cc.send_command(port_num, cmd.dump())) { ok({{"node_id", node_id}, {"connected_input", req_body["port_id"]}, {"flow_id", req_body["flow_id"]}}); } else { error_resp(500, "Failed to send command to node"); } } else if (path.find("/api/graph/nodes/") == 0 && path.find("/connect-output") != std::string::npos && method == "POST") { auto prefix = std::string("/api/graph/nodes/"); auto suffix_start = path.find("/connect-output"); auto node_id = path.substr(prefix.length(), suffix_start - prefix.length()); auto port_id = req_body.value("port_id", "video_out"); auto port_num = cc.get_port(node_id); if (port_num == 0) { error_resp(404, "Node not running or not found: " + node_id); return; } int fps_num = 50, fps_den = 1; int width = 1920, height = 1080; nlohmann::json status_cmd; status_cmd["cmd"] = "status"; auto status_resp = cc.send_command_with_response(port_num, status_cmd.dump()); if (!status_resp.empty()) { try { auto sr = nlohmann::json::parse(status_resp); if (sr.contains("data")) { auto& d = sr["data"]; if (d.contains("grain_rate")) { fps_num = d["grain_rate"].value("numerator", fps_num); fps_den = d["grain_rate"].value("denominator", fps_den); } width = d.value("width", width); height = d.value("height", height); } } catch (...) {} } auto flow_id = fm.create_flow_id(); auto flow_def = fm.create_v210_flow_def(flow_id, width, height, fps_num, fps_den); nlohmann::json cmd; cmd["cmd"] = "add_writer"; cmd["port_id"] = port_id; cmd["flow_id"] = flow_id; cmd["flow_def"] = flow_def; if (cc.send_command(port_num, cmd.dump())) { ok({{"node_id", node_id}, {"connected_output", port_id}, {"flow_id", flow_id}}); } else { error_resp(500, "Failed to send command to node"); } } else if (path.find("/api/graph/nodes/") == 0 && path.find("/disconnect-port") != std::string::npos && method == "POST") { auto prefix = std::string("/api/graph/nodes/"); auto suffix_start = path.find("/disconnect-port"); auto node_id = path.substr(prefix.length(), suffix_start - prefix.length()); if (!req_body.contains("port_id")) { error_resp(400, "Missing port_id"); return; } auto port_id = req_body["port_id"].get(); auto port_num = cc.get_port(node_id); if (port_num == 0) { error_resp(404, "Node not running or not found: " + node_id); return; } auto direction = req_body.value("direction", "input"); nlohmann::json cmd; if (direction == "output") { cmd["cmd"] = "remove_writer"; } else { cmd["cmd"] = "remove_reader"; } cmd["port_id"] = port_id; if (cc.send_command(port_num, cmd.dump())) { ok({{"node_id", node_id}, {"disconnected", port_id}}); } else { error_resp(500, "Failed to send command to node"); } } else if (path.find("/api/graph/nodes/") == 0 && path.find("/register") != std::string::npos && method == "POST") { auto prefix = std::string("/api/graph/nodes/"); auto suffix_start = path.find("/register"); auto node_id = path.substr(prefix.length(), suffix_start - prefix.length()); if (!req_body.contains("port")) { error_resp(400, "Missing port"); return; } auto control_port = static_cast(req_body["port"].get()); cc.register_node(node_id, control_port); auto* node = graph.get_node_mut(node_id); if (node) { node->control_port = control_port; node->state = NodeState::Running; } ok({{"node_id", node_id}, {"control_port", control_port}}); } else if (path == "/api/fabric/create-target" && method == "POST") { if (!req_body.contains("node_id")) { error_resp(400, "Missing node_id"); return; } auto node_id = req_body["node_id"].get(); auto port_id = req_body.value("port_id", "video_out"); auto flow_id = req_body.value("flow_id", fm.create_flow_id()); auto port_num = cc.get_port(node_id); if (port_num == 0) { error_resp(404, "Node not running: " + node_id); return; } int fps_num = 25, fps_den = 1; int width = 1920, height = 1080; nlohmann::json status_cmd; status_cmd["cmd"] = "status"; auto status_resp = cc.send_command_with_response(port_num, status_cmd.dump()); if (!status_resp.empty()) { try { auto sr = nlohmann::json::parse(status_resp); if (sr.contains("data")) { auto& d = sr["data"]; if (d.contains("grain_rate")) { fps_num = d["grain_rate"].value("numerator", fps_num); fps_den = d["grain_rate"].value("denominator", fps_den); } width = d.value("width", width); height = d.value("height", height); } } catch (...) {} } auto flow_def = fm.create_v210_flow_def(flow_id, width, height, fps_num, fps_den); nlohmann::json cmd; cmd["cmd"] = "add_writer"; cmd["port_id"] = port_id; cmd["flow_id"] = flow_id; cmd["flow_def"] = flow_def; if (!cc.send_command(port_num, cmd.dump())) { error_resp(500, "Failed to add writer to target node"); return; } std::string target_info; constexpr int max_poll_attempts = 30; for (int i = 0; i < max_poll_attempts; ++i) { auto resp = cc.send_command_with_response(port_num, status_cmd.dump()); if (!resp.empty()) { try { auto sr = nlohmann::json::parse(resp); if (sr.contains("data") && sr["data"].contains("target_info") && !sr["data"]["target_info"].get().empty()) { target_info = sr["data"]["target_info"].get(); break; } } catch (...) {} } std::this_thread::sleep_for(std::chrono::milliseconds(500)); } if (target_info.empty()) { error_resp(504, "Target node did not produce target_info in time"); return; } ok({ {"node_id", node_id}, {"flow_id", flow_id}, {"target_info", target_info} }); } else if (path == "/api/fabric/connect" && method == "POST") { if (!req_body.contains("target_node_id") || !req_body.contains("initiator_node_id")) { error_resp(400, "Missing target_node_id/initiator_node_id"); return; } auto target_node_id = req_body["target_node_id"].get(); auto initiator_node_id = req_body["initiator_node_id"].get(); auto target_port = cc.get_port(target_node_id); auto initiator_port = cc.get_port(initiator_node_id); if (target_port == 0) { error_resp(404, "Target node not running: " + target_node_id); return; } if (initiator_port == 0) { error_resp(404, "Initiator node not running: " + initiator_node_id); return; } nlohmann::json status_cmd; status_cmd["cmd"] = "status"; std::string target_info; constexpr int max_poll_attempts = 30; for (int i = 0; i < max_poll_attempts; ++i) { auto resp = cc.send_command_with_response(target_port, status_cmd.dump()); if (!resp.empty()) { try { auto sr = nlohmann::json::parse(resp); if (sr.contains("data") && sr["data"].contains("target_info") && !sr["data"]["target_info"].get().empty()) { target_info = sr["data"]["target_info"].get(); break; } } catch (...) {} } std::this_thread::sleep_for(std::chrono::milliseconds(500)); } if (target_info.empty()) { error_resp(504, "Target node did not produce target_info in time"); return; } nlohmann::json configure_cmd; configure_cmd["cmd"] = "configure"; configure_cmd["params"]["target_info"] = target_info; if (!cc.send_command(initiator_port, configure_cmd.dump())) { error_resp(500, "Failed to send target_info to initiator node"); return; } std::this_thread::sleep_for(std::chrono::milliseconds(200)); bool initiator_running = false; auto init_resp = cc.send_command_with_response(initiator_port, status_cmd.dump()); if (!init_resp.empty()) { try { auto ir = nlohmann::json::parse(init_resp); if (ir.contains("data") && ir["data"].contains("running")) { initiator_running = ir["data"]["running"].get(); } } catch (...) {} } ok({ {"target_node_id", target_node_id}, {"initiator_node_id", initiator_node_id}, {"target_info_length", target_info.size()}, {"initiator_running", initiator_running} }); } else if (path.find("/api/graph/nodes/") == 0 && path.find("/command") != std::string::npos && method == "POST") { auto prefix = std::string("/api/graph/nodes/"); auto suffix_start = path.find("/command"); auto node_id = path.substr(prefix.length(), suffix_start - prefix.length()); auto port_num = cc.get_port(node_id); if (port_num == 0) { error_resp(404, "Node not running or not found: " + node_id); return; } if (cc.send_command(port_num, req_body.dump())) { ok({{"node_id", node_id}, {"sent", true}}); } else { error_resp(500, "Failed to send command to node"); } } else { error_resp(404, "Not found: " + method + " " + path); } } catch (const nlohmann::json::exception& e) { error_resp(400, std::string("JSON error: ") + e.what()); } catch (const std::exception& e) { error_resp(500, e.what()); } } static int callback_http(struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t len) { auto* req = static_cast(user); switch (reason) { case LWS_CALLBACK_HTTP: { new (req) HttpRequest(); char* uri_ptr = nullptr; int uri_len = 0; int method = lws_http_get_uri_and_method(wsi, &uri_ptr, &uri_len); switch (method) { case LWSHUMETH_GET: req->method = "GET"; break; case LWSHUMETH_POST: req->method = "POST"; break; case LWSHUMETH_PUT: req->method = "PUT"; break; case LWSHUMETH_DELETE: req->method = "DELETE"; break; default: req->method = "GET"; break; } if (uri_ptr && uri_len > 0) { req->path.assign(uri_ptr, uri_len); } if (req->method == "GET" || req->method == "DELETE") { std::string status_str, response_body; handle_request(req->method, req->path, "", status_str, response_body); return send_json_response(wsi, status_str, response_body); } int body_len = lws_hdr_total_length(wsi, WSI_TOKEN_HTTP_CONTENT_LENGTH); if (body_len == 0) { std::string status_str, response_body; handle_request(req->method, req->path, "", status_str, response_body); return send_json_response(wsi, status_str, response_body); } req->body.reserve(body_len); break; } case LWS_CALLBACK_HTTP_BODY: { req->body.append(static_cast(in), len); break; } case LWS_CALLBACK_HTTP_BODY_COMPLETION: { std::string status_str, response_body; handle_request(req->method, req->path, req->body, status_str, response_body); return send_json_response(wsi, status_str, response_body); } default: break; } return 0; } static const struct lws_protocols protocols[] = { {"http-api", callback_http, sizeof(HttpRequest), 4096}, {nullptr, nullptr, 0, 0}, }; ApiServer::ApiServer(uint16_t port, Graph& graph, FlowManager& flow_manager, ProcessManager& process_manager, NodeControlClient& control_client, const std::string& mxl_domain) : impl_(std::make_unique()) { impl_->data = std::make_unique(); impl_->data->port = port; impl_->data->graph = &graph; impl_->data->flow_manager = &flow_manager; impl_->data->process_manager = &process_manager; impl_->data->control_client = &control_client; impl_->data->mxl_domain = mxl_domain; g_impl = impl_->data.get(); struct lws_context_creation_info info; std::memset(&info, 0, sizeof(info)); info.port = port; info.protocols = protocols; info.gid = -1; info.uid = -1; impl_->data->context = lws_create_context(&info); if (!impl_->data->context) { spdlog::error("Failed to create HTTP context on port {}", port); throw std::runtime_error("Failed to create HTTP context"); } spdlog::info("API server: listening on port {}", port); } ApiServer::~ApiServer() { if (impl_->data && impl_->data->context) { lws_context_destroy(impl_->data->context); } if (g_impl == impl_->data.get()) { g_impl = nullptr; } } void ApiServer::poll(int timeout_ms) { lws_service(impl_->data->context, timeout_ms); } } // namespace dmf_engine