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/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

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/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`:

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
- 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

View File

@@ -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"),

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

View File

@@ -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);

View File

@@ -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());

View File

@@ -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))

View File

@@ -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"

View File

@@ -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();