HTTP boundry
This commit is contained in:
@@ -34,7 +34,8 @@ Primary source areas:
|
||||
- `src/runtime/shader`: background Slang build bridge and prepared shader artifact types
|
||||
- `src/runtime/state`: runtime JSON helpers, parameter normalization, and debounced runtime-state persistence
|
||||
- `src/runtime/text`: MSDF/MTSDF font atlas build and CPU-side prepared text texture composition
|
||||
- `src/control`: HTTP routing, command parsing, OpenAPI state JSON
|
||||
- `src/control`: command parsing, HTTP/WebSocket transport helpers, OpenAPI state JSON
|
||||
- `src/app/RenderCadenceHttpRoutes.*`: this app's current HTTP endpoint map
|
||||
- `src/preview`: optional non-consuming preview window
|
||||
- `src/telemetry` and `src/logging`: runtime observation and logging
|
||||
|
||||
@@ -149,7 +150,7 @@ Screenshot routes are present in the UI/OpenAPI surface but are not implemented
|
||||
|
||||
## Control Surface
|
||||
|
||||
The HTTP server runs on its own thread. It serves:
|
||||
The HTTP server runs on its own thread. `HttpControlServer` owns socket lifetime, HTTP parsing, static asset helpers, OpenAPI/Swagger helper serving, and WebSocket state transport. `RenderCadenceHttpRoutes` owns this app's current endpoint map:
|
||||
|
||||
- UI assets
|
||||
- OpenAPI/Swagger docs
|
||||
@@ -167,6 +168,8 @@ Known but not implemented in the current native command path:
|
||||
|
||||
Unsupported routes return an action response with `ok: false`.
|
||||
|
||||
Forks can reuse the HTTP/WebSocket shell without keeping these endpoints by installing a different route callback.
|
||||
|
||||
## Tests
|
||||
|
||||
Native tests cover the main non-GL contracts:
|
||||
@@ -177,7 +180,7 @@ Native tests cover the main non-GL contracts:
|
||||
- supported shader catalog
|
||||
- runtime layer restore/reload behavior
|
||||
- runtime-state persistence writer
|
||||
- HTTP command parsing
|
||||
- HTTP transport and app-route dispatch
|
||||
- frame exchange and input mailbox behavior
|
||||
- video format and scheduling helpers
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ These parts are the useful base for the fork:
|
||||
- `src/render/readback`: BGRA8/UYVY8 PBO readback and completed-frame publication.
|
||||
- `src/platform`: hidden GL window/context support.
|
||||
- `src/app`: startup, config, video backend factory, runtime layer orchestration, preview, telemetry, and HTTP server hookup.
|
||||
- `src/control`, `src/telemetry`, `src/logging`, and `ui`: useful if the new repo still wants a local control surface.
|
||||
- `src/control/http`, `src/telemetry`, `src/logging`, and `ui`: useful if the new repo still wants a local control surface.
|
||||
- `src/app/RenderCadenceHttpRoutes.*`: useful only if the new repo keeps this app's current `/api/...` control surface.
|
||||
|
||||
## Replace Or Rework
|
||||
|
||||
@@ -32,10 +33,13 @@ These are most likely to change when the fork renders something other than shade
|
||||
- `src/shader`: shader package manifest parsing and Slang wrapper generation, unless the new renderer keeps the same shader package contract.
|
||||
- `shaders/`: bundled shader package library.
|
||||
- `runtime/templates/shader_wrapper.slang.in`: only needed for the current Slang package pipeline.
|
||||
- `src/app/RenderCadenceHttpRoutes.*`: replace this with a fork-owned route module if the new renderer has different controls, while keeping `src/control/http/HttpControlServer.*` as the socket/static/WebSocket shell.
|
||||
- Shader-specific UI affordances in `ui`, if the new renderer has a different control model.
|
||||
|
||||
The first fork step is now in place: `RenderThread` preserves the cadence/readback shell and calls a narrow render-content interface behind the draw call. A new repo can swap that implementation without touching video I/O scheduling.
|
||||
|
||||
The HTTP control boundary is also split now. `HttpControlServer` owns transport, static-file helpers, OpenAPI helper serving, and WebSocket state transport; `RenderCadenceHttpRoutes` owns this app's REST endpoint map. A fork that wants the same browser/server plumbing can provide its own route callback and leave the Render Cadence-specific endpoints behind.
|
||||
|
||||
## Current Swap Point
|
||||
|
||||
The render cadence loop now calls `IRenderContent` inside the readback queue call in `src/render/thread/RenderThread.cpp`:
|
||||
|
||||
@@ -261,7 +261,7 @@ Current endpoints:
|
||||
- `POST /api/reload`: rescans the shader library, refreshes manifests, and queues recompilation for every catalog-valid layer in the active stack
|
||||
- other OpenAPI POST routes are present but return `{ "ok": false, "error": "Endpoint is not implemented in RenderCadenceCompositor yet." }`
|
||||
|
||||
The HTTP server runs on its own thread. It serves static UI/docs files, samples/copies telemetry through callbacks, and translates POST bodies into runtime control commands. Command execution is app-owned, so future OSC ingress can create the same commands without depending on HTTP route code. Control commands may update the display layer model, request debounced runtime-state persistence, start background shader builds, or publish an already-built render-layer snapshot, but they do not call render work or DeckLink scheduling directly.
|
||||
The HTTP server runs on its own thread. `control/http/HttpControlServer` owns socket lifetime, HTTP parsing, static UI/docs helpers, and WebSocket transport. The Render Cadence endpoint map lives in `app/RenderCadenceHttpRoutes`, which samples/copies telemetry through callbacks and translates POST bodies into runtime control commands. A fork can keep the HTTP/WebSocket shell and install a different route callback without inheriting this app's `/api/...` surface. Command execution is app-owned, so future OSC ingress can create the same commands without depending on HTTP route code. Control commands may update the display layer model, request debounced runtime-state persistence, start background shader builds, or publish an already-built render-layer snapshot, but they do not call render work or DeckLink scheduling directly.
|
||||
|
||||
## Optional DeckLink Output
|
||||
|
||||
@@ -463,7 +463,8 @@ This app keeps the same core behavior but splits it into modules that can grow:
|
||||
- `runtime/state/`: runtime JSON helpers, parameter normalization, and debounced runtime-state persistence
|
||||
- `runtime/text/`: font atlas build and prepared text texture composition
|
||||
- `control/`: control action results and runtime-state JSON presentation
|
||||
- `control/http/`: local HTTP API, static UI serving, OpenAPI serving, and WebSocket updates
|
||||
- `control/http/`: local HTTP transport, static UI/OpenAPI serving helpers, and WebSocket updates
|
||||
- `app/RenderCadenceHttpRoutes`: this app's `/api/...` endpoint map behind the reusable HTTP server route callback
|
||||
- `json/`: compact JSON serialization helpers
|
||||
- `video/`: DeckLink output wrapper and scheduling thread
|
||||
- `telemetry/`: cadence telemetry
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "AppConfigJson.h"
|
||||
#include "AppConfigProvider.h"
|
||||
#include "AppRestart.h"
|
||||
#include "RenderCadenceHttpRoutes.h"
|
||||
#include "RuntimeLayerController.h"
|
||||
#include "../logging/Logger.h"
|
||||
#include "../control/RuntimeStateJson.h"
|
||||
@@ -246,23 +247,23 @@ private:
|
||||
|
||||
void StartHttpServer()
|
||||
{
|
||||
HttpControlServerCallbacks callbacks;
|
||||
callbacks.getStateJson = [this]() {
|
||||
RenderCadenceHttpRouteCallbacks routeCallbacks;
|
||||
routeCallbacks.getStateJson = [this]() {
|
||||
return BuildStateJson();
|
||||
};
|
||||
callbacks.getConfigJson = [this]() {
|
||||
routeCallbacks.getConfigJson = [this]() {
|
||||
return BuildConfigJson();
|
||||
};
|
||||
callbacks.getNdiSourcesJson = [this]() {
|
||||
routeCallbacks.getNdiSourcesJson = [this]() {
|
||||
return BuildNdiSourcesJson();
|
||||
};
|
||||
callbacks.addLayer = [this](const std::string& body) {
|
||||
routeCallbacks.addLayer = [this](const std::string& body) {
|
||||
return mRuntimeLayers.HandleAddLayer(body);
|
||||
};
|
||||
callbacks.removeLayer = [this](const std::string& body) {
|
||||
routeCallbacks.removeLayer = [this](const std::string& body) {
|
||||
return mRuntimeLayers.HandleRemoveLayer(body);
|
||||
};
|
||||
callbacks.executePost = [this](const std::string& path, const std::string& body) {
|
||||
routeCallbacks.executePost = [this](const std::string& path, const std::string& body) {
|
||||
if (path == "/api/config/save")
|
||||
return HandleConfigSave(body);
|
||||
if (path == "/api/app/restart")
|
||||
@@ -275,6 +276,12 @@ private:
|
||||
return mRuntimeLayers.HandleControlCommand(command);
|
||||
};
|
||||
|
||||
HttpControlServerCallbacks callbacks;
|
||||
callbacks.getWebSocketStateJson = routeCallbacks.getStateJson;
|
||||
callbacks.routeRequest = [this, routeCallbacks](const HttpRequest& request) {
|
||||
return RouteRenderCadenceHttpRequest(request, mHttpServer, routeCallbacks);
|
||||
};
|
||||
|
||||
std::string error;
|
||||
if (!mHttpServer.Start(
|
||||
FindRepoPath("ui/dist"),
|
||||
|
||||
120
src/app/RenderCadenceHttpRoutes.cpp
Normal file
120
src/app/RenderCadenceHttpRoutes.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "RenderCadenceHttpRoutes.h"
|
||||
|
||||
#include "../json/JsonWriter.h"
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace
|
||||
{
|
||||
bool IsKnownPostEndpoint(const std::string& path)
|
||||
{
|
||||
return path == "/api/layers/add"
|
||||
|| path == "/api/layers/remove"
|
||||
|| path == "/api/layers/move"
|
||||
|| path == "/api/layers/reorder"
|
||||
|| path == "/api/layers/set-bypass"
|
||||
|| path == "/api/layers/set-shader"
|
||||
|| path == "/api/layers/update-parameter"
|
||||
|| path == "/api/layers/reset-parameters"
|
||||
|| path == "/api/stack-presets/save"
|
||||
|| path == "/api/stack-presets/load"
|
||||
|| path == "/api/config/save"
|
||||
|| path == "/api/app/restart"
|
||||
|| path == "/api/reload"
|
||||
|| path == "/api/screenshot";
|
||||
}
|
||||
|
||||
std::string ActionResponse(bool ok, const std::string& error = std::string())
|
||||
{
|
||||
JsonWriter writer;
|
||||
writer.BeginObject();
|
||||
writer.KeyBool("ok", ok);
|
||||
if (!error.empty())
|
||||
writer.KeyString("error", error);
|
||||
writer.EndObject();
|
||||
return writer.StringValue();
|
||||
}
|
||||
|
||||
HttpResponse ServeRenderCadenceGet(
|
||||
const HttpRequest& request,
|
||||
const HttpControlServer& server,
|
||||
const RenderCadenceHttpRouteCallbacks& callbacks)
|
||||
{
|
||||
if (request.path == "/api/state")
|
||||
return HttpControlServer::JsonResponse("200 OK", callbacks.getStateJson ? callbacks.getStateJson() : "{}");
|
||||
if (request.path == "/api/config")
|
||||
return HttpControlServer::JsonResponse("200 OK", callbacks.getConfigJson ? callbacks.getConfigJson() : "{}");
|
||||
if (request.path == "/api/ndi/sources")
|
||||
return HttpControlServer::JsonResponse(
|
||||
"200 OK",
|
||||
callbacks.getNdiSourcesJson
|
||||
? callbacks.getNdiSourcesJson()
|
||||
: "{\"ok\":false,\"sources\":[],\"error\":\"NDI source discovery is not available.\"}");
|
||||
if (request.path == "/openapi.yaml" || request.path == "/docs/openapi.yaml")
|
||||
return server.ServeOpenApiSpec();
|
||||
if (request.path == "/docs" || request.path == "/docs/")
|
||||
return server.ServeSwaggerDocs();
|
||||
if (request.path == "/" || request.path == "/index.html")
|
||||
return server.ServeUiAsset("index.html");
|
||||
if (request.path.rfind("/assets/", 0) == 0)
|
||||
return server.ServeUiAsset(request.path.substr(1));
|
||||
if (request.path.size() > 1)
|
||||
{
|
||||
const HttpResponse asset = server.ServeUiAsset(request.path.substr(1));
|
||||
if (asset.status != "404 Not Found")
|
||||
return asset;
|
||||
}
|
||||
return server.ServeUiAsset("index.html");
|
||||
}
|
||||
|
||||
HttpResponse ServeRenderCadencePost(
|
||||
const HttpRequest& request,
|
||||
const RenderCadenceHttpRouteCallbacks& callbacks)
|
||||
{
|
||||
if (!IsKnownPostEndpoint(request.path))
|
||||
return HttpControlServer::TextResponse("404 Not Found", "Not Found");
|
||||
|
||||
if (callbacks.executePost)
|
||||
{
|
||||
const ControlActionResult result = callbacks.executePost(request.path, request.body);
|
||||
return HttpControlServer::JsonResponse(
|
||||
result.ok ? "200 OK" : "400 Bad Request",
|
||||
ActionResponse(result.ok, result.error));
|
||||
}
|
||||
|
||||
if (request.path == "/api/layers/add" && callbacks.addLayer)
|
||||
{
|
||||
const ControlActionResult result = callbacks.addLayer(request.body);
|
||||
return HttpControlServer::JsonResponse(
|
||||
result.ok ? "200 OK" : "400 Bad Request",
|
||||
ActionResponse(result.ok, result.error));
|
||||
}
|
||||
|
||||
if (request.path == "/api/layers/remove" && callbacks.removeLayer)
|
||||
{
|
||||
const ControlActionResult result = callbacks.removeLayer(request.body);
|
||||
return HttpControlServer::JsonResponse(
|
||||
result.ok ? "200 OK" : "400 Bad Request",
|
||||
ActionResponse(result.ok, result.error));
|
||||
}
|
||||
|
||||
return {
|
||||
"400 Bad Request",
|
||||
"application/json",
|
||||
ActionResponse(false, "Endpoint is not implemented in RenderCadenceCompositor yet.")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse RouteRenderCadenceHttpRequest(
|
||||
const HttpRequest& request,
|
||||
const HttpControlServer& server,
|
||||
const RenderCadenceHttpRouteCallbacks& callbacks)
|
||||
{
|
||||
if (request.method == "GET")
|
||||
return ServeRenderCadenceGet(request, server, callbacks);
|
||||
if (request.method == "POST")
|
||||
return ServeRenderCadencePost(request, callbacks);
|
||||
return HttpControlServer::TextResponse("404 Not Found", "Not Found");
|
||||
}
|
||||
}
|
||||
25
src/app/RenderCadenceHttpRoutes.h
Normal file
25
src/app/RenderCadenceHttpRoutes.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "../control/ControlActionResult.h"
|
||||
#include "../control/http/HttpControlServer.h"
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct RenderCadenceHttpRouteCallbacks
|
||||
{
|
||||
std::function<std::string()> getStateJson;
|
||||
std::function<std::string()> getConfigJson;
|
||||
std::function<std::string()> getNdiSourcesJson;
|
||||
std::function<ControlActionResult(const std::string&)> addLayer;
|
||||
std::function<ControlActionResult(const std::string&)> removeLayer;
|
||||
std::function<ControlActionResult(const std::string&, const std::string&)> executePost;
|
||||
};
|
||||
|
||||
HttpResponse RouteRenderCadenceHttpRequest(
|
||||
const HttpRequest& request,
|
||||
const HttpControlServer& server,
|
||||
const RenderCadenceHttpRouteCallbacks& callbacks);
|
||||
}
|
||||
@@ -231,12 +231,10 @@ bool HttpControlServer::SendResponse(SOCKET clientSocket, const HttpResponse& re
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::RouteRequest(const HttpRequest& request) const
|
||||
{
|
||||
if (request.method == "GET")
|
||||
return ServeGet(request);
|
||||
if (request.method == "POST")
|
||||
return ServePost(request);
|
||||
if (request.method == "OPTIONS")
|
||||
return TextResponse("204 No Content", std::string());
|
||||
if (mCallbacks.routeRequest)
|
||||
return mCallbacks.routeRequest(request);
|
||||
return TextResponse("404 Not Found", "Not Found");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "ControlActionResult.h"
|
||||
|
||||
#include <winsock2.h>
|
||||
|
||||
#include <atomic>
|
||||
@@ -23,14 +21,25 @@ struct HttpControlServerConfig
|
||||
std::chrono::milliseconds idleSleep = std::chrono::milliseconds(10);
|
||||
};
|
||||
|
||||
struct HttpRequest
|
||||
{
|
||||
std::string method;
|
||||
std::string path;
|
||||
std::map<std::string, std::string> headers;
|
||||
std::string body;
|
||||
};
|
||||
|
||||
struct HttpResponse
|
||||
{
|
||||
std::string status;
|
||||
std::string contentType;
|
||||
std::string body;
|
||||
};
|
||||
|
||||
struct HttpControlServerCallbacks
|
||||
{
|
||||
std::function<std::string()> getStateJson;
|
||||
std::function<std::string()> getConfigJson;
|
||||
std::function<std::string()> getNdiSourcesJson;
|
||||
std::function<ControlActionResult(const std::string&)> addLayer;
|
||||
std::function<ControlActionResult(const std::string&)> removeLayer;
|
||||
std::function<ControlActionResult(const std::string&, const std::string&)> executePost;
|
||||
std::function<HttpResponse(const HttpRequest&)> routeRequest;
|
||||
std::function<std::string()> getWebSocketStateJson;
|
||||
};
|
||||
|
||||
class UniqueSocket
|
||||
@@ -57,20 +66,8 @@ private:
|
||||
class HttpControlServer
|
||||
{
|
||||
public:
|
||||
struct HttpRequest
|
||||
{
|
||||
std::string method;
|
||||
std::string path;
|
||||
std::map<std::string, std::string> headers;
|
||||
std::string body;
|
||||
};
|
||||
|
||||
struct HttpResponse
|
||||
{
|
||||
std::string status;
|
||||
std::string contentType;
|
||||
std::string body;
|
||||
};
|
||||
using HttpRequest = RenderCadenceCompositor::HttpRequest;
|
||||
using HttpResponse = RenderCadenceCompositor::HttpResponse;
|
||||
|
||||
HttpControlServer() = default;
|
||||
~HttpControlServer();
|
||||
@@ -95,6 +92,13 @@ public:
|
||||
|
||||
static bool ParseHttpRequest(const std::string& rawRequest, HttpRequest& request);
|
||||
static std::string WebSocketAcceptKey(const std::string& clientKey);
|
||||
HttpResponse ServeOpenApiSpec() const;
|
||||
HttpResponse ServeSwaggerDocs() const;
|
||||
HttpResponse ServeUiAsset(const std::string& relativePath) const;
|
||||
|
||||
static HttpResponse JsonResponse(const std::string& status, const std::string& body);
|
||||
static HttpResponse TextResponse(const std::string& status, const std::string& body);
|
||||
static HttpResponse HtmlResponse(const std::string& status, const std::string& body);
|
||||
|
||||
private:
|
||||
void ThreadMain();
|
||||
@@ -105,17 +109,8 @@ private:
|
||||
void JoinFinishedClientThreads();
|
||||
bool SendResponse(SOCKET clientSocket, const HttpResponse& response) const;
|
||||
HttpResponse RouteRequest(const HttpRequest& request) const;
|
||||
HttpResponse ServeGet(const HttpRequest& request) const;
|
||||
HttpResponse ServePost(const HttpRequest& request) const;
|
||||
HttpResponse ServeOpenApiSpec() const;
|
||||
HttpResponse ServeSwaggerDocs() const;
|
||||
HttpResponse ServeUiAsset(const std::string& relativePath) const;
|
||||
std::string LoadTextFile(const std::filesystem::path& path) const;
|
||||
|
||||
static HttpResponse JsonResponse(const std::string& status, const std::string& body);
|
||||
static HttpResponse TextResponse(const std::string& status, const std::string& body);
|
||||
static HttpResponse HtmlResponse(const std::string& status, const std::string& body);
|
||||
static std::string ActionResponse(bool ok, const std::string& error = std::string());
|
||||
static bool SendWebSocketText(SOCKET clientSocket, const std::string& text);
|
||||
static std::string GuessContentType(const std::filesystem::path& path);
|
||||
static bool IsSafeRelativePath(const std::filesystem::path& path);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "HttpControlServer.h"
|
||||
|
||||
#include "../json/JsonWriter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
@@ -9,86 +7,6 @@
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace
|
||||
{
|
||||
bool IsKnownPostEndpoint(const std::string& path)
|
||||
{
|
||||
return path == "/api/layers/add"
|
||||
|| path == "/api/layers/remove"
|
||||
|| path == "/api/layers/move"
|
||||
|| path == "/api/layers/reorder"
|
||||
|| path == "/api/layers/set-bypass"
|
||||
|| path == "/api/layers/set-shader"
|
||||
|| path == "/api/layers/update-parameter"
|
||||
|| path == "/api/layers/reset-parameters"
|
||||
|| path == "/api/stack-presets/save"
|
||||
|| path == "/api/stack-presets/load"
|
||||
|| path == "/api/config/save"
|
||||
|| path == "/api/app/restart"
|
||||
|| path == "/api/reload"
|
||||
|| path == "/api/screenshot";
|
||||
}
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeGet(const HttpRequest& request) const
|
||||
{
|
||||
if (request.path == "/api/state")
|
||||
return JsonResponse("200 OK", mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}");
|
||||
if (request.path == "/api/config")
|
||||
return JsonResponse("200 OK", mCallbacks.getConfigJson ? mCallbacks.getConfigJson() : "{}");
|
||||
if (request.path == "/api/ndi/sources")
|
||||
return JsonResponse(
|
||||
"200 OK",
|
||||
mCallbacks.getNdiSourcesJson
|
||||
? mCallbacks.getNdiSourcesJson()
|
||||
: "{\"ok\":false,\"sources\":[],\"error\":\"NDI source discovery is not available.\"}");
|
||||
if (request.path == "/openapi.yaml" || request.path == "/docs/openapi.yaml")
|
||||
return ServeOpenApiSpec();
|
||||
if (request.path == "/docs" || request.path == "/docs/")
|
||||
return ServeSwaggerDocs();
|
||||
if (request.path == "/" || request.path == "/index.html")
|
||||
return ServeUiAsset("index.html");
|
||||
if (request.path.rfind("/assets/", 0) == 0)
|
||||
return ServeUiAsset(request.path.substr(1));
|
||||
if (request.path.size() > 1)
|
||||
{
|
||||
const HttpResponse asset = ServeUiAsset(request.path.substr(1));
|
||||
if (asset.status != "404 Not Found")
|
||||
return asset;
|
||||
}
|
||||
return ServeUiAsset("index.html");
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServePost(const HttpRequest& request) const
|
||||
{
|
||||
if (!IsKnownPostEndpoint(request.path))
|
||||
return TextResponse("404 Not Found", "Not Found");
|
||||
|
||||
if (mCallbacks.executePost)
|
||||
{
|
||||
const ControlActionResult result = mCallbacks.executePost(request.path, request.body);
|
||||
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||
}
|
||||
|
||||
if (request.path == "/api/layers/add" && mCallbacks.addLayer)
|
||||
{
|
||||
const ControlActionResult result = mCallbacks.addLayer(request.body);
|
||||
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||
}
|
||||
|
||||
if (request.path == "/api/layers/remove" && mCallbacks.removeLayer)
|
||||
{
|
||||
const ControlActionResult result = mCallbacks.removeLayer(request.body);
|
||||
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||
}
|
||||
|
||||
return {
|
||||
"400 Bad Request",
|
||||
"application/json",
|
||||
ActionResponse(false, "Endpoint is not implemented in RenderCadenceCompositor yet.")
|
||||
};
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeOpenApiSpec() const
|
||||
{
|
||||
const std::filesystem::path path = mDocsRoot / "openapi.yaml";
|
||||
@@ -153,17 +71,6 @@ HttpControlServer::HttpResponse HttpControlServer::HtmlResponse(const std::strin
|
||||
return { status, "text/html", body };
|
||||
}
|
||||
|
||||
std::string HttpControlServer::ActionResponse(bool ok, const std::string& error)
|
||||
{
|
||||
JsonWriter writer;
|
||||
writer.BeginObject();
|
||||
writer.KeyBool("ok", ok);
|
||||
if (!error.empty())
|
||||
writer.KeyString("error", error);
|
||||
writer.EndObject();
|
||||
return writer.StringValue();
|
||||
}
|
||||
|
||||
std::string HttpControlServer::GuessContentType(const std::filesystem::path& path)
|
||||
{
|
||||
const std::string extension = ToLower(path.extension().string());
|
||||
|
||||
@@ -155,7 +155,7 @@ void HttpControlServer::WebSocketClientMain(UniqueSocket clientSocket)
|
||||
std::string previousState;
|
||||
while (mRunning.load(std::memory_order_acquire))
|
||||
{
|
||||
const std::string state = mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}";
|
||||
const std::string state = mCallbacks.getWebSocketStateJson ? mCallbacks.getWebSocketStateJson() : "{}";
|
||||
if (state != previousState)
|
||||
{
|
||||
if (!SendWebSocketText(clientSocket.get(), state))
|
||||
|
||||
@@ -86,6 +86,7 @@ add_video_shader_test(RenderCadenceCompositorRuntimeStateJsonTests
|
||||
)
|
||||
|
||||
add_video_shader_test(RenderCadenceCompositorHttpControlServerTests
|
||||
"${SRC_DIR}/app/RenderCadenceHttpRoutes.cpp"
|
||||
"${SRC_DIR}/control/RuntimeControlCommand.cpp"
|
||||
"${SRC_DIR}/control/http/HttpControlServer.cpp"
|
||||
"${SRC_DIR}/control/http/HttpControlServerRoutes.cpp"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "HttpControlServer.h"
|
||||
#include "RenderCadenceHttpRoutes.h"
|
||||
#include "RuntimeControlCommand.h"
|
||||
|
||||
#include <filesystem>
|
||||
@@ -45,20 +46,41 @@ void TestParsesHttpRequest()
|
||||
ExpectEquals(request.headers["host"], "127.0.0.1", "headers are lower-cased and trimmed");
|
||||
}
|
||||
|
||||
void TestStateEndpointUsesCallback()
|
||||
void TestServerDelegatesToRouteCallback()
|
||||
{
|
||||
using namespace RenderCadenceCompositor;
|
||||
|
||||
HttpControlServer server;
|
||||
HttpControlServerCallbacks callbacks;
|
||||
callbacks.getStateJson = []() { return std::string("{\"ok\":true}"); };
|
||||
callbacks.routeRequest = [](const HttpRequest& request) {
|
||||
ExpectEquals(request.path, "/custom", "generic route callback receives request path");
|
||||
return HttpControlServer::JsonResponse("200 OK", "{\"custom\":true}");
|
||||
};
|
||||
server.SetCallbacksForTest(callbacks);
|
||||
|
||||
HttpControlServer::HttpRequest request;
|
||||
request.method = "GET";
|
||||
request.path = "/custom";
|
||||
|
||||
const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request);
|
||||
ExpectEquals(response.status, "200 OK", "server delegates HTTP request to route callback");
|
||||
ExpectEquals(response.contentType, "application/json", "route callback controls content type");
|
||||
ExpectEquals(response.body, "{\"custom\":true}", "route callback controls response body");
|
||||
}
|
||||
|
||||
void TestStateEndpointUsesCallback()
|
||||
{
|
||||
using namespace RenderCadenceCompositor;
|
||||
|
||||
HttpControlServer server;
|
||||
RenderCadenceHttpRouteCallbacks callbacks;
|
||||
callbacks.getStateJson = []() { return std::string("{\"ok\":true}"); };
|
||||
|
||||
HttpControlServer::HttpRequest request;
|
||||
request.method = "GET";
|
||||
request.path = "/api/state";
|
||||
|
||||
const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request);
|
||||
const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks);
|
||||
ExpectEquals(response.status, "200 OK", "state endpoint succeeds");
|
||||
ExpectEquals(response.contentType, "application/json", "state endpoint is JSON");
|
||||
ExpectEquals(response.body, "{\"ok\":true}", "state endpoint returns callback JSON");
|
||||
@@ -69,15 +91,14 @@ void TestConfigEndpointUsesCallback()
|
||||
using namespace RenderCadenceCompositor;
|
||||
|
||||
HttpControlServer server;
|
||||
HttpControlServerCallbacks callbacks;
|
||||
RenderCadenceHttpRouteCallbacks callbacks;
|
||||
callbacks.getConfigJson = []() { return std::string("{\"diskLoaded\":true}"); };
|
||||
server.SetCallbacksForTest(callbacks);
|
||||
|
||||
HttpControlServer::HttpRequest request;
|
||||
request.method = "GET";
|
||||
request.path = "/api/config";
|
||||
|
||||
const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request);
|
||||
const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks);
|
||||
ExpectEquals(response.status, "200 OK", "config endpoint succeeds");
|
||||
ExpectEquals(response.contentType, "application/json", "config endpoint is JSON");
|
||||
ExpectEquals(response.body, "{\"diskLoaded\":true}", "config endpoint returns callback JSON");
|
||||
@@ -88,15 +109,14 @@ void TestNdiSourcesEndpointUsesCallback()
|
||||
using namespace RenderCadenceCompositor;
|
||||
|
||||
HttpControlServer server;
|
||||
HttpControlServerCallbacks callbacks;
|
||||
RenderCadenceHttpRouteCallbacks callbacks;
|
||||
callbacks.getNdiSourcesJson = []() { return std::string("{\"ok\":true,\"sources\":[{\"name\":\"DESKTOP (Camera)\"}]}"); };
|
||||
server.SetCallbacksForTest(callbacks);
|
||||
|
||||
HttpControlServer::HttpRequest request;
|
||||
request.method = "GET";
|
||||
request.path = "/api/ndi/sources";
|
||||
|
||||
const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request);
|
||||
const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks);
|
||||
ExpectEquals(response.status, "200 OK", "NDI sources endpoint succeeds");
|
||||
ExpectEquals(response.contentType, "application/json", "NDI sources endpoint is JSON");
|
||||
Expect(response.body.find("DESKTOP (Camera)") != std::string::npos, "NDI sources endpoint returns callback JSON");
|
||||
@@ -127,7 +147,8 @@ void TestRootServesUiIndex()
|
||||
request.path = "/";
|
||||
|
||||
server.SetRootsForTest(root, std::filesystem::path());
|
||||
const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request);
|
||||
RenderCadenceHttpRouteCallbacks callbacks;
|
||||
const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks);
|
||||
|
||||
ExpectEquals(response.status, "200 OK", "root endpoint serves UI index");
|
||||
ExpectEquals(response.contentType, "text/html", "UI index content type is html");
|
||||
@@ -146,7 +167,8 @@ void TestKnownPostEndpointReturnsActionError()
|
||||
request.path = "/api/layers/add";
|
||||
request.body = "{\"shaderId\":\"happy-accident\"}";
|
||||
|
||||
const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request);
|
||||
RenderCadenceHttpRouteCallbacks callbacks;
|
||||
const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks);
|
||||
ExpectEquals(response.status, "400 Bad Request", "unimplemented post returns OpenAPI action error status");
|
||||
ExpectEquals(response.contentType, "application/json", "unimplemented post returns JSON");
|
||||
Expect(response.body.find("\"ok\":false") != std::string::npos, "unimplemented post reports ok false");
|
||||
@@ -158,7 +180,7 @@ void TestLayerPostEndpointsUseCallbacks()
|
||||
using namespace RenderCadenceCompositor;
|
||||
|
||||
HttpControlServer server;
|
||||
HttpControlServerCallbacks callbacks;
|
||||
RenderCadenceHttpRouteCallbacks callbacks;
|
||||
callbacks.addLayer = [](const std::string& body) {
|
||||
Expect(body.find("solid") != std::string::npos, "add callback receives request body");
|
||||
return ControlActionResult{ true, std::string() };
|
||||
@@ -167,13 +189,12 @@ void TestLayerPostEndpointsUseCallbacks()
|
||||
Expect(body.find("runtime-layer-1") != std::string::npos, "remove callback receives request body");
|
||||
return ControlActionResult{ false, "Unknown layer id." };
|
||||
};
|
||||
server.SetCallbacksForTest(callbacks);
|
||||
|
||||
HttpControlServer::HttpRequest addRequest;
|
||||
addRequest.method = "POST";
|
||||
addRequest.path = "/api/layers/add";
|
||||
addRequest.body = "{\"shaderId\":\"solid\"}";
|
||||
const HttpControlServer::HttpResponse addResponse = server.RouteRequestForTest(addRequest);
|
||||
const HttpControlServer::HttpResponse addResponse = RouteRenderCadenceHttpRequest(addRequest, server, callbacks);
|
||||
ExpectEquals(addResponse.status, "200 OK", "add layer callback success returns 200");
|
||||
Expect(addResponse.body.find("\"ok\":true") != std::string::npos, "add layer callback returns action success");
|
||||
|
||||
@@ -181,7 +202,7 @@ void TestLayerPostEndpointsUseCallbacks()
|
||||
removeRequest.method = "POST";
|
||||
removeRequest.path = "/api/layers/remove";
|
||||
removeRequest.body = "{\"layerId\":\"runtime-layer-1\"}";
|
||||
const HttpControlServer::HttpResponse removeResponse = server.RouteRequestForTest(removeRequest);
|
||||
const HttpControlServer::HttpResponse removeResponse = RouteRenderCadenceHttpRequest(removeRequest, server, callbacks);
|
||||
ExpectEquals(removeResponse.status, "400 Bad Request", "remove layer callback failure returns 400");
|
||||
Expect(removeResponse.body.find("Unknown layer id.") != std::string::npos, "remove layer callback returns diagnostic");
|
||||
}
|
||||
@@ -191,20 +212,19 @@ void TestGenericPostCallbackHandlesControlRoutes()
|
||||
using namespace RenderCadenceCompositor;
|
||||
|
||||
HttpControlServer server;
|
||||
HttpControlServerCallbacks callbacks;
|
||||
RenderCadenceHttpRouteCallbacks callbacks;
|
||||
callbacks.executePost = [](const std::string& path, const std::string& body) {
|
||||
ExpectEquals(path, "/api/layers/set-bypass", "generic callback receives route path");
|
||||
Expect(body.find("runtime-layer-1") != std::string::npos, "generic callback receives request body");
|
||||
return ControlActionResult{ true, std::string() };
|
||||
};
|
||||
server.SetCallbacksForTest(callbacks);
|
||||
|
||||
HttpControlServer::HttpRequest request;
|
||||
request.method = "POST";
|
||||
request.path = "/api/layers/set-bypass";
|
||||
request.body = "{\"layerId\":\"runtime-layer-1\",\"bypass\":true}";
|
||||
|
||||
const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request);
|
||||
const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks);
|
||||
ExpectEquals(response.status, "200 OK", "generic control callback success returns 200");
|
||||
Expect(response.body.find("\"ok\":true") != std::string::npos, "generic control callback returns action success");
|
||||
}
|
||||
@@ -214,20 +234,19 @@ void TestGenericPostCallbackHandlesConfigRoutes()
|
||||
using namespace RenderCadenceCompositor;
|
||||
|
||||
HttpControlServer server;
|
||||
HttpControlServerCallbacks callbacks;
|
||||
RenderCadenceHttpRouteCallbacks callbacks;
|
||||
callbacks.executePost = [](const std::string& path, const std::string& body) {
|
||||
ExpectEquals(path, "/api/config/save", "generic callback receives config route path");
|
||||
Expect(body.find("runtimeShaderId") != std::string::npos, "generic callback receives config request body");
|
||||
return ControlActionResult{ true, std::string() };
|
||||
};
|
||||
server.SetCallbacksForTest(callbacks);
|
||||
|
||||
HttpControlServer::HttpRequest request;
|
||||
request.method = "POST";
|
||||
request.path = "/api/config/save";
|
||||
request.body = "{\"runtimeShaderId\":\"solid-color\"}";
|
||||
|
||||
const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request);
|
||||
const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks);
|
||||
ExpectEquals(response.status, "200 OK", "config save callback success returns 200");
|
||||
Expect(response.body.find("\"ok\":true") != std::string::npos, "config save callback returns action success");
|
||||
}
|
||||
@@ -251,7 +270,8 @@ void TestUnknownEndpointReturns404()
|
||||
request.method = "GET";
|
||||
request.path = "/api/nope";
|
||||
|
||||
const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request);
|
||||
RenderCadenceHttpRouteCallbacks callbacks;
|
||||
const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks);
|
||||
ExpectEquals(response.status, "404 Not Found", "unknown endpoint returns 404");
|
||||
}
|
||||
}
|
||||
@@ -259,6 +279,7 @@ void TestUnknownEndpointReturns404()
|
||||
int main()
|
||||
{
|
||||
TestParsesHttpRequest();
|
||||
TestServerDelegatesToRouteCallback();
|
||||
TestStateEndpointUsesCallback();
|
||||
TestConfigEndpointUsesCallback();
|
||||
TestNdiSourcesEndpointUsesCallback();
|
||||
|
||||
Submodule video-io-3rdParty updated: 6325492f59...4755423da6
Reference in New Issue
Block a user