#pragma once #include "AppConfig.h" #include "AppConfigJson.h" #include "AppConfigProvider.h" #include "AppRestart.h" #include "RenderCadenceHttpRoutes.h" #include "RuntimeContentController.h" #include "../logging/Logger.h" #include "../control/RuntimeStateJson.h" #include "../control/osc/OscControlServer.h" #include "../json/JsonWriter.h" #include "../preview/PreviewWindowThread.h" #include "../telemetry/TelemetryHealthMonitor.h" #include "../video/ndi/NdiSourceDiscovery.h" #include "VideoIOEdges.h" #include "VideoOutputThread.h" #include #include #include #include #include #include #include #include #include namespace RenderCadenceCompositor { namespace detail { template auto StartRenderThread(RenderThread& renderThread, std::string& error, int) -> decltype(renderThread.Start(error), bool()) { return renderThread.Start(error); } template bool StartRenderThreadWithoutError(RenderThread& renderThread, std::true_type) { return renderThread.Start(); } template bool StartRenderThreadWithoutError(RenderThread& renderThread, std::false_type) { renderThread.Start(); return true; } template auto StartRenderThread(RenderThread& renderThread, std::string&, long) -> decltype(renderThread.Start(), bool()) { return StartRenderThreadWithoutError(renderThread, std::is_same()); } } template class RenderCadenceApp { public: RenderCadenceApp( RenderThread& renderThread, SystemFrameExchange& frameExchange, AppConfig config, std::unique_ptr output, std::unique_ptr runtimeContent, std::filesystem::path configPath = std::filesystem::path()) : mRenderThread(renderThread), mFrameExchange(frameExchange), mConfig(config), mConfigPath(std::move(configPath)), mOutput(std::move(output)), mRuntimeContent(std::move(runtimeContent)), mOutputThread(*mOutput, mFrameExchange, VideoOutputThreadConfig{ mConfig.outputThread.targetBufferedFrames, mConfig.outputThread.idleSleep }), mTelemetryHealth(mConfig.telemetry) { } RenderCadenceApp(const RenderCadenceApp&) = delete; RenderCadenceApp& operator=(const RenderCadenceApp&) = delete; ~RenderCadenceApp() { Stop(); } bool Start(std::string& error) { if (mRuntimeContent) mRuntimeContent->Initialize(mConfig); Log("app", "Starting render thread."); if (!detail::StartRenderThread(mRenderThread, error, 0)) { LogError("app", "Render thread start failed: " + error); Stop(); return false; } if (mRuntimeContent) mRuntimeContent->Start(); if (!BuildSettledOutputReserve(error)) { LogError("app", error); Stop(); return false; } StartPreviewWindow(); StartOptionalVideoOutput(); mTelemetryHealth.Start(mFrameExchange, *mOutput, mOutputThread, mRenderThread); StartOscServer(); StartHttpServer(); Log("app", "RenderCadenceCompositor started."); mStarted = true; return true; } void Stop() { mHttpServer.Stop(); mOscServer.Stop(); mTelemetryHealth.Stop(); mPreviewWindow.Stop(); mOutputThread.Stop(); mOutput->Stop(); if (mRuntimeContent) mRuntimeContent->Stop(); mRenderThread.Stop(); mOutput->ReleaseResources(); if (mStarted) Log("app", "RenderCadenceCompositor shutdown complete."); mStarted = false; } bool Started() const { return mStarted; } const IVideoOutputEdge& Output() const { return *mOutput; } void SetVideoInputMetricsProvider(std::function provider) { mVideoInputMetricsProvider = std::move(provider); } private: void StartOptionalVideoOutput() { if (mConfig.output.backend == "none") { mVideoOutputEnabled = false; mVideoOutputStatus = "Video output backend disabled by config."; Log("app", mVideoOutputStatus); return; } std::string outputError; Log("app", "Initializing optional video output backend: " + mConfig.output.backend + "."); VideoOutputEdgeConfig outputConfig; outputConfig.outputVideoMode = mConfig.output.videoMode; outputConfig.systemFramePixelFormat = mConfig.output.systemFramePixelFormat; outputConfig.externalKeyingEnabled = mConfig.output.externalKeyingEnabled; outputConfig.outputAlphaRequired = mConfig.output.outputAlphaRequired; if (!mOutput->Initialize( outputConfig, [this](const VideoIOCompletion& completion) { mFrameExchange.ReleaseScheduledByBytes(completion.outputFrameBuffer); }, outputError)) { DisableVideoOutput("Video output unavailable: " + outputError); return; } if (mOutput->RequiresPreroll()) { Log("app", "Starting video output thread."); if (!mOutputThread.Start()) { DisableVideoOutput("Video output thread failed to start."); return; } Log("app", "Waiting for video output preroll frames."); if (!WaitForPreroll()) { DisableVideoOutput("Timed out waiting for video output preroll frames."); return; } Log("app", "Starting scheduled video playback."); if (!mOutput->StartScheduledPlayback(outputError)) { DisableVideoOutput("Scheduled video playback failed: " + outputError); return; } } else { Log("app", "Starting video output backend without preroll."); if (!mOutput->StartScheduledPlayback(outputError)) { DisableVideoOutput("Video output failed to start: " + outputError); return; } if (!mOutputThread.Start()) { DisableVideoOutput("Video output thread failed to start."); return; } } mVideoOutputEnabled = true; mVideoOutputStatus = mConfig.output.backend + " scheduled output running."; Log("app", mVideoOutputStatus); Log( "app", "Video output mode: " + mOutput->State().outputDisplayModeName + ", frame budget " + std::to_string(mOutput->State().frameBudgetMilliseconds) + " ms."); } bool BuildSettledOutputReserve(std::string& error) { const auto reserveTimeout = mConfig.warmupTimeout; Log("app", "Building output preroll reserve: waiting for " + std::to_string(mConfig.warmupCompletedFrames) + " completed frame(s)."); if (mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, reserveTimeout)) { return true; } error = "Timed out waiting for output preroll reserve."; return false; } void DisableVideoOutput(const std::string& reason) { mOutputThread.Stop(); mOutput->Stop(); mOutput->ReleaseResources(); mFrameExchange.Clear(); mVideoOutputEnabled = false; mVideoOutputStatus = reason; LogWarning("app", reason + " Continuing without video output."); } void StartHttpServer() { RenderCadenceHttpRouteCallbacks routeCallbacks; routeCallbacks.getStateJson = [this]() { return BuildStateJson(); }; routeCallbacks.getConfigJson = [this]() { return BuildConfigJson(); }; routeCallbacks.getNdiSourcesJson = [this]() { return BuildNdiSourcesJson(); }; routeCallbacks.resolveShaderAssetPath = [this]( const std::string& shaderId, const std::string& assetPath, std::filesystem::path& resolvedPath, std::string& assetError) { if (!mRuntimeContent) { assetError = "No runtime content controller is active."; return false; } return mRuntimeContent->ResolveAssetPath(shaderId, assetPath, resolvedPath, assetError); }; routeCallbacks.executePost = [this](const std::string& path, const std::string& body) { if (path == "/api/config/save") return HandleConfigSave(body); if (path == "/api/app/restart") return HandleAppRestart(); if (mRuntimeContent) return mRuntimeContent->HandlePost(path, body); return ControlActionResult{ false, "No runtime content controller is active." }; }; 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"), FindRepoPath("docs"), mConfig.http, callbacks, error)) { LogWarning("http", "HTTP control server did not start: " + error); return; } } void StartPreviewWindow() { if (!mConfig.previewEnabled) return; PreviewWindowConfig previewConfig; previewConfig.enabled = true; previewConfig.fps = mConfig.previewFps; std::string error; if (mPreviewWindow.Start(mFrameExchange, previewConfig, error)) { Log("preview", "Preview window thread started."); return; } LogWarning("preview", "Preview window did not start: " + error); } std::string BuildStateJson() { CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, *mOutput, mOutputThread, mRenderThread); ApplyVideoInputMetrics(telemetry); RuntimeStateJsonInput stateInput{ mConfig, telemetry, mHttpServer.Port(), mVideoOutputEnabled, mVideoOutputStatus, &mOscServer.State() }; if (mRuntimeContent) { stateInput.writeRuntimeJson = [this, telemetry](JsonWriter& writer) { mRuntimeContent->WriteRuntimeJson(writer, telemetry); }; stateInput.writeCatalogJson = [this](JsonWriter& writer) { mRuntimeContent->WriteCatalogJson(writer); }; stateInput.writeLayersJson = [this, telemetry](JsonWriter& writer) { mRuntimeContent->WriteLayersJson(writer, telemetry); }; } return RuntimeStateToJson(stateInput); } void StartOscServer() { OscControlServerConfig oscConfig; oscConfig.bindAddress = mConfig.oscBindAddress; oscConfig.port = mConfig.oscPort; oscConfig.smoothing = mConfig.oscSmoothing; std::string error; if (!mOscServer.Start(oscConfig, error)) { LogWarning("osc", "OSC control stub did not start: " + error); return; } if (!mOscServer.State().statusMessage.empty()) Log("osc", mOscServer.State().statusMessage); } std::string BuildConfigJson() const { AppConfig diskConfig = mConfig; std::string loadError; bool diskLoaded = false; if (!mConfigPath.empty()) { AppConfigProvider provider; diskLoaded = provider.Load(mConfigPath, loadError); if (diskLoaded) diskConfig = provider.Config(); } JsonValue root = JsonValue::MakeObject(); root.set("ok", JsonValue(true)); root.set("path", JsonValue(mConfigPath.string())); root.set("active", AppConfigToJsonValue(mConfig)); root.set("disk", AppConfigToJsonValue(diskConfig)); root.set("diskLoaded", JsonValue(diskLoaded)); root.set("restartRequired", JsonValue(mRestartRequired.load(std::memory_order_relaxed))); if (!loadError.empty()) root.set("error", JsonValue(loadError)); return SerializeJson(root); } std::string BuildNdiSourcesJson() const { std::vector sources; std::string error; const bool discovered = DiscoverNdiSources(sources, error); JsonWriter writer; writer.BeginObject(); writer.KeyBool("ok", discovered); if (!error.empty()) writer.KeyString("error", error); writer.Key("sources"); writer.BeginArray(); for (const NdiSourceInfo& source : sources) { writer.BeginObject(); writer.KeyString("name", source.name); writer.KeyString("urlAddress", source.urlAddress); writer.EndObject(); } writer.EndArray(); writer.EndObject(); return writer.StringValue(); } ControlActionResult HandleConfigSave(const std::string& body) { AppConfig nextConfig; std::string error; if (!ParseAppConfigJson(body, nextConfig, error)) return ControlActionResult{ false, error }; if (!SaveAppConfigToFile(nextConfig, mConfigPath, error)) return ControlActionResult{ false, error }; mRestartRequired.store(true, std::memory_order_relaxed); Log("app", "Saved runtime host config to " + mConfigPath.string() + "; restart required for startup-owned services."); return ControlActionResult{ true }; } ControlActionResult HandleAppRestart() { std::string error; if (!ScheduleProcessRestart(error)) return ControlActionResult{ false, error }; Log("app", "App restart requested from HTTP control."); return ControlActionResult{ true }; } void ApplyVideoInputMetrics(CadenceTelemetrySnapshot& telemetry) { if (!mVideoInputMetricsProvider) return; const VideoInputEdgeMetrics inputMetrics = mVideoInputMetricsProvider(); telemetry.inputConvertMilliseconds = inputMetrics.convertMilliseconds; telemetry.inputSubmitMilliseconds = inputMetrics.submitMilliseconds; telemetry.inputNoSignalFrames = inputMetrics.noInputSourceFrames; telemetry.inputUnsupportedFrames = inputMetrics.unsupportedFrames; telemetry.inputSubmitMisses = inputMetrics.submitMisses; telemetry.inputCaptureFormat = inputMetrics.captureFormat ? inputMetrics.captureFormat : "none"; if (telemetry.sampleSeconds > 0.0) telemetry.inputCaptureFps = static_cast(inputMetrics.capturedFrames - mLastInputCapturedFrames) / telemetry.sampleSeconds; mLastInputCapturedFrames = inputMetrics.capturedFrames; } bool WaitForPreroll() const { const auto deadline = std::chrono::steady_clock::now() + mConfig.prerollTimeout; while (std::chrono::steady_clock::now() < deadline) { if (mFrameExchange.Metrics().scheduledCount >= mConfig.outputThread.targetBufferedFrames) return true; std::this_thread::sleep_for(mConfig.prerollPoll); } return false; } RenderThread& mRenderThread; SystemFrameExchange& mFrameExchange; AppConfig mConfig; std::filesystem::path mConfigPath; std::unique_ptr mOutput; std::unique_ptr mRuntimeContent; VideoOutputThread mOutputThread; TelemetryHealthMonitor mTelemetryHealth; CadenceTelemetry mHttpTelemetry; HttpControlServer mHttpServer; OscControlServer mOscServer; PreviewWindowThread mPreviewWindow; std::function mVideoInputMetricsProvider; uint64_t mLastInputCapturedFrames = 0; std::atomic mRestartRequired{ false }; bool mStarted = false; bool mVideoOutputEnabled = false; std::string mVideoOutputStatus = "Video output not started."; }; }