Compare commits
3 Commits
2fdb1741f9
...
e006fcc6ee
| Author | SHA1 | Date | |
|---|---|---|---|
| e006fcc6ee | |||
| 2058f94193 | |||
| 6e8f18e24c |
32
README.md
32
README.md
@@ -154,23 +154,32 @@ Current native test coverage includes:
|
||||
"oscBindAddress": "127.0.0.1",
|
||||
"oscPort": 9000,
|
||||
"oscSmoothing": 0.18,
|
||||
"inputVideoFormat": "1080p",
|
||||
"inputFrameRate": "59.94",
|
||||
"outputVideoFormat": "1080p",
|
||||
"outputFrameRate": "59.94",
|
||||
"videoInputBackend": "decklink",
|
||||
"videoOutputBackend": "decklink",
|
||||
"input": {
|
||||
"backend": "decklink",
|
||||
"device": "default",
|
||||
"resolution": "1080p",
|
||||
"frameRate": "59.94"
|
||||
},
|
||||
"output": {
|
||||
"backend": "decklink",
|
||||
"device": "default",
|
||||
"resolution": "1080p",
|
||||
"frameRate": "59.94",
|
||||
"keying": {
|
||||
"external": true,
|
||||
"alphaRequired": false
|
||||
}
|
||||
},
|
||||
"autoReload": true,
|
||||
"maxTemporalHistoryFrames": 12,
|
||||
"enableExternalKeying": true
|
||||
"maxTemporalHistoryFrames": 12
|
||||
}
|
||||
```
|
||||
|
||||
`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.
|
||||
`input.backend` and `output.backend` 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. `device` is currently accepted as a backend-neutral selector placeholder; DeckLink still chooses the first compatible device.
|
||||
|
||||
`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`.
|
||||
`input.resolution`/`input.frameRate` select the video capture mode. `output.resolution`/`output.frameRate` 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.
|
||||
The checked-in config uses the nested `input` and `output` objects as the supported shape.
|
||||
|
||||
The control UI is available at:
|
||||
|
||||
@@ -290,7 +299,6 @@ If `SLANG_ROOT` or `MSDF_ATLAS_GEN_ROOT` is not set, the workflow falls back to
|
||||
- Audio.
|
||||
- Genlock.
|
||||
- Logs.
|
||||
- Add more video I/O backends now that the DeckLink path is isolated under `src/video/decklink/`.
|
||||
- Support a separate sound shader `.slang` file in shader packages. (https://www.shadertoy.com/view/XsBXWt)
|
||||
- Add WebView2 for an embedded native control surface.
|
||||
- More shader-library organisation and filtering as the built-in library grows.
|
||||
|
||||
@@ -4,15 +4,24 @@
|
||||
"oscBindAddress": "0.0.0.0",
|
||||
"oscPort": 9000,
|
||||
"oscSmoothing": 0.18,
|
||||
"inputVideoFormat": "1080p",
|
||||
"inputFrameRate": "59.94",
|
||||
"outputVideoFormat": "1080p",
|
||||
"outputFrameRate": "59.94",
|
||||
"videoInputBackend": "decklink",
|
||||
"videoOutputBackend": "decklink",
|
||||
"input": {
|
||||
"backend": "decklink",
|
||||
"device": "default",
|
||||
"resolution": "1080p",
|
||||
"frameRate": "59.94"
|
||||
},
|
||||
"output": {
|
||||
"backend": "decklink",
|
||||
"device": "default",
|
||||
"resolution": "1080p",
|
||||
"frameRate": "59.94",
|
||||
"keying": {
|
||||
"external": true,
|
||||
"alphaRequired": false
|
||||
}
|
||||
},
|
||||
"autoReload": true,
|
||||
"maxTemporalHistoryFrames": 12,
|
||||
"previewEnabled": true,
|
||||
"previewFps": 59.94,
|
||||
"enableExternalKeying": true
|
||||
"previewFps": 59.94
|
||||
}
|
||||
|
||||
@@ -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. `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`.
|
||||
Video input and output are optional edges. `input.backend` and `output.backend` select the concrete backend through the app-side backend factory. DeckLink is the current concrete backend, and `none` disables that edge. `input` and `output` also carry the device selector plus resolution/frame-rate settings. 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.
|
||||
|
||||
|
||||
@@ -561,22 +561,37 @@ components:
|
||||
type: number
|
||||
previewFps:
|
||||
type: number
|
||||
enableExternalKeying:
|
||||
type: boolean
|
||||
videoInputBackend:
|
||||
type: string
|
||||
enum: [decklink, none]
|
||||
videoOutputBackend:
|
||||
type: string
|
||||
enum: [decklink, none]
|
||||
inputVideoFormat:
|
||||
type: string
|
||||
inputFrameRate:
|
||||
type: string
|
||||
outputVideoFormat:
|
||||
type: string
|
||||
outputFrameRate:
|
||||
type: string
|
||||
input:
|
||||
type: object
|
||||
properties:
|
||||
backend:
|
||||
type: string
|
||||
enum: [decklink, none]
|
||||
device:
|
||||
type: string
|
||||
resolution:
|
||||
type: string
|
||||
frameRate:
|
||||
type: string
|
||||
output:
|
||||
type: object
|
||||
properties:
|
||||
backend:
|
||||
type: string
|
||||
enum: [decklink, none]
|
||||
device:
|
||||
type: string
|
||||
resolution:
|
||||
type: string
|
||||
frameRate:
|
||||
type: string
|
||||
keying:
|
||||
type: object
|
||||
properties:
|
||||
external:
|
||||
type: boolean
|
||||
alphaRequired:
|
||||
type: boolean
|
||||
RuntimeStatus:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -200,19 +200,22 @@ Currently consumed fields:
|
||||
- `oscBindAddress` (reported for compatibility; OSC ingress is not wired yet)
|
||||
- `oscPort` (reported for compatibility; OSC ingress is not wired yet)
|
||||
- `oscSmoothing` (reported for compatibility; OSC ingress is not wired yet)
|
||||
- `inputVideoFormat`
|
||||
- `inputFrameRate`
|
||||
- `outputVideoFormat`
|
||||
- `outputFrameRate`
|
||||
- `videoInputBackend`
|
||||
- `videoOutputBackend`
|
||||
- `input.backend`
|
||||
- `input.device`
|
||||
- `input.resolution`
|
||||
- `input.frameRate`
|
||||
- `output.backend`
|
||||
- `output.device`
|
||||
- `output.resolution`
|
||||
- `output.frameRate`
|
||||
- `output.keying.external`
|
||||
- `output.keying.alphaRequired`
|
||||
- `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.
|
||||
`input.backend` and `output.backend` 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.
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ int main(int argc, char** argv)
|
||||
|
||||
SystemFrameExchangeConfig frameExchangeConfig;
|
||||
VideoFormatDimensions(
|
||||
appConfig.outputVideoFormat,
|
||||
appConfig.output.resolution,
|
||||
frameExchangeConfig.width,
|
||||
frameExchangeConfig.height);
|
||||
frameExchangeConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
@@ -95,7 +95,7 @@ int main(int argc, char** argv)
|
||||
|
||||
InputFrameMailboxConfig inputMailboxConfig;
|
||||
VideoFormatDimensions(
|
||||
appConfig.inputVideoFormat,
|
||||
appConfig.input.resolution,
|
||||
inputMailboxConfig.width,
|
||||
inputMailboxConfig.height);
|
||||
inputMailboxConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
@@ -107,24 +107,24 @@ int main(int argc, char** argv)
|
||||
VideoFormat inputVideoMode;
|
||||
VideoFormat outputVideoMode;
|
||||
std::string inputVideoModeError;
|
||||
const bool inputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.inputVideoFormat, appConfig.inputFrameRate, inputVideoMode);
|
||||
const bool outputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.outputVideoFormat, appConfig.outputFrameRate, outputVideoMode);
|
||||
const bool inputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.input.resolution, appConfig.input.frameRate, inputVideoMode);
|
||||
const bool outputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.output.resolution, appConfig.output.frameRate, outputVideoMode);
|
||||
if (!inputVideoModeResolved)
|
||||
{
|
||||
inputVideoModeError = "Unsupported inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
|
||||
appConfig.inputVideoFormat + " / " + appConfig.inputFrameRate;
|
||||
inputVideoModeError = "Unsupported input resolution/frameRate in config/runtime-host.json: " +
|
||||
appConfig.input.resolution + " / " + appConfig.input.frameRate;
|
||||
RenderCadenceCompositor::LogWarning("app", inputVideoModeError);
|
||||
}
|
||||
if (!outputVideoModeResolved)
|
||||
{
|
||||
RenderCadenceCompositor::LogWarning(
|
||||
"app",
|
||||
"Unsupported outputVideoFormat/outputFrameRate in config/runtime-host.json; render cadence will use parsed frame-rate fallback: " +
|
||||
appConfig.outputVideoFormat + " / " + appConfig.outputFrameRate);
|
||||
"Unsupported output resolution/frameRate in config/runtime-host.json; render cadence will use parsed frame-rate fallback: " +
|
||||
appConfig.output.resolution + " / " + appConfig.output.frameRate);
|
||||
}
|
||||
else
|
||||
{
|
||||
appConfig.deckLink.outputVideoMode = outputVideoMode;
|
||||
appConfig.output.videoMode = outputVideoMode;
|
||||
}
|
||||
|
||||
auto inputBackend = RenderCadenceCompositor::StartVideoInputBackend(
|
||||
@@ -137,7 +137,7 @@ int main(int argc, char** argv)
|
||||
RenderThread::Config renderConfig;
|
||||
renderConfig.width = frameExchangeConfig.width;
|
||||
renderConfig.height = frameExchangeConfig.height;
|
||||
const double fallbackFrameDurationMilliseconds = FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
||||
const double fallbackFrameDurationMilliseconds = FrameDurationMillisecondsFromRateString(appConfig.output.frameRate);
|
||||
renderConfig.frameDurationMilliseconds = outputVideoModeResolved
|
||||
? outputVideoMode.frameDurationMilliseconds
|
||||
: fallbackFrameDurationMilliseconds;
|
||||
|
||||
@@ -5,8 +5,16 @@ namespace RenderCadenceCompositor
|
||||
AppConfig DefaultAppConfig()
|
||||
{
|
||||
AppConfig config;
|
||||
config.deckLink.externalKeyingEnabled = false;
|
||||
config.deckLink.outputAlphaRequired = false;
|
||||
config.input.backend = "decklink";
|
||||
config.input.device = "default";
|
||||
config.input.resolution = "1080p";
|
||||
config.input.frameRate = "59.94";
|
||||
config.output.backend = "decklink";
|
||||
config.output.device = "default";
|
||||
config.output.resolution = "1080p";
|
||||
config.output.frameRate = "59.94";
|
||||
config.output.externalKeyingEnabled = false;
|
||||
config.output.outputAlphaRequired = false;
|
||||
config.outputThread.targetBufferedFrames = 4;
|
||||
config.telemetry.interval = std::chrono::seconds(1);
|
||||
config.logging.minimumLevel = LogLevel::Log;
|
||||
@@ -22,12 +30,6 @@ AppConfig DefaultAppConfig()
|
||||
config.oscBindAddress = "0.0.0.0";
|
||||
config.oscPort = 9000;
|
||||
config.oscSmoothing = 0.18;
|
||||
config.inputVideoFormat = "1080p";
|
||||
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;
|
||||
|
||||
@@ -13,9 +13,29 @@
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct VideoInputAppConfig
|
||||
{
|
||||
std::string backend = "decklink";
|
||||
std::string device = "default";
|
||||
std::string resolution = "1080p";
|
||||
std::string frameRate = "59.94";
|
||||
};
|
||||
|
||||
struct VideoOutputAppConfig
|
||||
{
|
||||
std::string backend = "decklink";
|
||||
std::string device = "default";
|
||||
std::string resolution = "1080p";
|
||||
std::string frameRate = "59.94";
|
||||
bool externalKeyingEnabled = false;
|
||||
bool outputAlphaRequired = false;
|
||||
VideoFormat videoMode;
|
||||
};
|
||||
|
||||
struct AppConfig
|
||||
{
|
||||
VideoOutputEdgeConfig deckLink;
|
||||
VideoInputAppConfig input;
|
||||
VideoOutputAppConfig output;
|
||||
VideoOutputThreadConfig outputThread;
|
||||
TelemetryHealthMonitorConfig telemetry;
|
||||
LoggerConfig logging;
|
||||
@@ -24,12 +44,6 @@ struct AppConfig
|
||||
std::string oscBindAddress = "0.0.0.0";
|
||||
unsigned short oscPort = 9000;
|
||||
double oscSmoothing = 0.18;
|
||||
std::string inputVideoFormat = "1080p";
|
||||
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;
|
||||
|
||||
@@ -79,6 +79,37 @@ void ApplyPort(const JsonValue& root, const char* key, unsigned short& target)
|
||||
if (port >= 1.0 && port <= 65535.0)
|
||||
target = static_cast<unsigned short>(port);
|
||||
}
|
||||
|
||||
void ApplyInputConfig(const JsonValue& root, AppConfig& config)
|
||||
{
|
||||
const JsonValue* input = Find(root, "input");
|
||||
if (!input || !input->isObject())
|
||||
return;
|
||||
|
||||
ApplyString(*input, "backend", config.input.backend);
|
||||
ApplyString(*input, "device", config.input.device);
|
||||
ApplyString(*input, "resolution", config.input.resolution);
|
||||
ApplyString(*input, "frameRate", config.input.frameRate);
|
||||
}
|
||||
|
||||
void ApplyOutputConfig(const JsonValue& root, AppConfig& config)
|
||||
{
|
||||
const JsonValue* output = Find(root, "output");
|
||||
if (!output || !output->isObject())
|
||||
return;
|
||||
|
||||
ApplyString(*output, "backend", config.output.backend);
|
||||
ApplyString(*output, "device", config.output.device);
|
||||
ApplyString(*output, "resolution", config.output.resolution);
|
||||
ApplyString(*output, "frameRate", config.output.frameRate);
|
||||
|
||||
const JsonValue* keying = Find(*output, "keying");
|
||||
if (keying && keying->isObject())
|
||||
{
|
||||
ApplyBool(*keying, "external", config.output.externalKeyingEnabled);
|
||||
ApplyBool(*keying, "alphaRequired", config.output.outputAlphaRequired);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppConfigProvider::AppConfigProvider() :
|
||||
@@ -124,17 +155,12 @@ bool AppConfigProvider::Load(const std::filesystem::path& path, std::string& err
|
||||
ApplyString(root, "oscBindAddress", mConfig.oscBindAddress);
|
||||
ApplyPort(root, "oscPort", mConfig.oscPort);
|
||||
ApplyDouble(root, "oscSmoothing", mConfig.oscSmoothing);
|
||||
ApplyString(root, "inputVideoFormat", mConfig.inputVideoFormat);
|
||||
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);
|
||||
ApplyInputConfig(root, mConfig);
|
||||
ApplyOutputConfig(root, mConfig);
|
||||
ApplyBool(root, "autoReload", mConfig.autoReload);
|
||||
ApplySize(root, "maxTemporalHistoryFrames", mConfig.maxTemporalHistoryFrames);
|
||||
ApplyBool(root, "previewEnabled", mConfig.previewEnabled);
|
||||
ApplyDouble(root, "previewFps", mConfig.previewFps);
|
||||
ApplyBool(root, "enableExternalKeying", mConfig.deckLink.externalKeyingEnabled);
|
||||
|
||||
mLoadedFromFile = true;
|
||||
error.clear();
|
||||
|
||||
@@ -137,7 +137,7 @@ public:
|
||||
private:
|
||||
void StartOptionalVideoOutput()
|
||||
{
|
||||
if (mConfig.videoOutputBackend == "none")
|
||||
if (mConfig.output.backend == "none")
|
||||
{
|
||||
mVideoOutputEnabled = false;
|
||||
mVideoOutputStatus = "Video output backend disabled by config.";
|
||||
@@ -146,9 +146,13 @@ private:
|
||||
}
|
||||
|
||||
std::string outputError;
|
||||
Log("app", "Initializing optional video output backend: " + mConfig.videoOutputBackend + ".");
|
||||
Log("app", "Initializing optional video output backend: " + mConfig.output.backend + ".");
|
||||
VideoOutputEdgeConfig outputConfig;
|
||||
outputConfig.outputVideoMode = mConfig.output.videoMode;
|
||||
outputConfig.externalKeyingEnabled = mConfig.output.externalKeyingEnabled;
|
||||
outputConfig.outputAlphaRequired = mConfig.output.outputAlphaRequired;
|
||||
if (!mOutput->Initialize(
|
||||
mConfig.deckLink,
|
||||
outputConfig,
|
||||
[this](const VideoIOCompletion& completion) {
|
||||
mFrameExchange.ReleaseScheduledByBytes(completion.outputFrameBuffer);
|
||||
},
|
||||
@@ -180,7 +184,7 @@ private:
|
||||
}
|
||||
|
||||
mVideoOutputEnabled = true;
|
||||
mVideoOutputStatus = mConfig.videoOutputBackend + " scheduled output running.";
|
||||
mVideoOutputStatus = mConfig.output.backend + " scheduled output running.";
|
||||
Log("app", mVideoOutputStatus);
|
||||
Log(
|
||||
"app",
|
||||
|
||||
@@ -7,9 +7,13 @@
|
||||
#include "video/decklink/DeckLinkOutput.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
@@ -26,15 +30,9 @@ std::string NormalizeBackendName(std::string name)
|
||||
return name;
|
||||
}
|
||||
|
||||
bool IsDeckLinkBackend(const std::string& name)
|
||||
bool BackendNameMatches(std::string_view normalizedName, const std::vector<std::string_view>& aliases)
|
||||
{
|
||||
return NormalizeBackendName(name) == "decklink";
|
||||
}
|
||||
|
||||
bool IsNoneBackend(const std::string& name)
|
||||
{
|
||||
const std::string normalized = NormalizeBackendName(name);
|
||||
return normalized == "none" || normalized == "disabled" || normalized == "off";
|
||||
return std::find(aliases.begin(), aliases.end(), normalizedName) != aliases.end();
|
||||
}
|
||||
|
||||
bool WaitForInputWarmup(InputFrameMailbox& mailbox, std::size_t targetReadyFrames, std::chrono::milliseconds timeout)
|
||||
@@ -50,6 +48,32 @@ bool WaitForInputWarmup(InputFrameMailbox& mailbox, std::size_t targetReadyFrame
|
||||
return false;
|
||||
}
|
||||
|
||||
struct VideoInputBackendStartContext
|
||||
{
|
||||
const AppConfig& config;
|
||||
InputFrameMailbox& mailbox;
|
||||
InputFrameMailboxConfig& mailboxConfig;
|
||||
const VideoFormat& inputVideoMode;
|
||||
bool inputVideoModeResolved = false;
|
||||
};
|
||||
|
||||
using VideoOutputBackendCreateFn = std::unique_ptr<IVideoOutputEdge> (*)();
|
||||
using VideoInputBackendStartFn = std::unique_ptr<VideoInputBackendSession> (*)(const VideoInputBackendStartContext&);
|
||||
|
||||
struct VideoOutputBackendRegistration
|
||||
{
|
||||
std::vector<std::string_view> aliases;
|
||||
bool requiresCom = false;
|
||||
VideoOutputBackendCreateFn create = nullptr;
|
||||
};
|
||||
|
||||
struct VideoInputBackendRegistration
|
||||
{
|
||||
std::vector<std::string_view> aliases;
|
||||
bool requiresCom = false;
|
||||
VideoInputBackendStartFn start = nullptr;
|
||||
};
|
||||
|
||||
class DisabledVideoOutputEdge final : public IVideoOutputEdge
|
||||
{
|
||||
public:
|
||||
@@ -174,22 +198,97 @@ private:
|
||||
DeckLinkInputThread mThread;
|
||||
bool mStarted = false;
|
||||
};
|
||||
|
||||
std::unique_ptr<IVideoOutputEdge> CreateDeckLinkOutputBackend()
|
||||
{
|
||||
return std::make_unique<DeckLinkOutput>();
|
||||
}
|
||||
|
||||
std::unique_ptr<IVideoOutputEdge> CreateDisabledOutputBackend()
|
||||
{
|
||||
return std::make_unique<DisabledVideoOutputEdge>("Video output backend is disabled by config.");
|
||||
}
|
||||
|
||||
std::unique_ptr<VideoInputBackendSession> StartNoInputBackend(const VideoInputBackendStartContext&)
|
||||
{
|
||||
Log("app", "Video input backend disabled by config.");
|
||||
return std::make_unique<NoInputBackendSession>("none");
|
||||
}
|
||||
|
||||
std::unique_ptr<VideoInputBackendSession> StartDeckLinkInputBackend(const VideoInputBackendStartContext& context)
|
||||
{
|
||||
if (!context.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>(context.mailbox);
|
||||
std::string error;
|
||||
if (!session->Start(context.mailbox, context.mailboxConfig, context.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;
|
||||
}
|
||||
|
||||
const std::vector<VideoOutputBackendRegistration>& VideoOutputBackendRegistry()
|
||||
{
|
||||
static const std::vector<VideoOutputBackendRegistration> registry = {
|
||||
VideoOutputBackendRegistration{ { "decklink" }, true, &CreateDeckLinkOutputBackend },
|
||||
VideoOutputBackendRegistration{ { "none", "disabled", "off" }, false, &CreateDisabledOutputBackend },
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
const std::vector<VideoInputBackendRegistration>& VideoInputBackendRegistry()
|
||||
{
|
||||
static const std::vector<VideoInputBackendRegistration> registry = {
|
||||
VideoInputBackendRegistration{ { "decklink" }, true, &StartDeckLinkInputBackend },
|
||||
VideoInputBackendRegistration{ { "none", "disabled", "off" }, false, &StartNoInputBackend },
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
const VideoOutputBackendRegistration* FindVideoOutputBackend(std::string_view backendName)
|
||||
{
|
||||
for (const VideoOutputBackendRegistration& backend : VideoOutputBackendRegistry())
|
||||
{
|
||||
if (BackendNameMatches(backendName, backend.aliases))
|
||||
return &backend;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const VideoInputBackendRegistration* FindVideoInputBackend(std::string_view backendName)
|
||||
{
|
||||
for (const VideoInputBackendRegistration& backend : VideoInputBackendRegistry())
|
||||
{
|
||||
if (BackendNameMatches(backendName, backend.aliases))
|
||||
return &backend;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool VideoBackendsRequireCom(const AppConfig& config)
|
||||
{
|
||||
return IsDeckLinkBackend(config.videoInputBackend) || IsDeckLinkBackend(config.videoOutputBackend);
|
||||
const std::string inputBackend = NormalizeBackendName(config.input.backend);
|
||||
const std::string outputBackend = NormalizeBackendName(config.output.backend);
|
||||
const VideoInputBackendRegistration* input = FindVideoInputBackend(inputBackend);
|
||||
const VideoOutputBackendRegistration* output = FindVideoOutputBackend(outputBackend);
|
||||
return (input != nullptr && input->requiresCom) || (output != nullptr && output->requiresCom);
|
||||
}
|
||||
|
||||
std::unique_ptr<IVideoOutputEdge> CreateVideoOutputBackend(const AppConfig& config)
|
||||
{
|
||||
if (IsDeckLinkBackend(config.videoOutputBackend))
|
||||
return std::make_unique<DeckLinkOutput>();
|
||||
const std::string backendName = NormalizeBackendName(config.output.backend);
|
||||
const VideoOutputBackendRegistration* backend = FindVideoOutputBackend(backendName);
|
||||
if (backend != nullptr && backend->create != nullptr)
|
||||
return backend->create();
|
||||
|
||||
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);
|
||||
return std::make_unique<DisabledVideoOutputEdge>("Unsupported output backend: " + config.output.backend);
|
||||
}
|
||||
|
||||
std::unique_ptr<VideoInputBackendSession> StartVideoInputBackend(
|
||||
@@ -199,31 +298,15 @@ std::unique_ptr<VideoInputBackendSession> StartVideoInputBackend(
|
||||
const VideoFormat& inputVideoMode,
|
||||
bool inputVideoModeResolved)
|
||||
{
|
||||
if (IsNoneBackend(config.videoInputBackend))
|
||||
const std::string backendName = NormalizeBackendName(config.input.backend);
|
||||
const VideoInputBackendRegistration* backend = FindVideoInputBackend(backendName);
|
||||
if (backend != nullptr && backend->start != nullptr)
|
||||
{
|
||||
Log("app", "Video input backend disabled by config.");
|
||||
return std::make_unique<NoInputBackendSession>("none");
|
||||
VideoInputBackendStartContext context{ config, mailbox, mailboxConfig, inputVideoMode, inputVideoModeResolved };
|
||||
return backend->start(context);
|
||||
}
|
||||
|
||||
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;
|
||||
LogWarning("app", "Unsupported input backend '" + config.input.backend + "'; runtime shaders will use fallback input.");
|
||||
return std::make_unique<NoInputBackendSession>(config.input.backend);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +27,13 @@ struct RuntimeStateJsonInput
|
||||
inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
|
||||
{
|
||||
writer.BeginObject();
|
||||
writer.KeyString("backend", input.config.videoOutputBackend);
|
||||
writer.KeyString("backend", input.config.output.backend);
|
||||
writer.KeyNull("modelName");
|
||||
writer.KeyBool("supportsInternalKeying", false);
|
||||
writer.KeyBool("supportsExternalKeying", false);
|
||||
writer.KeyBool("keyerInterfaceAvailable", false);
|
||||
writer.KeyBool("externalKeyingRequested", input.config.deckLink.externalKeyingEnabled);
|
||||
writer.KeyBool("externalKeyingActive", input.videoOutputEnabled && input.config.deckLink.externalKeyingEnabled);
|
||||
writer.KeyBool("externalKeyingRequested", input.config.output.externalKeyingEnabled);
|
||||
writer.KeyBool("externalKeyingActive", input.videoOutputEnabled && input.config.output.externalKeyingEnabled);
|
||||
writer.KeyString("statusMessage", input.videoOutputStatus);
|
||||
writer.EndObject();
|
||||
}
|
||||
@@ -41,7 +41,7 @@ inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInp
|
||||
inline void WriteVideoOutputBackendMetricsJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
|
||||
{
|
||||
writer.BeginObject();
|
||||
if (input.config.videoOutputBackend == "decklink")
|
||||
if (input.config.output.backend == "decklink")
|
||||
{
|
||||
writer.KeyBool("bufferedAvailable", input.telemetry.deckLinkBufferedAvailable);
|
||||
writer.Key("buffered");
|
||||
@@ -68,7 +68,7 @@ inline void WriteVideoOutputTelemetryJson(JsonWriter& writer, const RuntimeState
|
||||
{
|
||||
writer.BeginObject();
|
||||
writer.KeyBool("enabled", input.videoOutputEnabled);
|
||||
writer.KeyString("backend", input.config.videoOutputBackend);
|
||||
writer.KeyString("backend", input.config.output.backend);
|
||||
writer.KeyString("statusMessage", input.videoOutputStatus);
|
||||
writer.KeyUInt("scheduleFailures", input.telemetry.scheduleFailures);
|
||||
writer.KeyUInt("completions", input.telemetry.completions);
|
||||
@@ -81,7 +81,7 @@ inline void WriteVideoOutputTelemetryJson(JsonWriter& writer, const RuntimeState
|
||||
|
||||
inline void OutputDimensions(const RuntimeStateJsonInput& input, unsigned& width, unsigned& height)
|
||||
{
|
||||
VideoFormatDimensions(input.config.outputVideoFormat, width, height);
|
||||
VideoFormatDimensions(input.config.output.resolution, width, height);
|
||||
}
|
||||
|
||||
inline const char* ShaderParameterTypeName(ShaderParameterType type)
|
||||
@@ -291,13 +291,25 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
|
||||
writer.KeyBool("autoReload", input.config.autoReload);
|
||||
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);
|
||||
writer.KeyString("outputFrameRate", input.config.outputFrameRate);
|
||||
writer.Key("input");
|
||||
writer.BeginObject();
|
||||
writer.KeyString("backend", input.config.input.backend);
|
||||
writer.KeyString("device", input.config.input.device);
|
||||
writer.KeyString("resolution", input.config.input.resolution);
|
||||
writer.KeyString("frameRate", input.config.input.frameRate);
|
||||
writer.EndObject();
|
||||
writer.Key("output");
|
||||
writer.BeginObject();
|
||||
writer.KeyString("backend", input.config.output.backend);
|
||||
writer.KeyString("device", input.config.output.device);
|
||||
writer.KeyString("resolution", input.config.output.resolution);
|
||||
writer.KeyString("frameRate", input.config.output.frameRate);
|
||||
writer.Key("keying");
|
||||
writer.BeginObject();
|
||||
writer.KeyBool("external", input.config.output.externalKeyingEnabled);
|
||||
writer.KeyBool("alphaRequired", input.config.output.outputAlphaRequired);
|
||||
writer.EndObject();
|
||||
writer.EndObject();
|
||||
writer.EndObject();
|
||||
|
||||
writer.Key("runtime");
|
||||
@@ -315,7 +327,7 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
|
||||
writer.KeyBool("hasSignal", input.videoOutputEnabled);
|
||||
writer.KeyUInt("width", outputWidth);
|
||||
writer.KeyUInt("height", outputHeight);
|
||||
writer.KeyString("modeName", input.config.outputVideoFormat + " output-only");
|
||||
writer.KeyString("modeName", input.config.output.resolution + " output-only");
|
||||
writer.EndObject();
|
||||
|
||||
writer.Key("videoOutput");
|
||||
@@ -327,7 +339,7 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
|
||||
|
||||
writer.Key("performance");
|
||||
writer.BeginObject();
|
||||
writer.KeyDouble("frameBudgetMs", FrameDurationMillisecondsFromRateString(input.config.outputFrameRate));
|
||||
writer.KeyDouble("frameBudgetMs", FrameDurationMillisecondsFromRateString(input.config.output.frameRate));
|
||||
writer.KeyDouble("renderMs", input.telemetry.renderFrameMilliseconds);
|
||||
writer.KeyNull("smoothedRenderMs");
|
||||
writer.KeyDouble("budgetUsedPercent", input.telemetry.renderFrameBudgetUsedPercent);
|
||||
|
||||
@@ -42,16 +42,6 @@ struct VideoIOState
|
||||
bool keyerInterfaceAvailable = false;
|
||||
bool externalKeyingActive = false;
|
||||
double frameBudgetMilliseconds = 0.0;
|
||||
bool actualDeckLinkBufferedFramesAvailable = false;
|
||||
uint64_t actualDeckLinkBufferedFrames = 0;
|
||||
double deckLinkScheduleCallMilliseconds = 0.0;
|
||||
uint64_t deckLinkScheduleFailureCount = 0;
|
||||
bool deckLinkScheduleLeadAvailable = false;
|
||||
int64_t deckLinkPlaybackStreamTime = 0;
|
||||
uint64_t deckLinkPlaybackFrameIndex = 0;
|
||||
uint64_t deckLinkNextScheduleFrameIndex = 0;
|
||||
int64_t deckLinkScheduleLeadFrames = 0;
|
||||
uint64_t deckLinkScheduleRealignmentCount = 0;
|
||||
};
|
||||
|
||||
struct VideoIOFrame
|
||||
|
||||
@@ -135,24 +135,24 @@ bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::str
|
||||
}
|
||||
|
||||
bool ResolveConfiguredVideoFormats(
|
||||
const std::string& inputVideoFormat,
|
||||
const std::string& inputResolution,
|
||||
const std::string& inputFrameRate,
|
||||
const std::string& outputVideoFormat,
|
||||
const std::string& outputResolution,
|
||||
const std::string& outputFrameRate,
|
||||
VideoFormatSelection& videoModes,
|
||||
std::string& error)
|
||||
{
|
||||
if (!ResolveConfiguredVideoFormat(inputVideoFormat, inputFrameRate, videoModes.input))
|
||||
if (!ResolveConfiguredVideoFormat(inputResolution, inputFrameRate, videoModes.input))
|
||||
{
|
||||
error = "Unsupported inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
|
||||
inputVideoFormat + " / " + inputFrameRate;
|
||||
error = "Unsupported input resolution/frameRate in config/runtime-host.json: " +
|
||||
inputResolution + " / " + inputFrameRate;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ResolveConfiguredVideoFormat(outputVideoFormat, outputFrameRate, videoModes.output))
|
||||
if (!ResolveConfiguredVideoFormat(outputResolution, outputFrameRate, videoModes.output))
|
||||
{
|
||||
error = "Unsupported outputVideoFormat/outputFrameRate in config/runtime-host.json: " +
|
||||
outputVideoFormat + " / " + outputFrameRate;
|
||||
error = "Unsupported output resolution/frameRate in config/runtime-host.json: " +
|
||||
outputResolution + " / " + outputFrameRate;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,9 +40,9 @@ double FrameDurationMillisecondsFromRateString(const std::string& rateText, doub
|
||||
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height);
|
||||
bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::string& frameRate, VideoFormat& videoMode);
|
||||
bool ResolveConfiguredVideoFormats(
|
||||
const std::string& inputVideoFormat,
|
||||
const std::string& inputResolution,
|
||||
const std::string& inputFrameRate,
|
||||
const std::string& outputVideoFormat,
|
||||
const std::string& outputResolution,
|
||||
const std::string& outputFrameRate,
|
||||
VideoFormatSelection& videoModes,
|
||||
std::string& error);
|
||||
|
||||
@@ -72,17 +72,17 @@ DeckLinkOutputMetrics DeckLinkOutput::Metrics() const
|
||||
metrics.dropped = mDropped.load();
|
||||
metrics.flushed = mFlushed.load();
|
||||
|
||||
const VideoIOState& state = mSession.State();
|
||||
metrics.scheduleFailures = state.deckLinkScheduleFailureCount;
|
||||
metrics.actualBufferedFramesAvailable = state.actualDeckLinkBufferedFramesAvailable;
|
||||
metrics.actualBufferedFrames = state.actualDeckLinkBufferedFrames;
|
||||
metrics.scheduleCallMilliseconds = state.deckLinkScheduleCallMilliseconds;
|
||||
metrics.scheduleLeadAvailable = state.deckLinkScheduleLeadAvailable;
|
||||
metrics.playbackStreamTime = state.deckLinkPlaybackStreamTime;
|
||||
metrics.playbackFrameIndex = state.deckLinkPlaybackFrameIndex;
|
||||
metrics.nextScheduleFrameIndex = state.deckLinkNextScheduleFrameIndex;
|
||||
metrics.scheduleLeadFrames = state.deckLinkScheduleLeadFrames;
|
||||
metrics.scheduleRealignmentCount = state.deckLinkScheduleRealignmentCount;
|
||||
const DeckLinkSessionTelemetry& telemetry = mSession.Telemetry();
|
||||
metrics.scheduleFailures = telemetry.scheduleFailureCount;
|
||||
metrics.actualBufferedFramesAvailable = telemetry.actualBufferedFramesAvailable;
|
||||
metrics.actualBufferedFrames = telemetry.actualBufferedFrames;
|
||||
metrics.scheduleCallMilliseconds = telemetry.scheduleCallMilliseconds;
|
||||
metrics.scheduleLeadAvailable = telemetry.scheduleLeadAvailable;
|
||||
metrics.playbackStreamTime = telemetry.playbackStreamTime;
|
||||
metrics.playbackFrameIndex = telemetry.playbackFrameIndex;
|
||||
metrics.nextScheduleFrameIndex = telemetry.nextScheduleFrameIndex;
|
||||
metrics.scheduleLeadFrames = telemetry.scheduleLeadFrames;
|
||||
metrics.scheduleRealignmentCount = telemetry.scheduleRealignmentCount;
|
||||
return metrics;
|
||||
}
|
||||
|
||||
|
||||
@@ -360,7 +360,7 @@ bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, const VideoF
|
||||
}
|
||||
else if (mState.supportsExternalKeying)
|
||||
{
|
||||
mState.statusMessage = "Selected DeckLink output supports external keying. Set enableExternalKeying to true in runtime-host.json to request it.";
|
||||
mState.statusMessage = "Selected DeckLink output supports external keying. Set output.keying.external to true in runtime-host.json to request it.";
|
||||
}
|
||||
|
||||
const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
|
||||
@@ -443,7 +443,7 @@ bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame
|
||||
{
|
||||
if (outputVideoFrame == nullptr || output == nullptr)
|
||||
{
|
||||
++mState.deckLinkScheduleFailureCount;
|
||||
++mTelemetry.scheduleFailureCount;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -459,9 +459,9 @@ bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame
|
||||
const auto scheduleStart = std::chrono::steady_clock::now();
|
||||
const HRESULT result = output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale);
|
||||
const auto scheduleEnd = std::chrono::steady_clock::now();
|
||||
mState.deckLinkScheduleCallMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(scheduleEnd - scheduleStart).count();
|
||||
mTelemetry.scheduleCallMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(scheduleEnd - scheduleStart).count();
|
||||
if (result != S_OK)
|
||||
++mState.deckLinkScheduleFailureCount;
|
||||
++mTelemetry.scheduleFailureCount;
|
||||
RefreshBufferedVideoFrameCount();
|
||||
return result == S_OK;
|
||||
}
|
||||
@@ -470,7 +470,7 @@ void DeckLinkSession::UpdateScheduleLeadTelemetry()
|
||||
{
|
||||
if (output == nullptr)
|
||||
{
|
||||
mState.deckLinkScheduleLeadAvailable = false;
|
||||
mTelemetry.scheduleLeadAvailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -478,7 +478,7 @@ void DeckLinkSession::UpdateScheduleLeadTelemetry()
|
||||
double playbackSpeed = 0.0;
|
||||
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0)
|
||||
{
|
||||
mState.deckLinkScheduleLeadAvailable = false;
|
||||
mTelemetry.scheduleLeadAvailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -486,25 +486,25 @@ void DeckLinkSession::UpdateScheduleLeadTelemetry()
|
||||
? static_cast<uint64_t>(streamTime / mScheduler.FrameDuration())
|
||||
: 0;
|
||||
const uint64_t nextScheduleFrameIndex = mScheduler.ScheduledFrameIndex();
|
||||
mState.deckLinkScheduleLeadAvailable = true;
|
||||
mState.deckLinkPlaybackStreamTime = streamTime;
|
||||
mState.deckLinkPlaybackFrameIndex = playbackFrameIndex;
|
||||
mState.deckLinkNextScheduleFrameIndex = nextScheduleFrameIndex;
|
||||
mState.deckLinkScheduleLeadFrames = static_cast<int64_t>(nextScheduleFrameIndex) - static_cast<int64_t>(playbackFrameIndex);
|
||||
mTelemetry.scheduleLeadAvailable = true;
|
||||
mTelemetry.playbackStreamTime = streamTime;
|
||||
mTelemetry.playbackFrameIndex = playbackFrameIndex;
|
||||
mTelemetry.nextScheduleFrameIndex = nextScheduleFrameIndex;
|
||||
mTelemetry.scheduleLeadFrames = static_cast<int64_t>(nextScheduleFrameIndex) - static_cast<int64_t>(playbackFrameIndex);
|
||||
}
|
||||
|
||||
void DeckLinkSession::MaybeRealignScheduleCursorForLowLead()
|
||||
{
|
||||
if (!mState.deckLinkScheduleLeadAvailable)
|
||||
if (!mTelemetry.scheduleLeadAvailable)
|
||||
return;
|
||||
|
||||
if (mState.deckLinkScheduleLeadFrames >= kMinimumHealthyScheduleLeadFrames)
|
||||
if (mTelemetry.scheduleLeadFrames >= kMinimumHealthyScheduleLeadFrames)
|
||||
{
|
||||
mProactiveScheduleRealignmentArmed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mProactiveScheduleRealignmentArmed || mState.deckLinkScheduleLeadFrames > kProactiveScheduleLeadFloorFrames)
|
||||
if (!mProactiveScheduleRealignmentArmed || mTelemetry.scheduleLeadFrames > kProactiveScheduleLeadFloorFrames)
|
||||
return;
|
||||
|
||||
RealignScheduleCursorToPlayback();
|
||||
@@ -523,7 +523,7 @@ void DeckLinkSession::RealignScheduleCursorToPlayback()
|
||||
|
||||
const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
|
||||
mScheduler.AlignNextScheduleTimeToPlayback(streamTime, policy.targetPrerollFrames);
|
||||
++mState.deckLinkScheduleRealignmentCount;
|
||||
++mTelemetry.scheduleRealignmentCount;
|
||||
UpdateScheduleLeadTelemetry();
|
||||
}
|
||||
|
||||
@@ -593,19 +593,19 @@ void DeckLinkSession::RefreshBufferedVideoFrameCount()
|
||||
{
|
||||
if (output == nullptr)
|
||||
{
|
||||
mState.actualDeckLinkBufferedFramesAvailable = false;
|
||||
mTelemetry.actualBufferedFramesAvailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned int bufferedFrameCount = 0;
|
||||
if (output->GetBufferedVideoFrameCount(&bufferedFrameCount) == S_OK)
|
||||
{
|
||||
mState.actualDeckLinkBufferedFrames = bufferedFrameCount;
|
||||
mState.actualDeckLinkBufferedFramesAvailable = true;
|
||||
mTelemetry.actualBufferedFrames = bufferedFrameCount;
|
||||
mTelemetry.actualBufferedFramesAvailable = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
mState.actualDeckLinkBufferedFramesAvailable = false;
|
||||
mTelemetry.actualBufferedFramesAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,20 @@
|
||||
|
||||
class OpenGLComposite;
|
||||
|
||||
struct DeckLinkSessionTelemetry
|
||||
{
|
||||
bool actualBufferedFramesAvailable = false;
|
||||
uint64_t actualBufferedFrames = 0;
|
||||
double scheduleCallMilliseconds = 0.0;
|
||||
uint64_t scheduleFailureCount = 0;
|
||||
bool scheduleLeadAvailable = false;
|
||||
int64_t playbackStreamTime = 0;
|
||||
uint64_t playbackFrameIndex = 0;
|
||||
uint64_t nextScheduleFrameIndex = 0;
|
||||
int64_t scheduleLeadFrames = 0;
|
||||
uint64_t scheduleRealignmentCount = 0;
|
||||
};
|
||||
|
||||
class DeckLinkSession
|
||||
{
|
||||
public:
|
||||
@@ -63,6 +77,7 @@ public:
|
||||
void SetStatusMessage(const std::string& message) { mState.statusMessage = message; }
|
||||
const VideoIOState& State() const { return mState; }
|
||||
VideoIOState& MutableState() { return mState; }
|
||||
const DeckLinkSessionTelemetry& Telemetry() const { return mTelemetry; }
|
||||
double FrameBudgetMilliseconds() const;
|
||||
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult completionResult, uint64_t readyQueueDepth);
|
||||
bool BeginOutputFrame(VideoIOOutputFrame& frame);
|
||||
@@ -89,6 +104,7 @@ private:
|
||||
std::mutex mScheduledSystemFrameMutex;
|
||||
std::unordered_map<IDeckLinkVideoFrame*, void*> mScheduledSystemFrameBuffers;
|
||||
VideoIOState mState;
|
||||
DeckLinkSessionTelemetry mTelemetry;
|
||||
VideoPlayoutPolicy mPlayoutPolicy;
|
||||
VideoPlayoutScheduler mScheduler;
|
||||
bool mScheduleRealignmentPending = false;
|
||||
|
||||
@@ -31,17 +31,26 @@ std::filesystem::path WriteConfigFixture()
|
||||
<< " \"oscBindAddress\": \"127.0.0.1\",\n"
|
||||
<< " \"oscPort\": 9100,\n"
|
||||
<< " \"oscSmoothing\": 0.25,\n"
|
||||
<< " \"inputVideoFormat\": \"720p\",\n"
|
||||
<< " \"inputFrameRate\": \"50\",\n"
|
||||
<< " \"outputVideoFormat\": \"2160p\",\n"
|
||||
<< " \"outputFrameRate\": \"60\",\n"
|
||||
<< " \"videoInputBackend\": \"none\",\n"
|
||||
<< " \"videoOutputBackend\": \"decklink\",\n"
|
||||
<< " \"input\": {\n"
|
||||
<< " \"backend\": \"none\",\n"
|
||||
<< " \"device\": \"input-card-1\",\n"
|
||||
<< " \"resolution\": \"720p\",\n"
|
||||
<< " \"frameRate\": \"50\"\n"
|
||||
<< " },\n"
|
||||
<< " \"output\": {\n"
|
||||
<< " \"backend\": \"decklink\",\n"
|
||||
<< " \"device\": \"output-card-1\",\n"
|
||||
<< " \"resolution\": \"2160p\",\n"
|
||||
<< " \"frameRate\": \"60\",\n"
|
||||
<< " \"keying\": {\n"
|
||||
<< " \"external\": true,\n"
|
||||
<< " \"alphaRequired\": true\n"
|
||||
<< " }\n"
|
||||
<< " },\n"
|
||||
<< " \"autoReload\": false,\n"
|
||||
<< " \"maxTemporalHistoryFrames\": 8,\n"
|
||||
<< " \"previewEnabled\": true,\n"
|
||||
<< " \"previewFps\": 24,\n"
|
||||
<< " \"enableExternalKeying\": true\n"
|
||||
<< " \"previewFps\": 24\n"
|
||||
<< "}\n";
|
||||
return path;
|
||||
}
|
||||
@@ -64,17 +73,20 @@ void TestLoadsRuntimeHostConfig()
|
||||
Expect(config.oscBindAddress == "127.0.0.1", "OSC bind address loads");
|
||||
Expect(config.oscPort == 9100, "OSC port loads");
|
||||
Expect(config.oscSmoothing == 0.25, "OSC smoothing loads");
|
||||
Expect(config.inputVideoFormat == "720p", "input format loads");
|
||||
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.input.resolution == "720p", "input resolution loads");
|
||||
Expect(config.input.frameRate == "50", "input frame rate loads");
|
||||
Expect(config.input.device == "input-card-1", "input device loads");
|
||||
Expect(config.output.resolution == "2160p", "output resolution loads");
|
||||
Expect(config.output.frameRate == "60", "output frame rate loads");
|
||||
Expect(config.output.device == "output-card-1", "output device loads");
|
||||
Expect(config.input.backend == "none", "video input backend loads");
|
||||
Expect(config.output.backend == "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");
|
||||
Expect(config.previewFps == 24.0, "preview fps loads");
|
||||
Expect(config.deckLink.externalKeyingEnabled, "external keying loads");
|
||||
Expect(config.output.externalKeyingEnabled, "external keying loads");
|
||||
Expect(config.output.outputAlphaRequired, "output alpha requirement loads");
|
||||
|
||||
std::filesystem::remove(path);
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ void WriteFile(const std::filesystem::path& path, const std::string& contents)
|
||||
int main()
|
||||
{
|
||||
RenderCadenceCompositor::AppConfig config = RenderCadenceCompositor::DefaultAppConfig();
|
||||
config.outputVideoFormat = "1080p";
|
||||
config.outputFrameRate = "59.94";
|
||||
config.output.resolution = "1080p";
|
||||
config.output.frameRate = "59.94";
|
||||
|
||||
RenderCadenceCompositor::CadenceTelemetrySnapshot telemetry;
|
||||
telemetry.renderFps = 59.94;
|
||||
@@ -117,8 +117,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, "\"input\":{\"backend\":\"decklink\",\"device\":\"default\",\"resolution\":\"1080p\",\"frameRate\":\"59.94\"}", "state JSON should expose nested input config");
|
||||
ExpectContains(json, "\"output\":{\"backend\":\"decklink\",\"device\":\"default\",\"resolution\":\"1080p\",\"frameRate\":\"59.94\",\"keying\"", "state JSON should expose nested output config");
|
||||
ExpectContains(json, "\"videoOutput\":{\"enabled\":true,\"backend\":\"decklink\"", "state JSON should expose neutral video output status");
|
||||
ExpectContains(json, "\"scheduleFailures\":2", "state JSON should expose neutral video output schedule failures");
|
||||
ExpectContains(json, "\"backendMetrics\":{\"bufferedAvailable\":true,\"buffered\":4", "state JSON should expose backend-specific video output metrics");
|
||||
|
||||
Reference in New Issue
Block a user