diff --git a/README.md b/README.md index 3984b56..c41a09a 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ Current native test coverage includes: { "shaderLibrary": "shaders", "serverPort": 8080, + "oscBindAddress": "127.0.0.1", "oscPort": 9000, "inputVideoFormat": "1080p", "inputFrameRate": "59.94", @@ -203,13 +204,13 @@ runtime/screenshots/ ## 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 /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 diff --git a/apps/LoopThroughWithOpenGLCompositing/control/OscServer.cpp b/apps/LoopThroughWithOpenGLCompositing/control/OscServer.cpp index cc38780..4cbd039 100644 --- a/apps/LoopThroughWithOpenGLCompositing/control/OscServer.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/control/OscServer.cpp @@ -55,7 +55,7 @@ OscServer::~OscServer() 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) return true; @@ -78,11 +78,15 @@ bool OscServer::Start(unsigned short port, const Callbacks& callbacks, std::stri sockaddr_in address = {}; 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(port)); if (bind(mSocket.get(), reinterpret_cast(&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(); return false; } @@ -92,6 +96,24 @@ bool OscServer::Start(unsigned short port, const Callbacks& callbacks, std::stri 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() { mRunning = false; diff --git a/apps/LoopThroughWithOpenGLCompositing/control/OscServer.h b/apps/LoopThroughWithOpenGLCompositing/control/OscServer.h index f4fb0b4..c98392e 100644 --- a/apps/LoopThroughWithOpenGLCompositing/control/OscServer.h +++ b/apps/LoopThroughWithOpenGLCompositing/control/OscServer.h @@ -20,7 +20,7 @@ public: 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(); unsigned short GetPort() const { return mPort; } @@ -37,6 +37,7 @@ private: 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 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 ReadPaddedString(const char* data, int byteCount, int& offset, std::string& value); static bool ReadInt32(const char* data, int byteCount, int& offset, int& value); diff --git a/apps/LoopThroughWithOpenGLCompositing/control/RuntimeControlBridge.cpp b/apps/LoopThroughWithOpenGLCompositing/control/RuntimeControlBridge.cpp index 11764bd..72bc15b 100644 --- a/apps/LoopThroughWithOpenGLCompositing/control/RuntimeControlBridge.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/control/RuntimeControlBridge.cpp @@ -44,7 +44,7 @@ bool StartRuntimeControlServices( 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); }; - 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 true; diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h index a97918e..f1586c6 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h @@ -60,6 +60,7 @@ public: bool RequestScreenshot(std::string& error); unsigned short GetControlServerPort() const; unsigned short GetOscPort() const; + std::string GetOscBindAddress() const; std::string GetControlUrl() const; std::string GetDocsUrl() const; std::string GetOscAddress() const; diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLCompositeRuntimeControls.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLCompositeRuntimeControls.cpp index 38344ca..a0f7d2f 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLCompositeRuntimeControls.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLCompositeRuntimeControls.cpp @@ -15,6 +15,11 @@ unsigned short OpenGLComposite::GetOscPort() const return mRuntimeHost ? mRuntimeHost->GetOscPort() : 0; } +std::string OpenGLComposite::GetOscBindAddress() const +{ + return mRuntimeHost ? mRuntimeHost->GetOscBindAddress() : "127.0.0.1"; +} + std::string OpenGLComposite::GetControlUrl() const { return "http://127.0.0.1:" + std::to_string(GetControlServerPort()) + "/"; @@ -27,7 +32,7 @@ std::string OpenGLComposite::GetDocsUrl() 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) diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp index 7368ef1..bbea694 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp @@ -1474,6 +1474,8 @@ bool RuntimeHost::LoadConfig(std::string& error) mConfig.serverPort = static_cast(serverPortValue->asNumber(mConfig.serverPort)); if (const JsonValue* oscPortValue = configJson.find("oscPort")) mConfig.oscPort = static_cast(oscPortValue->asNumber(mConfig.oscPort)); + if (const JsonValue* oscBindAddressValue = configJson.find("oscBindAddress")) + mConfig.oscBindAddress = oscBindAddressValue->asString(); if (const JsonValue* autoReloadValue = configJson.find("autoReload")) mConfig.autoReload = autoReloadValue->asBoolean(mConfig.autoReload); if (const JsonValue* maxTemporalHistoryFramesValue = configJson.find("maxTemporalHistoryFrames")) @@ -1870,6 +1872,7 @@ JsonValue RuntimeHost::BuildStateValue() const JsonValue app = JsonValue::MakeObject(); app.set("serverPort", JsonValue(static_cast(mServerPort))); app.set("oscPort", JsonValue(static_cast(mConfig.oscPort))); + app.set("oscBindAddress", JsonValue(mConfig.oscBindAddress)); app.set("autoReload", JsonValue(mAutoReloadEnabled)); app.set("maxTemporalHistoryFrames", JsonValue(static_cast(mConfig.maxTemporalHistoryFrames))); app.set("previewFps", JsonValue(static_cast(mConfig.previewFps))); diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h index c27f8fb..cba1d47 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h @@ -64,6 +64,7 @@ public: const std::filesystem::path& GetRuntimeRoot() const { return mRuntimeRoot; } unsigned short GetServerPort() const { return mServerPort; } unsigned short GetOscPort() const { return mConfig.oscPort; } + const std::string& GetOscBindAddress() const { return mConfig.oscBindAddress; } unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; } unsigned GetPreviewFps() const { return mConfig.previewFps; } bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; } @@ -80,6 +81,7 @@ private: std::string shaderLibrary = "shaders"; unsigned short serverPort = 8080; unsigned short oscPort = 9000; + std::string oscBindAddress = "127.0.0.1"; bool autoReload = true; unsigned maxTemporalHistoryFrames = 4; unsigned previewFps = 30; diff --git a/config/runtime-host.json b/config/runtime-host.json index 18babc0..e20ef5b 100644 --- a/config/runtime-host.json +++ b/config/runtime-host.json @@ -1,6 +1,7 @@ { "shaderLibrary": "shaders", "serverPort": 8080, + "oscBindAddress": "127.0.0.1", "oscPort": 9000, "inputVideoFormat": "1080p", "inputFrameRate": "59.94", diff --git a/docs/OSC_CONTROL.md b/docs/OSC_CONTROL.md index 3e9c557..69ac619 100644 --- a/docs/OSC_CONTROL.md +++ b/docs/OSC_CONTROL.md @@ -8,11 +8,13 @@ Set the UDP port in `config/runtime-host.json`: ```json { + "oscBindAddress": "127.0.0.1", "oscPort": 9000 } ``` 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 @@ -114,10 +116,21 @@ send('127.0.0.1:9000', '/VideoShaderToys/fisheye-reproject/tiltDegrees', {type: ## Network -The listener binds to localhost only: +By default the listener binds to localhost only: ```text 127.0.0.1: ``` 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. diff --git a/tests/OscServerTests.cpp b/tests/OscServerTests.cpp index 6dc8fbd..ef1d75f 100644 --- a/tests/OscServerTests.cpp +++ b/tests/OscServerTests.cpp @@ -74,6 +74,11 @@ struct OscServerTestAccess 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( OscServer& server, const std::function& callback) @@ -191,6 +196,23 @@ void TestRejectsUnsupportedAddress() Expect(!called, "unsupported OSC namespace does not invoke callback"); 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() @@ -201,6 +223,7 @@ int main() TestDecodeIntStringAndBoolMessages(); TestDispatchValidAddress(); TestRejectsUnsupportedAddress(); + TestParsesOscBindAddress(); if (gFailures != 0) {