#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; struct lws_context* context = nullptr; }; struct ApiServer::Impl { std::unique_ptr data; }; struct HttpRequest { std::string method; std::string path; std::string body; bool body_done = false; }; static ApiServerImpl* g_impl = nullptr; 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(); if (lws_hdr_total_length(wsi, WSI_TOKEN_POST_URI) > 0) { req->method = "POST"; char buf[256] = {}; lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_POST_URI); req->path = buf; } else { req->method = "GET"; char buf[256] = {}; lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_GET_URI); req->path = buf; } break; } case LWS_CALLBACK_HTTP_BODY: { req->body.append(static_cast(in), len); break; } case LWS_CALLBACK_HTTP_BODY_COMPLETION: { if (!g_impl || !g_impl->graph) { lws_return_http_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, "Server error"); return -1; } std::string status_str, content_type, response_body; 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(); }; content_type = "application/json"; try { nlohmann::json req_body = req->body.empty() ? nlohmann::json::object() : nlohmann::json::parse(req->body); auto& graph = *g_impl->graph; auto& fm = *g_impl->flow_manager; auto& pm = *g_impl->process_manager; if (req->path == "/api/graph" && req->method == "GET") { ok(graph.serialize()); } else if (req->path == "/api/graph/nodes" && req->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()); auto id = graph.add_node(type, config); created({{"id", id}}); } } else if (req->path.find("/api/graph/nodes/") == 0 && req->method == "DELETE") { auto node_id = req->path.substr(std::string("/api/graph/nodes/").length()); if (graph.remove_node(node_id)) { ok({{"deleted", node_id}}); } else { error_resp(404, "Node not found: " + node_id); } } else if (req->path == "/api/graph/edges" && req->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); created({{"id", edge_id}, {"flow_id", flow_id}}); } } else if (req->path.find("/api/graph/edges/") == 0 && req->method == "DELETE") { auto edge_id = req->path.substr(std::string("/api/graph/edges/").length()); if (graph.remove_edge(edge_id)) { ok({{"deleted", edge_id}}); } else { error_resp(404, "Edge not found: " + edge_id); } } else if (req->path == "/api/graph/start" && req->method == "POST") { auto nodes = graph.get_nodes(); uint16_t port = 9100; for (auto& node : nodes) { pm.start_node(const_cast(node), "/dev/shm/mxl", port++); } ok({{"status", "started"}}); } else if (req->path == "/api/graph/stop" && req->method == "POST") { auto nodes = graph.get_nodes(); for (auto& node : nodes) { pm.stop_node(const_cast(node)); } ok({{"status", "stopped"}}); } else { error_resp(404, "Not found: " + req->method + " " + req->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()); } auto headers = "HTTP/1.1 " + status_str + "\r\n" "Content-Type: " + content_type + "\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(response_body.size()) + "\r\n" "\r\n"; std::vector buf(LWS_PRE + headers.size() + response_body.size()); std::memcpy(buf.data() + LWS_PRE, headers.data(), headers.size()); std::memcpy(buf.data() + LWS_PRE + headers.size(), response_body.data(), response_body.size()); lws_write(wsi, buf.data() + LWS_PRE, headers.size() + response_body.size(), LWS_WRITE_HTTP); if (lws_http_transaction_completed(wsi)) { return -1; } return 0; } default: break; } return 0; } static const struct lws_protocols protocols[] = { {"http-api", callback_http, sizeof(HttpRequest), 0}, {nullptr, nullptr, 0, 0}, }; ApiServer::ApiServer(uint16_t port, Graph& graph, FlowManager& flow_manager, ProcessManager& process_manager) : 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; 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