decklink start up to separate factory
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"inputFrameRate": "59.94",
|
||||
"outputVideoFormat": "1080p",
|
||||
"outputFrameRate": "59.94",
|
||||
"videoInputBackend": "decklink",
|
||||
"videoOutputBackend": "decklink",
|
||||
"autoReload": true,
|
||||
"maxTemporalHistoryFrames": 12,
|
||||
"previewEnabled": true,
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.";
|
||||
};
|
||||
}
|
||||
|
||||
229
src/app/VideoBackendFactory.cpp
Normal file
229
src/app/VideoBackendFactory.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
31
src/app/VideoBackendFactory.h
Normal file
31
src/app/VideoBackendFactory.h
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user