From 64a6125c3f699b87e2c47cde9797151ede104413 Mon Sep 17 00:00:00 2001 From: Aiden Date: Fri, 22 May 2026 15:27:46 +1000 Subject: [PATCH] decklink start up to separate factory --- README.md | 4 + config/runtime-host.json | 2 + docs/CURRENT_SYSTEM_ARCHITECTURE.md | 2 +- src/README.md | 4 + src/RenderCadenceCompositor.cpp | 99 ++------ src/app/AppConfig.cpp | 2 + src/app/AppConfig.h | 2 + src/app/AppConfigProvider.cpp | 2 + src/app/RenderCadenceApp.h | 33 ++- src/app/VideoBackendFactory.cpp | 229 ++++++++++++++++++ src/app/VideoBackendFactory.h | 31 +++ src/control/RuntimeStateJson.h | 2 + ...adenceCompositorAppConfigProviderTests.cpp | 4 + ...CadenceCompositorRuntimeStateJsonTests.cpp | 2 + 14 files changed, 330 insertions(+), 88 deletions(-) create mode 100644 src/app/VideoBackendFactory.cpp create mode 100644 src/app/VideoBackendFactory.h diff --git a/README.md b/README.md index f51f1c8..ac57e99 100644 --- a/README.md +++ b/README.md @@ -158,12 +158,16 @@ Current native test coverage includes: "inputFrameRate": "59.94", "outputVideoFormat": "1080p", "outputFrameRate": "59.94", + "videoInputBackend": "decklink", + "videoOutputBackend": "decklink", "autoReload": true, "maxTemporalHistoryFrames": 12, "enableExternalKeying": true } ``` +`videoInputBackend` and `videoOutputBackend` select the concrete video I/O backend. Today the app supports `decklink` and `none`; future backends such as NDI, Spout, or file playback can be added behind the same factory boundary. + `inputVideoFormat`/`inputFrameRate` select the video capture mode. `outputVideoFormat`/`outputFrameRate` select the playout mode through a backend-neutral mode description; the current DeckLink backend maps that mode to a `BMDDisplayMode` at the DeckLink boundary. Supported modes still depend on the installed card and driver. The shader stack runs at input resolution and the final rendered frame is scaled once into the configured output mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`. Legacy `videoFormat` and `frameRate` keys are still accepted and apply to both input and output unless the explicit input/output keys are present. diff --git a/config/runtime-host.json b/config/runtime-host.json index cde908e..290c04f 100644 --- a/config/runtime-host.json +++ b/config/runtime-host.json @@ -8,6 +8,8 @@ "inputFrameRate": "59.94", "outputVideoFormat": "1080p", "outputFrameRate": "59.94", + "videoInputBackend": "decklink", + "videoOutputBackend": "decklink", "autoReload": true, "maxTemporalHistoryFrames": 12, "previewEnabled": true, diff --git a/docs/CURRENT_SYSTEM_ARCHITECTURE.md b/docs/CURRENT_SYSTEM_ARCHITECTURE.md index beef382..5519e0b 100644 --- a/docs/CURRENT_SYSTEM_ARCHITECTURE.md +++ b/docs/CURRENT_SYSTEM_ARCHITECTURE.md @@ -133,7 +133,7 @@ When a runtime shader build completes, the app publishes a render-layer artifact ## Video And Preview -Video input and output are optional edges. DeckLink is the current concrete backend. Configured video modes are represented in `src/video/core` and translated to DeckLink display modes only inside `src/video/decklink`. +Video input and output are optional edges. `videoInputBackend` and `videoOutputBackend` select the concrete backend through the app-side backend factory. DeckLink is the current concrete backend, and `none` disables that edge. Configured video modes are represented in `src/video/core` and translated to DeckLink display modes only inside `src/video/decklink`. The input edge writes CPU frames into `InputFrameMailbox`. The current DeckLink backend captures BGRA8 directly where possible, or raw UYVY8 for render-thread GPU decode. The input edge does not call GL, render, preview, screenshot, shader, or output scheduling code. diff --git a/src/README.md b/src/README.md index fe7fa68..6cc361c 100644 --- a/src/README.md +++ b/src/README.md @@ -204,12 +204,16 @@ Currently consumed fields: - `inputFrameRate` - `outputVideoFormat` - `outputFrameRate` +- `videoInputBackend` +- `videoOutputBackend` - `autoReload` - `maxTemporalHistoryFrames` - `previewEnabled` - `previewFps` - `enableExternalKeying` +`videoInputBackend` and `videoOutputBackend` currently support `decklink` and `none`. Backend creation is routed through the app-side video backend factory, so new concrete backends can be added without making `main` or the render cadence path own their startup details. + When `previewEnabled` is true, the preview window runs on `PreviewWindowThread`. It paints BGRA8 system-memory frames with Win32/GDI after render readback has already completed, so it does not bind GL and does not consume frames from DeckLink output. `previewFps` controls the preview repaint cadence; the default is 60 fps and `config/runtime-host.json` tracks the shipped 59.94 output cadence. The loaded config is treated as a read-only startup snapshot. Subsystems that need config should receive this snapshot or a narrowed config struct from app orchestration; they should not reload files independently. diff --git a/src/RenderCadenceCompositor.cpp b/src/RenderCadenceCompositor.cpp index ba06335..33f0e95 100644 --- a/src/RenderCadenceCompositor.cpp +++ b/src/RenderCadenceCompositor.cpp @@ -1,13 +1,11 @@ #include "app/AppConfig.h" #include "app/AppConfigProvider.h" #include "app/RenderCadenceApp.h" +#include "app/VideoBackendFactory.h" #include "frames/InputFrameMailbox.h" #include "frames/SystemFrameExchange.h" #include "logging/Logger.h" #include "render/thread/RenderThread.h" -#include "video/decklink/DeckLinkInput.h" -#include "video/decklink/DeckLinkInputThread.h" -#include "video/decklink/DeckLinkDisplayMode.h" #include "video/core/VideoIOFormat.h" #include "video/core/VideoMode.h" @@ -17,7 +15,6 @@ #include #include #include -#include namespace { @@ -49,18 +46,6 @@ private: HRESULT mResult = S_OK; }; -bool WaitForInputWarmup(InputFrameMailbox& mailbox, std::size_t targetReadyFrames, std::chrono::milliseconds timeout) -{ - const auto start = std::chrono::steady_clock::now(); - while (std::chrono::steady_clock::now() - start < timeout) - { - const InputFrameMailboxMetrics metrics = mailbox.Metrics(); - if (metrics.readyCount >= targetReadyFrames) - return true; - std::this_thread::sleep_for(std::chrono::milliseconds(2)); - } - return false; -} } int main(int argc, char** argv) @@ -80,11 +65,11 @@ int main(int argc, char** argv) RenderCadenceCompositor::Logger::Instance().Start(appConfig.logging); RenderCadenceCompositor::Log( "app", - "RenderCadenceCompositor starting. Starts render cadence, system-memory exchange, DeckLink scheduled output, and telemetry. Press Enter to stop."); + "RenderCadenceCompositor starting. Starts render cadence, configured video I/O backends, and telemetry. Press Enter to stop."); RenderCadenceCompositor::Log("app", "Loaded config from " + configProvider.SourcePath().string()); ComInitGuard com; - if (!com.Initialize()) + if (RenderCadenceCompositor::VideoBackendsRequireCom(appConfig) && !com.Initialize()) { std::ostringstream message; message << "COM initialization failed: 0x" << std::hex << com.Result(); @@ -126,7 +111,7 @@ int main(int argc, char** argv) const bool outputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.outputVideoFormat, appConfig.outputFrameRate, outputVideoMode); if (!inputVideoModeResolved) { - inputVideoModeError = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " + + inputVideoModeError = "Unsupported inputVideoFormat/inputFrameRate in config/runtime-host.json: " + appConfig.inputVideoFormat + " / " + appConfig.inputFrameRate; RenderCadenceCompositor::LogWarning("app", inputVideoModeError); } @@ -134,7 +119,7 @@ int main(int argc, char** argv) { RenderCadenceCompositor::LogWarning( "app", - "Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json; render cadence will use parsed frame-rate fallback: " + + "Unsupported outputVideoFormat/outputFrameRate in config/runtime-host.json; render cadence will use parsed frame-rate fallback: " + appConfig.outputVideoFormat + " / " + appConfig.outputFrameRate); } else @@ -142,55 +127,12 @@ int main(int argc, char** argv) appConfig.deckLink.outputVideoMode = outputVideoMode; } - RenderCadenceCompositor::DeckLinkInput deckLinkInput(inputMailbox); - RenderCadenceCompositor::DeckLinkInputThread deckLinkInputThread(deckLinkInput); - bool deckLinkInputStarted = false; - if (inputVideoModeResolved) - { - RenderCadenceCompositor::DeckLinkInputConfig deckLinkInputConfig; - deckLinkInputConfig.videoFormat = inputVideoMode; - std::string deckLinkInputError; - if (deckLinkInput.Initialize(deckLinkInputConfig, deckLinkInputError)) - { - inputMailboxConfig.pixelFormat = deckLinkInput.CapturePixelFormat(); - inputMailboxConfig.rowBytes = VideoIORowBytes(inputMailboxConfig.pixelFormat, inputMailboxConfig.width); - inputMailbox.Configure(inputMailboxConfig); - } - - if (deckLinkInput.IsInitialized() && deckLinkInputThread.Start(deckLinkInputError)) - { - deckLinkInputStarted = true; - RenderCadenceCompositor::Log("app", "DeckLink input edge started for " + inputVideoMode.displayName + "."); - RenderCadenceCompositor::Log("app", "Waiting for DeckLink input warmup frames."); - constexpr std::size_t kInputStartupBufferedFrames = 3; - constexpr std::chrono::milliseconds kInputWarmupTimeout(1000); - if (WaitForInputWarmup(inputMailbox, kInputStartupBufferedFrames, kInputWarmupTimeout)) - { - const InputFrameMailboxMetrics metrics = inputMailbox.Metrics(); - RenderCadenceCompositor::Log( - "app", - "DeckLink input warmup complete. ready=" + std::to_string(metrics.readyCount) + - " submitted=" + std::to_string(metrics.submittedFrames) + "."); - } - else - { - const InputFrameMailboxMetrics metrics = inputMailbox.Metrics(); - RenderCadenceCompositor::LogWarning( - "app", - "DeckLink input warmup timed out; starting render cadence with current input buffer. ready=" + - std::to_string(metrics.readyCount) + " submitted=" + std::to_string(metrics.submittedFrames) + "."); - } - } - else - { - RenderCadenceCompositor::LogWarning("app", "DeckLink input edge unavailable; runtime shaders will use fallback input until a real input edge is available. " + deckLinkInputError); - deckLinkInput.ReleaseResources(); - } - } - else - { - RenderCadenceCompositor::LogWarning("app", "DeckLink input mode was not resolved; runtime shaders will use fallback input until a real input edge is available."); - } + auto inputBackend = RenderCadenceCompositor::StartVideoInputBackend( + appConfig, + inputMailbox, + inputMailboxConfig, + inputVideoMode, + inputVideoModeResolved); RenderThread::Config renderConfig; renderConfig.width = frameExchangeConfig.width; @@ -203,17 +145,22 @@ int main(int argc, char** argv) RenderThread renderThread(frameExchange, &inputMailbox, renderConfig); - RenderCadenceCompositor::RenderCadenceApp app(renderThread, frameExchange, appConfig); - app.SetVideoInputMetricsProvider([&deckLinkInput]() { - return deckLinkInput.Metrics(); + auto outputBackend = RenderCadenceCompositor::CreateVideoOutputBackend(appConfig); + RenderCadenceCompositor::RenderCadenceApp app( + renderThread, + frameExchange, + appConfig, + std::move(outputBackend)); + app.SetVideoInputMetricsProvider([inputBackend = inputBackend.get()]() { + return inputBackend ? inputBackend->Metrics() : RenderCadenceCompositor::VideoInputEdgeMetrics(); }); std::string error; if (!app.Start(error)) { RenderCadenceCompositor::LogError("app", "RenderCadenceCompositor start failed: " + error); - if (deckLinkInputStarted) - deckLinkInputThread.Stop(); + if (inputBackend) + inputBackend->Stop(); RenderCadenceCompositor::Logger::Instance().Stop(); return 1; } @@ -221,8 +168,8 @@ int main(int argc, char** argv) std::string line; std::getline(std::cin, line); app.Stop(); - if (deckLinkInputStarted) - deckLinkInputThread.Stop(); + if (inputBackend) + inputBackend->Stop(); RenderCadenceCompositor::Log("app", "RenderCadenceCompositor stopped."); RenderCadenceCompositor::Logger::Instance().Stop(); return 0; diff --git a/src/app/AppConfig.cpp b/src/app/AppConfig.cpp index 2241c09..f8752ef 100644 --- a/src/app/AppConfig.cpp +++ b/src/app/AppConfig.cpp @@ -26,6 +26,8 @@ AppConfig DefaultAppConfig() config.inputFrameRate = "59.94"; config.outputVideoFormat = "1080p"; config.outputFrameRate = "59.94"; + config.videoInputBackend = "decklink"; + config.videoOutputBackend = "decklink"; config.autoReload = true; config.maxTemporalHistoryFrames = 12; config.previewEnabled = false; diff --git a/src/app/AppConfig.h b/src/app/AppConfig.h index 0b38db8..865dc57 100644 --- a/src/app/AppConfig.h +++ b/src/app/AppConfig.h @@ -28,6 +28,8 @@ struct AppConfig std::string inputFrameRate = "59.94"; std::string outputVideoFormat = "1080p"; std::string outputFrameRate = "59.94"; + std::string videoInputBackend = "decklink"; + std::string videoOutputBackend = "decklink"; bool autoReload = true; std::size_t maxTemporalHistoryFrames = 12; bool previewEnabled = false; diff --git a/src/app/AppConfigProvider.cpp b/src/app/AppConfigProvider.cpp index 301b4a9..2c25fe9 100644 --- a/src/app/AppConfigProvider.cpp +++ b/src/app/AppConfigProvider.cpp @@ -128,6 +128,8 @@ bool AppConfigProvider::Load(const std::filesystem::path& path, std::string& err ApplyString(root, "inputFrameRate", mConfig.inputFrameRate); ApplyString(root, "outputVideoFormat", mConfig.outputVideoFormat); ApplyString(root, "outputFrameRate", mConfig.outputFrameRate); + ApplyString(root, "videoInputBackend", mConfig.videoInputBackend); + ApplyString(root, "videoOutputBackend", mConfig.videoOutputBackend); ApplyBool(root, "autoReload", mConfig.autoReload); ApplySize(root, "maxTemporalHistoryFrames", mConfig.maxTemporalHistoryFrames); ApplyBool(root, "previewEnabled", mConfig.previewEnabled); diff --git a/src/app/RenderCadenceApp.h b/src/app/RenderCadenceApp.h index 807dc19..0e8de99 100644 --- a/src/app/RenderCadenceApp.h +++ b/src/app/RenderCadenceApp.h @@ -7,7 +7,6 @@ #include "../control/RuntimeStateJson.h" #include "../preview/PreviewWindowThread.h" #include "../telemetry/TelemetryHealthMonitor.h" -#include "DeckLinkOutput.h" #include "VideoIOEdges.h" #include "VideoOutputThread.h" @@ -53,11 +52,15 @@ template class RenderCadenceApp { public: - RenderCadenceApp(RenderThread& renderThread, SystemFrameExchange& frameExchange, AppConfig config = DefaultAppConfig()) : + RenderCadenceApp( + RenderThread& renderThread, + SystemFrameExchange& frameExchange, + AppConfig config, + std::unique_ptr output) : mRenderThread(renderThread), mFrameExchange(frameExchange), mConfig(config), - mOutput(std::make_unique()), + mOutput(std::move(output)), mOutputThread(*mOutput, mFrameExchange, VideoOutputThreadConfig{ mConfig.outputThread.targetBufferedFrames, mConfig.outputThread.idleSleep @@ -134,8 +137,16 @@ public: private: void StartOptionalVideoOutput() { + if (mConfig.videoOutputBackend == "none") + { + mVideoOutputEnabled = false; + mVideoOutputStatus = "Video output backend disabled by config."; + Log("app", mVideoOutputStatus); + return; + } + std::string outputError; - Log("app", "Initializing optional DeckLink output."); + Log("app", "Initializing optional video output backend: " + mConfig.videoOutputBackend + "."); if (!mOutput->Initialize( mConfig.deckLink, [this](const VideoIOCompletion& completion) { @@ -143,7 +154,7 @@ private: }, outputError)) { - DisableVideoOutput("DeckLink output unavailable: " + outputError); + DisableVideoOutput("Video output unavailable: " + outputError); return; } @@ -154,26 +165,26 @@ private: return; } - Log("app", "Waiting for DeckLink preroll frames."); + Log("app", "Waiting for video output preroll frames."); if (!WaitForPreroll()) { DisableVideoOutput("Timed out waiting for DeckLink preroll frames."); return; } - Log("app", "Starting DeckLink scheduled playback."); + Log("app", "Starting scheduled video playback."); if (!mOutput->StartScheduledPlayback(outputError)) { - DisableVideoOutput("DeckLink scheduled playback failed: " + outputError); + DisableVideoOutput("Scheduled video playback failed: " + outputError); return; } mVideoOutputEnabled = true; - mVideoOutputStatus = "DeckLink scheduled output running."; + mVideoOutputStatus = mConfig.videoOutputBackend + " scheduled output running."; Log("app", mVideoOutputStatus); Log( "app", - "DeckLink output mode: " + mOutput->State().outputDisplayModeName + + "Video output mode: " + mOutput->State().outputDisplayModeName + ", frame budget " + std::to_string(mOutput->State().frameBudgetMilliseconds) + " ms."); } @@ -313,6 +324,6 @@ private: uint64_t mLastInputCapturedFrames = 0; bool mStarted = false; bool mVideoOutputEnabled = false; - std::string mVideoOutputStatus = "DeckLink output not started."; + std::string mVideoOutputStatus = "Video output not started."; }; } diff --git a/src/app/VideoBackendFactory.cpp b/src/app/VideoBackendFactory.cpp new file mode 100644 index 0000000..def8f4e --- /dev/null +++ b/src/app/VideoBackendFactory.cpp @@ -0,0 +1,229 @@ +#include "VideoBackendFactory.h" + +#include "logging/Logger.h" +#include "video/core/VideoIOFormat.h" +#include "video/decklink/DeckLinkInput.h" +#include "video/decklink/DeckLinkInputThread.h" +#include "video/decklink/DeckLinkOutput.h" + +#include +#include +#include +#include + +namespace RenderCadenceCompositor +{ +namespace +{ +constexpr std::size_t kInputStartupBufferedFrames = 3; +constexpr std::chrono::milliseconds kInputWarmupTimeout(1000); + +std::string NormalizeBackendName(std::string name) +{ + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return name; +} + +bool IsDeckLinkBackend(const std::string& name) +{ + return NormalizeBackendName(name) == "decklink"; +} + +bool IsNoneBackend(const std::string& name) +{ + const std::string normalized = NormalizeBackendName(name); + return normalized == "none" || normalized == "disabled" || normalized == "off"; +} + +bool WaitForInputWarmup(InputFrameMailbox& mailbox, std::size_t targetReadyFrames, std::chrono::milliseconds timeout) +{ + const auto start = std::chrono::steady_clock::now(); + while (std::chrono::steady_clock::now() - start < timeout) + { + const InputFrameMailboxMetrics metrics = mailbox.Metrics(); + if (metrics.readyCount >= targetReadyFrames) + return true; + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } + return false; +} + +class DisabledVideoOutputEdge final : public IVideoOutputEdge +{ +public: + explicit DisabledVideoOutputEdge(std::string reason) : + mReason(std::move(reason)) + { + mState.statusMessage = mReason; + } + + bool Initialize(const VideoOutputEdgeConfig&, CompletionCallback, std::string& error) override + { + error = mReason; + return false; + } + + bool StartScheduledPlayback(std::string& error) override + { + error = mReason; + return false; + } + + bool ScheduleFrame(const VideoIOOutputFrame&) override { return false; } + void Stop() override {} + void ReleaseResources() override {} + const VideoIOState& State() const override { return mState; } + VideoOutputEdgeMetrics Metrics() const override { return VideoOutputEdgeMetrics(); } + +private: + std::string mReason; + VideoIOState mState; +}; + +class NoInputBackendSession final : public VideoInputBackendSession +{ +public: + explicit NoInputBackendSession(std::string backendName) : + mBackendName(std::move(backendName)) + { + } + + void Stop() override {} + bool Started() const override { return false; } + VideoInputEdgeMetrics Metrics() const override { return VideoInputEdgeMetrics(); } + const std::string& BackendName() const override { return mBackendName; } + +private: + std::string mBackendName; +}; + +class DeckLinkInputBackendSession final : public VideoInputBackendSession +{ +public: + DeckLinkInputBackendSession(InputFrameMailbox& mailbox) : + mInput(mailbox), + mThread(mInput) + { + } + + ~DeckLinkInputBackendSession() override + { + Stop(); + } + + bool Start( + InputFrameMailbox& mailbox, + InputFrameMailboxConfig& mailboxConfig, + const VideoFormat& inputVideoMode, + std::string& error) + { + DeckLinkInputConfig inputConfig; + inputConfig.videoFormat = inputVideoMode; + if (mInput.Initialize(inputConfig, error)) + { + mailboxConfig.pixelFormat = mInput.CapturePixelFormat(); + mailboxConfig.rowBytes = VideoIORowBytes(mailboxConfig.pixelFormat, mailboxConfig.width); + mailbox.Configure(mailboxConfig); + } + + if (!mInput.IsInitialized() || !mThread.Start(error)) + { + mInput.ReleaseResources(); + return false; + } + + mStarted = true; + Log("app", "DeckLink input edge started for " + inputVideoMode.displayName + "."); + Log("app", "Waiting for DeckLink input warmup frames."); + if (WaitForInputWarmup(mailbox, kInputStartupBufferedFrames, kInputWarmupTimeout)) + { + const InputFrameMailboxMetrics metrics = mailbox.Metrics(); + Log( + "app", + "DeckLink input warmup complete. ready=" + std::to_string(metrics.readyCount) + + " submitted=" + std::to_string(metrics.submittedFrames) + "."); + } + else + { + const InputFrameMailboxMetrics metrics = mailbox.Metrics(); + LogWarning( + "app", + "DeckLink input warmup timed out; starting render cadence with current input buffer. ready=" + + std::to_string(metrics.readyCount) + " submitted=" + std::to_string(metrics.submittedFrames) + "."); + } + return true; + } + + void Stop() override + { + if (mStarted) + mThread.Stop(); + mInput.ReleaseResources(); + mStarted = false; + } + + bool Started() const override { return mStarted; } + VideoInputEdgeMetrics Metrics() const override { return mInput.Metrics(); } + const std::string& BackendName() const override { return mBackendName; } + +private: + std::string mBackendName = "decklink"; + DeckLinkInput mInput; + DeckLinkInputThread mThread; + bool mStarted = false; +}; +} + +bool VideoBackendsRequireCom(const AppConfig& config) +{ + return IsDeckLinkBackend(config.videoInputBackend) || IsDeckLinkBackend(config.videoOutputBackend); +} + +std::unique_ptr CreateVideoOutputBackend(const AppConfig& config) +{ + if (IsDeckLinkBackend(config.videoOutputBackend)) + return std::make_unique(); + + if (IsNoneBackend(config.videoOutputBackend)) + return std::make_unique("Video output backend is disabled by config."); + + return std::make_unique("Unsupported videoOutputBackend: " + config.videoOutputBackend); +} + +std::unique_ptr StartVideoInputBackend( + const AppConfig& config, + InputFrameMailbox& mailbox, + InputFrameMailboxConfig& mailboxConfig, + const VideoFormat& inputVideoMode, + bool inputVideoModeResolved) +{ + if (IsNoneBackend(config.videoInputBackend)) + { + Log("app", "Video input backend disabled by config."); + return std::make_unique("none"); + } + + if (!IsDeckLinkBackend(config.videoInputBackend)) + { + LogWarning("app", "Unsupported videoInputBackend '" + config.videoInputBackend + "'; runtime shaders will use fallback input."); + return std::make_unique(config.videoInputBackend); + } + + if (!inputVideoModeResolved) + { + LogWarning("app", "DeckLink input mode was not resolved; runtime shaders will use fallback input until a real input edge is available."); + return std::make_unique("decklink"); + } + + auto session = std::make_unique(mailbox); + std::string error; + if (!session->Start(mailbox, mailboxConfig, inputVideoMode, error)) + { + LogWarning("app", "DeckLink input edge unavailable; runtime shaders will use fallback input until a real input edge is available. " + error); + return std::make_unique("decklink"); + } + return session; +} +} diff --git a/src/app/VideoBackendFactory.h b/src/app/VideoBackendFactory.h new file mode 100644 index 0000000..3301146 --- /dev/null +++ b/src/app/VideoBackendFactory.h @@ -0,0 +1,31 @@ +#pragma once + +#include "AppConfig.h" +#include "frames/InputFrameMailbox.h" +#include "video/core/VideoIOEdges.h" +#include "video/core/VideoMode.h" + +#include +#include + +namespace RenderCadenceCompositor +{ +class VideoInputBackendSession +{ +public: + virtual ~VideoInputBackendSession() = default; + virtual void Stop() = 0; + virtual bool Started() const = 0; + virtual VideoInputEdgeMetrics Metrics() const = 0; + virtual const std::string& BackendName() const = 0; +}; + +bool VideoBackendsRequireCom(const AppConfig& config); +std::unique_ptr CreateVideoOutputBackend(const AppConfig& config); +std::unique_ptr StartVideoInputBackend( + const AppConfig& config, + InputFrameMailbox& mailbox, + InputFrameMailboxConfig& mailboxConfig, + const VideoFormat& inputVideoMode, + bool inputVideoModeResolved); +} diff --git a/src/control/RuntimeStateJson.h b/src/control/RuntimeStateJson.h index 9404302..17d5713 100644 --- a/src/control/RuntimeStateJson.h +++ b/src/control/RuntimeStateJson.h @@ -251,6 +251,8 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input) writer.KeyUInt("maxTemporalHistoryFrames", static_cast(input.config.maxTemporalHistoryFrames)); writer.KeyDouble("previewFps", input.config.previewFps); writer.KeyBool("enableExternalKeying", input.config.deckLink.externalKeyingEnabled); + writer.KeyString("videoInputBackend", input.config.videoInputBackend); + writer.KeyString("videoOutputBackend", input.config.videoOutputBackend); writer.KeyString("inputVideoFormat", input.config.inputVideoFormat); writer.KeyString("inputFrameRate", input.config.inputFrameRate); writer.KeyString("outputVideoFormat", input.config.outputVideoFormat); diff --git a/tests/RenderCadenceCompositorAppConfigProviderTests.cpp b/tests/RenderCadenceCompositorAppConfigProviderTests.cpp index 8fb6fba..697b97d 100644 --- a/tests/RenderCadenceCompositorAppConfigProviderTests.cpp +++ b/tests/RenderCadenceCompositorAppConfigProviderTests.cpp @@ -35,6 +35,8 @@ std::filesystem::path WriteConfigFixture() << " \"inputFrameRate\": \"50\",\n" << " \"outputVideoFormat\": \"2160p\",\n" << " \"outputFrameRate\": \"60\",\n" + << " \"videoInputBackend\": \"none\",\n" + << " \"videoOutputBackend\": \"decklink\",\n" << " \"autoReload\": false,\n" << " \"maxTemporalHistoryFrames\": 8,\n" << " \"previewEnabled\": true,\n" @@ -66,6 +68,8 @@ void TestLoadsRuntimeHostConfig() Expect(config.inputFrameRate == "50", "input frame rate loads"); Expect(config.outputVideoFormat == "2160p", "output format loads"); Expect(config.outputFrameRate == "60", "output frame rate loads"); + Expect(config.videoInputBackend == "none", "video input backend loads"); + Expect(config.videoOutputBackend == "decklink", "video output backend loads"); Expect(!config.autoReload, "auto reload loads"); Expect(config.maxTemporalHistoryFrames == 8, "history length loads"); Expect(config.previewEnabled, "preview enabled toggle loads"); diff --git a/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp b/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp index 14d7839..1e742da 100644 --- a/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp +++ b/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp @@ -105,6 +105,8 @@ int main() ExpectContains(json, "\"type\":\"color\"", "state JSON should serialize parameter types for the UI"); ExpectContains(json, "\"width\":1920", "state JSON should expose output width"); ExpectContains(json, "\"height\":1080", "state JSON should expose output height"); + ExpectContains(json, "\"videoInputBackend\":\"decklink\"", "state JSON should expose input backend"); + ExpectContains(json, "\"videoOutputBackend\":\"decklink\"", "state JSON should expose output backend"); ExpectContains(json, "\"renderMs\":2.5", "state JSON should expose top-level render timing"); ExpectContains(json, "\"budgetUsedPercent\":15", "state JSON should expose top-level render budget percentage"); ExpectContains(json, "\"renderFrameMs\":2.5", "state JSON should expose cadence render timing");