Added OSC
This commit is contained in:
@@ -30,6 +30,8 @@ set(APP_SOURCES
|
||||
"${APP_DIR}/NativeSockets.h"
|
||||
"${APP_DIR}/OpenGLComposite.cpp"
|
||||
"${APP_DIR}/OpenGLComposite.h"
|
||||
"${APP_DIR}/OscServer.cpp"
|
||||
"${APP_DIR}/OscServer.h"
|
||||
"${APP_DIR}/resource.h"
|
||||
"${APP_DIR}/RuntimeHost.cpp"
|
||||
"${APP_DIR}/RuntimeHost.h"
|
||||
|
||||
10
README.md
10
README.md
@@ -158,6 +158,16 @@ http://127.0.0.1:<serverPort>/docs
|
||||
|
||||
Use those docs to inspect the `/api/state`, layer control, stack preset, and reload endpoints. Live state updates are also sent over the `/ws` WebSocket.
|
||||
|
||||
## OSC Control
|
||||
|
||||
The native host also listens for local OSC parameter control on the configured `oscPort`:
|
||||
|
||||
```text
|
||||
/VideoShaderToys/{LayerNameOrID}/{ParameterNameOrID}
|
||||
```
|
||||
|
||||
For example, `/VideoShaderToys/VHS/intensity` updates the `intensity` parameter on the first matching `VHS` layer. The listener accepts float, integer, string, and boolean OSC values, and validates them through the same shader parameter path as the REST API. See `docs/OSC_CONTROL.md` for details.
|
||||
|
||||
## Shader Packages
|
||||
|
||||
Each shader package lives under:
|
||||
|
||||
@@ -66,6 +66,8 @@ std::string GuessContentType(const std::filesystem::path& assetPath)
|
||||
return "image/x-icon";
|
||||
if (extension == ".map")
|
||||
return "application/json";
|
||||
if (extension == ".md")
|
||||
return "text/markdown";
|
||||
return "text/html";
|
||||
}
|
||||
}
|
||||
@@ -255,6 +257,10 @@ ControlServer::HttpResponse ControlServer::ServeGetRequest(const HttpRequest& re
|
||||
if (request.path == "/docs" || request.path == "/docs/")
|
||||
return ServeSwaggerDocs();
|
||||
|
||||
const std::string docsPrefix = "/docs/";
|
||||
if (request.path.rfind(docsPrefix, 0) == 0)
|
||||
return ServeDocsAsset(request.path.substr(docsPrefix.size()));
|
||||
|
||||
if (request.path.size() > 1)
|
||||
{
|
||||
const HttpResponse assetResponse = ServeUiAsset(request.path.substr(1));
|
||||
@@ -274,6 +280,19 @@ ControlServer::HttpResponse ControlServer::ServeUiAsset(const std::string& relat
|
||||
: HttpResponse{ "200 OK", contentType, body };
|
||||
}
|
||||
|
||||
ControlServer::HttpResponse ControlServer::ServeDocsAsset(const std::string& relativePath) const
|
||||
{
|
||||
const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal();
|
||||
if (!IsSafeUiPath(sanitizedPath))
|
||||
return { "404 Not Found", "text/plain", "Not Found" };
|
||||
|
||||
const std::filesystem::path docsPath = mDocsRoot / sanitizedPath;
|
||||
const std::string body = LoadTextFile(docsPath);
|
||||
return body.empty()
|
||||
? HttpResponse{ "404 Not Found", "text/plain", "Not Found" }
|
||||
: HttpResponse{ "200 OK", GuessContentType(docsPath), body };
|
||||
}
|
||||
|
||||
ControlServer::HttpResponse ControlServer::ServeOpenApiSpec() const
|
||||
{
|
||||
const std::filesystem::path specPath = mDocsRoot / "openapi.yaml";
|
||||
|
||||
@@ -76,6 +76,7 @@ private:
|
||||
HttpResponse RouteHttpRequest(const HttpRequest& request);
|
||||
HttpResponse ServeGetRequest(const HttpRequest& request) const;
|
||||
HttpResponse ServeUiAsset(const std::string& relativePath) const;
|
||||
HttpResponse ServeDocsAsset(const std::string& relativePath) const;
|
||||
HttpResponse ServeOpenApiSpec() const;
|
||||
HttpResponse ServeSwaggerDocs() const;
|
||||
HttpResponse HandleApiPost(const HttpRequest& request);
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include "ControlServer.h"
|
||||
#include "OpenGLComposite.h"
|
||||
#include "GLExtensions.h"
|
||||
#include "OscServer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -325,6 +326,7 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
||||
InitializeCriticalSection(&pMutex);
|
||||
mRuntimeHost = std::make_unique<RuntimeHost>();
|
||||
mControlServer = std::make_unique<ControlServer>();
|
||||
mOscServer = std::make_unique<OscServer>();
|
||||
}
|
||||
|
||||
OpenGLComposite::~OpenGLComposite()
|
||||
@@ -411,6 +413,8 @@ OpenGLComposite::~OpenGLComposite()
|
||||
destroyTemporalHistoryResources();
|
||||
destroyLayerPrograms();
|
||||
destroyDecodeShaderProgram();
|
||||
if (mOscServer)
|
||||
mOscServer->Stop();
|
||||
if (mControlServer)
|
||||
mControlServer->Stop();
|
||||
|
||||
@@ -843,6 +847,16 @@ bool OpenGLComposite::InitOpenGLState()
|
||||
}
|
||||
mRuntimeHost->SetServerPort(mControlServer->GetPort());
|
||||
|
||||
OscServer::Callbacks oscCallbacks;
|
||||
oscCallbacks.updateParameter = [this](const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error) {
|
||||
return UpdateLayerParameterByControlKeyJson(layerKey, parameterKey, valueJson, error);
|
||||
};
|
||||
if (mRuntimeHost->GetOscPort() > 0 && !mOscServer->Start(mRuntimeHost->GetOscPort(), oscCallbacks, runtimeError))
|
||||
{
|
||||
MessageBoxA(NULL, runtimeError.c_str(), "OSC control server failed to start", MB_OK);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prepare the runtime shader program generated from the active shader package.
|
||||
char compilerErrorMessage[1024];
|
||||
if (! compileDecodeShader(sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||
@@ -1163,6 +1177,9 @@ bool OpenGLComposite::Start()
|
||||
|
||||
bool OpenGLComposite::Stop()
|
||||
{
|
||||
if (mOscServer)
|
||||
mOscServer->Stop();
|
||||
|
||||
if (mControlServer)
|
||||
mControlServer->Stop();
|
||||
|
||||
@@ -2120,6 +2137,19 @@ bool OpenGLComposite::UpdateLayerParameterJson(const std::string& layerId, const
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::UpdateLayerParameterByControlKeyJson(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error)
|
||||
{
|
||||
JsonValue parsedValue;
|
||||
if (!ParseJson(valueJson, parsedValue, error))
|
||||
return false;
|
||||
|
||||
if (!mRuntimeHost->UpdateLayerParameterByControlKey(layerKey, parameterKey, parsedValue, error))
|
||||
return false;
|
||||
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::ResetLayerParameters(const std::string& layerId, std::string& error)
|
||||
{
|
||||
if (!mRuntimeHost->ResetLayerParameters(layerId, error))
|
||||
|
||||
@@ -66,6 +66,7 @@ class PlayoutDelegate;
|
||||
class CaptureDelegate;
|
||||
class PinnedMemoryAllocator;
|
||||
class ControlServer;
|
||||
class OscServer;
|
||||
|
||||
|
||||
class OpenGLComposite
|
||||
@@ -86,6 +87,7 @@ public:
|
||||
bool SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error);
|
||||
bool SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error);
|
||||
bool UpdateLayerParameterJson(const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& error);
|
||||
bool UpdateLayerParameterByControlKeyJson(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error);
|
||||
bool ResetLayerParameters(const std::string& layerId, std::string& error);
|
||||
bool SaveStackPreset(const std::string& presetName, std::string& error);
|
||||
bool LoadStackPreset(const std::string& presetName, std::string& error);
|
||||
@@ -149,6 +151,7 @@ private:
|
||||
int mViewHeight;
|
||||
std::unique_ptr<RuntimeHost> mRuntimeHost;
|
||||
std::unique_ptr<ControlServer> mControlServer;
|
||||
std::unique_ptr<OscServer> mOscServer;
|
||||
|
||||
struct LayerProgram
|
||||
{
|
||||
|
||||
249
apps/LoopThroughWithOpenGLCompositing/OscServer.cpp
Normal file
249
apps/LoopThroughWithOpenGLCompositing/OscServer.cpp
Normal file
@@ -0,0 +1,249 @@
|
||||
#include "stdafx.h"
|
||||
#include "OscServer.h"
|
||||
|
||||
#include <ws2tcpip.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
#pragma comment(lib, "Ws2_32.lib")
|
||||
|
||||
namespace
|
||||
{
|
||||
bool InitializeWinsock(std::string& error)
|
||||
{
|
||||
WSADATA wsaData = {};
|
||||
const int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
|
||||
if (result != 0)
|
||||
{
|
||||
error = "WSAStartup failed.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> SplitAddress(const std::string& address)
|
||||
{
|
||||
std::vector<std::string> parts;
|
||||
std::size_t start = !address.empty() && address[0] == '/' ? 1 : 0;
|
||||
|
||||
while (start <= address.size())
|
||||
{
|
||||
const std::size_t slash = address.find('/', start);
|
||||
const std::size_t end = slash == std::string::npos ? address.size() : slash;
|
||||
if (end > start)
|
||||
parts.push_back(address.substr(start, end - start));
|
||||
if (slash == std::string::npos)
|
||||
break;
|
||||
start = slash + 1;
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
|
||||
OscServer::OscServer()
|
||||
: mPort(0), mRunning(false)
|
||||
{
|
||||
}
|
||||
|
||||
OscServer::~OscServer()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool OscServer::Start(unsigned short port, const Callbacks& callbacks, std::string& error)
|
||||
{
|
||||
if (port == 0)
|
||||
return true;
|
||||
|
||||
mCallbacks = callbacks;
|
||||
mPort = port;
|
||||
|
||||
if (!InitializeWinsock(error))
|
||||
return false;
|
||||
|
||||
mSocket.reset(socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP));
|
||||
if (!mSocket.valid())
|
||||
{
|
||||
error = "Could not create OSC UDP socket.";
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD timeoutMilliseconds = 100;
|
||||
setsockopt(mSocket.get(), SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&timeoutMilliseconds), sizeof(timeoutMilliseconds));
|
||||
|
||||
sockaddr_in address = {};
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
address.sin_port = htons(static_cast<u_short>(port));
|
||||
if (bind(mSocket.get(), reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0)
|
||||
{
|
||||
error = "Could not bind OSC listener to UDP port " + std::to_string(port) + ".";
|
||||
mSocket.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
mRunning = true;
|
||||
mThread = std::thread(&OscServer::ServerLoop, this);
|
||||
return true;
|
||||
}
|
||||
|
||||
void OscServer::Stop()
|
||||
{
|
||||
mRunning = false;
|
||||
mSocket.reset();
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
}
|
||||
|
||||
void OscServer::ServerLoop()
|
||||
{
|
||||
std::array<char, 4096> buffer = {};
|
||||
while (mRunning)
|
||||
{
|
||||
sockaddr_in sender = {};
|
||||
int senderLength = sizeof(sender);
|
||||
const int byteCount = recvfrom(mSocket.get(), buffer.data(), static_cast<int>(buffer.size()), 0,
|
||||
reinterpret_cast<sockaddr*>(&sender), &senderLength);
|
||||
if (byteCount <= 0)
|
||||
continue;
|
||||
|
||||
OscMessage message;
|
||||
std::string error;
|
||||
if (DecodeMessage(buffer.data(), byteCount, message, error))
|
||||
DispatchMessage(message, error);
|
||||
}
|
||||
}
|
||||
|
||||
bool OscServer::DecodeMessage(const char* data, int byteCount, OscMessage& message, std::string& error) const
|
||||
{
|
||||
int offset = 0;
|
||||
if (!ReadPaddedString(data, byteCount, offset, message.address) || message.address.empty() || message.address[0] != '/')
|
||||
{
|
||||
error = "Invalid OSC address.";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string typeTags;
|
||||
if (!ReadPaddedString(data, byteCount, offset, typeTags) || typeTags.empty() || typeTags[0] != ',')
|
||||
{
|
||||
error = "Invalid OSC type tag string.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeTags.size() < 2)
|
||||
{
|
||||
error = "OSC message has no parameter value.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const char valueType = typeTags[1];
|
||||
if (valueType == 'f')
|
||||
{
|
||||
double value = 0.0;
|
||||
if (!ReadFloat32(data, byteCount, offset, value))
|
||||
return false;
|
||||
std::ostringstream stream;
|
||||
stream << std::setprecision(9) << value;
|
||||
message.valueJson = stream.str();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (valueType == 'i')
|
||||
{
|
||||
int value = 0;
|
||||
if (!ReadInt32(data, byteCount, offset, value))
|
||||
return false;
|
||||
message.valueJson = std::to_string(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (valueType == 's')
|
||||
{
|
||||
std::string value;
|
||||
if (!ReadPaddedString(data, byteCount, offset, value))
|
||||
return false;
|
||||
message.valueJson = BuildJsonString(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (valueType == 'T' || valueType == 'F')
|
||||
{
|
||||
message.valueJson = valueType == 'T' ? "true" : "false";
|
||||
return true;
|
||||
}
|
||||
|
||||
error = "Unsupported OSC value type.";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OscServer::DispatchMessage(const OscMessage& message, std::string& error) const
|
||||
{
|
||||
const std::vector<std::string> parts = SplitAddress(message.address);
|
||||
if (parts.size() != 3 || parts[0] != "VideoShaderToys")
|
||||
{
|
||||
error = "Unsupported OSC address: " + message.address;
|
||||
return false;
|
||||
}
|
||||
|
||||
return mCallbacks.updateParameter &&
|
||||
mCallbacks.updateParameter(parts[1], parts[2], message.valueJson, error);
|
||||
}
|
||||
|
||||
bool OscServer::ReadPaddedString(const char* data, int byteCount, int& offset, std::string& value)
|
||||
{
|
||||
if (offset < 0 || offset >= byteCount)
|
||||
return false;
|
||||
|
||||
const int start = offset;
|
||||
while (offset < byteCount && data[offset] != '\0')
|
||||
++offset;
|
||||
if (offset >= byteCount)
|
||||
return false;
|
||||
|
||||
value.assign(data + start, data + offset);
|
||||
++offset;
|
||||
while (offset % 4 != 0)
|
||||
++offset;
|
||||
return offset <= byteCount;
|
||||
}
|
||||
|
||||
bool OscServer::ReadInt32(const char* data, int byteCount, int& offset, int& value)
|
||||
{
|
||||
if (offset + 4 > byteCount)
|
||||
return false;
|
||||
const unsigned char* bytes = reinterpret_cast<const unsigned char*>(data + offset);
|
||||
value = static_cast<int>((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]);
|
||||
offset += 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OscServer::ReadFloat32(const char* data, int byteCount, int& offset, double& value)
|
||||
{
|
||||
int bits = 0;
|
||||
if (!ReadInt32(data, byteCount, offset, bits))
|
||||
return false;
|
||||
|
||||
float floatValue = 0.0f;
|
||||
const unsigned int unsignedBits = static_cast<unsigned int>(bits);
|
||||
std::memcpy(&floatValue, &unsignedBits, sizeof(floatValue));
|
||||
value = static_cast<double>(floatValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string OscServer::BuildJsonString(const std::string& value)
|
||||
{
|
||||
std::ostringstream stream;
|
||||
stream << '"';
|
||||
for (char ch : value)
|
||||
{
|
||||
if (ch == '"' || ch == '\\')
|
||||
stream << '\\';
|
||||
stream << ch;
|
||||
}
|
||||
stream << '"';
|
||||
return stream.str();
|
||||
}
|
||||
48
apps/LoopThroughWithOpenGLCompositing/OscServer.h
Normal file
48
apps/LoopThroughWithOpenGLCompositing/OscServer.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "NativeSockets.h"
|
||||
|
||||
#include <winsock2.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
class OscServer
|
||||
{
|
||||
public:
|
||||
struct Callbacks
|
||||
{
|
||||
std::function<bool(const std::string&, const std::string&, const std::string&, std::string&)> updateParameter;
|
||||
};
|
||||
|
||||
OscServer();
|
||||
~OscServer();
|
||||
|
||||
bool Start(unsigned short port, const Callbacks& callbacks, std::string& error);
|
||||
void Stop();
|
||||
|
||||
unsigned short GetPort() const { return mPort; }
|
||||
|
||||
private:
|
||||
struct OscMessage
|
||||
{
|
||||
std::string address;
|
||||
std::string valueJson;
|
||||
};
|
||||
|
||||
void ServerLoop();
|
||||
bool DecodeMessage(const char* data, int byteCount, OscMessage& message, std::string& error) const;
|
||||
bool DispatchMessage(const OscMessage& message, std::string& error) const;
|
||||
static bool ReadPaddedString(const char* data, int byteCount, int& offset, std::string& value);
|
||||
static bool ReadInt32(const char* data, int byteCount, int& offset, int& value);
|
||||
static bool ReadFloat32(const char* data, int byteCount, int& offset, double& value);
|
||||
static std::string BuildJsonString(const std::string& value);
|
||||
|
||||
Callbacks mCallbacks;
|
||||
UniqueSocket mSocket;
|
||||
unsigned short mPort;
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mRunning;
|
||||
};
|
||||
@@ -37,6 +37,22 @@ std::string ToLowerCopy(std::string text)
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string SimplifyControlKey(const std::string& text)
|
||||
{
|
||||
std::string simplified;
|
||||
for (unsigned char ch : text)
|
||||
{
|
||||
if (std::isalnum(ch))
|
||||
simplified.push_back(static_cast<char>(std::tolower(ch)));
|
||||
}
|
||||
return simplified;
|
||||
}
|
||||
|
||||
bool MatchesControlKey(const std::string& candidate, const std::string& key)
|
||||
{
|
||||
return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key);
|
||||
}
|
||||
|
||||
std::vector<double> JsonArrayToNumbers(const JsonValue& value)
|
||||
{
|
||||
std::vector<double> numbers;
|
||||
@@ -871,6 +887,52 @@ bool RuntimeHost::UpdateLayerParameter(const std::string& layerId, const std::st
|
||||
return SavePersistentState(error);
|
||||
}
|
||||
|
||||
bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
|
||||
LayerPersistentState* matchedLayer = nullptr;
|
||||
const ShaderPackage* matchedPackage = nullptr;
|
||||
for (LayerPersistentState& layer : mPersistentState.layers)
|
||||
{
|
||||
auto shaderIt = mPackagesById.find(layer.shaderId);
|
||||
if (shaderIt == mPackagesById.end())
|
||||
continue;
|
||||
|
||||
if (MatchesControlKey(layer.id, layerKey) || MatchesControlKey(shaderIt->second.id, layerKey) ||
|
||||
MatchesControlKey(shaderIt->second.displayName, layerKey))
|
||||
{
|
||||
matchedLayer = &layer;
|
||||
matchedPackage = &shaderIt->second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchedLayer || !matchedPackage)
|
||||
{
|
||||
error = "Unknown OSC layer key: " + layerKey;
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto parameterIt = std::find_if(matchedPackage->parameters.begin(), matchedPackage->parameters.end(),
|
||||
[¶meterKey](const ShaderParameterDefinition& definition)
|
||||
{
|
||||
return MatchesControlKey(definition.id, parameterKey) || MatchesControlKey(definition.label, parameterKey);
|
||||
});
|
||||
if (parameterIt == matchedPackage->parameters.end())
|
||||
{
|
||||
error = "Unknown OSC parameter key: " + parameterKey;
|
||||
return false;
|
||||
}
|
||||
|
||||
ShaderParameterValue normalized;
|
||||
if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error))
|
||||
return false;
|
||||
|
||||
matchedLayer->parameterValues[parameterIt->id] = normalized;
|
||||
return SavePersistentState(error);
|
||||
}
|
||||
|
||||
bool RuntimeHost::ResetLayerParameters(const std::string& layerId, std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
@@ -1109,6 +1171,8 @@ bool RuntimeHost::LoadConfig(std::string& error)
|
||||
mConfig.shaderLibrary = shaderLibraryValue->asString();
|
||||
if (const JsonValue* serverPortValue = configJson.find("serverPort"))
|
||||
mConfig.serverPort = static_cast<unsigned short>(serverPortValue->asNumber(mConfig.serverPort));
|
||||
if (const JsonValue* oscPortValue = configJson.find("oscPort"))
|
||||
mConfig.oscPort = static_cast<unsigned short>(oscPortValue->asNumber(mConfig.oscPort));
|
||||
if (const JsonValue* autoReloadValue = configJson.find("autoReload"))
|
||||
mConfig.autoReload = autoReloadValue->asBoolean(mConfig.autoReload);
|
||||
if (const JsonValue* maxTemporalHistoryFramesValue = configJson.find("maxTemporalHistoryFrames"))
|
||||
@@ -1412,6 +1476,7 @@ JsonValue RuntimeHost::BuildStateValue() const
|
||||
|
||||
JsonValue app = JsonValue::MakeObject();
|
||||
app.set("serverPort", JsonValue(static_cast<double>(mServerPort)));
|
||||
app.set("oscPort", JsonValue(static_cast<double>(mConfig.oscPort)));
|
||||
app.set("autoReload", JsonValue(mAutoReloadEnabled));
|
||||
app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(mConfig.maxTemporalHistoryFrames)));
|
||||
app.set("enableExternalKeying", JsonValue(mConfig.enableExternalKeying));
|
||||
|
||||
@@ -28,6 +28,7 @@ public:
|
||||
bool SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error);
|
||||
bool SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error);
|
||||
bool UpdateLayerParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue, std::string& error);
|
||||
bool UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error);
|
||||
bool ResetLayerParameters(const std::string& layerId, std::string& error);
|
||||
bool SaveStackPreset(const std::string& presetName, std::string& error) const;
|
||||
bool LoadStackPreset(const std::string& presetName, std::string& error);
|
||||
@@ -48,6 +49,7 @@ public:
|
||||
const std::filesystem::path& GetDocsRoot() const { return mDocsRoot; }
|
||||
const std::filesystem::path& GetRuntimeRoot() const { return mRuntimeRoot; }
|
||||
unsigned short GetServerPort() const { return mServerPort; }
|
||||
unsigned short GetOscPort() const { return mConfig.oscPort; }
|
||||
unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; }
|
||||
bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; }
|
||||
const std::string& GetVideoFormat() const { return mConfig.videoFormat; }
|
||||
@@ -60,6 +62,7 @@ private:
|
||||
{
|
||||
std::string shaderLibrary = "shaders";
|
||||
unsigned short serverPort = 8080;
|
||||
unsigned short oscPort = 9000;
|
||||
bool autoReload = true;
|
||||
unsigned maxTemporalHistoryFrames = 4;
|
||||
bool enableExternalKeying = false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"shaderLibrary": "shaders",
|
||||
"serverPort": 8080,
|
||||
"oscPort": 9000,
|
||||
"videoFormat": "1080p",
|
||||
"frameRate": "59.94",
|
||||
"autoReload": true,
|
||||
|
||||
65
docs/OSC_CONTROL.md
Normal file
65
docs/OSC_CONTROL.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# OSC Control
|
||||
|
||||
Video Shader Toys can listen for local OSC messages and map them onto shader layer parameters.
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the UDP port in `config/runtime-host.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"oscPort": 9000
|
||||
}
|
||||
```
|
||||
|
||||
Set `oscPort` to `0` to disable the OSC listener.
|
||||
|
||||
## Address Pattern
|
||||
|
||||
Send OSC messages to:
|
||||
|
||||
```text
|
||||
/VideoShaderToys/{LayerNameOrID}/{ParameterNameOrID}
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
/VideoShaderToys/layer-1/brightness
|
||||
/VideoShaderToys/VHS/intensity
|
||||
/VideoShaderToys/TemporalLowFPS/frameRate
|
||||
```
|
||||
|
||||
Layer keys are resolved against:
|
||||
|
||||
- Layer ID, such as `layer-1`
|
||||
- Shader package ID, such as `vhs`
|
||||
- Shader display name, such as `VHS`
|
||||
|
||||
Parameter keys are resolved against:
|
||||
|
||||
- Parameter ID from `shader.json`
|
||||
- Parameter label from `shader.json`
|
||||
|
||||
Matching is exact first. If that fails, names are compared in a simplified form that ignores spaces, underscores, hyphens, and casing. For live control, prefer stable IDs where possible.
|
||||
|
||||
## Values
|
||||
|
||||
The listener accepts one OSC argument per message:
|
||||
|
||||
- `f`: float
|
||||
- `i`: integer
|
||||
- `s`: string
|
||||
- `T` / `F`: boolean true/false
|
||||
|
||||
Values are validated with the same shader parameter rules used by the REST API. Invalid values or unknown addresses are ignored.
|
||||
|
||||
## Network
|
||||
|
||||
The listener binds to localhost only:
|
||||
|
||||
```text
|
||||
127.0.0.1:<oscPort>
|
||||
```
|
||||
|
||||
This keeps the control surface local to the machine running Video Shader Toys.
|
||||
@@ -359,6 +359,8 @@ components:
|
||||
properties:
|
||||
serverPort:
|
||||
type: number
|
||||
oscPort:
|
||||
type: number
|
||||
autoReload:
|
||||
type: boolean
|
||||
maxTemporalHistoryFrames:
|
||||
|
||||
@@ -9,6 +9,7 @@ Tracked files:
|
||||
Packaged documentation:
|
||||
|
||||
- `../docs/openapi.yaml`: OpenAPI/Swagger spec for the local control API.
|
||||
- `../docs/OSC_CONTROL.md`: OSC address and value reference.
|
||||
- `http://127.0.0.1:<serverPort>/docs`: Swagger UI page served by the native control server.
|
||||
|
||||
Generated files:
|
||||
|
||||
Reference in New Issue
Block a user