OSC bind address
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m41s
CI / Windows Release Package (push) Successful in 2m30s

This commit is contained in:
Aiden
2026-05-10 17:23:28 +10:00
parent a3635b5d31
commit f11d531e0c
11 changed files with 81 additions and 9 deletions

View File

@@ -141,6 +141,7 @@ Current native test coverage includes:
{ {
"shaderLibrary": "shaders", "shaderLibrary": "shaders",
"serverPort": 8080, "serverPort": 8080,
"oscBindAddress": "127.0.0.1",
"oscPort": 9000, "oscPort": 9000,
"inputVideoFormat": "1080p", "inputVideoFormat": "1080p",
"inputFrameRate": "59.94", "inputFrameRate": "59.94",
@@ -203,13 +204,13 @@ runtime/screenshots/
## OSC Control ## OSC Control
The native host also listens for local OSC parameter control on the configured `oscPort`: The native host also listens for OSC parameter control on the configured `oscBindAddress` and `oscPort`:
```text ```text
/VideoShaderToys/{LayerNameOrID}/{ParameterNameOrID} /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. 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. The default bind address is `127.0.0.1`; set `oscBindAddress` to `0.0.0.0` to accept OSC on all IPv4 interfaces. See `docs/OSC_CONTROL.md` for details.
## Shader Packages ## Shader Packages

View File

@@ -55,7 +55,7 @@ OscServer::~OscServer()
Stop(); Stop();
} }
bool OscServer::Start(unsigned short port, const Callbacks& callbacks, std::string& error) bool OscServer::Start(const std::string& bindAddress, unsigned short port, const Callbacks& callbacks, std::string& error)
{ {
if (port == 0) if (port == 0)
return true; return true;
@@ -78,11 +78,15 @@ bool OscServer::Start(unsigned short port, const Callbacks& callbacks, std::stri
sockaddr_in address = {}; sockaddr_in address = {};
address.sin_family = AF_INET; address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); if (!TryParseBindAddress(bindAddress, address.sin_addr, error))
{
mSocket.reset();
return false;
}
address.sin_port = htons(static_cast<u_short>(port)); address.sin_port = htons(static_cast<u_short>(port));
if (bind(mSocket.get(), reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0) if (bind(mSocket.get(), reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0)
{ {
error = "Could not bind OSC listener to UDP port " + std::to_string(port) + "."; error = "Could not bind OSC listener to " + bindAddress + ":" + std::to_string(port) + ".";
mSocket.reset(); mSocket.reset();
return false; return false;
} }
@@ -92,6 +96,24 @@ bool OscServer::Start(unsigned short port, const Callbacks& callbacks, std::stri
return true; return true;
} }
bool OscServer::TryParseBindAddress(const std::string& bindAddress, in_addr& address, std::string& error)
{
if (bindAddress.empty())
{
error = "OSC bind address must not be empty.";
return false;
}
address = {};
if (InetPtonA(AF_INET, bindAddress.c_str(), &address) != 1)
{
error = "Invalid OSC bind address '" + bindAddress + "'. Use an IPv4 address such as 127.0.0.1 or 0.0.0.0.";
return false;
}
return true;
}
void OscServer::Stop() void OscServer::Stop()
{ {
mRunning = false; mRunning = false;

View File

@@ -20,7 +20,7 @@ public:
OscServer(); OscServer();
~OscServer(); ~OscServer();
bool Start(unsigned short port, const Callbacks& callbacks, std::string& error); bool Start(const std::string& bindAddress, unsigned short port, const Callbacks& callbacks, std::string& error);
void Stop(); void Stop();
unsigned short GetPort() const { return mPort; } unsigned short GetPort() const { return mPort; }
@@ -37,6 +37,7 @@ private:
void ServerLoop(); void ServerLoop();
bool DecodeMessage(const char* data, int byteCount, OscMessage& message, std::string& error) const; bool DecodeMessage(const char* data, int byteCount, OscMessage& message, std::string& error) const;
bool DispatchMessage(const OscMessage& message, std::string& error) const; bool DispatchMessage(const OscMessage& message, std::string& error) const;
static bool TryParseBindAddress(const std::string& bindAddress, in_addr& address, std::string& error);
static bool DecodeArgument(const char* data, int byteCount, int& offset, char valueType, std::string& valueJson); static bool DecodeArgument(const char* data, int byteCount, int& offset, char valueType, std::string& valueJson);
static bool ReadPaddedString(const char* data, int byteCount, int& offset, std::string& value); 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 ReadInt32(const char* data, int byteCount, int& offset, int& value);

View File

@@ -44,7 +44,7 @@ bool StartRuntimeControlServices(
oscCallbacks.updateParameter = [&composite](const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& actionError) { oscCallbacks.updateParameter = [&composite](const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& actionError) {
return composite.UpdateLayerParameterByControlKeyJson(layerKey, parameterKey, valueJson, actionError); return composite.UpdateLayerParameterByControlKeyJson(layerKey, parameterKey, valueJson, actionError);
}; };
if (runtimeHost.GetOscPort() > 0 && !oscServer.Start(runtimeHost.GetOscPort(), oscCallbacks, error)) if (runtimeHost.GetOscPort() > 0 && !oscServer.Start(runtimeHost.GetOscBindAddress(), runtimeHost.GetOscPort(), oscCallbacks, error))
return false; return false;
return true; return true;

View File

@@ -60,6 +60,7 @@ public:
bool RequestScreenshot(std::string& error); bool RequestScreenshot(std::string& error);
unsigned short GetControlServerPort() const; unsigned short GetControlServerPort() const;
unsigned short GetOscPort() const; unsigned short GetOscPort() const;
std::string GetOscBindAddress() const;
std::string GetControlUrl() const; std::string GetControlUrl() const;
std::string GetDocsUrl() const; std::string GetDocsUrl() const;
std::string GetOscAddress() const; std::string GetOscAddress() const;

View File

@@ -15,6 +15,11 @@ unsigned short OpenGLComposite::GetOscPort() const
return mRuntimeHost ? mRuntimeHost->GetOscPort() : 0; return mRuntimeHost ? mRuntimeHost->GetOscPort() : 0;
} }
std::string OpenGLComposite::GetOscBindAddress() const
{
return mRuntimeHost ? mRuntimeHost->GetOscBindAddress() : "127.0.0.1";
}
std::string OpenGLComposite::GetControlUrl() const std::string OpenGLComposite::GetControlUrl() const
{ {
return "http://127.0.0.1:" + std::to_string(GetControlServerPort()) + "/"; return "http://127.0.0.1:" + std::to_string(GetControlServerPort()) + "/";
@@ -27,7 +32,7 @@ std::string OpenGLComposite::GetDocsUrl() const
std::string OpenGLComposite::GetOscAddress() const std::string OpenGLComposite::GetOscAddress() const
{ {
return "udp://127.0.0.1:" + std::to_string(GetOscPort()) + " /VideoShaderToys/{Layer}/{Parameter}"; return "udp://" + GetOscBindAddress() + ":" + std::to_string(GetOscPort()) + " /VideoShaderToys/{Layer}/{Parameter}";
} }
bool OpenGLComposite::AddLayer(const std::string& shaderId, std::string& error) bool OpenGLComposite::AddLayer(const std::string& shaderId, std::string& error)

View File

@@ -1474,6 +1474,8 @@ bool RuntimeHost::LoadConfig(std::string& error)
mConfig.serverPort = static_cast<unsigned short>(serverPortValue->asNumber(mConfig.serverPort)); mConfig.serverPort = static_cast<unsigned short>(serverPortValue->asNumber(mConfig.serverPort));
if (const JsonValue* oscPortValue = configJson.find("oscPort")) if (const JsonValue* oscPortValue = configJson.find("oscPort"))
mConfig.oscPort = static_cast<unsigned short>(oscPortValue->asNumber(mConfig.oscPort)); mConfig.oscPort = static_cast<unsigned short>(oscPortValue->asNumber(mConfig.oscPort));
if (const JsonValue* oscBindAddressValue = configJson.find("oscBindAddress"))
mConfig.oscBindAddress = oscBindAddressValue->asString();
if (const JsonValue* autoReloadValue = configJson.find("autoReload")) if (const JsonValue* autoReloadValue = configJson.find("autoReload"))
mConfig.autoReload = autoReloadValue->asBoolean(mConfig.autoReload); mConfig.autoReload = autoReloadValue->asBoolean(mConfig.autoReload);
if (const JsonValue* maxTemporalHistoryFramesValue = configJson.find("maxTemporalHistoryFrames")) if (const JsonValue* maxTemporalHistoryFramesValue = configJson.find("maxTemporalHistoryFrames"))
@@ -1870,6 +1872,7 @@ JsonValue RuntimeHost::BuildStateValue() const
JsonValue app = JsonValue::MakeObject(); JsonValue app = JsonValue::MakeObject();
app.set("serverPort", JsonValue(static_cast<double>(mServerPort))); app.set("serverPort", JsonValue(static_cast<double>(mServerPort)));
app.set("oscPort", JsonValue(static_cast<double>(mConfig.oscPort))); app.set("oscPort", JsonValue(static_cast<double>(mConfig.oscPort)));
app.set("oscBindAddress", JsonValue(mConfig.oscBindAddress));
app.set("autoReload", JsonValue(mAutoReloadEnabled)); app.set("autoReload", JsonValue(mAutoReloadEnabled));
app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(mConfig.maxTemporalHistoryFrames))); app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(mConfig.maxTemporalHistoryFrames)));
app.set("previewFps", JsonValue(static_cast<double>(mConfig.previewFps))); app.set("previewFps", JsonValue(static_cast<double>(mConfig.previewFps)));

View File

@@ -64,6 +64,7 @@ public:
const std::filesystem::path& GetRuntimeRoot() const { return mRuntimeRoot; } const std::filesystem::path& GetRuntimeRoot() const { return mRuntimeRoot; }
unsigned short GetServerPort() const { return mServerPort; } unsigned short GetServerPort() const { return mServerPort; }
unsigned short GetOscPort() const { return mConfig.oscPort; } unsigned short GetOscPort() const { return mConfig.oscPort; }
const std::string& GetOscBindAddress() const { return mConfig.oscBindAddress; }
unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; } unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; }
unsigned GetPreviewFps() const { return mConfig.previewFps; } unsigned GetPreviewFps() const { return mConfig.previewFps; }
bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; } bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; }
@@ -80,6 +81,7 @@ private:
std::string shaderLibrary = "shaders"; std::string shaderLibrary = "shaders";
unsigned short serverPort = 8080; unsigned short serverPort = 8080;
unsigned short oscPort = 9000; unsigned short oscPort = 9000;
std::string oscBindAddress = "127.0.0.1";
bool autoReload = true; bool autoReload = true;
unsigned maxTemporalHistoryFrames = 4; unsigned maxTemporalHistoryFrames = 4;
unsigned previewFps = 30; unsigned previewFps = 30;

View File

@@ -1,6 +1,7 @@
{ {
"shaderLibrary": "shaders", "shaderLibrary": "shaders",
"serverPort": 8080, "serverPort": 8080,
"oscBindAddress": "127.0.0.1",
"oscPort": 9000, "oscPort": 9000,
"inputVideoFormat": "1080p", "inputVideoFormat": "1080p",
"inputFrameRate": "59.94", "inputFrameRate": "59.94",

View File

@@ -8,11 +8,13 @@ Set the UDP port in `config/runtime-host.json`:
```json ```json
{ {
"oscBindAddress": "127.0.0.1",
"oscPort": 9000 "oscPort": 9000
} }
``` ```
Set `oscPort` to `0` to disable the OSC listener. Set `oscPort` to `0` to disable the OSC listener.
Set `oscBindAddress` to `127.0.0.1` to keep OSC local to the host, or `0.0.0.0` to listen on all IPv4 interfaces.
## Address Pattern ## Address Pattern
@@ -114,10 +116,21 @@ send('127.0.0.1:9000', '/VideoShaderToys/fisheye-reproject/tiltDegrees', {type:
## Network ## Network
The listener binds to localhost only: By default the listener binds to localhost only:
```text ```text
127.0.0.1:<oscPort> 127.0.0.1:<oscPort>
``` ```
This keeps the control surface local to the machine running Video Shader Toys. This keeps the control surface local to the machine running Video Shader Toys.
To accept OSC from other machines on the network, set:
```json
{
"oscBindAddress": "0.0.0.0",
"oscPort": 9000
}
```
That listens on all IPv4 interfaces, so make sure your firewall and network are configured appropriately.

View File

@@ -74,6 +74,11 @@ struct OscServerTestAccess
return server.DispatchMessage(message, error); return server.DispatchMessage(message, error);
} }
static bool TryParseBindAddress(const std::string& bindAddress, in_addr& address, std::string& error)
{
return OscServer::TryParseBindAddress(bindAddress, address, error);
}
static void SetUpdateParameterCallback( static void SetUpdateParameterCallback(
OscServer& server, OscServer& server,
const std::function<bool(const std::string&, const std::string&, const std::string&, std::string&)>& callback) const std::function<bool(const std::string&, const std::string&, const std::string&, std::string&)>& callback)
@@ -191,6 +196,23 @@ void TestRejectsUnsupportedAddress()
Expect(!called, "unsupported OSC namespace does not invoke callback"); Expect(!called, "unsupported OSC namespace does not invoke callback");
Expect(!error.empty(), "unsupported OSC address reports an error"); Expect(!error.empty(), "unsupported OSC address reports an error");
} }
void TestParsesOscBindAddress()
{
in_addr loopback = {};
std::string error;
Expect(OscServerTestAccess::TryParseBindAddress("127.0.0.1", loopback, error), "loopback OSC bind address parses");
Expect(loopback.S_un.S_addr != 0, "loopback OSC bind address produces a socket address");
in_addr wildcard = {};
error.clear();
Expect(OscServerTestAccess::TryParseBindAddress("0.0.0.0", wildcard, error), "wildcard OSC bind address parses");
in_addr invalid = {};
error.clear();
Expect(!OscServerTestAccess::TryParseBindAddress("localhost", invalid, error), "hostname OSC bind address is rejected");
Expect(!error.empty(), "invalid OSC bind address reports an error");
}
} }
int main() int main()
@@ -201,6 +223,7 @@ int main()
TestDecodeIntStringAndBoolMessages(); TestDecodeIntStringAndBoolMessages();
TestDispatchValidAddress(); TestDispatchValidAddress();
TestRejectsUnsupportedAddress(); TestRejectsUnsupportedAddress();
TestParsesOscBindAddress();
if (gFailures != 0) if (gFailures != 0)
{ {