HTTP boundry
All checks were successful
CI / React UI Build (push) Successful in 12s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
Aiden
2026-05-30 20:34:52 +10:00
parent aa33d72b6e
commit 04e0802ef2
13 changed files with 247 additions and 165 deletions

View File

@@ -34,7 +34,8 @@ Primary source areas:
- `src/runtime/shader`: background Slang build bridge and prepared shader artifact types - `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/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/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/preview`: optional non-consuming preview window
- `src/telemetry` and `src/logging`: runtime observation and logging - `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 ## 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 - UI assets
- OpenAPI/Swagger docs - 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`. 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 ## Tests
Native tests cover the main non-GL contracts: Native tests cover the main non-GL contracts:
@@ -177,7 +180,7 @@ Native tests cover the main non-GL contracts:
- supported shader catalog - supported shader catalog
- runtime layer restore/reload behavior - runtime layer restore/reload behavior
- runtime-state persistence writer - runtime-state persistence writer
- HTTP command parsing - HTTP transport and app-route dispatch
- frame exchange and input mailbox behavior - frame exchange and input mailbox behavior
- video format and scheduling helpers - video format and scheduling helpers

View File

@@ -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/render/readback`: BGRA8/UYVY8 PBO readback and completed-frame publication.
- `src/platform`: hidden GL window/context support. - `src/platform`: hidden GL window/context support.
- `src/app`: startup, config, video backend factory, runtime layer orchestration, preview, telemetry, and HTTP server hookup. - `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 ## 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. - `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. - `shaders/`: bundled shader package library.
- `runtime/templates/shader_wrapper.slang.in`: only needed for the current Slang package pipeline. - `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. - 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 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 ## Current Swap Point
The render cadence loop now calls `IRenderContent` inside the readback queue call in `src/render/thread/RenderThread.cpp`: The render cadence loop now calls `IRenderContent` inside the readback queue call in `src/render/thread/RenderThread.cpp`:

View File

@@ -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 - `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." }` - 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 ## 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/state/`: runtime JSON helpers, parameter normalization, and debounced runtime-state persistence
- `runtime/text/`: font atlas build and prepared text texture composition - `runtime/text/`: font atlas build and prepared text texture composition
- `control/`: control action results and runtime-state JSON presentation - `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 - `json/`: compact JSON serialization helpers
- `video/`: DeckLink output wrapper and scheduling thread - `video/`: DeckLink output wrapper and scheduling thread
- `telemetry/`: cadence telemetry - `telemetry/`: cadence telemetry

View File

@@ -4,6 +4,7 @@
#include "AppConfigJson.h" #include "AppConfigJson.h"
#include "AppConfigProvider.h" #include "AppConfigProvider.h"
#include "AppRestart.h" #include "AppRestart.h"
#include "RenderCadenceHttpRoutes.h"
#include "RuntimeLayerController.h" #include "RuntimeLayerController.h"
#include "../logging/Logger.h" #include "../logging/Logger.h"
#include "../control/RuntimeStateJson.h" #include "../control/RuntimeStateJson.h"
@@ -246,23 +247,23 @@ private:
void StartHttpServer() void StartHttpServer()
{ {
HttpControlServerCallbacks callbacks; RenderCadenceHttpRouteCallbacks routeCallbacks;
callbacks.getStateJson = [this]() { routeCallbacks.getStateJson = [this]() {
return BuildStateJson(); return BuildStateJson();
}; };
callbacks.getConfigJson = [this]() { routeCallbacks.getConfigJson = [this]() {
return BuildConfigJson(); return BuildConfigJson();
}; };
callbacks.getNdiSourcesJson = [this]() { routeCallbacks.getNdiSourcesJson = [this]() {
return BuildNdiSourcesJson(); return BuildNdiSourcesJson();
}; };
callbacks.addLayer = [this](const std::string& body) { routeCallbacks.addLayer = [this](const std::string& body) {
return mRuntimeLayers.HandleAddLayer(body); return mRuntimeLayers.HandleAddLayer(body);
}; };
callbacks.removeLayer = [this](const std::string& body) { routeCallbacks.removeLayer = [this](const std::string& body) {
return mRuntimeLayers.HandleRemoveLayer(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") if (path == "/api/config/save")
return HandleConfigSave(body); return HandleConfigSave(body);
if (path == "/api/app/restart") if (path == "/api/app/restart")
@@ -275,6 +276,12 @@ private:
return mRuntimeLayers.HandleControlCommand(command); 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; std::string error;
if (!mHttpServer.Start( if (!mHttpServer.Start(
FindRepoPath("ui/dist"), FindRepoPath("ui/dist"),

View 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");
}
}

View 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);
}

View File

@@ -231,12 +231,10 @@ bool HttpControlServer::SendResponse(SOCKET clientSocket, const HttpResponse& re
HttpControlServer::HttpResponse HttpControlServer::RouteRequest(const HttpRequest& request) const 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") if (request.method == "OPTIONS")
return TextResponse("204 No Content", std::string()); return TextResponse("204 No Content", std::string());
if (mCallbacks.routeRequest)
return mCallbacks.routeRequest(request);
return TextResponse("404 Not Found", "Not Found"); return TextResponse("404 Not Found", "Not Found");
} }

View File

@@ -1,7 +1,5 @@
#pragma once #pragma once
#include "ControlActionResult.h"
#include <winsock2.h> #include <winsock2.h>
#include <atomic> #include <atomic>
@@ -23,14 +21,25 @@ struct HttpControlServerConfig
std::chrono::milliseconds idleSleep = std::chrono::milliseconds(10); 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 struct HttpControlServerCallbacks
{ {
std::function<std::string()> getStateJson; std::function<HttpResponse(const HttpRequest&)> routeRequest;
std::function<std::string()> getConfigJson; std::function<std::string()> getWebSocketStateJson;
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;
}; };
class UniqueSocket class UniqueSocket
@@ -57,20 +66,8 @@ private:
class HttpControlServer class HttpControlServer
{ {
public: public:
struct HttpRequest using HttpRequest = RenderCadenceCompositor::HttpRequest;
{ using HttpResponse = RenderCadenceCompositor::HttpResponse;
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;
};
HttpControlServer() = default; HttpControlServer() = default;
~HttpControlServer(); ~HttpControlServer();
@@ -95,6 +92,13 @@ public:
static bool ParseHttpRequest(const std::string& rawRequest, HttpRequest& request); static bool ParseHttpRequest(const std::string& rawRequest, HttpRequest& request);
static std::string WebSocketAcceptKey(const std::string& clientKey); 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: private:
void ThreadMain(); void ThreadMain();
@@ -105,17 +109,8 @@ private:
void JoinFinishedClientThreads(); void JoinFinishedClientThreads();
bool SendResponse(SOCKET clientSocket, const HttpResponse& response) const; bool SendResponse(SOCKET clientSocket, const HttpResponse& response) const;
HttpResponse RouteRequest(const HttpRequest& request) 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; 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 bool SendWebSocketText(SOCKET clientSocket, const std::string& text);
static std::string GuessContentType(const std::filesystem::path& path); static std::string GuessContentType(const std::filesystem::path& path);
static bool IsSafeRelativePath(const std::filesystem::path& path); static bool IsSafeRelativePath(const std::filesystem::path& path);

View File

@@ -1,7 +1,5 @@
#include "HttpControlServer.h" #include "HttpControlServer.h"
#include "../json/JsonWriter.h"
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <fstream> #include <fstream>
@@ -9,86 +7,6 @@
namespace RenderCadenceCompositor 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 HttpControlServer::HttpResponse HttpControlServer::ServeOpenApiSpec() const
{ {
const std::filesystem::path path = mDocsRoot / "openapi.yaml"; const std::filesystem::path path = mDocsRoot / "openapi.yaml";
@@ -153,17 +71,6 @@ HttpControlServer::HttpResponse HttpControlServer::HtmlResponse(const std::strin
return { status, "text/html", body }; 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) std::string HttpControlServer::GuessContentType(const std::filesystem::path& path)
{ {
const std::string extension = ToLower(path.extension().string()); const std::string extension = ToLower(path.extension().string());

View File

@@ -155,7 +155,7 @@ void HttpControlServer::WebSocketClientMain(UniqueSocket clientSocket)
std::string previousState; std::string previousState;
while (mRunning.load(std::memory_order_acquire)) 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 (state != previousState)
{ {
if (!SendWebSocketText(clientSocket.get(), state)) if (!SendWebSocketText(clientSocket.get(), state))

View File

@@ -86,6 +86,7 @@ add_video_shader_test(RenderCadenceCompositorRuntimeStateJsonTests
) )
add_video_shader_test(RenderCadenceCompositorHttpControlServerTests add_video_shader_test(RenderCadenceCompositorHttpControlServerTests
"${SRC_DIR}/app/RenderCadenceHttpRoutes.cpp"
"${SRC_DIR}/control/RuntimeControlCommand.cpp" "${SRC_DIR}/control/RuntimeControlCommand.cpp"
"${SRC_DIR}/control/http/HttpControlServer.cpp" "${SRC_DIR}/control/http/HttpControlServer.cpp"
"${SRC_DIR}/control/http/HttpControlServerRoutes.cpp" "${SRC_DIR}/control/http/HttpControlServerRoutes.cpp"

View File

@@ -1,4 +1,5 @@
#include "HttpControlServer.h" #include "HttpControlServer.h"
#include "RenderCadenceHttpRoutes.h"
#include "RuntimeControlCommand.h" #include "RuntimeControlCommand.h"
#include <filesystem> #include <filesystem>
@@ -45,20 +46,41 @@ void TestParsesHttpRequest()
ExpectEquals(request.headers["host"], "127.0.0.1", "headers are lower-cased and trimmed"); ExpectEquals(request.headers["host"], "127.0.0.1", "headers are lower-cased and trimmed");
} }
void TestStateEndpointUsesCallback() void TestServerDelegatesToRouteCallback()
{ {
using namespace RenderCadenceCompositor; using namespace RenderCadenceCompositor;
HttpControlServer server; HttpControlServer server;
HttpControlServerCallbacks callbacks; 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); 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; HttpControlServer::HttpRequest request;
request.method = "GET"; request.method = "GET";
request.path = "/api/state"; 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.status, "200 OK", "state endpoint succeeds");
ExpectEquals(response.contentType, "application/json", "state endpoint is JSON"); ExpectEquals(response.contentType, "application/json", "state endpoint is JSON");
ExpectEquals(response.body, "{\"ok\":true}", "state endpoint returns callback JSON"); ExpectEquals(response.body, "{\"ok\":true}", "state endpoint returns callback JSON");
@@ -69,15 +91,14 @@ void TestConfigEndpointUsesCallback()
using namespace RenderCadenceCompositor; using namespace RenderCadenceCompositor;
HttpControlServer server; HttpControlServer server;
HttpControlServerCallbacks callbacks; RenderCadenceHttpRouteCallbacks callbacks;
callbacks.getConfigJson = []() { return std::string("{\"diskLoaded\":true}"); }; callbacks.getConfigJson = []() { return std::string("{\"diskLoaded\":true}"); };
server.SetCallbacksForTest(callbacks);
HttpControlServer::HttpRequest request; HttpControlServer::HttpRequest request;
request.method = "GET"; request.method = "GET";
request.path = "/api/config"; 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.status, "200 OK", "config endpoint succeeds");
ExpectEquals(response.contentType, "application/json", "config endpoint is JSON"); ExpectEquals(response.contentType, "application/json", "config endpoint is JSON");
ExpectEquals(response.body, "{\"diskLoaded\":true}", "config endpoint returns callback JSON"); ExpectEquals(response.body, "{\"diskLoaded\":true}", "config endpoint returns callback JSON");
@@ -88,15 +109,14 @@ void TestNdiSourcesEndpointUsesCallback()
using namespace RenderCadenceCompositor; using namespace RenderCadenceCompositor;
HttpControlServer server; HttpControlServer server;
HttpControlServerCallbacks callbacks; RenderCadenceHttpRouteCallbacks callbacks;
callbacks.getNdiSourcesJson = []() { return std::string("{\"ok\":true,\"sources\":[{\"name\":\"DESKTOP (Camera)\"}]}"); }; callbacks.getNdiSourcesJson = []() { return std::string("{\"ok\":true,\"sources\":[{\"name\":\"DESKTOP (Camera)\"}]}"); };
server.SetCallbacksForTest(callbacks);
HttpControlServer::HttpRequest request; HttpControlServer::HttpRequest request;
request.method = "GET"; request.method = "GET";
request.path = "/api/ndi/sources"; 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.status, "200 OK", "NDI sources endpoint succeeds");
ExpectEquals(response.contentType, "application/json", "NDI sources endpoint is JSON"); 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"); Expect(response.body.find("DESKTOP (Camera)") != std::string::npos, "NDI sources endpoint returns callback JSON");
@@ -127,7 +147,8 @@ void TestRootServesUiIndex()
request.path = "/"; request.path = "/";
server.SetRootsForTest(root, std::filesystem::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.status, "200 OK", "root endpoint serves UI index");
ExpectEquals(response.contentType, "text/html", "UI index content type is html"); ExpectEquals(response.contentType, "text/html", "UI index content type is html");
@@ -146,7 +167,8 @@ void TestKnownPostEndpointReturnsActionError()
request.path = "/api/layers/add"; request.path = "/api/layers/add";
request.body = "{\"shaderId\":\"happy-accident\"}"; 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.status, "400 Bad Request", "unimplemented post returns OpenAPI action error status");
ExpectEquals(response.contentType, "application/json", "unimplemented post returns JSON"); ExpectEquals(response.contentType, "application/json", "unimplemented post returns JSON");
Expect(response.body.find("\"ok\":false") != std::string::npos, "unimplemented post reports ok false"); Expect(response.body.find("\"ok\":false") != std::string::npos, "unimplemented post reports ok false");
@@ -158,7 +180,7 @@ void TestLayerPostEndpointsUseCallbacks()
using namespace RenderCadenceCompositor; using namespace RenderCadenceCompositor;
HttpControlServer server; HttpControlServer server;
HttpControlServerCallbacks callbacks; RenderCadenceHttpRouteCallbacks callbacks;
callbacks.addLayer = [](const std::string& body) { callbacks.addLayer = [](const std::string& body) {
Expect(body.find("solid") != std::string::npos, "add callback receives request body"); Expect(body.find("solid") != std::string::npos, "add callback receives request body");
return ControlActionResult{ true, std::string() }; 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"); Expect(body.find("runtime-layer-1") != std::string::npos, "remove callback receives request body");
return ControlActionResult{ false, "Unknown layer id." }; return ControlActionResult{ false, "Unknown layer id." };
}; };
server.SetCallbacksForTest(callbacks);
HttpControlServer::HttpRequest addRequest; HttpControlServer::HttpRequest addRequest;
addRequest.method = "POST"; addRequest.method = "POST";
addRequest.path = "/api/layers/add"; addRequest.path = "/api/layers/add";
addRequest.body = "{\"shaderId\":\"solid\"}"; 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"); 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"); 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.method = "POST";
removeRequest.path = "/api/layers/remove"; removeRequest.path = "/api/layers/remove";
removeRequest.body = "{\"layerId\":\"runtime-layer-1\"}"; 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"); 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"); Expect(removeResponse.body.find("Unknown layer id.") != std::string::npos, "remove layer callback returns diagnostic");
} }
@@ -191,20 +212,19 @@ void TestGenericPostCallbackHandlesControlRoutes()
using namespace RenderCadenceCompositor; using namespace RenderCadenceCompositor;
HttpControlServer server; HttpControlServer server;
HttpControlServerCallbacks callbacks; RenderCadenceHttpRouteCallbacks callbacks;
callbacks.executePost = [](const std::string& path, const std::string& body) { callbacks.executePost = [](const std::string& path, const std::string& body) {
ExpectEquals(path, "/api/layers/set-bypass", "generic callback receives route path"); 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"); Expect(body.find("runtime-layer-1") != std::string::npos, "generic callback receives request body");
return ControlActionResult{ true, std::string() }; return ControlActionResult{ true, std::string() };
}; };
server.SetCallbacksForTest(callbacks);
HttpControlServer::HttpRequest request; HttpControlServer::HttpRequest request;
request.method = "POST"; request.method = "POST";
request.path = "/api/layers/set-bypass"; request.path = "/api/layers/set-bypass";
request.body = "{\"layerId\":\"runtime-layer-1\",\"bypass\":true}"; 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"); 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"); Expect(response.body.find("\"ok\":true") != std::string::npos, "generic control callback returns action success");
} }
@@ -214,20 +234,19 @@ void TestGenericPostCallbackHandlesConfigRoutes()
using namespace RenderCadenceCompositor; using namespace RenderCadenceCompositor;
HttpControlServer server; HttpControlServer server;
HttpControlServerCallbacks callbacks; RenderCadenceHttpRouteCallbacks callbacks;
callbacks.executePost = [](const std::string& path, const std::string& body) { callbacks.executePost = [](const std::string& path, const std::string& body) {
ExpectEquals(path, "/api/config/save", "generic callback receives config route path"); ExpectEquals(path, "/api/config/save", "generic callback receives config route path");
Expect(body.find("runtimeShaderId") != std::string::npos, "generic callback receives config request body"); Expect(body.find("runtimeShaderId") != std::string::npos, "generic callback receives config request body");
return ControlActionResult{ true, std::string() }; return ControlActionResult{ true, std::string() };
}; };
server.SetCallbacksForTest(callbacks);
HttpControlServer::HttpRequest request; HttpControlServer::HttpRequest request;
request.method = "POST"; request.method = "POST";
request.path = "/api/config/save"; request.path = "/api/config/save";
request.body = "{\"runtimeShaderId\":\"solid-color\"}"; 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"); 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"); 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.method = "GET";
request.path = "/api/nope"; 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"); ExpectEquals(response.status, "404 Not Found", "unknown endpoint returns 404");
} }
} }
@@ -259,6 +279,7 @@ void TestUnknownEndpointReturns404()
int main() int main()
{ {
TestParsesHttpRequest(); TestParsesHttpRequest();
TestServerDelegatesToRouteCallback();
TestStateEndpointUsesCallback(); TestStateEndpointUsesCallback();
TestConfigEndpointUsesCallback(); TestConfigEndpointUsesCallback();
TestNdiSourcesEndpointUsesCallback(); TestNdiSourcesEndpointUsesCallback();