decklink start up to separate factory

This commit is contained in:
2026-05-22 15:27:46 +10:00
parent 315cbda9d1
commit 64a6125c3f
14 changed files with 330 additions and 88 deletions

View File

@@ -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.

View File

@@ -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 <iostream>
#include <sstream>
#include <string>
#include <thread>
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<RenderThread, SystemFrameExchange> app(renderThread, frameExchange, appConfig);
app.SetVideoInputMetricsProvider([&deckLinkInput]() {
return deckLinkInput.Metrics();
auto outputBackend = RenderCadenceCompositor::CreateVideoOutputBackend(appConfig);
RenderCadenceCompositor::RenderCadenceApp<RenderThread, SystemFrameExchange> 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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 <typename RenderThread, typename SystemFrameExchange>
class RenderCadenceApp
{
public:
RenderCadenceApp(RenderThread& renderThread, SystemFrameExchange& frameExchange, AppConfig config = DefaultAppConfig()) :
RenderCadenceApp(
RenderThread& renderThread,
SystemFrameExchange& frameExchange,
AppConfig config,
std::unique_ptr<IVideoOutputEdge> output) :
mRenderThread(renderThread),
mFrameExchange(frameExchange),
mConfig(config),
mOutput(std::make_unique<DeckLinkOutput>()),
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.";
};
}

View File

@@ -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 <algorithm>
#include <chrono>
#include <memory>
#include <thread>
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<char>(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<IVideoOutputEdge> CreateVideoOutputBackend(const AppConfig& config)
{
if (IsDeckLinkBackend(config.videoOutputBackend))
return std::make_unique<DeckLinkOutput>();
if (IsNoneBackend(config.videoOutputBackend))
return std::make_unique<DisabledVideoOutputEdge>("Video output backend is disabled by config.");
return std::make_unique<DisabledVideoOutputEdge>("Unsupported videoOutputBackend: " + config.videoOutputBackend);
}
std::unique_ptr<VideoInputBackendSession> 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<NoInputBackendSession>("none");
}
if (!IsDeckLinkBackend(config.videoInputBackend))
{
LogWarning("app", "Unsupported videoInputBackend '" + config.videoInputBackend + "'; runtime shaders will use fallback input.");
return std::make_unique<NoInputBackendSession>(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<NoInputBackendSession>("decklink");
}
auto session = std::make_unique<DeckLinkInputBackendSession>(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<NoInputBackendSession>("decklink");
}
return session;
}
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include "AppConfig.h"
#include "frames/InputFrameMailbox.h"
#include "video/core/VideoIOEdges.h"
#include "video/core/VideoMode.h"
#include <memory>
#include <string>
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<IVideoOutputEdge> CreateVideoOutputBackend(const AppConfig& config);
std::unique_ptr<VideoInputBackendSession> StartVideoInputBackend(
const AppConfig& config,
InputFrameMailbox& mailbox,
InputFrameMailboxConfig& mailboxConfig,
const VideoFormat& inputVideoMode,
bool inputVideoModeResolved);
}

View File

@@ -251,6 +251,8 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
writer.KeyUInt("maxTemporalHistoryFrames", static_cast<uint64_t>(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);